From 1baf3ebad6ec9fff90fdd88db1b53d66412e2e75 Mon Sep 17 00:00:00 2001 From: Julien Huang Date: Thu, 27 Feb 2025 22:24:54 +0100 Subject: [PATCH] refactor(nuxt): re-organize internal `runtime/nitro` files (#31131) --- docs/1.getting-started/12.upgrade.md | 2 +- docs/3.api/6.advanced/1.hooks.md | 6 +- eslint.config.mjs | 2 +- packages/nuxt/.gitignore | 4 +- packages/nuxt/src/app/types.ts | 2 +- packages/nuxt/src/core/nitro.ts | 22 +- packages/nuxt/src/core/nuxt.ts | 2 +- .../runtime/nitro/{ => handlers}/error.ts | 0 .../runtime/nitro/{ => handlers}/renderer.ts | 210 +----------------- .../runtime/nitro/{ => middleware}/no-ssr.ts | 0 .../nitro/{ => plugins}/dev-server-logs.ts | 0 .../runtime/nitro/{ => utils}/app-config.ts | 0 .../core/runtime/nitro/utils/build-files.ts | 119 ++++++++++ .../runtime/nitro/{ => utils}/cache-driver.js | 0 .../src/core/runtime/nitro/utils/cache.ts | 24 ++ .../core/runtime/nitro/{ => utils}/config.ts | 0 .../core/runtime/nitro/{ => utils}/paths.ts | 0 .../src/core/runtime/nitro/utils/payload.ts | 80 +++++++ packages/nuxt/src/core/templates.ts | 2 +- packages/ui-templates/lib/render.ts | 2 +- test/basic.test.ts | 2 +- 21 files changed, 252 insertions(+), 227 deletions(-) rename packages/nuxt/src/core/runtime/nitro/{ => handlers}/error.ts (100%) rename packages/nuxt/src/core/runtime/nitro/{ => handlers}/renderer.ts (70%) rename packages/nuxt/src/core/runtime/nitro/{ => middleware}/no-ssr.ts (100%) rename packages/nuxt/src/core/runtime/nitro/{ => plugins}/dev-server-logs.ts (100%) rename packages/nuxt/src/core/runtime/nitro/{ => utils}/app-config.ts (100%) create mode 100644 packages/nuxt/src/core/runtime/nitro/utils/build-files.ts rename packages/nuxt/src/core/runtime/nitro/{ => utils}/cache-driver.js (100%) create mode 100644 packages/nuxt/src/core/runtime/nitro/utils/cache.ts rename packages/nuxt/src/core/runtime/nitro/{ => utils}/config.ts (100%) rename packages/nuxt/src/core/runtime/nitro/{ => utils}/paths.ts (100%) create mode 100644 packages/nuxt/src/core/runtime/nitro/utils/payload.ts diff --git a/docs/1.getting-started/12.upgrade.md b/docs/1.getting-started/12.upgrade.md index d6d463dbda..29b747464e 100644 --- a/docs/1.getting-started/12.upgrade.md +++ b/docs/1.getting-started/12.upgrade.md @@ -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+ diff --git a/docs/3.api/6.advanced/1.hooks.md b/docs/3.api/6.advanced/1.hooks.md index dcd66fe7be..39f328ddfd 100644 --- a/docs/3.api/6.advanced/1.hooks.md +++ b/docs/3.api/6.advanced/1.hooks.md @@ -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) diff --git a/eslint.config.mjs b/eslint.config.mjs index 36d9d8b893..7e1f5f3e11 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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-*', ], }, { diff --git a/packages/nuxt/.gitignore b/packages/nuxt/.gitignore index 95c0486757..afa5e6a973 100644 --- a/packages/nuxt/.gitignore +++ b/packages/nuxt/.gitignore @@ -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 diff --git a/packages/nuxt/src/app/types.ts b/packages/nuxt/src/app/types.ts index 54bfe868de..e81dd3d6fb 100644 --- a/packages/nuxt/src/app/types.ts +++ b/packages/nuxt/src/app/types.ts @@ -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' diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts index cc794a0526..921b48f1e0 100644 --- a/packages/nuxt/src/core/nitro.ts +++ b/packages/nuxt/src/core/nitro.ts @@ -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') diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index e853cf7694..5b636c3108 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -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/], diff --git a/packages/nuxt/src/core/runtime/nitro/error.ts b/packages/nuxt/src/core/runtime/nitro/handlers/error.ts similarity index 100% rename from packages/nuxt/src/core/runtime/nitro/error.ts rename to packages/nuxt/src/core/runtime/nitro/handlers/error.ts diff --git a/packages/nuxt/src/core/runtime/nitro/renderer.ts b/packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts similarity index 70% rename from packages/nuxt/src/core/runtime/nitro/renderer.ts rename to packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts index ac4dc40400..0f8fd21e61 100644 --- a/packages/nuxt/src/core/runtime/nitro/renderer.ts +++ b/packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts @@ -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 } -// @ts-expect-error file will be produced after app build -const getClientManifest: () => Promise = () => import('#build/dist/server/client.manifest.mjs') - .then(r => r.default || r) - .then(r => typeof r === 'function' ? r() : r) as Promise - -const getEntryIds: () => Promise = () => 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 Promise>> => 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 - 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 = `` - 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() - 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>() : null -const sharedPrerenderKeys = new Set() -const sharedPrerenderCache = import.meta.prerender && process.env.NUXT_SHARED_DATA - ? { - get (key: string): Promise | undefined { - if (sharedPrerenderKeys.has(key)) { - return sharedPrerenderPromises!.get(key) ?? useStorage('internal:nuxt:prerender:shared').getItem(key) as Promise - } - }, - async set (key: string, value: Promise): Promise { - 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 { // 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 ? `` : '' -const APP_ROOT_OPEN_TAG = `<${appRootTag}${propsToString(appRootAttrs)}>` -const APP_ROOT_CLOSE_TAG = `` - 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 (fn: () => Promise): () => Promise { - let res: Promise | 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[]): 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 */ diff --git a/packages/nuxt/src/core/runtime/nitro/no-ssr.ts b/packages/nuxt/src/core/runtime/nitro/middleware/no-ssr.ts similarity index 100% rename from packages/nuxt/src/core/runtime/nitro/no-ssr.ts rename to packages/nuxt/src/core/runtime/nitro/middleware/no-ssr.ts diff --git a/packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts b/packages/nuxt/src/core/runtime/nitro/plugins/dev-server-logs.ts similarity index 100% rename from packages/nuxt/src/core/runtime/nitro/dev-server-logs.ts rename to packages/nuxt/src/core/runtime/nitro/plugins/dev-server-logs.ts diff --git a/packages/nuxt/src/core/runtime/nitro/app-config.ts b/packages/nuxt/src/core/runtime/nitro/utils/app-config.ts similarity index 100% rename from packages/nuxt/src/core/runtime/nitro/app-config.ts rename to packages/nuxt/src/core/runtime/nitro/utils/app-config.ts diff --git a/packages/nuxt/src/core/runtime/nitro/utils/build-files.ts b/packages/nuxt/src/core/runtime/nitro/utils/build-files.ts new file mode 100644 index 0000000000..f6e71c7649 --- /dev/null +++ b/packages/nuxt/src/core/runtime/nitro/utils/build-files.ts @@ -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 = `` + +// @ts-expect-error file will be produced after app build +export const getClientManifest: () => Promise = () => import('#build/dist/server/client.manifest.mjs') + .then(r => r.default || r) + .then(r => typeof r === 'function' ? r() : r) as Promise + +export const getEntryIds: () => Promise = () => 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 Promise>> => 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 + 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 = `` + 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() + ssrContext.payload.serverRendered = false + ssrContext.config = { + public: config.public, + app: config.app, + } + return Promise.resolve(result) + } + + return { + rendererContext: renderer.rendererContext, + renderToString, + } +}) + +function lazyCachedFunction (fn: () => Promise): () => Promise { + let res: Promise | null = null + return () => { + if (res === null) { + res = fn().catch((err) => { res = null; throw err }) + } + return res + } +} diff --git a/packages/nuxt/src/core/runtime/nitro/cache-driver.js b/packages/nuxt/src/core/runtime/nitro/utils/cache-driver.js similarity index 100% rename from packages/nuxt/src/core/runtime/nitro/cache-driver.js rename to packages/nuxt/src/core/runtime/nitro/utils/cache-driver.js diff --git a/packages/nuxt/src/core/runtime/nitro/utils/cache.ts b/packages/nuxt/src/core/runtime/nitro/utils/cache.ts new file mode 100644 index 0000000000..888d2bc034 --- /dev/null +++ b/packages/nuxt/src/core/runtime/nitro/utils/cache.ts @@ -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>() : null + +const sharedPrerenderKeys = new Set() +export const sharedPrerenderCache = import.meta.prerender && process.env.NUXT_SHARED_DATA + ? { + get (key: string): Promise | undefined { + if (sharedPrerenderKeys.has(key)) { + return sharedPrerenderPromises!.get(key) ?? useStorage('internal:nuxt:prerender:shared').getItem(key) as Promise + } + }, + async set (key: string, value: Promise): Promise { + 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 diff --git a/packages/nuxt/src/core/runtime/nitro/config.ts b/packages/nuxt/src/core/runtime/nitro/utils/config.ts similarity index 100% rename from packages/nuxt/src/core/runtime/nitro/config.ts rename to packages/nuxt/src/core/runtime/nitro/utils/config.ts diff --git a/packages/nuxt/src/core/runtime/nitro/paths.ts b/packages/nuxt/src/core/runtime/nitro/utils/paths.ts similarity index 100% rename from packages/nuxt/src/core/runtime/nitro/paths.ts rename to packages/nuxt/src/core/runtime/nitro/utils/paths.ts diff --git a/packages/nuxt/src/core/runtime/nitro/utils/payload.ts b/packages/nuxt/src/core/runtime/nitro/utils/payload.ts new file mode 100644 index 0000000000..f1375e0014 --- /dev/null +++ b/packages/nuxt/src/core/runtime/nitro/utils/payload.ts @@ -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 }, + } +} diff --git a/packages/nuxt/src/core/templates.ts b/packages/nuxt/src/core/templates.ts index 76f364f6ba..197204a14d 100644 --- a/packages/nuxt/src/core/templates.ts +++ b/packages/nuxt/src/core/templates.ts @@ -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', diff --git a/packages/ui-templates/lib/render.ts b/packages/ui-templates/lib/render.ts index 267d95a017..a48039182a 100644 --- a/packages/ui-templates/lib/render.ts +++ b/packages/ui-templates/lib/render.ts @@ -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)) } }, } diff --git a/test/basic.test.ts b/test/basic.test.ts index 1120da9bc6..5ac880798d 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -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(`