import { existsSync } from "node:fs"; import { dirname, join, relative, resolve } from "pathe"; import { addPlugin, addTemplate, addTypeTemplate, addVitePlugin, getLayerDirectories, logger, resolvePath, tryUseNitro } from "@nuxt/kit"; import { bundlerDiagnostics, getServerRuntime, setServerBuild, useServerBuild } from "@nuxt/kit/internal"; import { defu } from "defu"; import { resolveModulePath } from "exsolve"; import { joinRelativeURL, joinURL, parseURL, withBase, withTrailingSlash, withoutBase } from "ufo"; import { fileURLToPath, pathToFileURL } from "node:url"; import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { link } from "clickable-path"; import { cpus } from "node:os"; import { parse, walk } from "ultrahtml"; import { addRoute, createRouter, findAllRoutes } from "rou3"; import { compileRouterToString } from "rou3/compiler"; import { NodeRequest, sendNodeResponse } from "srvx/node"; import { staticMiddleware } from "srvx/static"; import process from "node:process"; import { ERROR_CHANNEL_ENV, clearErrorReport, createDevErrorReporter, createErrorReport, setErrorChannelForwarding } from "nuxt/internal/dev-error"; //#region src/dirs.ts let _distDir = dirname(fileURLToPath(import.meta.url)); if (/(?:chunks|shared)$/.test(_distDir)) _distDir = dirname(_distDir); const distDir = _distDir; //#endregion //#region src/templates/spa-loading-icon.ts const template = () => { return ""; }; //#endregion //#region src/output.ts /** Static hosts commonly map unknown paths to one of these files. */ const SPA_FALLBACK_FILES = [ "index.html", "200.html", "404.html" ]; async function writeStaticOutput(nuxt, publicDir, options = {}) { const document = resolve(publicDir, "index.html"); if (!existsSync(document)) throw new Error(`[nuxt:vite-server] Expected \`${document}\` to exist. Did the client build run?`); const html = await readFile(document, "utf-8"); await rm(resolve(publicDir, "manifest.json"), { force: true }); if (options.ssr) await rm(document, { force: true }); for (const dirs of getLayerDirectories(nuxt)) if (existsSync(dirs.public)) await cp(dirs.public, publicDir, { recursive: true }); if (options.ssr) { if (!options.prerender) logger.success(`Server output written to ${link(resolve(publicDir, ".."))}`); return; } for (const file of SPA_FALLBACK_FILES) await writeFile(join(publicDir, file), html, "utf-8"); logger.success(`Static SPA output written to ${link(publicDir)}`); } /** Drop the bundle the crawl rendered with, leaving the static site as the whole output. */ async function finishStaticOutput(outputDir, publicDir) { await rm(join(outputDir, "server"), { recursive: true, force: true }); logger.success(`Prerendered output written to ${link(publicDir)}`); } async function spaLoadingTemplate(nuxt) { if (nuxt.options.spaLoadingTemplate === false) return ""; const candidates = typeof nuxt.options.spaLoadingTemplate === "string" ? [resolve(nuxt.options.srcDir, nuxt.options.spaLoadingTemplate)] : nuxt.options._layers.map((layer) => resolve(layer.config.srcDir, layer.config.dir?.app || "app", "spa-loading-template.html")); for (const candidate of candidates) if (existsSync(candidate)) return (await readFile(candidate, "utf-8")).trim(); if (nuxt.options.spaLoadingTemplate === true) return template(); if (nuxt.options.spaLoadingTemplate) bundlerDiagnostics.NUXT_B7016({ path: nuxt.options.spaLoadingTemplate }); return ""; } function publicDirs(nuxt) { return getLayerDirectories(nuxt).map((dirs) => withTrailingSlash(dirs.public)).filter((dir) => existsSync(dir)); } //#endregion //#region ../nuxt/src/core/utils/index.ts /** * Decode a route path for route-rule matching, returning it unchanged if it is not * percent-encoded or cannot be decoded. Reserved characters (`%2F`, `%3F`, …) are * left encoded, so decoding cannot introduce additional path segments. Only the path * portion is decoded; a query string, if present, is left untouched to avoid * double-decoding it. */ function decodeRoutePath(path) { if (!path.includes("%")) return path; const queryIndex = path.indexOf("?"); const pathname = queryIndex === -1 ? path : path.slice(0, queryIndex); try { return queryIndex === -1 ? decodeURI(pathname) : decodeURI(pathname) + path.slice(queryIndex); } catch { return path; } } //#endregion //#region ../nuxt/src/core/utils/route-rules.ts /** * Normalise a route-rule key or lookup path: decode percent-encoding, then case-fold when * `fold` is set. Keys are matched in decoded form so that a rule may be authored either * decoded (`/测试`, the usual case) or encoded (`/%E6%B5%8B%E8%AF%95`, the form the page route * and the request path take). Decoding must precede case folding, or a percent-encoded * non-ASCII character would never fold. * * @internal */ function normalizeRouteRulePath(path, fold) { const decoded = decodeRoutePath(path); return fold ? decoded.toLowerCase() : decoded; } /** * Expand the rule shorthands a server runtime would expand: `redirect` as a string, and * `swr`/`cache` as the single `cache` rule everything downstream reads. * * @internal */ function normalizeRouteRules(config) { const normalized = {}; for (const key in config) { const { redirect, swr, cache, ...rules } = config[key] || {}; if (redirect) { const options = { to: "/", status: 307, ...typeof redirect === "string" ? { to: redirect } : redirect }; if (key.endsWith("/**")) options.base = key.slice(0, -3); rules.redirect = options; } else if (redirect === false) rules.redirect = false; if (swr !== void 0 && swr !== false) rules.cache = { ...cache || void 0, swr: true, ...typeof swr === "number" ? { maxAge: swr } : {} }; else if (swr === false && cache === void 0) rules.cache = false; else if (cache !== void 0) rules.cache = cache === false ? false : cache; normalized[key] = rules; } return normalized; } /** * The route rules the build has resolved, as router entries. * * A server builder resolves its own (a module may push rules through `nitro:config`, and the * server may add rules of its own), so its resolved set is preferred where there is one. * * @internal */ function resolveRouteRulesRoutes(nuxt) { const nitro = tryUseNitro(); const rules = nitro ? nitro.options.routeRules : normalizeRouteRules(nuxt.options.routeRules); const baseURL = (nitro ? nitro.options.baseURL : nuxt.options.app.baseURL) || ""; return { routes: Object.entries(rules).map(([route, data]) => ({ route, method: "", data })), baseURL }; } /** * Build a route-rules router: patterns are registered under `baseURL`, and a lookup resolves * every pattern it matches, least specific first. * * @internal */ function createRouteRulesRouter(routes, baseURL) { const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL; const router = createRouter(); for (const route of routes) addRoute(router, route.method || "", base + route.route, route.data); return { routes, matchAll: (method, path) => findAllRoutes(router, method, path).map((route) => route.data), compileToString(options) { if (base || routes.length !== 1 || routes[0].route !== "/**" || !!routes[0].method) return compileRouterToString(router, void 0, options); const data = (options?.serialize || JSON.stringify)(routes[0].data); const match = `{data,params:{"_":p.slice(1)}}`; return `/* @__PURE__ */ (() => {const data=${data};return ((_m, p)=>(${options?.matchAll ? `[${match}]` : match}));})()`; } }; } /** * Source of the `normalizePath` helper a compiled matcher looks rules up through: `decodeRoutePath` * is inlined by source so that the runtime lookup and the build-time key normalisation cannot * drift apart. * * @internal */ const normalizePathCode = [ `const decodeRoutePath = ${decodeRoutePath.toString()}`, `const normalizePath = (path, fold) => {`, ` if (typeof path !== 'string') { return path }`, ` const decoded = decodeRoutePath(path)`, ` return fold ? decoded.toLowerCase() : decoded`, `}` ].join("\n"); /** * Build a route-rules router whose keys are decoded (and, when `fold` is set, lower-cased), * mirroring the normalised lookup path used at runtime. * * `onDuplicate` is called when two distinct keys normalise onto the same path; the later * key wins. * * @internal */ function createNormalizedRouteRulesRouter(routes, baseURL, fold, onDuplicate) { const normalizedKeys = /* @__PURE__ */ new Map(); return createRouteRulesRouter(routes.map((route) => { if (typeof route.route !== "string") return route; const key = normalizeRouteRulePath(route.route, fold); const existing = normalizedKeys.get(key); if (existing !== void 0 && existing !== route.route) onDuplicate?.(existing, route.route, key); normalizedKeys.set(key, route.route); return { ...route, route: key }; }), normalizeRouteRulePath(String(baseURL || ""), fold)); } //#endregion //#region src/route-rules.ts /** * The rules this builder resolves for itself: the renderer reads `ssr`, `streaming`, * `noScripts` and `prerender`, and the fetch handler answers `redirect` and sets `headers`. * Caching, proxying and CORS belong to an HTTP layer, which this builder does not provide. */ const SUPPORTED_RULES = /* @__PURE__ */ new Set([ "ssr", "streaming", "noScripts", "prerender", "redirect", "headers" ]); const defuPath = resolveModulePath("defu", { try: true, from: import.meta.url }) ?? "defu"; /** * Build-time matcher over the resolved route rules, looked up by a path without the base URL: * the routes the build reasons about (page routes, prerender seeds) are written that way. */ function createRouteRulesMatcher(nuxt) { const fold = !nuxt.options.router.options.sensitive; const router = createNormalizedRouteRulesRouter(resolveRouteRulesRoutes(nuxt).routes, "", fold); return (path) => defu({}, ...router.matchAll("", normalizeRouteRulePath(path, fold)).reverse()); } /** * Emit the matcher the server bundle resolves rules through, so that a render and the * handler in front of it read the same rules the build compiled. */ function addRouteRulesTemplate() { const { dst } = addTemplate({ filename: "vite-server/route-rules.mjs", write: true, getContents: ({ nuxt }) => { const fold = !nuxt.options.router.options.sensitive; const { routes, baseURL } = resolveRouteRulesRoutes(nuxt); const matcher = createNormalizedRouteRulesRouter(routes, baseURL, fold).compileToString({ matchAll: true, serialize: (rules) => `{${Object.entries(rules).filter(([name, value]) => value !== void 0 && SUPPORTED_RULES.has(name)).map(([name, value]) => `${name}: ${JSON.stringify(value)}`).join(",")}}` }); return [ `import { defu } from ${JSON.stringify(defuPath)}`, `const matcher = ${matcher}`, normalizePathCode, `export default path => defu({}, ...matcher('', normalizePath(path, ${fold})).map(r => r.data).reverse())` ].join("\n"); } }); return dst; } //#endregion //#region src/prerender.ts /** Header the renderer reports the routes a render asked to prerender as well on. */ const PRERENDER_HINTS_HEADER = "x-nuxt-prerender"; const CRAWLABLE_EXTENSIONS = /* @__PURE__ */ new Set(["", ".json"]); const EXTENSION_RE = /\.[\da-z]+$/; const JSON_SIGNATURE_RE = /^\s*["[{]|^\s*-?\d{1,16}(?:\.\d{1,17})?(?:e[+-]?\d+)?\s*$/i; const HTML_ENTITIES = { "<": "<", ">": ">", "&": "&", "'": "'", """: "\"" }; const HTML_ENTITY_RE = /&(?:lt|gt|amp|apos|quot);/g; const REDIRECT_STATUSES = /* @__PURE__ */ new Set([ 301, 302, 303, 307, 308 ]); const WRITABLE_STATUSES = /* @__PURE__ */ new Set([ 200, 304, ...REDIRECT_STATUSES ]); /** Longest path segment most file systems accept. */ const FS_MAX_SEGMENT = 255; /** Whether the build produces a prerendered site rather than a server. */ function isPrerendering(nuxt) { return !nuxt.options.dev && nuxt.options.ssr !== false && !!nuxt.options.nitro.static; } function resolvePrerenderConfig(nuxt) { const config = nuxt.options.nitro.prerender || {}; const shared = nuxt.options.prerender || {}; return { routes: (shared.routes || []).filter(Boolean), ignore: shared.ignore || [], crawlLinks: shared.crawlLinks ?? true, autoSubfolderIndex: config.autoSubfolderIndex ?? true, concurrency: config.concurrency || cpus().length * 4 || 4, interval: config.interval || 0, failOnError: config.failOnError ?? true }; } /** * Crawl the built application and write a static copy of every route it reaches. * * The handler is a web-standard `fetch`, so it is driven in-process: there is no port to * allocate and no process to wait on. Routes a render reports while it runs are queued as * they arrive, so one pass reaches the whole reachable app. */ async function prerenderRoutes(nuxt, options) { const config = resolvePrerenderConfig(nuxt); const baseURL = nuxt.options.app.baseURL || "/"; const queue = /* @__PURE__ */ new Set(); for (const route of seedRoutes(nuxt, config)) queue.add(route); await nuxt.callHook("prerender:routes", { routes: queue }); const { fetch } = await import(pathToFileURL(options.handler).href); const canPrerender = createPrerenderFilter(nuxt, config); const generated = /* @__PURE__ */ new Set(); const failed = []; const written = []; const linkParents = /* @__PURE__ */ new Map(); async function generateRoute(route) { route = decodeURI(route); if (generated.has(route) || !canPrerender(route)) return; generated.add(route); const request = new Request(new URL(withBase(encodeURI(route), baseURL), "http://localhost")); const response = await fetch(request); const body = Buffer.from(await response.arrayBuffer()); const entry = { route }; if (!WRITABLE_STATUSES.has(response.status)) { entry.error = { status: response.status, statusText: response.statusText }; failed.push(entry); logRoute(entry, linkParents); return; } const contentType = response.headers.get("content-type") || ""; const isRedirect = REDIRECT_STATUSES.has(response.status); const isImplicitHTML = !route.endsWith(".html") && (isRedirect || contentType.includes("html")) && !JSON_SIGNATURE_RE.test(body.subarray(0, 32).toString("utf-8")); const htmlPath = route.endsWith("/") || config.autoSubfolderIndex ? joinURL(route, "index.html") : route + ".html"; const fileName = withoutBase(isImplicitHTML ? htmlPath : route.endsWith("/") ? route + "index" : route, baseURL); if (canWriteToDisk(route, fileName)) { entry.fileName = fileName; const path = join(options.publicDir, fileName); await mkdir(dirname(path), { recursive: true }); await writeFile(path, body); written.push(entry); } logRoute(entry, linkParents); if (isImplicitHTML || route.endsWith(".html")) for (const link of await extractLinks(body.toString("utf-8"), route, response, config.crawlLinks)) { const parents = linkParents.get(link); if (parents) parents.add(route); else linkParents.set(link, /* @__PURE__ */ new Set([route])); if (!link.includes("?") && canPrerender(link) && !generated.has(link)) queue.add(link); } } const start = Date.now(); logger.info(config.crawlLinks ? `Prerendering ${queue.size} initial routes with crawler` : `Prerendering ${queue.size} routes`); await runParallel(queue, generateRoute, config, (route, error) => { const entry = { route, error: { status: 500, statusText: error instanceof Error ? error.message : String(error) } }; failed.push(entry); logRoute(entry, linkParents); }); if (failed.length && config.failOnError) throw new Error(`[nuxt:vite-server] Exiting due to prerender errors: ${failed.map((entry) => `"${entry.route}" (${entry.error.status})`).join(", ")}.`); logger.info(`Prerendered ${written.length} routes in ${(Date.now() - start) / 1e3} seconds`); return written.map((entry) => entry.route); } /** * The routes the crawl starts from. A dynamic page route or a wildcard rule key cannot be * turned back into a path, so reaching those is left to the hints and the crawler. */ function seedRoutes(nuxt, config) { const routes = new Set(config.routes); const errorPageOption = nuxt.options.experimental.prerenderErrorPages; const errorPages = errorPageOption === true ? [404] : errorPageOption || []; for (const route of ["/200.html", "/404.html"]) routes.add(route); for (const status of errorPages) routes.add(`/${status}.html`); const rules = nuxt.options.routeRules || {}; for (const path in rules) if (rules[path]?.prerender && !path.includes("*")) routes.add(path); const pages = nuxt.apps.default?.pages; if (pages && !nuxt.options.router.options.hashMode) { const matcher = createRouteRulesMatcher(nuxt); for (const route of staticPageRoutes(pages)) if (config.crawlLinks || matcher(route).prerender) routes.add(route); } if (!routes.size && config.crawlLinks) routes.add("/"); return routes; } const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/; function staticPageRoutes(pages, currentPath = "/", routes = /* @__PURE__ */ new Set()) { for (const page of pages) { if (page._sync) continue; if (OPTIONAL_PARAM_RE.test(page.path) && !page.children?.length) routes.add(currentPath); if (page.path.includes(":")) continue; const route = joinURL(currentPath, page.path); routes.add(route); if (page.children) staticPageRoutes(page.children, route, routes); } return routes; } function createPrerenderFilter(nuxt, config) { const baseURL = nuxt.options.app.baseURL || "/"; const matcher = createRouteRulesMatcher(nuxt); return function canPrerender(route) { for (const pattern of config.ignore) if (typeof pattern === "string" ? route.startsWith(pattern) : pattern instanceof RegExp ? matchesPattern(pattern, route) : pattern(route) === true) return false; return matcher(withoutBase(route, baseURL)).prerender !== false; }; } function matchesPattern(pattern, route) { pattern.lastIndex = 0; return pattern.test(route); } function canWriteToDisk(route, fileName) { if (route.includes("?") || route.includes("..")) return false; if (fileName.split("/").some((segment) => segment.length > FS_MAX_SEGMENT)) { logger.warn(`Skipping prerender of "${route}": a path segment exceeds the ${FS_MAX_SEGMENT}-character limit.`); return false; } return true; } async function extractLinks(html, from, response, crawlLinks) { const candidates = []; if (crawlLinks) await walk(parse(html), (node) => { const href = node.attributes?.href; if (!href) return; const link = href.replace(HTML_ENTITY_RE, (entity) => HTML_ENTITIES[entity] || entity); const decoded = decode(link); if (decoded !== void 0 && !decoded.startsWith("#") && CRAWLABLE_EXTENSIONS.has(parseURL(link).pathname.match(EXTENSION_RE)?.[0] || "")) candidates.push(link); }); for (const hint of (response.headers.get(PRERENDER_HINTS_HEADER) || "").split(",")) { const decoded = hint.trim() && decode(hint.trim()); if (decoded) candidates.push(decoded); } const links = []; for (const candidate of candidates) { const url = parseURL(candidate); if (url.protocol || url.host) continue; if (!url.pathname.startsWith("/")) url.pathname = new URL(url.pathname, new URL(from, "http://localhost")).pathname; links.push(url.pathname + url.search); } return links; } /** Percent-decode a link, or report it as one to leave alone. */ function decode(value) { try { return decodeURIComponent(value); } catch { return; } } function logRoute(route, linkParents) { if (!route.error) { logger.log(` ├─ ${route.route}${route.fileName ? "" : " (skipped)"}`); return; } const parents = linkParents.get(route.route); logger.log([` ├─ ${route.route} (${route.error.status} ${route.error.statusText})`, ...[...parents || []].map((parent) => ` │ └── Linked from ${parent}`)].join("\n")); } /** Drain a set the tasks themselves add to, running at most `concurrency` at a time. */ async function runParallel(queue, task, options, onError) { const running = /* @__PURE__ */ new Set(); function next() { const route = queue.values().next().value; if (!route) return; queue.delete(route); const started = (options.interval ? new Promise((resolve) => setTimeout(resolve, options.interval)) : Promise.resolve()).then(() => task(route)).catch((error) => onError(route, error)); running.add(started); return started.then(async () => { running.delete(started); if (queue.size > 0) await refill(); }); } function refill() { const workers = Math.min(options.concurrency - running.size, queue.size); return Promise.all(Array.from({ length: workers }, () => next())); } await refill(); } /** * Write the app manifest, without which a client-side navigation cannot tell that a route * has a payload file to load, and refetches the data instead. */ async function writeAppManifest(nuxt, publicDir, routes) { const buildId = nuxt.options.runtimeConfig.app.buildId; const timestamp = manifestTimestamp(nuxt); const baseURL = nuxt.options.app.baseURL || "/"; const matcher = createRouteRulesMatcher(nuxt); const prerendered = /* @__PURE__ */ new Set(); for (const route of routes) { if (!route.endsWith(PAYLOAD_SUFFIX)) continue; const path = route.slice(0, -PAYLOAD_SUFFIX.length) || "/"; if (!matcher(withoutBase(path, baseURL)).prerender) prerendered.add(path); } const dir = resolve(publicDir, joinURL(nuxt.options.app.buildAssetsDir, "builds").replace(/^\//, "")); await mkdir(join(dir, "meta"), { recursive: true }); await writeFile(join(dir, "latest.json"), JSON.stringify({ id: buildId, timestamp })); await writeFile(join(dir, `meta/${buildId}.json`), JSON.stringify({ id: buildId, timestamp, prerendered: [...prerendered].sort() })); } const PAYLOAD_SUFFIX = "/_payload.json"; const timestamps = /* @__PURE__ */ new WeakMap(); /** One timestamp per build, shared by the stub the bundle inlines and the file the client fetches. */ function manifestTimestamp(nuxt) { const existing = timestamps.get(nuxt); if (existing !== void 0) return existing; const timestamp = Date.now(); timestamps.set(nuxt, timestamp); return timestamp; } //#endregion //#region src/ssr.ts /** The contract version this builder is written against. */ const SUPPORTED_SERVER_RUNTIME_VERSION = 1; /** * Stand up the SSR renderer for the vite server build: the modules it renders with, the * entry that creates it, and the environment configuration that compiles it. * * The server environment is the app build's own ssr environment, with its input replaced * by the generated entry: the renderer and the SSR app are bundled together, so the app's * entry is reached through the renderer rather than emitted on its own. */ function setupSSR(nuxt, outputDir) { const prerender = isPrerendering(nuxt); const errorPageOption = nuxt.options.experimental.prerenderErrorPages; const errorPages = errorPageOption === true ? [404] : errorPageOption || []; const noSSRRoutes = [ "/index.html", "/200.html", "/404.html" ].filter((route) => !errorPages.includes(Number(route.slice(1, -5)))); const serverRuntime = getServerRuntime({ phase: prerender ? "prerender" : "server", overrides: async () => ({ spaTemplate: JSON.stringify(await spaLoadingTemplate(nuxt)), ...prerender ? { NUXT_PRERENDER_ERROR_PAGES: JSON.stringify(errorPages), NUXT_PRERENDER_NO_SSR_ROUTES: JSON.stringify(noSSRRoutes) } : {} }) }, nuxt); if (serverRuntime.version !== SUPPORTED_SERVER_RUNTIME_VERSION) throw new Error(`[nuxt:vite-server] This builder renders with v${SUPPORTED_SERVER_RUNTIME_VERSION} of the Nuxt server runtime contract, and Nuxt provides v${serverRuntime.version}. Update \`@nuxt/vite-server\`.`); const unsupported = disableUnsupported(nuxt); const serverDir = resolve(outputDir, "server"); const { entry, handler } = addServerEntry(nuxt, serverRuntime, prerender, addRouteRulesTemplate()); nuxt.options.vite.plugins ||= []; nuxt.options.vite.plugins.push(AppServerEnvironmentsPlugin(nuxt), ServerEnvironmentPlugin(nuxt, serverRuntime, entry, serverDir, prerender), BundledVuePlugin(nuxt), ServerRuntimeModulesPlugin(serverRuntime), StaticAssetsURLPlugin(nuxt)); return { entry, handler, unsupported }; } /** * Names every server environment as one that renders the app, so that the app's own * bundler plugins (the virtual file system behind `#build`, and everything else registered * with `addVitePlugin()`) apply there too. This builder brings no server runtime, so a * server environment a deploy target contributes is a build of the app rather than a * service beside it. */ function AppServerEnvironmentsPlugin(nuxt) { return { name: "nuxt:vite-server:app-server-environments", enforce: "pre", configResolved(config) { nuxt._appServerEnvironments ??= /* @__PURE__ */ new Set(); for (const [name, environment] of Object.entries(config.environments)) if (environment.consumer === "server") nuxt._appServerEnvironments.add(name); } }; } /** * The modules the render is reached through. Everything they import is imported by absolute * path, so neither needs an alias of its own to resolve. * * The handler is the render and nothing else, importing no node built-in, so an environment * a deploy target owns can build it for whatever it runs on. The entry is what this * builder's own environment builds: the handler behind a node server that serves the static * output in front of it and listens when it is run as the main module. */ function addServerEntry(nuxt, serverRuntime, prerender, routeRules) { const { dst: handler } = addTemplate({ filename: "vite-server/server-handler.mjs", write: true, getContents: () => [ `import { createNuxtRenderer } from ${JSON.stringify(serverRuntime.entry)}`, `import { useRuntimeConfig } from ${JSON.stringify(resolve(nuxt.options.buildDir, "vite-server/runtime-config.mjs"))}`, `import { createFetchHandler, createRendererOptions } from ${JSON.stringify(resolve(distDir, "runtime/renderer"))}`, `import matchRouteRules from ${JSON.stringify(routeRules)}`, ...prerender ? [`import { createPrerenderOptions } from ${JSON.stringify(resolve(distDir, "runtime/prerender"))}`] : [], "", `const renderer = createNuxtRenderer(createRendererOptions(useRuntimeConfig, matchRouteRules${prerender ? `, createPrerenderOptions({ sharedData: ${!!nuxt.options.experimental.sharedPrerenderData} })` : ""}))`, `export const fetch = createFetchHandler(renderer, matchRouteRules)`, `export default { fetch }` ].join("\n") }); addTemplate({ filename: "vite-server/server.mjs", write: true, getContents: () => [ `import match from ${JSON.stringify(routeRules)}`, `import { createServerHelpers } from ${JSON.stringify(resolve(distDir, "runtime/server"))}`, "", `export * from 'nuxt/internal/server-default'`, "", `const helpers = createServerHelpers(match, () => import(${JSON.stringify(handler)}).then(m => m.fetch), ${JSON.stringify(nuxt.options.app.baseURL || "/")})`, `export const getRouteRules = helpers.getRouteRules`, `export const matchRouteRules = helpers.matchRouteRules`, `export const serverFetch = helpers.serverFetch` ].join("\n") }); const { dst: entry } = addTemplate({ filename: "vite-server/server-entry.mjs", write: true, getContents: () => [ `import { fetch } from ${JSON.stringify(handler)}`, `import { createNodeServer, listen } from ${JSON.stringify(resolve(distDir, "runtime/node"))}`, "", `export { fetch }`, "", ...nuxt.options.dev ? [`export { setDevErrorContext } from ${JSON.stringify(resolve(distDir, "runtime/dev-error"))}`, ""] : [], `const server = import.meta.dev ? undefined : createNodeServer({ fetch, publicDir: new URL('../public/', import.meta.url) })`, `export default server`, "", `if (server && import.meta.main) {`, ` listen(server)`, `}` ].join("\n") }); return { entry, handler }; } /** * Configures the environments that render: the defines the renderer must be compiled with, * everywhere it is compiled, and for this builder's own environment the server entry as * its only input and the output a deployable expects. * * The input is replaced rather than added to: the app entry the app build would give this * environment is reached through the renderer instead, so building it as an entry of its * own would emit a second copy of the app. * * A deploy target that builds its own module as the input keeps it, and reaches the render * through `#server-entry`; the environment then emits the target's artifact. */ function ServerEnvironmentPlugin(nuxt, serverRuntime, entry, serverDir, prerender) { return { name: "nuxt:vite-server:server-environment", applyToEnvironment: (environment) => environment.config.consumer === "server", configEnvironment(name, config) { if (config.consumer === "client") return; config.define = { ...config.define, ...serverRuntime.defines }; if (nuxt.options.dev || name !== "ssr") return; config.build ||= {}; config.build.outDir = serverDir; config.build.emptyOutDir = true; config.build.rolldownOptions ||= {}; const input = config.build.rolldownOptions.input; const named = !!input && typeof input === "object" && !Array.isArray(input) ? input : void 0; if (!!named && !("server" in named) && named.index !== entry) { if (prerender) throw new Error("[nuxt:vite-server] `nuxt generate` cannot prerender through a deploy target: the target owns the server environment and its output is not a node handler. Remove the target, or run `nuxt build`."); useServerBuild(nuxt).runtime.handler = void 0; } else config.build.rolldownOptions.input = { index: entry }; config.build.rolldownOptions.output = { ...defu({ chunkFileNames: "_chunks/[name]-[hash].mjs" }, config.build.rolldownOptions.output), entryFileNames: "index.mjs" }; config.resolve ||= {}; config.resolve.noExternal = true; config.resolve.external = []; config.build.rolldownOptions.external = []; } }; } const ASSETS_URL_RE = /^(?:globalThis\.)?__(build|public)AssetsURL\(("(?:[^"\\]|\\.)*")\)$/; /** * Resolves the asset URLs of the server build at build time. The server reads the `app` * runtime config serialised by the build, so each URL is a constant. */ function StaticAssetsURLPlugin(nuxt) { return { name: "nuxt:vite-server:static-assets-url", apply: "build", config: { order: "post", handler(config) { const renderBuiltUrl = config.experimental?.renderBuiltUrl; if (!renderBuiltUrl) return; config.experimental.renderBuiltUrl = (filename, context) => { const result = renderBuiltUrl(filename, context); const match = context.ssr && result && typeof result === "object" && "runtime" in result && result.runtime?.match(ASSETS_URL_RE); if (!match) return result; const { cdnURL, baseURL, buildAssetsDir } = nuxt.options.runtimeConfig.app; const path = JSON.parse(match[2]); return match[1] === "build" ? joinRelativeURL(cdnURL || baseURL, buildAssetsDir, path) : joinRelativeURL(cdnURL || baseURL, path); }; } } }; } /** * Resolves Vue to its bundler builds for the server bundle. * * Bundling every dependency means resolving Vue ourselves: its `node` export condition is * the CommonJS full build, which carries the template compiler the SSR app never calls * (~700kB of it, between `vue` and `vue/server-renderer`) and cannot be tree-shaken * through cjs interop. The bundler builds are the same runtime without the compiler. */ function BundledVuePlugin(nuxt) { const BUNDLER_BUILDS = { "vue": "vue/dist/vue.runtime.esm-bundler.js", "vue/server-renderer": "@vue/server-renderer/dist/server-renderer.esm-bundler.js" }; return { name: "nuxt:vite-server:bundled-vue", apply: "build", applyToEnvironment: (environment) => environment.config.consumer === "server", resolveId: { order: "pre", filter: { id: /^vue(?:\/server-renderer)?$/ }, handler(id, importer) { if (nuxt.options.vue.runtimeCompiler) return; return this.resolve(BUNDLER_BUILDS[id], importer, { skipSelf: true }); } } }; } /** * Resolves the modules the renderer imports for the environment that renders. * * Every specifier comes from the record, so this plugin never names one: the set is Nuxt's * to change. Each body is read when the environment loads it, which is after the client * environment has built, so the values it derives from the client build are final. */ function ServerRuntimeModulesPlugin(serverRuntime) { const prefix = "\0nuxt-server-runtime:"; return { name: "nuxt:vite-server:server-runtime", applyToEnvironment: (environment) => environment.config.consumer === "server", resolveId: { order: "pre", handler: (id) => id in serverRuntime.modules ? prefix + id : void 0 }, load: { order: "pre", async handler(id) { if (!id.startsWith(prefix)) return; const module = serverRuntime.modules[id.slice(21)]; return module ? { code: await module.code(), map: null } : void 0; } } }; } /** * Turn off what this build cannot render with, and report it for the builder's warning. */ function disableUnsupported(nuxt) { const unsupported = []; if (nuxt.options.features.inlineStyles) { nuxt.options.features.inlineStyles = false; unsupported.push("inlined styles (stylesheets are linked instead)"); } if (nuxt.options.experimental.componentIslands && nuxt.options.experimental.componentIslands !== "auto") unsupported.push("server components and islands"); return unsupported; } //#endregion //#region src/runtime/remote-dev-error.ts const REMOTE_DEV_ERROR_EVENT = "nuxt:vite-server:dev-error"; const REMOTE_DEV_ERROR_REPORT_EVENT = "nuxt:vite-server:dev-error:report"; const REMOTE_DEV_ERROR_CLEAR_EVENT = "nuxt:vite-server:dev-error:clear"; function deserializeError(serialized) { if (!serialized || typeof serialized !== "object") return serialized; const { cause, ...properties } = serialized; return Object.assign(new Error(serialized.message, cause === void 0 ? void 0 : { cause: deserializeError(cause) }), properties); } //#endregion //#region src/dev-errors.ts /** * Report errors raised by renders a deploy target runs outside this process, whose stacks * arrive mapped to source positions. */ function listenForRemoteDevErrors(nuxt, server, channel) { const forwarding = () => !!process.env[ERROR_CHANNEL_ENV]; setErrorChannelForwarding(forwarding); const channelPath = () => process.env[ERROR_CHANNEL_ENV] || channel; const observe = createDevErrorReporter({ cwd: nuxt.options.rootDir, channel: channelPath, createReport: (error, request) => createErrorReport(error, { cwd: nuxt.options.rootDir, context: { request } }), requestInfo: (request) => ({ method: request.method, url: new URL(request.url), headers: new Headers(request.headers) }) }); for (const environment of Object.values(server.environments)) { if (environment.config.consumer !== "server") continue; environment.hot.on(REMOTE_DEV_ERROR_EVENT, async (message, client) => { const error = deserializeError(message.error); if (error instanceof Error && message.status !== void 0) Object.assign(error, { status: message.status }); const result = await observe(error, message.request).catch(() => void 0); const [overlay, page] = result ? await Promise.all([result.overlay(""), result.page()]).catch(() => []) : []; client.send(REMOTE_DEV_ERROR_REPORT_EVENT, { id: message.id, report: result?.report, overlay, page }); }); environment.hot.on(REMOTE_DEV_ERROR_CLEAR_EVENT, () => { clearErrorReport().catch(() => {}); }); } } //#endregion //#region src/html.ts const ESCAPES = { "&": "&", "<": "<", ">": ">", "\"": """ }; function escapeAttr(value) { return value.replace(/[&<>"]/g, (c) => ESCAPES[c]); } function renderAttrs(attrs) { let rendered = ""; for (const key in attrs) { const value = attrs[key]; if (value === void 0 || value === null || value === false) continue; rendered += value === true ? ` ${key}` : ` ${key}="${escapeAttr(String(value))}"`; } return rendered; } function renderTag(tag, input) { const { innerHTML, children, textContent, ...attrs } = input; const content = innerHTML ?? children ?? textContent; const open = `<${tag}${renderAttrs(attrs)}>`; return VOID_TAGS.has(tag) ? open : `${open}${content ?? ""}`; } const VOID_TAGS = /* @__PURE__ */ new Set([ "base", "link", "meta" ]); /** * Renders the head entries configured in `app.head`. Anything registered at runtime * (`useHead`, `definePageMeta`) is applied by unhead once the app mounts, so it is * intentionally absent from the static shell. */ function renderConfiguredHead(nuxt) { const head = nuxt.options.app.head; const tags = []; if (head.title) tags.push(`${escapeAttr(head.title)}`); if (head.base) tags.push(renderTag("base", head.base)); for (const meta of head.meta || []) tags.push(renderTag("meta", meta)); for (const link of head.link || []) tags.push(renderTag("link", link)); for (const style of head.style || []) tags.push(renderTag("style", style)); for (const script of head.script || []) tags.push(renderTag("script", script)); for (const noscript of head.noscript || []) tags.push(renderTag("noscript", noscript)); return tags.join(""); } /** * The client reads `window.__NUXT__` for public runtime config and to decide whether * it is hydrating server-rendered markup. */ function renderPayload(nuxt) { const payload = { serverRendered: false, config: { public: nuxt.options.runtimeConfig.public, app: nuxt.options.runtimeConfig.app } }; return `