nuxt
Version:
[](https://nuxt.com)
364 lines (363 loc) • 15.5 kB
JavaScript
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;
}