UNPKG

nuxt

Version:

[![Nuxt banner](./.github/assets/banner.png)](https://nuxt.com)

364 lines (363 loc) 15.5 kB
import { createRenderer, renderResourceHeaders } from "vue-bundle-renderer/runtime"; import { appendResponseHeader, createError, getQuery, readBody, writeEarlyHints } from "h3"; import devalue from "@nuxt/devalue"; import { stringify, uneval } from "devalue"; import destr from "destr"; import { joinURL, withoutTrailingSlash } from "ufo"; import { renderToString as _renderToString } from "vue/server-renderer"; import { hash } from "ohash"; import { defineRenderHandler, getRouteRules, useRuntimeConfig } from "#internal/nitro"; import { useNitroApp } from "#internal/nitro/app"; import { appRootId, appRootTag } from "#internal/nuxt.config.mjs"; import { buildAssetsURL, publicAssetsURL } from "#paths"; globalThis.__buildAssetsURL = buildAssetsURL; globalThis.__publicAssetsURL = publicAssetsURL; const getClientManifest = () => import("#build/dist/server/client.manifest.mjs").then((r) => r.default || r).then((r) => typeof r === "function" ? r() : r); const getEntryId = () => getClientManifest().then((r) => Object.values(r).find((r2) => r2.isEntry).src); const getStaticRenderedHead = () => import("#head-static").then((r) => r.default || r); const getServerEntry = () => import("#build/dist/server/server.mjs").then((r) => r.default || r); const getSSRStyles = lazyCachedFunction(() => import("#build/dist/server/styles.mjs").then((r) => r.default || r)); const getSSRRenderer = lazyCachedFunction(async () => { const manifest = await getClientManifest(); if (!manifest) { throw new Error("client.manifest is not available"); } const createSSRApp = await getServerEntry(); if (!createSSRApp) { throw new Error("Server bundle is not available"); } const options = { manifest, renderToString, buildAssetsURL }; const renderer = createRenderer(createSSRApp, options); async function renderToString(input, context) { const html = await _renderToString(input, context); if (process.dev && process.env.NUXT_VITE_NODE_OPTIONS) { renderer.rendererContext.updateManifest(await getClientManifest()); } return `<${appRootTag} id="${appRootId}">${html}</${appRootTag}>`; } return renderer; }); const getSPARenderer = lazyCachedFunction(async () => { const manifest = await getClientManifest(); const spaTemplate = await import("#spa-template").then((r) => r.template).catch(() => ""); const options = { manifest, renderToString: () => `<${appRootTag} id="${appRootId}">${spaTemplate}</${appRootTag}>`, buildAssetsURL }; const renderer = createRenderer(() => () => { }, options); const result = await renderer.renderToString({}); const renderToString = (ssrContext) => { const config = useRuntimeConfig(); ssrContext.payload = { path: ssrContext.url, _errors: {}, serverRendered: false, data: {}, state: {} }; ssrContext.config = { public: config.public, app: config.app }; ssrContext.renderMeta = ssrContext.renderMeta ?? getStaticRenderedHead; return Promise.resolve(result); }; return { rendererContext: renderer.rendererContext, renderToString }; }); async function getIslandContext(event) { const url = event.node.req.url?.substring("/__nuxt_island".length + 1) || ""; const [componentName, hashId] = url.split("?")[0].split("_"); const context = event.node.req.method === "GET" ? getQuery(event) : await readBody(event); const ctx = { url: "/", ...context, id: hashId, name: componentName, props: destr(context.props) || {}, uid: destr(context.uid) || void 0 }; return ctx; } const PAYLOAD_CACHE = process.env.NUXT_PAYLOAD_EXTRACTION && process.env.prerender ? /* @__PURE__ */ new Map() : null; const ISLAND_CACHE = process.env.NUXT_COMPONENT_ISLANDS && process.env.prerender ? /* @__PURE__ */ new Map() : null; const PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /\/_payload(\.[a-zA-Z0-9]+)?.json(\?.*)?$/ : /\/_payload(\.[a-zA-Z0-9]+)?.js(\?.*)?$/; const ROOT_NODE_REGEX = new RegExp(`^<${appRootTag} id="${appRootId}">([\\s\\S]*)</${appRootTag}>$`); const PRERENDER_NO_SSR_ROUTES = /* @__PURE__ */ new Set(["/index.html", "/200.html", "/404.html"]); export default defineRenderHandler(async (event) => { const nitroApp = useNitroApp(); const ssrError = event.node.req.url?.startsWith("/__nuxt_error") ? getQuery(event) : null; if (ssrError && ssrError.statusCode) { ssrError.statusCode = parseInt(ssrError.statusCode); } if (ssrError && event.node.req.socket.readyState !== "readOnly") { throw createError({ statusCode: 404, statusMessage: "Page Not Found: /__nuxt_error" }); } const islandContext = process.env.NUXT_COMPONENT_ISLANDS && event.node.req.url?.startsWith("/__nuxt_island") ? await getIslandContext(event) : void 0; if (process.env.prerender && islandContext && ISLAND_CACHE.has(event.node.req.url)) { return ISLAND_CACHE.get(event.node.req.url); } let url = ssrError?.url || islandContext?.url || event.node.req.url; const isRenderingPayload = PAYLOAD_URL_RE.test(url) && !islandContext; if (isRenderingPayload) { url = url.substring(0, url.lastIndexOf("/")) || "/"; event.node.req.url = url; if (process.env.prerender && PAYLOAD_CACHE.has(url)) { return PAYLOAD_CACHE.get(url); } } const routeOptions = getRouteRules(event); const ssrContext = { url, event, runtimeConfig: useRuntimeConfig(), noSSR: !!process.env.NUXT_NO_SSR || event.context.nuxt?.noSSR || routeOptions.ssr === false || (process.env.prerender ? PRERENDER_NO_SSR_ROUTES.has(url) : false), error: !!ssrError, nuxt: void 0, /* NuxtApp */ payload: ssrError ? { error: ssrError } : {}, _payloadReducers: {}, islandContext }; const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !ssrContext.noSSR && !islandContext; const payloadURL = _PAYLOAD_EXTRACTION ? joinURL(useRuntimeConfig().app.baseURL, url, process.env.NUXT_JSON_PAYLOADS ? "_payload.json" : "_payload.js") : void 0; if (process.env.prerender) { ssrContext.payload.prerenderedAt = Date.now(); } const renderer = process.env.NUXT_NO_SSR || ssrContext.noSSR ? await getSPARenderer() : await getSSRRenderer(); if (process.env.NUXT_EARLY_HINTS && !isRenderingPayload && !process.env.prerender) { const { link } = renderResourceHeaders({}, renderer.rendererContext); writeEarlyHints(event, link); } 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; }); 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 response2 = renderPayloadResponse(ssrContext); if (process.env.prerender) { PAYLOAD_CACHE.set(url, response2); } return response2; } if (_PAYLOAD_EXTRACTION) { appendResponseHeader(event, "x-nitro-prerender", joinURL(url, process.env.NUXT_JSON_PAYLOADS ? "_payload.json" : "_payload.js")); PAYLOAD_CACHE.set(withoutTrailingSlash(url), renderPayloadResponse(ssrContext)); } const renderedMeta = await ssrContext.renderMeta?.() ?? {}; if (process.env.NUXT_INLINE_STYLES && !islandContext) { const entryId = await getEntryId(); if (ssrContext.modules) { ssrContext.modules.add(entryId); } else if (ssrContext._registeredComponents) { ssrContext._registeredComponents.add(entryId); } } const inlinedStyles = process.env.NUXT_INLINE_STYLES || Boolean(islandContext) ? await renderInlineStyles(ssrContext.modules ?? ssrContext._registeredComponents ?? []) : ""; const NO_SCRIPTS = process.env.NUXT_NO_SCRIPTS || routeOptions.experimentalNoScripts; const htmlContext = { island: Boolean(islandContext), htmlAttrs: normalizeChunks([renderedMeta.htmlAttrs]), head: normalizeChunks([ renderedMeta.headTags, process.env.NUXT_JSON_PAYLOADS ? _PAYLOAD_EXTRACTION ? `<link rel="preload" as="fetch" crossorigin="anonymous" href="${payloadURL}">` : null : _PAYLOAD_EXTRACTION ? `<link rel="modulepreload" href="${payloadURL}">` : null, NO_SCRIPTS ? null : _rendered.renderResourceHints(), _rendered.renderStyles(), inlinedStyles, ssrContext.styles ]), bodyAttrs: normalizeChunks([renderedMeta.bodyAttrs]), bodyPrepend: normalizeChunks([ renderedMeta.bodyScriptsPrepend, ssrContext.teleports?.body ]), body: [process.env.NUXT_COMPONENT_ISLANDS ? replaceServerOnlyComponentsSlots(ssrContext, _rendered.html) : _rendered.html], bodyAppend: normalizeChunks([ NO_SCRIPTS ? void 0 : _PAYLOAD_EXTRACTION ? process.env.NUXT_JSON_PAYLOADS ? renderPayloadJsonScript({ id: "__NUXT_DATA__", ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL }) : renderPayloadScript({ ssrContext, data: splitPayload(ssrContext).initial, src: payloadURL }) : process.env.NUXT_JSON_PAYLOADS ? renderPayloadJsonScript({ id: "__NUXT_DATA__", ssrContext, data: ssrContext.payload }) : renderPayloadScript({ ssrContext, data: ssrContext.payload }), routeOptions.experimentalNoScripts ? void 0 : _rendered.renderScripts(), // Note: bodyScripts may contain tags other than <script> renderedMeta.bodyScripts ]) }; await nitroApp.hooks.callHook("render:html", htmlContext, { event }); if (process.env.NUXT_COMPONENT_ISLANDS && islandContext) { const _tags = htmlContext.head.flatMap((head2) => extractHTMLTags(head2)); const head = { link: _tags.filter((tag) => tag.tagName === "link" && tag.attrs.rel === "stylesheet" && tag.attrs.href.includes("scoped") && !tag.attrs.href.includes("pages/")).map((tag) => ({ key: "island-link-" + hash(tag.attrs.href), ...tag.attrs })), style: _tags.filter((tag) => tag.tagName === "style" && tag.innerHTML).map((tag) => ({ key: "island-style-" + hash(tag.innerHTML), innerHTML: tag.innerHTML })) }; const islandResponse = { id: islandContext.id, head, html: getServerComponentHTML(htmlContext.body), state: ssrContext.payload.state }; await nitroApp.hooks.callHook("render:island", islandResponse, { event, islandContext }); const response2 = { body: JSON.stringify(islandResponse, null, 2), statusCode: event.node.res.statusCode, statusMessage: event.node.res.statusMessage, headers: { "content-type": "application/json;charset=utf-8", "x-powered-by": "Nuxt" } }; if (process.env.prerender) { ISLAND_CACHE.set(`/__nuxt_island/${islandContext.name}_${islandContext.id}`, response2); } return response2; } const response = { body: renderHTMLDocument(htmlContext), statusCode: event.node.res.statusCode, statusMessage: event.node.res.statusMessage, headers: { "content-type": "text/html;charset=utf-8", "x-powered-by": "Nuxt" } }; return response; }); function lazyCachedFunction(fn) { let res = null; return () => { if (res === null) { res = fn().catch((err) => { res = null; throw err; }); } return res; }; } function normalizeChunks(chunks) { return chunks.filter(Boolean).map((i) => i.trim()); } function joinTags(tags) { return tags.join(""); } function joinAttrs(chunks) { return chunks.join(" "); } function renderHTMLDocument(html) { return `<!DOCTYPE html> <html ${joinAttrs(html.htmlAttrs)}> <head>${joinTags(html.head)}</head> <body ${joinAttrs(html.bodyAttrs)}>${joinTags(html.bodyPrepend)}${joinTags(html.body)}${joinTags(html.bodyAppend)}</body> </html>`; } const HTML_TAG_RE = /<(?<tag>[a-z]+)(?<rawAttrs> [^>]*)?>(?:(?<innerHTML>[\s\S]*?)<\/\k<tag>)?/g; const HTML_TAG_ATTR_RE = /(?<name>[a-z]+)="(?<value>[^"]*)"/g; function extractHTMLTags(html) { const tags = []; for (const tagMatch of html.matchAll(HTML_TAG_RE)) { const attrs = {}; for (const attrMatch of tagMatch.groups.rawAttrs?.matchAll(HTML_TAG_ATTR_RE) || []) { attrs[attrMatch.groups.name] = attrMatch.groups.value; } const innerHTML = tagMatch.groups.innerHTML || ""; tags.push({ tagName: tagMatch.groups.tag, attrs, innerHTML }); } return tags; } async function renderInlineStyles(usedModules) { const styleMap = await getSSRStyles(); const inlinedStyles = /* @__PURE__ */ new Set(); for (const mod of usedModules) { if (mod in styleMap) { for (const style of await styleMap[mod]()) { inlinedStyles.add(`<style>${style}</style>`); } } } return Array.from(inlinedStyles).join(""); } function renderPayloadResponse(ssrContext) { return { body: process.env.NUXT_JSON_PAYLOADS ? stringify(splitPayload(ssrContext).payload, ssrContext._payloadReducers) : `export default ${devalue(splitPayload(ssrContext).payload)}`, statusCode: ssrContext.event.node.res.statusCode, statusMessage: ssrContext.event.node.res.statusMessage, headers: { "content-type": process.env.NUXT_JSON_PAYLOADS ? "application/json;charset=utf-8" : "text/javascript;charset=utf-8", "x-powered-by": "Nuxt" } }; } function renderPayloadJsonScript(opts) { const attrs = [ 'type="application/json"', `id="${opts.id}"`, `data-ssr="${!(process.env.NUXT_NO_SSR || opts.ssrContext.noSSR)}"`, opts.src ? `data-src="${opts.src}"` : "" ].filter(Boolean); const contents = opts.data ? stringify(opts.data, opts.ssrContext._payloadReducers) : ""; return `<script ${attrs.join(" ")}>${contents}<\/script><script>window.__NUXT__={};window.__NUXT__.config=${uneval(opts.ssrContext.config)}<\/script>`; } function renderPayloadScript(opts) { opts.data.config = opts.ssrContext.config; const _PAYLOAD_EXTRACTION = process.env.prerender && process.env.NUXT_PAYLOAD_EXTRACTION && !opts.ssrContext.noSSR; if (_PAYLOAD_EXTRACTION) { return `<script type="module">import p from "${opts.src}";window.__NUXT__={...p,...(${devalue(opts.data)})}<\/script>`; } return `<script>window.__NUXT__=${devalue(opts.data)}<\/script>`; } function splitPayload(ssrContext) { const { data, prerenderedAt, ...initial } = ssrContext.payload; return { initial: { ...initial, prerenderedAt }, payload: { data, prerenderedAt } }; } function getServerComponentHTML(body) { const match = body[0].match(ROOT_NODE_REGEX); return match ? match[1] : body[0]; } const SSR_TELEPORT_MARKER = /^uid=([^;]*);slot=(.*)$/; function replaceServerOnlyComponentsSlots(ssrContext, html) { const { teleports, islandContext } = ssrContext; if (islandContext || !teleports) { return html; } for (const key in teleports) { const match = key.match(SSR_TELEPORT_MARKER); if (!match) { continue; } const [, uid, slot] = match; if (!uid || !slot) { continue; } html = html.replace(new RegExp(`<div nuxt-ssr-component-uid="${uid}"[^>]*>((?!nuxt-ssr-slot-name="${slot}"|nuxt-ssr-component-uid)[\\s\\S])*<div [^>]*nuxt-ssr-slot-name="${slot}"[^>]*>`), (full) => { return full + teleports[key]; }); } return html; }