refactor(nuxt): re-organize internal runtime/nitro files (#31131)

This commit is contained in:
Julien Huang 2025-02-27 22:24:54 +01:00 committed by GitHub
parent 563d449010
commit 1baf3ebad6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 252 additions and 227 deletions

View File

@ -700,7 +700,7 @@ These options have been set to their current values for some time and we do not
* `polyfillVueUseHead` is implementable in user-land with [this plugin](https://github.com/nuxt/nuxt/blob/f209158352b09d1986aa320e29ff36353b91c358/packages/nuxt/src/head/runtime/plugins/vueuse-head-polyfill.ts#L10-L11)
* `respectNoSSRHeader`is implementable in user-land with [server middleware](https://github.com/nuxt/nuxt/blob/c660b39447f0d5b8790c0826092638d321cd6821/packages/nuxt/src/core/runtime/nitro/no-ssr.ts#L8-L9)
* `respectNoSSRHeader`is implementable in user-land with [server middleware](https://github.com/nuxt/nuxt/blob/c660b39447f0d5b8790c0826092638d321cd6821/packages/nuxt/src/core/runtime/nitro/middleware/no-ssr.ts#L8-L9)
## Nuxt 2 vs Nuxt 3+

View File

@ -95,9 +95,9 @@ See [Nitro](https://nitro.unjs.io/guide/plugins#available-hooks) for all availab
Hook | Arguments | Description | Types
-----------------------|-----------------------|--------------------------------------|------------------
`dev:ssr-logs` | `{ path, logs }` | Server | Called at the end of a request cycle with an array of server-side logs.
`render:response` | `response, { event }` | Called before sending the response. | [response](https://github.com/nuxt/nuxt/blob/71ef8bd3ff207fd51c2ca18d5a8c7140476780c7/packages/nuxt/src/core/runtime/nitro/renderer.ts#L24), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38)
`render:html` | `html, { event }` | Called before constructing the HTML. | [html](https://github.com/nuxt/nuxt/blob/71ef8bd3ff207fd51c2ca18d5a8c7140476780c7/packages/nuxt/src/core/runtime/nitro/renderer.ts#L15), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38)
`render:island` | `islandResponse, { event, islandContext }` | Called before constructing the island HTML. | [islandResponse](https://github.com/nuxt/nuxt/blob/e50cabfed1984c341af0d0c056a325a8aec26980/packages/nuxt/src/core/runtime/nitro/renderer.ts#L28), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38), [islandContext](https://github.com/nuxt/nuxt/blob/e50cabfed1984c341af0d0c056a325a8aec26980/packages/nuxt/src/core/runtime/nitro/renderer.ts#L38)
`render:response` | `response, { event }` | Called before sending the response. | [response](https://github.com/nuxt/nuxt/blob/71ef8bd3ff207fd51c2ca18d5a8c7140476780c7/packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts#L24), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38)
`render:html` | `html, { event }` | Called before constructing the HTML. | [html](https://github.com/nuxt/nuxt/blob/71ef8bd3ff207fd51c2ca18d5a8c7140476780c7/packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts#L15), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38)
`render:island` | `islandResponse, { event, islandContext }` | Called before constructing the island HTML. | [islandResponse](https://github.com/nuxt/nuxt/blob/e50cabfed1984c341af0d0c056a325a8aec26980/packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts#L28), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38), [islandContext](https://github.com/nuxt/nuxt/blob/e50cabfed1984c341af0d0c056a325a8aec26980/packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts#L38)
`close` | - | Called when Nitro is closed. | -
`error` | `error, { event? }` | Called when an error occurs. | [error](https://github.com/nitrojs/nitro/blob/d20ffcbd16fc4003b774445e1a01e698c2bb078a/src/types/runtime/nitro.ts#L48), [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38)
`request` | `event` | Called when a request is received. | [event](https://github.com/unjs/h3/blob/f6ceb5581043dc4d8b6eab91e9be4531e0c30f8e/src/types.ts#L38)

View File

@ -21,7 +21,7 @@ export default createConfigForNuxt({
'packages/schema/schema/**',
'packages/nuxt/src/app/components/welcome.vue',
'packages/nuxt/src/app/components/error-*.vue',
'packages/nuxt/src/core/runtime/nitro/error-*',
'packages/nuxt/src/core/runtime/nitro/handlers/error-*',
],
},
{

View File

@ -2,5 +2,5 @@ src/app/components/error-404.vue
src/app/components/error-500.vue
src/app/components/error-dev.vue
src/app/components/welcome.vue
src/core/runtime/nitro/error-500.ts
src/core/runtime/nitro/error-dev.ts
src/core/runtime/nitro/handlers/error-500.ts
src/core/runtime/nitro/handlers/error-dev.ts

View File

@ -4,4 +4,4 @@ export interface NuxtAppLiterals {
[key: string]: string
}
export type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from '../core/runtime/nitro/renderer'
export type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from '../core/runtime/nitro/handlers/renderer'

View File

@ -84,18 +84,18 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
{
as: '__buildAssetsURL',
name: 'buildAssetsURL',
from: resolve(distDir, 'core/runtime/nitro/paths'),
from: resolve(distDir, 'core/runtime/nitro/utils/paths'),
},
{
as: '__publicAssetsURL',
name: 'publicAssetsURL',
from: resolve(distDir, 'core/runtime/nitro/paths'),
from: resolve(distDir, 'core/runtime/nitro/utils/paths'),
},
{
// TODO: Remove after https://github.com/nitrojs/nitro/issues/1049
as: 'defineAppConfig',
name: 'defineAppConfig',
from: resolve(distDir, 'core/runtime/nitro/config'),
from: resolve(distDir, 'core/runtime/nitro/utils/config'),
priority: -1,
},
],
@ -112,7 +112,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
}
: false,
scanDirs: nuxt.options._layers.map(layer => (layer.config.serverDir || layer.config.srcDir) && resolve(layer.cwd, layer.config.serverDir || resolve(layer.config.srcDir, 'server'))).filter(Boolean),
renderer: resolve(distDir, 'core/runtime/nitro/renderer'),
renderer: resolve(distDir, 'core/runtime/nitro/handlers/renderer'),
nodeModulesDirs: nuxt.options.modulesDir,
handlers: nuxt.options.serverHandlers,
devHandlers: [],
@ -210,7 +210,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
...nuxt.options.alias,
// Paths
'#internal/nuxt/paths': resolve(distDir, 'core/runtime/nitro/paths'),
'#internal/nuxt/paths': resolve(distDir, 'core/runtime/nitro/utils/paths'),
},
replace: {
'process.env.NUXT_NO_SSR': nuxt.options.ssr === false,
@ -234,13 +234,13 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
nitroConfig.imports.imports ||= []
nitroConfig.imports.imports.push({
name: 'useAppConfig',
from: resolve(distDir, 'core/runtime/nitro/app-config'),
from: resolve(distDir, 'core/runtime/nitro/utils/app-config'),
})
}
// add error handler
if (!nitroConfig.errorHandler && (nuxt.options.dev || !nuxt.options.experimental.noVueServer)) {
nitroConfig.errorHandler = resolve(distDir, 'core/runtime/nitro/error')
nitroConfig.errorHandler = resolve(distDir, 'core/runtime/nitro/handlers/error')
}
// Resolve user-provided paths
@ -421,7 +421,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
ImpoundPlugin.rollup({
cwd: nuxt.options.rootDir,
patterns: createImportProtectionPatterns(nuxt, { context: 'nitro-app' }),
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/, ...sharedPatterns],
exclude: [/core[\\/]runtime[\\/]nitro[\\/](?:handlers|utils)/, ...sharedPatterns],
}),
)
@ -443,7 +443,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
})
const cacheDir = resolve(nuxt.options.buildDir, 'cache/nitro/prerender')
const cacheDriverPath = join(distDir, 'core/runtime/nitro/cache-driver.js')
const cacheDriverPath = join(distDir, 'core/runtime/nitro/utils/cache-driver.js')
await fsp.rm(cacheDir, { recursive: true, force: true }).catch(() => {})
nitro.options._config.storage = defu(nitro.options._config.storage, {
'internal:nuxt:prerender': {
@ -506,13 +506,13 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
nitro.options.handlers.unshift({
route: '/__nuxt_error',
lazy: true,
handler: resolve(distDir, 'core/runtime/nitro/renderer'),
handler: resolve(distDir, 'core/runtime/nitro/handlers/renderer'),
})
if (!nuxt.options.dev && nuxt.options.experimental.noVueServer) {
nitro.hooks.hook('rollup:before', (nitro) => {
if (nitro.options.preset === 'nitro-prerender') {
nitro.options.errorHandler = resolve(distDir, 'core/runtime/nitro/error')
nitro.options.errorHandler = resolve(distDir, 'core/runtime/nitro/handlers/error')
return
}
const nuxtErrorHandler = nitro.options.handlers.findIndex(h => h.route === '/__nuxt_error')

View File

@ -431,7 +431,7 @@ async function initNuxt (nuxt: Nuxt) {
if (nuxt.options.dev && nuxt.options.features.devLogs) {
addPlugin(resolve(nuxt.options.appDir, 'plugins/dev-server-logs'))
addServerPlugin(resolve(distDir, 'core/runtime/nitro/dev-server-logs'))
addServerPlugin(resolve(distDir, 'core/runtime/nitro/plugins/dev-server-logs'))
nuxt.options.nitro = defu(nuxt.options.nitro, {
externals: {
inline: [/#internal\/dev-server-logs-options/],

View File

@ -1,36 +1,34 @@
import { AsyncLocalStorage } from 'node:async_hooks'
import {
createRenderer,
getPrefetchLinks,
getPreloadLinks,
getRequestDependencies,
renderResourceHeaders,
} from 'vue-bundle-renderer/runtime'
import type { Manifest as ClientManifest } from 'vue-bundle-renderer'
import type { RenderResponse } from 'nitro/types'
import type { Manifest } from 'vite'
import type { H3Event } from 'h3'
import { appendResponseHeader, createError, getQuery, getResponseStatus, getResponseStatusText, readBody, writeEarlyHints } from 'h3'
import devalue from '@nuxt/devalue'
import { stringify, uneval } from 'devalue'
import destr from 'destr'
import { getQuery as getURLQuery, joinURL, withoutTrailingSlash } from 'ufo'
import { renderToString as _renderToString } from 'vue/server-renderer'
import { propsToString, renderSSRHead } from '@unhead/ssr'
import type { Head, HeadEntryOptions } from '@unhead/schema'
import type { Link, Script, Style } from '@unhead/vue'
import { createServerHead, resolveUnrefHeadInput } from '@unhead/vue'
import { defineRenderHandler, getRouteRules, useNitroApp, useRuntimeConfig, useStorage } from 'nitro/runtime'
import { defineRenderHandler, getRouteRules, useNitroApp, useRuntimeConfig } from 'nitro/runtime'
import type { NuxtPayload, NuxtSSRContext } from 'nuxt/app'
import { getEntryIds, getSPARenderer, getSSRRenderer, getSSRStyles } from '../utils/build-files'
import { islandCache, islandPropCache, payloadCache, sharedPrerenderCache } from '../utils/cache'
import { renderPayloadJsonScript, renderPayloadResponse, renderPayloadScript, splitPayload } from '../utils/payload'
// @ts-expect-error virtual file
import unheadPlugins from '#internal/unhead-plugins.mjs'
// @ts-expect-error virtual file
import { renderSSRHeadOptions } from '#internal/unhead.config.mjs'
// @ts-expect-error virtual file
import { appHead, appId, appRootAttrs, appRootTag, appSpaLoaderAttrs, appSpaLoaderTag, appTeleportAttrs, appTeleportTag, componentIslands, appManifest as isAppManifestEnabled, multiApp, spaLoadingTemplateOutside } from '#internal/nuxt.config.mjs'
import { appHead, appRootTag, appTeleportAttrs, appTeleportTag, componentIslands, appManifest as isAppManifestEnabled } from '#internal/nuxt.config.mjs'
// @ts-expect-error virtual file
import { buildAssetsURL, publicAssetsURL } from '#internal/nuxt/paths'
@ -91,119 +89,6 @@ export interface NuxtRenderResponse {
headers: Record<string, string>
}
// @ts-expect-error file will be produced after app build
const getClientManifest: () => Promise<Manifest> = () => import('#build/dist/server/client.manifest.mjs')
.then(r => r.default || r)
.then(r => typeof r === 'function' ? r() : r) as Promise<ClientManifest>
const getEntryIds: () => Promise<string[]> = () => getClientManifest().then(r => Object.values(r).filter(r =>
// @ts-expect-error internal key set by CSS inlining configuration
r._globalCSS,
).map(r => r.src!))
// @ts-expect-error file will be produced after app build
const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r)
// @ts-expect-error file will be produced after app build
const getSSRStyles = lazyCachedFunction((): Promise<Record<string, () => Promise<string[]>>> => import('#build/dist/server/styles.mjs').then(r => r.default || r))
// -- SSR Renderer --
const getSSRRenderer = lazyCachedFunction(async () => {
// Load client manifest
const manifest = await getClientManifest()
if (!manifest) { throw new Error('client.manifest is not available') }
// Load server bundle
const createSSRApp = await getServerEntry()
if (!createSSRApp) { throw new Error('Server bundle is not available') }
const options = {
manifest,
renderToString,
buildAssetsURL,
}
// Create renderer
const renderer = createRenderer(createSSRApp, options)
type RenderToStringParams = Parameters<typeof _renderToString>
async function renderToString (input: RenderToStringParams[0], context: RenderToStringParams[1]) {
const html = await _renderToString(input, context)
// In development with vite-node, the manifest is on-demand and will be available after rendering
if (import.meta.dev && process.env.NUXT_VITE_NODE_OPTIONS) {
renderer.rendererContext.updateManifest(await getClientManifest())
}
return APP_ROOT_OPEN_TAG + html + APP_ROOT_CLOSE_TAG
}
return renderer
})
// -- SPA Renderer --
const getSPARenderer = lazyCachedFunction(async () => {
const manifest = await getClientManifest()
// @ts-expect-error virtual file
const spaTemplate = await import('#spa-template').then(r => r.template).catch(() => '')
.then((r) => {
if (spaLoadingTemplateOutside) {
const APP_SPA_LOADER_OPEN_TAG = `<${appSpaLoaderTag}${propsToString(appSpaLoaderAttrs)}>`
const APP_SPA_LOADER_CLOSE_TAG = `</${appSpaLoaderTag}>`
const appTemplate = APP_ROOT_OPEN_TAG + APP_ROOT_CLOSE_TAG
const loaderTemplate = r ? APP_SPA_LOADER_OPEN_TAG + r + APP_SPA_LOADER_CLOSE_TAG : ''
return appTemplate + loaderTemplate
} else {
return APP_ROOT_OPEN_TAG + r + APP_ROOT_CLOSE_TAG
}
})
const options = {
manifest,
renderToString: () => spaTemplate,
buildAssetsURL,
}
// Create SPA renderer and cache the result for all requests
const renderer = createRenderer(() => () => {}, options)
const result = await renderer.renderToString({})
const renderToString = (ssrContext: NuxtSSRContext) => {
const config = useRuntimeConfig(ssrContext.event)
ssrContext.modules ||= new Set<string>()
ssrContext.payload.serverRendered = false
ssrContext.config = {
public: config.public,
app: config.app,
}
return Promise.resolve(result)
}
return {
rendererContext: renderer.rendererContext,
renderToString,
}
})
const payloadCache = import.meta.prerender ? useStorage('internal:nuxt:prerender:payload') : null
const islandCache = import.meta.prerender ? useStorage('internal:nuxt:prerender:island') : null
const islandPropCache = import.meta.prerender ? useStorage('internal:nuxt:prerender:island-props') : null
const sharedPrerenderPromises = import.meta.prerender && process.env.NUXT_SHARED_DATA ? new Map<string, Promise<any>>() : null
const sharedPrerenderKeys = new Set<string>()
const sharedPrerenderCache = import.meta.prerender && process.env.NUXT_SHARED_DATA
? {
get<T = unknown> (key: string): Promise<T> | undefined {
if (sharedPrerenderKeys.has(key)) {
return sharedPrerenderPromises!.get(key) ?? useStorage('internal:nuxt:prerender:shared').getItem(key) as Promise<T>
}
},
async set<T> (key: string, value: Promise<T>): Promise<void> {
sharedPrerenderKeys.add(key)
sharedPrerenderPromises!.set(key, value)
useStorage('internal:nuxt:prerender:shared').setItem(key, await value as any)
// free up memory after the promise is resolved
.finally(() => sharedPrerenderPromises!.delete(key))
},
}
: null
const ISLAND_SUFFIX_RE = /\.json(\?.*)?$/
async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
// TODO: Strict validation for url
@ -236,9 +121,6 @@ const HAS_APP_TELEPORTS = !!(appTeleportTag && appTeleportAttrs.id)
const APP_TELEPORT_OPEN_TAG = HAS_APP_TELEPORTS ? `<${appTeleportTag}${propsToString(appTeleportAttrs)}>` : ''
const APP_TELEPORT_CLOSE_TAG = HAS_APP_TELEPORTS ? `</${appTeleportTag}>` : ''
const APP_ROOT_OPEN_TAG = `<${appRootTag}${propsToString(appRootAttrs)}>`
const APP_ROOT_CLOSE_TAG = `</${appRootTag}>`
const PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /\/_payload.json(\?.*)?$/ : /\/_payload.js(\?.*)?$/
const ROOT_NODE_REGEX = new RegExp(`^<${appRootTag}[^>]*>([\\s\\S]*)<\\/${appRootTag}>$`)
@ -548,16 +430,6 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
return response
})
function lazyCachedFunction<T> (fn: () => Promise<T>): () => Promise<T> {
let res: Promise<T> | null = null
return () => {
if (res === null) {
res = fn().catch((err) => { res = null; throw err })
}
return res
}
}
function normalizeChunks (chunks: (string | undefined)[]) {
return chunks.filter(Boolean).map(i => i!.trim())
}
@ -592,76 +464,6 @@ async function renderInlineStyles (usedModules: Set<string> | string[]): Promise
return Array.from(inlinedStyles).map(style => ({ innerHTML: style }))
}
function renderPayloadResponse (ssrContext: NuxtSSRContext) {
return {
body: process.env.NUXT_JSON_PAYLOADS
? stringify(splitPayload(ssrContext).payload, ssrContext._payloadReducers)
: `export default ${devalue(splitPayload(ssrContext).payload)}`,
statusCode: getResponseStatus(ssrContext.event),
statusMessage: getResponseStatusText(ssrContext.event),
headers: {
'content-type': process.env.NUXT_JSON_PAYLOADS ? 'application/json;charset=utf-8' : 'text/javascript;charset=utf-8',
'x-powered-by': 'Nuxt',
},
} satisfies RenderResponse
}
function renderPayloadJsonScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : ''
const payload: Script = {
'type': 'application/json',
'innerHTML': contents,
'data-nuxt-data': appId,
'data-ssr': !(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR),
}
if (!multiApp) {
payload.id = '__NUXT_DATA__'
}
if (opts.src) {
payload['data-src'] = opts.src
}
const config = uneval(opts.ssrContext.config)
return [
payload,
{
innerHTML: multiApp
? `window.__NUXT__=window.__NUXT__||{};window.__NUXT__[${JSON.stringify(appId)}]={config:${config}}`
: `window.__NUXT__={};window.__NUXT__.config=${config}`,
},
]
}
function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
opts.data.config = opts.ssrContext.config
const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR
const nuxtData = devalue(opts.data)
if (_PAYLOAD_EXTRACTION) {
const singleAppPayload = `import p from "${opts.src}";window.__NUXT__={...p,...(${nuxtData})}`
const multiAppPayload = `import p from "${opts.src}";window.__NUXT__=window.__NUXT__||{};window.__NUXT__[${JSON.stringify(appId)}]={...p,...(${nuxtData})}`
return [
{
type: 'module',
innerHTML: multiApp ? multiAppPayload : singleAppPayload,
},
]
}
const singleAppPayload = `window.__NUXT__=${nuxtData}`
const multiAppPayload = `window.__NUXT__=window.__NUXT__||{};window.__NUXT__[${JSON.stringify(appId)}]=${nuxtData}`
return [
{
innerHTML: multiApp ? multiAppPayload : singleAppPayload,
},
]
}
function splitPayload (ssrContext: NuxtSSRContext) {
const { data, prerenderedAt, ...initial } = ssrContext.payload
return {
initial: { ...initial, prerenderedAt },
payload: { data, prerenderedAt },
}
}
/**
* remove the root node from the html body
*/

View File

@ -0,0 +1,119 @@
import {
createRenderer,
} from 'vue-bundle-renderer/runtime'
import type { Manifest as ClientManifest } from 'vue-bundle-renderer'
import type { Manifest } from 'vite'
import { renderToString as _renderToString } from 'vue/server-renderer'
import { propsToString } from '@unhead/ssr'
import { useRuntimeConfig } from 'nitro/runtime'
import type { NuxtSSRContext } from 'nuxt/app'
// @ts-expect-error virtual file
import { appRootAttrs, appRootTag, appSpaLoaderAttrs, appSpaLoaderTag, spaLoadingTemplateOutside } from '#internal/nuxt.config.mjs'
// @ts-expect-error virtual file
import { buildAssetsURL } from '#internal/nuxt/paths'
const APP_ROOT_OPEN_TAG = `<${appRootTag}${propsToString(appRootAttrs)}>`
const APP_ROOT_CLOSE_TAG = `</${appRootTag}>`
// @ts-expect-error file will be produced after app build
export const getClientManifest: () => Promise<Manifest> = () => import('#build/dist/server/client.manifest.mjs')
.then(r => r.default || r)
.then(r => typeof r === 'function' ? r() : r) as Promise<ClientManifest>
export const getEntryIds: () => Promise<string[]> = () => getClientManifest().then(r => Object.values(r).filter(r =>
// @ts-expect-error internal key set by CSS inlining configuration
r._globalCSS,
).map(r => r.src!))
// @ts-expect-error file will be produced after app build
export const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r)
// @ts-expect-error file will be produced after app build
export const getSSRStyles = lazyCachedFunction((): Promise<Record<string, () => Promise<string[]>>> => import('#build/dist/server/styles.mjs').then(r => r.default || r))
// -- SSR Renderer --
export const getSSRRenderer = lazyCachedFunction(async () => {
// Load client manifest
const manifest = await getClientManifest()
if (!manifest) { throw new Error('client.manifest is not available') }
// Load server bundle
const createSSRApp = await getServerEntry()
if (!createSSRApp) { throw new Error('Server bundle is not available') }
const options = {
manifest,
renderToString,
buildAssetsURL,
}
// Create renderer
const renderer = createRenderer(createSSRApp, options)
type RenderToStringParams = Parameters<typeof _renderToString>
async function renderToString (input: RenderToStringParams[0], context: RenderToStringParams[1]) {
const html = await _renderToString(input, context)
// In development with vite-node, the manifest is on-demand and will be available after rendering
if (import.meta.dev && process.env.NUXT_VITE_NODE_OPTIONS) {
renderer.rendererContext.updateManifest(await getClientManifest())
}
return APP_ROOT_OPEN_TAG + html + APP_ROOT_CLOSE_TAG
}
return renderer
})
// -- SPA Renderer --
export const getSPARenderer = lazyCachedFunction(async () => {
const manifest = await getClientManifest()
// @ts-expect-error virtual file
const spaTemplate = await import('#spa-template').then(r => r.template).catch(() => '')
.then((r) => {
if (spaLoadingTemplateOutside) {
const APP_SPA_LOADER_OPEN_TAG = `<${appSpaLoaderTag}${propsToString(appSpaLoaderAttrs)}>`
const APP_SPA_LOADER_CLOSE_TAG = `</${appSpaLoaderTag}>`
const appTemplate = APP_ROOT_OPEN_TAG + APP_ROOT_CLOSE_TAG
const loaderTemplate = r ? APP_SPA_LOADER_OPEN_TAG + r + APP_SPA_LOADER_CLOSE_TAG : ''
return appTemplate + loaderTemplate
} else {
return APP_ROOT_OPEN_TAG + r + APP_ROOT_CLOSE_TAG
}
})
const options = {
manifest,
renderToString: () => spaTemplate,
buildAssetsURL,
}
// Create SPA renderer and cache the result for all requests
const renderer = createRenderer(() => () => {}, options)
const result = await renderer.renderToString({})
const renderToString = (ssrContext: NuxtSSRContext) => {
const config = useRuntimeConfig(ssrContext.event)
ssrContext.modules ||= new Set<string>()
ssrContext.payload.serverRendered = false
ssrContext.config = {
public: config.public,
app: config.app,
}
return Promise.resolve(result)
}
return {
rendererContext: renderer.rendererContext,
renderToString,
}
})
function lazyCachedFunction<T> (fn: () => Promise<T>): () => Promise<T> {
let res: Promise<T> | null = null
return () => {
if (res === null) {
res = fn().catch((err) => { res = null; throw err })
}
return res
}
}

View File

@ -0,0 +1,24 @@
import { useStorage } from 'nitro/runtime'
export const payloadCache = import.meta.prerender ? useStorage('internal:nuxt:prerender:payload') : null
export const islandCache = import.meta.prerender ? useStorage('internal:nuxt:prerender:island') : null
export const islandPropCache = import.meta.prerender ? useStorage('internal:nuxt:prerender:island-props') : null
export const sharedPrerenderPromises = import.meta.prerender && process.env.NUXT_SHARED_DATA ? new Map<string, Promise<any>>() : null
const sharedPrerenderKeys = new Set<string>()
export const sharedPrerenderCache = import.meta.prerender && process.env.NUXT_SHARED_DATA
? {
get<T = unknown> (key: string): Promise<T> | undefined {
if (sharedPrerenderKeys.has(key)) {
return sharedPrerenderPromises!.get(key) ?? useStorage('internal:nuxt:prerender:shared').getItem(key) as Promise<T>
}
},
async set<T> (key: string, value: Promise<T>): Promise<void> {
sharedPrerenderKeys.add(key)
sharedPrerenderPromises!.set(key, value)
useStorage('internal:nuxt:prerender:shared').setItem(key, await value as any)
// free up memory after the promise is resolved
.finally(() => sharedPrerenderPromises!.delete(key))
},
}
: null

View File

@ -0,0 +1,80 @@
import type { RenderResponse } from 'nitro/types'
import { getResponseStatus, getResponseStatusText } from 'h3'
import devalue from '@nuxt/devalue'
import { stringify, uneval } from 'devalue'
import type { Script } from '@unhead/vue'
import type { NuxtSSRContext } from 'nuxt/app'
// @ts-expect-error virtual file
import { appId, multiApp } from '#internal/nuxt.config.mjs'
export function renderPayloadResponse (ssrContext: NuxtSSRContext) {
return {
body: process.env.NUXT_JSON_PAYLOADS
? stringify(splitPayload(ssrContext).payload, ssrContext._payloadReducers)
: `export default ${devalue(splitPayload(ssrContext).payload)}`,
statusCode: getResponseStatus(ssrContext.event),
statusMessage: getResponseStatusText(ssrContext.event),
headers: {
'content-type': process.env.NUXT_JSON_PAYLOADS ? 'application/json;charset=utf-8' : 'text/javascript;charset=utf-8',
'x-powered-by': 'Nuxt',
},
} satisfies RenderResponse
}
export function renderPayloadJsonScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : ''
const payload: Script = {
'type': 'application/json',
'innerHTML': contents,
'data-nuxt-data': appId,
'data-ssr': !(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR),
}
if (!multiApp) {
payload.id = '__NUXT_DATA__'
}
if (opts.src) {
payload['data-src'] = opts.src
}
const config = uneval(opts.ssrContext.config)
return [
payload,
{
innerHTML: multiApp
? `window.__NUXT__=window.__NUXT__||{};window.__NUXT__[${JSON.stringify(appId)}]={config:${config}}`
: `window.__NUXT__={};window.__NUXT__.config=${config}`,
},
]
}
export function renderPayloadScript (opts: { ssrContext: NuxtSSRContext, data?: any, src?: string }): Script[] {
opts.data.config = opts.ssrContext.config
const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR
const nuxtData = devalue(opts.data)
if (_PAYLOAD_EXTRACTION) {
const singleAppPayload = `import p from "${opts.src}";window.__NUXT__={...p,...(${nuxtData})}`
const multiAppPayload = `import p from "${opts.src}";window.__NUXT__=window.__NUXT__||{};window.__NUXT__[${JSON.stringify(appId)}]={...p,...(${nuxtData})}`
return [
{
type: 'module',
innerHTML: multiApp ? multiAppPayload : singleAppPayload,
},
]
}
const singleAppPayload = `window.__NUXT__=${nuxtData}`
const multiAppPayload = `window.__NUXT__=window.__NUXT__||{};window.__NUXT__[${JSON.stringify(appId)}]=${nuxtData}`
return [
{
innerHTML: multiApp ? multiAppPayload : singleAppPayload,
},
]
}
export function splitPayload (ssrContext: NuxtSSRContext) {
const { data, prerenderedAt, ...initial } = ssrContext.payload
return {
initial: { ...initial, prerenderedAt },
payload: { data, prerenderedAt },
}
}

View File

@ -483,7 +483,7 @@ export const publicPathTemplate: NuxtTemplate = {
' return path.length ? joinRelativeURL(publicBase, ...path) : publicBase',
'}',
// On server these are registered directly in packages/nuxt/src/core/runtime/nitro/renderer.ts
// On server these are registered directly in packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts
'if (import.meta.client) {',
' globalThis.__buildAssetsURL = buildAssetsURL',
' globalThis.__publicAssetsURL = publicAssetsURL',

View File

@ -201,7 +201,7 @@ export const RenderPlugin = () => {
await copyFile(r(`dist/templates/${file}`), join(nuxtRoot, 'src/app/components', file))
}
for (const file of ['error-500.ts', 'error-dev.ts']) {
await copyFile(r(`dist/templates/${file}`), join(nuxtRoot, 'src/core/runtime/nitro', file))
await copyFile(r(`dist/templates/${file}`), join(nuxtRoot, 'src/core/runtime/nitro/handlers', file))
}
},
}

View File

@ -2426,7 +2426,7 @@ describe('component islands', () => {
`)
} else if (isDev() && !isWebpack) {
// TODO: resolve dev bug triggered by earlier fetch of /vueuse-head page
// https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/runtime/nitro/renderer.ts#L139
// https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts#L139
result.head.link = result.head.link?.filter(l => typeof l.href !== 'string' || !l.href.includes('SharedComponent'))
expect(result.head).toMatchInlineSnapshot(`