import { AsyncLocalStorage } from "node:async_hooks"; 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 { defineRenderHandler, getRouteRules, useNitroApp } from "nitropack/runtime"; import { getRenderer } from "../utils/renderer/build-files.mjs"; import { payloadCache, prerenderRenderingURLs } from "../utils/cache.mjs"; import { renderPayloadJsonScript, renderPayloadResponse, renderPayloadScript, splitPayload } from "../utils/renderer/payload.mjs"; import { createSSRContext, setSSRError } from "../utils/renderer/app.mjs"; import { renderInlineStyles } from "../utils/renderer/inline-styles.mjs"; import { replaceIslandTeleports } from "../utils/renderer/islands.mjs"; // @ts-expect-error virtual file import { renderSSRHeadOptions } from "#internal/unhead.config.mjs"; // @ts-expect-error virtual file import { NUXT_ASYNC_CONTEXT, NUXT_EARLY_HINTS, NUXT_INLINE_STYLES, NUXT_JSON_PAYLOADS, NUXT_NO_SCRIPTS, NUXT_PAYLOAD_EXTRACTION, NUXT_PAYLOAD_INLINE, NUXT_RUNTIME_PAYLOAD_EXTRACTION, PARSE_ERROR_DATA } from "#internal/nuxt/nitro-config.mjs"; // @ts-expect-error virtual file import { appHead, appTeleportAttrs, appTeleportTag, componentIslands } from "#internal/nuxt.config.mjs"; // @ts-expect-error virtual file import entryIds from "#internal/nuxt/entry-ids.mjs"; // @ts-expect-error virtual file import { entryFileName } from "#internal/entry-chunk.mjs"; // @ts-expect-error virtual file import { buildAssetsURL, publicAssetsURL } from "#internal/nuxt/paths"; import { relative } from "pathe"; // @ts-expect-error private property consumed by vite-generated url helpers globalThis.__buildAssetsURL = buildAssetsURL; // @ts-expect-error private property consumed by vite-generated url helpers globalThis.__publicAssetsURL = publicAssetsURL; if (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 = NUXT_JSON_PAYLOADS ? /^[^?]*\/_payload.json(?:\?.*)?$/ : /^[^?]*\/_payload.js(?:\?.*)?$/; const PAYLOAD_FILENAME = NUXT_JSON_PAYLOADS ? "_payload.json" : "_payload.js"; let entryPath; const handler = defineRenderHandler((event) => { const ssrError = event.path.startsWith("/__nuxt_error") ? getQuery(event) : null; if (ssrError && !("__unenv__" in event.node.req)) { throw createError({ status: 404, statusText: "Page Not Found: /__nuxt_error", message: "Page Not Found: /__nuxt_error" }); } if (import.meta.prerender && prerenderRenderingURLs) { const renderingURL = event.path; const stack = prerenderRenderingURLs.getStore(); if (stack?.includes(renderingURL)) { const chain = [...stack, renderingURL].filter((url) => !url.startsWith("/__nuxt_error")).map((url) => `"${url}"`).join(" -> "); throw createError({ status: 508, statusText: `Loop detected while prerendering "${renderingURL}" (${chain}). Check for \`useFetch\`/\`$fetch\` calls targeting a URL that is currently being rendered.` }); } return prerenderRenderingURLs.run([...stack || [], renderingURL], () => renderRoute(event, ssrError)); } return renderRoute(event, ssrError); }); async function renderRoute(event, ssrError) { const nitroApp = useNitroApp(); const ssrContext = createSSRContext(event); const headEntryOptions = { mode: "server" }; ssrContext.head.push(appHead, headEntryOptions); if (ssrError) { const status = ssrError.status || ssrError.statusCode; if (status) { ssrError.status = ssrError.statusCode = Number.parseInt(status); } if (PARSE_ERROR_DATA && typeof ssrError.data === "string") { try { ssrError.data = destr(ssrError.data); } catch {} } setSSRError(ssrContext, ssrError); } const routeOptions = getRouteRules(event); if (routeOptions.ssr === false) { ssrContext.noSSR = true; } const _PAYLOAD_EXTRACTION = !ssrContext.noSSR && (import.meta.prerender && NUXT_PAYLOAD_EXTRACTION || NUXT_RUNTIME_PAYLOAD_EXTRACTION && (routeOptions.isr || routeOptions.cache)); const _PAYLOAD_INLINE = !_PAYLOAD_EXTRACTION || NUXT_PAYLOAD_INLINE; const isRenderingPayload = (_PAYLOAD_EXTRACTION || import.meta.dev && routeOptions.prerender) && 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 (payloadCache && await payloadCache.hasItem(url + ".json")) { return payloadCache.getItem(url + ".json"); } } const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(ssrContext.runtimeConfig.app.cdnURL || ssrContext.runtimeConfig.app.baseURL, ssrContext.url.replace(/\?.*$/, ""), PAYLOAD_FILENAME) + "?" + ssrContext.runtimeConfig.app.buildId : undefined; const renderer = await getRenderer(ssrContext); if (NUXT_EARLY_HINTS && !isRenderingPayload && !import.meta.prerender) { const { link } = renderResourceHeaders({}, renderer.rendererContext); if (link) { writeEarlyHints(event, link); } } if (NUXT_INLINE_STYLES) { for (const id of entryIds) { ssrContext.modules.add(id); } } const _rendered = await renderer.renderToString(ssrContext).catch(async (error) => { if ((ssrContext["~renderResponse"] || 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 = NUXT_INLINE_STYLES && !ssrContext["~renderResponse"] && !ssrContext._renderResponse && !isRenderingPayload ? await renderInlineStyles(ssrContext.modules ?? []) : []; await ssrContext.nuxt?.hooks.callHook("app:rendered", { ssrContext, renderResult: _rendered }); if (ssrContext["~renderResponse"] || ssrContext._renderResponse) { return ssrContext["~renderResponse"] || ssrContext._renderResponse; } if (ssrContext.payload?.error && !ssrError) { throw ssrContext.payload.error; } if (isRenderingPayload) { const response = renderPayloadResponse(ssrContext); if (payloadCache) { await payloadCache.setItem(ssrContext.url + ".json", response); } return response; } if (_PAYLOAD_EXTRACTION) { if (import.meta.prerender) { appendResponseHeader(event, "x-nitro-prerender", joinURL(ssrContext.url.replace(/\?.*$/, ""), PAYLOAD_FILENAME)); } if (payloadCache) { await payloadCache.setItem((ssrContext.url === "/" ? "/" : ssrContext.url.replace(/\/$/, "")) + ".json", renderPayloadResponse(ssrContext)); } } const NO_SCRIPTS = 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 && !_PAYLOAD_INLINE && !NO_SCRIPTS) { ssrContext.head.push({ link: [NUXT_JSON_PAYLOADS ? { rel: "preload", as: "fetch", crossorigin: "anonymous", href: payloadURL } : { rel: "modulepreload", crossorigin: "", href: payloadURL }] }, headEntryOptions); } 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) { if (ssrContext["~lazyHydratedModules"]) { for (const id of ssrContext["~lazyHydratedModules"]) { ssrContext.modules?.delete(id); } } ssrContext.head.push({ link: getPreloadLinks(ssrContext, renderer.rendererContext) }, headEntryOptions); ssrContext.head.push({ link: getPrefetchLinks(ssrContext, renderer.rendererContext) }, headEntryOptions); ssrContext.head.push({ script: _PAYLOAD_INLINE ? NUXT_JSON_PAYLOADS ? renderPayloadJsonScript({ ssrContext, data: ssrContext.payload }) : renderPayloadScript({ ssrContext, data: ssrContext.payload, routeOptions }) : NUXT_JSON_PAYLOADS ? renderPayloadJsonScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL }) : renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, routeOptions, src: payloadURL }) }, { ...headEntryOptions, tagPosition: "bodyClose", tagPriority: "high" }); } if (!routeOptions.noScripts) { const tagPosition = _PAYLOAD_EXTRACTION && !_PAYLOAD_INLINE && !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, 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" } }; } export default handler; 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)}` + ""; }