import { AsyncLocalStorage } from "node:async_hooks"; import process from "node:process"; import { getPrefetchLinks, getPreloadLinks, getRequestDependencies, renderResourceHeaders } from "vue-bundle-renderer/runtime"; import { appendResponseHeader, createError, getQuery, getResponseStatus, getResponseStatusText, writeEarlyHints } from "h3"; import { getQuery as getURLQuery, joinURL } from "ufo"; import { propsToString, renderSSRHead } from "@unhead/vue/server"; import destr from "destr"; import { getRenderer } from "../utils/renderer/build-files.js"; import { payloadCache } from "../utils/cache.js"; import { renderPayloadJsonScript, renderPayloadResponse, renderPayloadScript, splitPayload } from "../utils/renderer/payload.js"; import { createSSRContext, setSSRError } from "../utils/renderer/app.js"; import { renderInlineStyles } from "../utils/renderer/inline-styles.js"; import { replaceIslandTeleports } from "../utils/renderer/islands.js"; import { defineRenderHandler, getRouteRules, useNitroApp } from "#internal/nitro"; import { renderSSRHeadOptions } from "#internal/unhead.config.mjs"; import { appHead, appTeleportAttrs, appTeleportTag, componentIslands, appManifest as isAppManifestEnabled } from "#internal/nuxt.config.mjs"; import entryIds from "#internal/nuxt/entry-ids.mjs"; import { entryFileName } from "#internal/entry-chunk.mjs"; import { buildAssetsURL, publicAssetsURL } from "#internal/nuxt/paths"; import { relative } from "pathe"; globalThis.__buildAssetsURL = buildAssetsURL; globalThis.__publicAssetsURL = publicAssetsURL; if (process.env.NUXT_ASYNC_CONTEXT && !("AsyncLocalStorage" in globalThis)) { globalThis.AsyncLocalStorage = AsyncLocalStorage; } 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 PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /^[^?]*\/_payload.json(?:\?.*)?$/ : /^[^?]*\/_payload.js(?:\?.*)?$/; const PAYLOAD_FILENAME = process.env.NUXT_JSON_PAYLOADS ? "_payload.json" : "_payload.js"; let entryPath; export default defineRenderHandler(async (event) => { const nitroApp = useNitroApp(); const ssrError = event.path.startsWith("/__nuxt_error") ? getQuery(event) : null; if (ssrError && !("__unenv__" in event.node.req)) { throw createError({ statusCode: 404, statusMessage: "Page Not Found: /__nuxt_error" }); } const ssrContext = createSSRContext(event); const headEntryOptions = { mode: "server" }; ssrContext.head.push(appHead, headEntryOptions); if (ssrError) { ssrError.statusCode &&= Number.parseInt(ssrError.statusCode); if (process.env.PARSE_ERROR_DATA && typeof ssrError.data === "string") { try { ssrError.data = destr(ssrError.data); } catch { } } setSSRError(ssrContext, ssrError); } const isRenderingPayload = process.env.NUXT_PAYLOAD_EXTRACTION && PAYLOAD_URL_RE.test(ssrContext.url); if (isRenderingPayload) { const url = ssrContext.url.substring(0, ssrContext.url.lastIndexOf("/")) || "/"; ssrContext.url = url; event._path = event.node.req.url = url; if (import.meta.prerender && await payloadCache.hasItem(url)) { return payloadCache.getItem(url); } } const routeOptions = getRouteRules(event); if (routeOptions.ssr === false) { ssrContext.noSSR = true; } const _PAYLOAD_EXTRACTION = import.meta.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !ssrContext.noSSR; const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(ssrContext.runtimeConfig.app.cdnURL || ssrContext.runtimeConfig.app.baseURL, ssrContext.url.replace(/\?.*$/, ""), PAYLOAD_FILENAME) + "?" + ssrContext.runtimeConfig.app.buildId : void 0; const renderer = await getRenderer(ssrContext); if (process.env.NUXT_EARLY_HINTS && !isRenderingPayload && !import.meta.prerender) { const { link: link2 } = renderResourceHeaders({}, renderer.rendererContext); if (link2) { writeEarlyHints(event, link2); } } if (process.env.NUXT_INLINE_STYLES) { for (const id of entryIds) { ssrContext.modules.add(id); } } const _rendered = await renderer.renderToString(ssrContext).catch(async (error) => { if (ssrContext._renderResponse && error.message === "skipping render") { return {}; } const _err = !ssrError && ssrContext.payload?.error || error; await ssrContext.nuxt?.hooks.callHook("app:error", _err); throw _err; }); const inlinedStyles = process.env.NUXT_INLINE_STYLES && !ssrContext._renderResponse && !isRenderingPayload ? await renderInlineStyles(ssrContext.modules ?? []) : []; await ssrContext.nuxt?.hooks.callHook("app:rendered", { ssrContext, renderResult: _rendered }); if (ssrContext._renderResponse) { return ssrContext._renderResponse; } if (ssrContext.payload?.error && !ssrError) { throw ssrContext.payload.error; } if (isRenderingPayload) { const response = renderPayloadResponse(ssrContext); if (import.meta.prerender) { await payloadCache.setItem(ssrContext.url, response); } return response; } if (_PAYLOAD_EXTRACTION) { appendResponseHeader(event, "x-nitro-prerender", joinURL(ssrContext.url.replace(/\?.*$/, ""), PAYLOAD_FILENAME)); await payloadCache.setItem(ssrContext.url === "/" ? "/" : ssrContext.url.replace(/\/$/, ""), renderPayloadResponse(ssrContext)); } const NO_SCRIPTS = process.env.NUXT_NO_SCRIPTS || routeOptions.noScripts; const { styles, scripts } = getRequestDependencies(ssrContext, renderer.rendererContext); if (entryFileName && !NO_SCRIPTS) { let path = entryPath; if (!path) { path = buildAssetsURL(entryFileName); if (ssrContext.runtimeConfig.app.cdnURL || /^(?:\/|\.+\/)/.test(path)) { entryPath = path; } else { path = relative(event.path.replace(/\/[^/]+$/, "/"), joinURL("/", path)); if (!/^(?:\/|\.+\/)/.test(path)) { path = `./${path}`; } } } ssrContext.head.push({ script: [{ tagPosition: "head", tagPriority: -2, type: "importmap", innerHTML: JSON.stringify({ imports: { "#entry": path } }) }] }, headEntryOptions); } if (_PAYLOAD_EXTRACTION && !NO_SCRIPTS) { ssrContext.head.push({ link: [ process.env.NUXT_JSON_PAYLOADS ? { rel: "preload", as: "fetch", crossorigin: "anonymous", href: payloadURL } : { rel: "modulepreload", crossorigin: "", href: payloadURL } ] }, headEntryOptions); } if (isAppManifestEnabled && ssrContext._preloadManifest && !NO_SCRIPTS) { ssrContext.head.push({ link: [ { rel: "preload", as: "fetch", fetchpriority: "low", crossorigin: "anonymous", href: buildAssetsURL(`builds/meta/${ssrContext.runtimeConfig.app.buildId}.json`) } ] }, { ...headEntryOptions, tagPriority: "low" }); } if (inlinedStyles.length) { ssrContext.head.push({ style: inlinedStyles }); } const link = []; for (const resource of Object.values(styles)) { if (import.meta.dev && "inline" in getURLQuery(resource.file)) { continue; } link.push({ rel: "stylesheet", href: renderer.rendererContext.buildAssetsURL(resource.file), crossorigin: "" }); } if (link.length) { ssrContext.head.push({ link }, headEntryOptions); } if (!NO_SCRIPTS) { ssrContext.head.push({ link: getPreloadLinks(ssrContext, renderer.rendererContext) }, headEntryOptions); ssrContext.head.push({ link: getPrefetchLinks(ssrContext, renderer.rendererContext) }, headEntryOptions); ssrContext.head.push({ script: _PAYLOAD_EXTRACTION ? process.env.NUXT_JSON_PAYLOADS ? renderPayloadJsonScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL }) : renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL }) : process.env.NUXT_JSON_PAYLOADS ? renderPayloadJsonScript({ ssrContext, data: ssrContext.payload }) : renderPayloadScript({ ssrContext, data: ssrContext.payload }) }, { ...headEntryOptions, // this should come before another end of body scripts tagPosition: "bodyClose", tagPriority: "high" }); } if (!routeOptions.noScripts) { const tagPosition = _PAYLOAD_EXTRACTION && !process.env.NUXT_JSON_PAYLOADS ? "bodyClose" : "head"; ssrContext.head.push({ script: Object.values(scripts).map((resource) => ({ type: resource.module ? "module" : null, src: renderer.rendererContext.buildAssetsURL(resource.file), defer: resource.module ? null : true, // if we are rendering script tag payloads that import an async payload // we need to ensure this resolves before executing the Nuxt entry tagPosition, crossorigin: "" })) }, headEntryOptions); } const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = await renderSSRHead(ssrContext.head, renderSSRHeadOptions); const htmlContext = { htmlAttrs: htmlAttrs ? [htmlAttrs] : [], head: normalizeChunks([headTags]), bodyAttrs: bodyAttrs ? [bodyAttrs] : [], bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]), body: [ componentIslands ? replaceIslandTeleports(ssrContext, _rendered.html) : _rendered.html, APP_TELEPORT_OPEN_TAG + (HAS_APP_TELEPORTS ? joinTags([ssrContext.teleports?.[`#${appTeleportAttrs.id}`]]) : "") + APP_TELEPORT_CLOSE_TAG ], bodyAppend: [bodyTags] }; await nitroApp.hooks.callHook("render:html", htmlContext, { event }); return { body: renderHTMLDocument(htmlContext), statusCode: getResponseStatus(event), statusMessage: getResponseStatusText(event), headers: { "content-type": "text/html;charset=utf-8", "x-powered-by": "Nuxt" } }; }); function normalizeChunks(chunks) { const result = []; for (const _chunk of chunks) { const chunk = _chunk?.trim(); if (chunk) { result.push(chunk); } } return result; } function joinTags(tags) { return tags.join(""); } function joinAttrs(chunks) { if (chunks.length === 0) { return ""; } return " " + chunks.join(" "); } function renderHTMLDocument(html) { return `${joinTags(html.head)}${joinTags(html.bodyPrepend)}${joinTags(html.body)}${joinTags(html.bodyAppend)}`; }