@nuxt/vite-server-nightly 0.0.0 → 4.5.2-2610052030-052d82e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1194 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join, relative, resolve } from "pathe";
3
+ import { addPlugin, addTemplate, addTypeTemplate, addVitePlugin, getLayerDirectories, logger, resolvePath, tryUseNitro } from "@nuxt/kit";
4
+ import { bundlerDiagnostics, getServerRuntime, setServerBuild, useServerBuild } from "@nuxt/kit/internal";
5
+ import { defu } from "defu";
6
+ import { resolveModulePath } from "exsolve";
7
+ import { joinURL, parseURL, withBase, withTrailingSlash, withoutBase } from "ufo";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
9
+ import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
10
+ import { link } from "clickable-path";
11
+ import { cpus } from "node:os";
12
+ import { parse, walk } from "ultrahtml";
13
+ import { addRoute, createRouter, findAllRoutes } from "rou3";
14
+ import { compileRouterToString } from "rou3/compiler";
15
+ import { NodeRequest, sendNodeResponse } from "srvx/node";
16
+ import { staticMiddleware } from "srvx/static";
17
+ //#region src/dirs.ts
18
+ let _distDir = dirname(fileURLToPath(import.meta.url));
19
+ if (/(?:chunks|shared)$/.test(_distDir)) _distDir = dirname(_distDir);
20
+ const distDir = _distDir;
21
+ //#endregion
22
+ //#region src/templates/spa-loading-icon.ts
23
+ const template = () => {
24
+ return "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"80\" fill=\"none\" class=\"nuxt-spa-loading\" viewBox=\"0 0 37 25\"><path d=\"M24.236 22.006h10.742L25.563 5.822l-8.979 14.31a4 4 0 0 1-3.388 1.874H2.978l11.631-20 5.897 10.567\"/></svg><style>.nuxt-spa-loading{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%)}.nuxt-spa-loading>path{fill:none;stroke:#00dc82;stroke-width:4px;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:128;stroke-dashoffset:128px;animation:nuxt-spa-loading-move 3s linear infinite}@keyframes nuxt-spa-loading-move{to{stroke-dashoffset:-128px}}</style>";
25
+ };
26
+ //#endregion
27
+ //#region src/output.ts
28
+ /** Static hosts commonly map unknown paths to one of these files. */
29
+ const SPA_FALLBACK_FILES = [
30
+ "index.html",
31
+ "200.html",
32
+ "404.html"
33
+ ];
34
+ async function writeStaticOutput(nuxt, publicDir, options = {}) {
35
+ const document = resolve(publicDir, "index.html");
36
+ if (!existsSync(document)) throw new Error(`[nuxt:vite-server] Expected \`${document}\` to exist. Did the client build run?`);
37
+ const html = await readFile(document, "utf-8");
38
+ await rm(resolve(publicDir, "manifest.json"), { force: true });
39
+ if (options.ssr) await rm(document, { force: true });
40
+ for (const dirs of getLayerDirectories(nuxt)) if (existsSync(dirs.public)) await cp(dirs.public, publicDir, { recursive: true });
41
+ if (options.ssr) {
42
+ if (!options.prerender) logger.success(`Server output written to ${link(resolve(publicDir, ".."))}`);
43
+ return;
44
+ }
45
+ for (const file of SPA_FALLBACK_FILES) await writeFile(join(publicDir, file), html, "utf-8");
46
+ logger.success(`Static SPA output written to ${link(publicDir)}`);
47
+ }
48
+ /** Drop the bundle the crawl rendered with, leaving the static site as the whole output. */
49
+ async function finishStaticOutput(outputDir, publicDir) {
50
+ await rm(join(outputDir, "server"), {
51
+ recursive: true,
52
+ force: true
53
+ });
54
+ logger.success(`Prerendered output written to ${link(publicDir)}`);
55
+ }
56
+ async function spaLoadingTemplate(nuxt) {
57
+ if (nuxt.options.spaLoadingTemplate === false) return "";
58
+ 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"));
59
+ for (const candidate of candidates) if (existsSync(candidate)) return (await readFile(candidate, "utf-8")).trim();
60
+ if (nuxt.options.spaLoadingTemplate === true) return template();
61
+ if (nuxt.options.spaLoadingTemplate) bundlerDiagnostics.NUXT_B7016({ path: nuxt.options.spaLoadingTemplate });
62
+ return "";
63
+ }
64
+ function publicDirs(nuxt) {
65
+ return getLayerDirectories(nuxt).map((dirs) => withTrailingSlash(dirs.public)).filter((dir) => existsSync(dir));
66
+ }
67
+ //#endregion
68
+ //#region ../nuxt/src/core/utils/index.ts
69
+ /**
70
+ * Decode a route path for route-rule matching, returning it unchanged if it is not
71
+ * percent-encoded or cannot be decoded. Reserved characters (`%2F`, `%3F`, …) are
72
+ * left encoded, so decoding cannot introduce additional path segments. Only the path
73
+ * portion is decoded; a query string, if present, is left untouched to avoid
74
+ * double-decoding it.
75
+ */
76
+ function decodeRoutePath(path) {
77
+ if (!path.includes("%")) return path;
78
+ const queryIndex = path.indexOf("?");
79
+ const pathname = queryIndex === -1 ? path : path.slice(0, queryIndex);
80
+ try {
81
+ return queryIndex === -1 ? decodeURI(pathname) : decodeURI(pathname) + path.slice(queryIndex);
82
+ } catch {
83
+ return path;
84
+ }
85
+ }
86
+ //#endregion
87
+ //#region ../nuxt/src/core/utils/route-rules.ts
88
+ /**
89
+ * Normalise a route-rule key or lookup path: decode percent-encoding, then case-fold when
90
+ * `fold` is set. Keys are matched in decoded form so that a rule may be authored either
91
+ * decoded (`/测试`, the usual case) or encoded (`/%E6%B5%8B%E8%AF%95`, the form the page route
92
+ * and the request path take). Decoding must precede case folding, or a percent-encoded
93
+ * non-ASCII character would never fold.
94
+ *
95
+ * @internal
96
+ */
97
+ function normalizeRouteRulePath(path, fold) {
98
+ const decoded = decodeRoutePath(path);
99
+ return fold ? decoded.toLowerCase() : decoded;
100
+ }
101
+ /**
102
+ * Expand the rule shorthands a server runtime would expand: `redirect` as a string, and
103
+ * `swr`/`cache` as the single `cache` rule everything downstream reads.
104
+ *
105
+ * @internal
106
+ */
107
+ function normalizeRouteRules(config) {
108
+ const normalized = {};
109
+ for (const key in config) {
110
+ const { redirect, swr, cache, ...rules } = config[key] || {};
111
+ if (redirect) {
112
+ const options = {
113
+ to: "/",
114
+ status: 307,
115
+ ...typeof redirect === "string" ? { to: redirect } : redirect
116
+ };
117
+ if (key.endsWith("/**")) options.base = key.slice(0, -3);
118
+ rules.redirect = options;
119
+ } else if (redirect === false) rules.redirect = false;
120
+ if (swr !== void 0 && swr !== false) rules.cache = {
121
+ ...cache || void 0,
122
+ swr: true,
123
+ ...typeof swr === "number" ? { maxAge: swr } : {}
124
+ };
125
+ else if (swr === false && cache === void 0) rules.cache = false;
126
+ else if (cache !== void 0) rules.cache = cache === false ? false : cache;
127
+ normalized[key] = rules;
128
+ }
129
+ return normalized;
130
+ }
131
+ /**
132
+ * The route rules the build has resolved, as router entries.
133
+ *
134
+ * A server builder resolves its own (a module may push rules through `nitro:config`, and the
135
+ * server may add rules of its own), so its resolved set is preferred where there is one.
136
+ *
137
+ * @internal
138
+ */
139
+ function resolveRouteRulesRoutes(nuxt) {
140
+ const nitro = tryUseNitro();
141
+ const rules = nitro ? nitro.options.routeRules : normalizeRouteRules(nuxt.options.routeRules);
142
+ const baseURL = (nitro ? nitro.options.baseURL : nuxt.options.app.baseURL) || "";
143
+ return {
144
+ routes: Object.entries(rules).map(([route, data]) => ({
145
+ route,
146
+ method: "",
147
+ data
148
+ })),
149
+ baseURL
150
+ };
151
+ }
152
+ /**
153
+ * Build a route-rules router: patterns are registered under `baseURL`, and a lookup resolves
154
+ * every pattern it matches, least specific first.
155
+ *
156
+ * @internal
157
+ */
158
+ function createRouteRulesRouter(routes, baseURL) {
159
+ const base = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
160
+ const router = createRouter();
161
+ for (const route of routes) addRoute(router, route.method || "", base + route.route, route.data);
162
+ return {
163
+ routes,
164
+ matchAll: (method, path) => findAllRoutes(router, method, path).map((route) => route.data),
165
+ compileToString(options) {
166
+ if (base || routes.length !== 1 || routes[0].route !== "/**" || !!routes[0].method) return compileRouterToString(router, void 0, options);
167
+ const data = (options?.serialize || JSON.stringify)(routes[0].data);
168
+ const match = `{data,params:{"_":p.slice(1)}}`;
169
+ return `/* @__PURE__ */ (() => {const data=${data};return ((_m, p)=>(${options?.matchAll ? `[${match}]` : match}));})()`;
170
+ }
171
+ };
172
+ }
173
+ /**
174
+ * Source of the `normalizePath` helper a compiled matcher looks rules up through: `decodeRoutePath`
175
+ * is inlined by source so that the runtime lookup and the build-time key normalisation cannot
176
+ * drift apart.
177
+ *
178
+ * @internal
179
+ */
180
+ const normalizePathCode = [
181
+ `const decodeRoutePath = ${decodeRoutePath.toString()}`,
182
+ `const normalizePath = (path, fold) => {`,
183
+ ` if (typeof path !== 'string') { return path }`,
184
+ ` const decoded = decodeRoutePath(path)`,
185
+ ` return fold ? decoded.toLowerCase() : decoded`,
186
+ `}`
187
+ ].join("\n");
188
+ /**
189
+ * Build a route-rules router whose keys are decoded (and, when `fold` is set, lower-cased),
190
+ * mirroring the normalised lookup path used at runtime.
191
+ *
192
+ * `onDuplicate` is called when two distinct keys normalise onto the same path; the later
193
+ * key wins.
194
+ *
195
+ * @internal
196
+ */
197
+ function createNormalizedRouteRulesRouter(routes, baseURL, fold, onDuplicate) {
198
+ const normalizedKeys = /* @__PURE__ */ new Map();
199
+ return createRouteRulesRouter(routes.map((route) => {
200
+ if (typeof route.route !== "string") return route;
201
+ const key = normalizeRouteRulePath(route.route, fold);
202
+ const existing = normalizedKeys.get(key);
203
+ if (existing !== void 0 && existing !== route.route) onDuplicate?.(existing, route.route, key);
204
+ normalizedKeys.set(key, route.route);
205
+ return {
206
+ ...route,
207
+ route: key
208
+ };
209
+ }), normalizeRouteRulePath(String(baseURL || ""), fold));
210
+ }
211
+ //#endregion
212
+ //#region src/route-rules.ts
213
+ /**
214
+ * The rules this builder resolves for itself: the renderer reads `ssr`, `streaming`,
215
+ * `noScripts` and `prerender`, and the fetch handler answers `redirect` and sets `headers`.
216
+ * Caching, proxying and CORS belong to an HTTP layer, which this builder does not provide.
217
+ */
218
+ const SUPPORTED_RULES = /* @__PURE__ */ new Set([
219
+ "ssr",
220
+ "streaming",
221
+ "noScripts",
222
+ "prerender",
223
+ "redirect",
224
+ "headers"
225
+ ]);
226
+ const defuPath = resolveModulePath("defu", {
227
+ try: true,
228
+ from: import.meta.url
229
+ }) ?? "defu";
230
+ /**
231
+ * Build-time matcher over the resolved route rules, looked up by a path without the base URL:
232
+ * the routes the build reasons about (page routes, prerender seeds) are written that way.
233
+ */
234
+ function createRouteRulesMatcher(nuxt) {
235
+ const fold = !nuxt.options.router.options.sensitive;
236
+ const router = createNormalizedRouteRulesRouter(resolveRouteRulesRoutes(nuxt).routes, "", fold);
237
+ return (path) => defu({}, ...router.matchAll("", normalizeRouteRulePath(path, fold)).reverse());
238
+ }
239
+ /**
240
+ * Emit the matcher the server bundle resolves rules through, so that a render and the
241
+ * handler in front of it read the same rules the build compiled.
242
+ */
243
+ function addRouteRulesTemplate() {
244
+ const { dst } = addTemplate({
245
+ filename: "vite-server/route-rules.mjs",
246
+ write: true,
247
+ getContents: ({ nuxt }) => {
248
+ const fold = !nuxt.options.router.options.sensitive;
249
+ const { routes, baseURL } = resolveRouteRulesRoutes(nuxt);
250
+ const matcher = createNormalizedRouteRulesRouter(routes, baseURL, fold).compileToString({
251
+ matchAll: true,
252
+ serialize: (rules) => `{${Object.entries(rules).filter(([name, value]) => value !== void 0 && SUPPORTED_RULES.has(name)).map(([name, value]) => `${name}: ${JSON.stringify(value)}`).join(",")}}`
253
+ });
254
+ return [
255
+ `import { defu } from ${JSON.stringify(defuPath)}`,
256
+ `const matcher = ${matcher}`,
257
+ normalizePathCode,
258
+ `export default path => defu({}, ...matcher('', normalizePath(path, ${fold})).map(r => r.data).reverse())`
259
+ ].join("\n");
260
+ }
261
+ });
262
+ return dst;
263
+ }
264
+ //#endregion
265
+ //#region src/prerender.ts
266
+ /** Header the renderer reports the routes a render asked to prerender as well on. */
267
+ const PRERENDER_HINTS_HEADER = "x-nuxt-prerender";
268
+ const CRAWLABLE_EXTENSIONS = /* @__PURE__ */ new Set(["", ".json"]);
269
+ const EXTENSION_RE = /\.[\da-z]+$/;
270
+ const JSON_SIGNATURE_RE = /^\s*["[{]|^\s*-?\d{1,16}(?:\.\d{1,17})?(?:e[+-]?\d+)?\s*$/i;
271
+ const HTML_ENTITIES = {
272
+ "&lt;": "<",
273
+ "&gt;": ">",
274
+ "&amp;": "&",
275
+ "&apos;": "'",
276
+ "&quot;": "\""
277
+ };
278
+ const HTML_ENTITY_RE = /&(?:lt|gt|amp|apos|quot);/g;
279
+ const REDIRECT_STATUSES = /* @__PURE__ */ new Set([
280
+ 301,
281
+ 302,
282
+ 303,
283
+ 307,
284
+ 308
285
+ ]);
286
+ const WRITABLE_STATUSES = /* @__PURE__ */ new Set([
287
+ 200,
288
+ 304,
289
+ ...REDIRECT_STATUSES
290
+ ]);
291
+ /** Longest path segment most file systems accept. */
292
+ const FS_MAX_SEGMENT = 255;
293
+ /** Whether the build produces a prerendered site rather than a server. */
294
+ function isPrerendering(nuxt) {
295
+ return !nuxt.options.dev && nuxt.options.ssr !== false && !!nuxt.options.nitro.static;
296
+ }
297
+ function resolvePrerenderConfig(nuxt) {
298
+ const config = nuxt.options.nitro.prerender || {};
299
+ const shared = nuxt.options.prerender || {};
300
+ return {
301
+ routes: (shared.routes || []).filter(Boolean),
302
+ ignore: shared.ignore || [],
303
+ crawlLinks: shared.crawlLinks ?? true,
304
+ autoSubfolderIndex: config.autoSubfolderIndex ?? true,
305
+ concurrency: config.concurrency || cpus().length * 4 || 4,
306
+ interval: config.interval || 0,
307
+ failOnError: config.failOnError ?? true
308
+ };
309
+ }
310
+ /**
311
+ * Crawl the built application and write a static copy of every route it reaches.
312
+ *
313
+ * The handler is a web-standard `fetch`, so it is driven in-process: there is no port to
314
+ * allocate and no process to wait on. Routes a render reports while it runs are queued as
315
+ * they arrive, so one pass reaches the whole reachable app.
316
+ */
317
+ async function prerenderRoutes(nuxt, options) {
318
+ const config = resolvePrerenderConfig(nuxt);
319
+ const baseURL = nuxt.options.app.baseURL || "/";
320
+ const queue = /* @__PURE__ */ new Set();
321
+ for (const route of seedRoutes(nuxt, config)) queue.add(route);
322
+ await nuxt.callHook("prerender:routes", { routes: queue });
323
+ const { fetch } = await import(pathToFileURL(options.handler).href);
324
+ const canPrerender = createPrerenderFilter(nuxt, config);
325
+ const generated = /* @__PURE__ */ new Set();
326
+ const failed = [];
327
+ const written = [];
328
+ const linkParents = /* @__PURE__ */ new Map();
329
+ async function generateRoute(route) {
330
+ route = decodeURI(route);
331
+ if (generated.has(route) || !canPrerender(route)) return;
332
+ generated.add(route);
333
+ const request = new Request(new URL(withBase(encodeURI(route), baseURL), "http://localhost"));
334
+ const response = await fetch(request);
335
+ const body = Buffer.from(await response.arrayBuffer());
336
+ const entry = { route };
337
+ if (!WRITABLE_STATUSES.has(response.status)) {
338
+ entry.error = {
339
+ status: response.status,
340
+ statusText: response.statusText
341
+ };
342
+ failed.push(entry);
343
+ logRoute(entry, linkParents);
344
+ return;
345
+ }
346
+ const contentType = response.headers.get("content-type") || "";
347
+ const isRedirect = REDIRECT_STATUSES.has(response.status);
348
+ const isImplicitHTML = !route.endsWith(".html") && (isRedirect || contentType.includes("html")) && !JSON_SIGNATURE_RE.test(body.subarray(0, 32).toString("utf-8"));
349
+ const htmlPath = route.endsWith("/") || config.autoSubfolderIndex ? joinURL(route, "index.html") : route + ".html";
350
+ const fileName = withoutBase(isImplicitHTML ? htmlPath : route.endsWith("/") ? route + "index" : route, baseURL);
351
+ if (canWriteToDisk(route, fileName)) {
352
+ entry.fileName = fileName;
353
+ const path = join(options.publicDir, fileName);
354
+ await mkdir(dirname(path), { recursive: true });
355
+ await writeFile(path, body);
356
+ written.push(entry);
357
+ }
358
+ logRoute(entry, linkParents);
359
+ if (isImplicitHTML || route.endsWith(".html")) for (const link of await extractLinks(body.toString("utf-8"), route, response, config.crawlLinks)) {
360
+ const parents = linkParents.get(link);
361
+ if (parents) parents.add(route);
362
+ else linkParents.set(link, /* @__PURE__ */ new Set([route]));
363
+ if (!link.includes("?") && canPrerender(link) && !generated.has(link)) queue.add(link);
364
+ }
365
+ }
366
+ const start = Date.now();
367
+ logger.info(config.crawlLinks ? `Prerendering ${queue.size} initial routes with crawler` : `Prerendering ${queue.size} routes`);
368
+ await runParallel(queue, generateRoute, config, (route, error) => {
369
+ const entry = {
370
+ route,
371
+ error: {
372
+ status: 500,
373
+ statusText: error instanceof Error ? error.message : String(error)
374
+ }
375
+ };
376
+ failed.push(entry);
377
+ logRoute(entry, linkParents);
378
+ });
379
+ 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(", ")}.`);
380
+ logger.info(`Prerendered ${written.length} routes in ${(Date.now() - start) / 1e3} seconds`);
381
+ return written.map((entry) => entry.route);
382
+ }
383
+ /**
384
+ * The routes the crawl starts from. A dynamic page route or a wildcard rule key cannot be
385
+ * turned back into a path, so reaching those is left to the hints and the crawler.
386
+ */
387
+ function seedRoutes(nuxt, config) {
388
+ const routes = new Set(config.routes);
389
+ const errorPageOption = nuxt.options.experimental.prerenderErrorPages;
390
+ const errorPages = errorPageOption === true ? [404] : errorPageOption || [];
391
+ for (const route of ["/200.html", "/404.html"]) routes.add(route);
392
+ for (const status of errorPages) routes.add(`/${status}.html`);
393
+ const rules = nuxt.options.routeRules || {};
394
+ for (const path in rules) if (rules[path]?.prerender && !path.includes("*")) routes.add(path);
395
+ const pages = nuxt.apps.default?.pages;
396
+ if (pages && !nuxt.options.router.options.hashMode) {
397
+ const matcher = createRouteRulesMatcher(nuxt);
398
+ for (const route of staticPageRoutes(pages)) if (config.crawlLinks || matcher(route).prerender) routes.add(route);
399
+ }
400
+ if (!routes.size && config.crawlLinks) routes.add("/");
401
+ return routes;
402
+ }
403
+ const OPTIONAL_PARAM_RE = /^\/?:.*(?:\?|\(\.\*\)\*)$/;
404
+ function staticPageRoutes(pages, currentPath = "/", routes = /* @__PURE__ */ new Set()) {
405
+ for (const page of pages) {
406
+ if (page._sync) continue;
407
+ if (OPTIONAL_PARAM_RE.test(page.path) && !page.children?.length) routes.add(currentPath);
408
+ if (page.path.includes(":")) continue;
409
+ const route = joinURL(currentPath, page.path);
410
+ routes.add(route);
411
+ if (page.children) staticPageRoutes(page.children, route, routes);
412
+ }
413
+ return routes;
414
+ }
415
+ function createPrerenderFilter(nuxt, config) {
416
+ const baseURL = nuxt.options.app.baseURL || "/";
417
+ const matcher = createRouteRulesMatcher(nuxt);
418
+ return function canPrerender(route) {
419
+ for (const pattern of config.ignore) if (typeof pattern === "string" ? route.startsWith(pattern) : pattern instanceof RegExp ? matchesPattern(pattern, route) : pattern(route) === true) return false;
420
+ return matcher(withoutBase(route, baseURL)).prerender !== false;
421
+ };
422
+ }
423
+ function matchesPattern(pattern, route) {
424
+ pattern.lastIndex = 0;
425
+ return pattern.test(route);
426
+ }
427
+ function canWriteToDisk(route, fileName) {
428
+ if (route.includes("?") || route.includes("..")) return false;
429
+ if (fileName.split("/").some((segment) => segment.length > FS_MAX_SEGMENT)) {
430
+ logger.warn(`Skipping prerender of "${route}": a path segment exceeds the ${FS_MAX_SEGMENT}-character limit.`);
431
+ return false;
432
+ }
433
+ return true;
434
+ }
435
+ async function extractLinks(html, from, response, crawlLinks) {
436
+ const candidates = [];
437
+ if (crawlLinks) await walk(parse(html), (node) => {
438
+ const href = node.attributes?.href;
439
+ if (!href) return;
440
+ const link = href.replace(HTML_ENTITY_RE, (entity) => HTML_ENTITIES[entity] || entity);
441
+ const decoded = decode(link);
442
+ if (decoded !== void 0 && !decoded.startsWith("#") && CRAWLABLE_EXTENSIONS.has(parseURL(link).pathname.match(EXTENSION_RE)?.[0] || "")) candidates.push(link);
443
+ });
444
+ for (const hint of (response.headers.get(PRERENDER_HINTS_HEADER) || "").split(",")) {
445
+ const decoded = hint.trim() && decode(hint.trim());
446
+ if (decoded) candidates.push(decoded);
447
+ }
448
+ const links = [];
449
+ for (const candidate of candidates) {
450
+ const url = parseURL(candidate);
451
+ if (url.protocol || url.host) continue;
452
+ if (!url.pathname.startsWith("/")) url.pathname = new URL(url.pathname, new URL(from, "http://localhost")).pathname;
453
+ links.push(url.pathname + url.search);
454
+ }
455
+ return links;
456
+ }
457
+ /** Percent-decode a link, or report it as one to leave alone. */
458
+ function decode(value) {
459
+ try {
460
+ return decodeURIComponent(value);
461
+ } catch {
462
+ return;
463
+ }
464
+ }
465
+ function logRoute(route, linkParents) {
466
+ if (!route.error) {
467
+ logger.log(` ├─ ${route.route}${route.fileName ? "" : " (skipped)"}`);
468
+ return;
469
+ }
470
+ const parents = linkParents.get(route.route);
471
+ logger.log([` ├─ ${route.route} (${route.error.status} ${route.error.statusText})`, ...[...parents || []].map((parent) => ` │ └── Linked from ${parent}`)].join("\n"));
472
+ }
473
+ /** Drain a set the tasks themselves add to, running at most `concurrency` at a time. */
474
+ async function runParallel(queue, task, options, onError) {
475
+ const running = /* @__PURE__ */ new Set();
476
+ function next() {
477
+ const route = queue.values().next().value;
478
+ if (!route) return;
479
+ queue.delete(route);
480
+ const started = (options.interval ? new Promise((resolve) => setTimeout(resolve, options.interval)) : Promise.resolve()).then(() => task(route)).catch((error) => onError(route, error));
481
+ running.add(started);
482
+ return started.then(async () => {
483
+ running.delete(started);
484
+ if (queue.size > 0) await refill();
485
+ });
486
+ }
487
+ function refill() {
488
+ const workers = Math.min(options.concurrency - running.size, queue.size);
489
+ return Promise.all(Array.from({ length: workers }, () => next()));
490
+ }
491
+ await refill();
492
+ }
493
+ /**
494
+ * Write the app manifest, without which a client-side navigation cannot tell that a route
495
+ * has a payload file to load, and refetches the data instead.
496
+ */
497
+ async function writeAppManifest(nuxt, publicDir, routes) {
498
+ const buildId = nuxt.options.runtimeConfig.app.buildId;
499
+ const timestamp = manifestTimestamp(nuxt);
500
+ const baseURL = nuxt.options.app.baseURL || "/";
501
+ const matcher = createRouteRulesMatcher(nuxt);
502
+ const prerendered = /* @__PURE__ */ new Set();
503
+ for (const route of routes) {
504
+ if (!route.endsWith(PAYLOAD_SUFFIX)) continue;
505
+ const path = route.slice(0, -PAYLOAD_SUFFIX.length) || "/";
506
+ if (!matcher(withoutBase(path, baseURL)).prerender) prerendered.add(path);
507
+ }
508
+ const dir = resolve(publicDir, joinURL(nuxt.options.app.buildAssetsDir, "builds").replace(/^\//, ""));
509
+ await mkdir(join(dir, "meta"), { recursive: true });
510
+ await writeFile(join(dir, "latest.json"), JSON.stringify({
511
+ id: buildId,
512
+ timestamp
513
+ }));
514
+ await writeFile(join(dir, `meta/${buildId}.json`), JSON.stringify({
515
+ id: buildId,
516
+ timestamp,
517
+ prerendered: [...prerendered].sort()
518
+ }));
519
+ }
520
+ const PAYLOAD_SUFFIX = "/_payload.json";
521
+ const timestamps = /* @__PURE__ */ new WeakMap();
522
+ /** One timestamp per build, shared by the stub the bundle inlines and the file the client fetches. */
523
+ function manifestTimestamp(nuxt) {
524
+ const existing = timestamps.get(nuxt);
525
+ if (existing !== void 0) return existing;
526
+ const timestamp = Date.now();
527
+ timestamps.set(nuxt, timestamp);
528
+ return timestamp;
529
+ }
530
+ //#endregion
531
+ //#region src/ssr.ts
532
+ /** The contract version this builder is written against. */
533
+ const SUPPORTED_SERVER_RUNTIME_VERSION = 1;
534
+ /**
535
+ * Stand up the SSR renderer for the vite server build: the modules it renders with, the
536
+ * entry that creates it, and the environment configuration that compiles it.
537
+ *
538
+ * The server environment is the app build's own ssr environment, with its input replaced
539
+ * by the generated entry: the renderer and the SSR app are bundled together, so the app's
540
+ * entry is reached through the renderer rather than emitted on its own.
541
+ */
542
+ function setupSSR(nuxt, outputDir) {
543
+ const prerender = isPrerendering(nuxt);
544
+ const errorPageOption = nuxt.options.experimental.prerenderErrorPages;
545
+ const errorPages = errorPageOption === true ? [404] : errorPageOption || [];
546
+ const noSSRRoutes = [
547
+ "/index.html",
548
+ "/200.html",
549
+ "/404.html"
550
+ ].filter((route) => !errorPages.includes(Number(route.slice(1, -5))));
551
+ const serverRuntime = getServerRuntime({
552
+ phase: prerender ? "prerender" : "server",
553
+ overrides: async () => ({
554
+ spaTemplate: JSON.stringify(await spaLoadingTemplate(nuxt)),
555
+ ...prerender ? {
556
+ NUXT_PRERENDER_ERROR_PAGES: JSON.stringify(errorPages),
557
+ NUXT_PRERENDER_NO_SSR_ROUTES: JSON.stringify(noSSRRoutes)
558
+ } : {}
559
+ })
560
+ }, nuxt);
561
+ 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\`.`);
562
+ const unsupported = disableUnsupported(nuxt);
563
+ const serverDir = resolve(outputDir, "server");
564
+ const { entry, handler } = addServerEntry(nuxt, serverRuntime, prerender, addRouteRulesTemplate());
565
+ nuxt.options.vite.plugins ||= [];
566
+ nuxt.options.vite.plugins.push(AppServerEnvironmentsPlugin(nuxt), ServerEnvironmentPlugin(nuxt, serverRuntime, entry, serverDir, prerender), BundledVuePlugin(nuxt), ServerRuntimeModulesPlugin(serverRuntime));
567
+ return {
568
+ entry,
569
+ handler,
570
+ unsupported
571
+ };
572
+ }
573
+ /**
574
+ * Names every server environment as one that renders the app, so that the app's own
575
+ * bundler plugins (the virtual file system behind `#build`, and everything else registered
576
+ * with `addVitePlugin()`) apply there too. This builder brings no server runtime, so a
577
+ * server environment a deploy target contributes is a build of the app rather than a
578
+ * service beside it.
579
+ */
580
+ function AppServerEnvironmentsPlugin(nuxt) {
581
+ return {
582
+ name: "nuxt:vite-server:app-server-environments",
583
+ enforce: "pre",
584
+ configResolved(config) {
585
+ nuxt._appServerEnvironments ??= /* @__PURE__ */ new Set();
586
+ for (const [name, environment] of Object.entries(config.environments)) if (environment.consumer === "server") nuxt._appServerEnvironments.add(name);
587
+ }
588
+ };
589
+ }
590
+ /**
591
+ * The modules the render is reached through. Everything they import is imported by absolute
592
+ * path, so neither needs an alias of its own to resolve.
593
+ *
594
+ * The handler is the render and nothing else, importing no node built-in, so an environment
595
+ * a deploy target owns can build it for whatever it runs on. The entry is what this
596
+ * builder's own environment builds: the handler behind a node server that serves the static
597
+ * output in front of it and listens when it is run as the main module.
598
+ */
599
+ function addServerEntry(nuxt, serverRuntime, prerender, routeRules) {
600
+ const { dst: handler } = addTemplate({
601
+ filename: "vite-server/server-handler.mjs",
602
+ write: true,
603
+ getContents: () => [
604
+ `import { createNuxtRenderer } from ${JSON.stringify(serverRuntime.entry)}`,
605
+ `import { useRuntimeConfig } from ${JSON.stringify(resolve(nuxt.options.buildDir, "vite-server/runtime-config.mjs"))}`,
606
+ `import { createFetchHandler, createRendererOptions } from ${JSON.stringify(resolve(distDir, "runtime/renderer"))}`,
607
+ `import matchRouteRules from ${JSON.stringify(routeRules)}`,
608
+ ...prerender ? [`import { createPrerenderOptions } from ${JSON.stringify(resolve(distDir, "runtime/prerender"))}`] : [],
609
+ "",
610
+ `const renderer = createNuxtRenderer(createRendererOptions(useRuntimeConfig, matchRouteRules${prerender ? `, createPrerenderOptions({ sharedData: ${!!nuxt.options.experimental.sharedPrerenderData} })` : ""}))`,
611
+ `export const fetch = createFetchHandler(renderer, matchRouteRules)`,
612
+ `export default { fetch }`
613
+ ].join("\n")
614
+ });
615
+ addTemplate({
616
+ filename: "vite-server/server.mjs",
617
+ write: true,
618
+ getContents: () => [
619
+ `import match from ${JSON.stringify(routeRules)}`,
620
+ `import { createServerHelpers } from ${JSON.stringify(resolve(distDir, "runtime/server"))}`,
621
+ "",
622
+ `export * from 'nuxt/internal/server-default'`,
623
+ "",
624
+ `const helpers = createServerHelpers(match, () => import(${JSON.stringify(handler)}).then(m => m.fetch), ${JSON.stringify(nuxt.options.app.baseURL || "/")})`,
625
+ `export const getRouteRules = helpers.getRouteRules`,
626
+ `export const matchRouteRules = helpers.matchRouteRules`,
627
+ `export const serverFetch = helpers.serverFetch`
628
+ ].join("\n")
629
+ });
630
+ const { dst: entry } = addTemplate({
631
+ filename: "vite-server/server-entry.mjs",
632
+ write: true,
633
+ getContents: () => [
634
+ `import { fetch } from ${JSON.stringify(handler)}`,
635
+ `import { createNodeServer, listen } from ${JSON.stringify(resolve(distDir, "runtime/node"))}`,
636
+ "",
637
+ `export { fetch }`,
638
+ "",
639
+ ...nuxt.options.dev ? [`export { setDevErrorContext } from ${JSON.stringify(resolve(distDir, "runtime/dev-error"))}`, ""] : [],
640
+ `const server = import.meta.dev ? undefined : createNodeServer({ fetch, publicDir: new URL('../public/', import.meta.url) })`,
641
+ `export default server`,
642
+ "",
643
+ `if (server && import.meta.main) {`,
644
+ ` listen(server)`,
645
+ `}`
646
+ ].join("\n")
647
+ });
648
+ return {
649
+ entry,
650
+ handler
651
+ };
652
+ }
653
+ /**
654
+ * Configures the environments that render: the defines the renderer must be compiled with,
655
+ * everywhere it is compiled, and for this builder's own environment the server entry as
656
+ * its only input and the output a deployable expects.
657
+ *
658
+ * The input is replaced rather than added to: the app entry the app build would give this
659
+ * environment is reached through the renderer instead, so building it as an entry of its
660
+ * own would emit a second copy of the app.
661
+ *
662
+ * A deploy target that builds its own module as the input keeps it, and reaches the render
663
+ * through `#server-entry`; the environment then emits the target's artifact.
664
+ */
665
+ function ServerEnvironmentPlugin(nuxt, serverRuntime, entry, serverDir, prerender) {
666
+ return {
667
+ name: "nuxt:vite-server:server-environment",
668
+ applyToEnvironment: (environment) => environment.config.consumer === "server",
669
+ configEnvironment(name, config) {
670
+ if (config.consumer === "client") return;
671
+ config.define = {
672
+ ...config.define,
673
+ ...serverRuntime.defines
674
+ };
675
+ if (nuxt.options.dev || name !== "ssr") return;
676
+ config.build ||= {};
677
+ config.build.outDir = serverDir;
678
+ config.build.emptyOutDir = true;
679
+ config.build.rolldownOptions ||= {};
680
+ const input = config.build.rolldownOptions.input;
681
+ const named = !!input && typeof input === "object" && !Array.isArray(input) ? input : void 0;
682
+ if (!!named && !("server" in named) && named.index !== entry) {
683
+ 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`.");
684
+ useServerBuild(nuxt).runtime.handler = void 0;
685
+ } else config.build.rolldownOptions.input = { index: entry };
686
+ config.build.rolldownOptions.output = {
687
+ ...defu({ chunkFileNames: "_chunks/[name]-[hash].mjs" }, config.build.rolldownOptions.output),
688
+ entryFileNames: "index.mjs"
689
+ };
690
+ config.resolve ||= {};
691
+ config.resolve.noExternal = true;
692
+ config.resolve.external = [];
693
+ config.build.rolldownOptions.external = [];
694
+ }
695
+ };
696
+ }
697
+ /**
698
+ * Resolves Vue to its bundler builds for the server bundle.
699
+ *
700
+ * Bundling every dependency means resolving Vue ourselves: its `node` export condition is
701
+ * the CommonJS full build, which carries the template compiler the SSR app never calls
702
+ * (~700kB of it, between `vue` and `vue/server-renderer`) and cannot be tree-shaken
703
+ * through cjs interop. The bundler builds are the same runtime without the compiler.
704
+ */
705
+ function BundledVuePlugin(nuxt) {
706
+ const BUNDLER_BUILDS = {
707
+ "vue": "vue/dist/vue.runtime.esm-bundler.js",
708
+ "vue/server-renderer": "@vue/server-renderer/dist/server-renderer.esm-bundler.js"
709
+ };
710
+ return {
711
+ name: "nuxt:vite-server:bundled-vue",
712
+ apply: "build",
713
+ applyToEnvironment: (environment) => environment.config.consumer === "server",
714
+ resolveId: {
715
+ order: "pre",
716
+ filter: { id: /^vue(?:\/server-renderer)?$/ },
717
+ handler(id, importer) {
718
+ if (nuxt.options.vue.runtimeCompiler) return;
719
+ return this.resolve(BUNDLER_BUILDS[id], importer, { skipSelf: true });
720
+ }
721
+ }
722
+ };
723
+ }
724
+ /**
725
+ * Resolves the modules the renderer imports for the environment that renders.
726
+ *
727
+ * Every specifier comes from the record, so this plugin never names one: the set is Nuxt's
728
+ * to change. Each body is read when the environment loads it, which is after the client
729
+ * environment has built, so the values it derives from the client build are final.
730
+ */
731
+ function ServerRuntimeModulesPlugin(serverRuntime) {
732
+ const prefix = "\0nuxt-server-runtime:";
733
+ return {
734
+ name: "nuxt:vite-server:server-runtime",
735
+ applyToEnvironment: (environment) => environment.config.consumer === "server",
736
+ resolveId: {
737
+ order: "pre",
738
+ handler: (id) => id in serverRuntime.modules ? prefix + id : void 0
739
+ },
740
+ load: {
741
+ order: "pre",
742
+ async handler(id) {
743
+ if (!id.startsWith(prefix)) return;
744
+ const module = serverRuntime.modules[id.slice(21)];
745
+ return module ? {
746
+ code: await module.code(),
747
+ map: null
748
+ } : void 0;
749
+ }
750
+ }
751
+ };
752
+ }
753
+ /**
754
+ * Turn off what this build cannot render with, and report it for the builder's warning.
755
+ */
756
+ function disableUnsupported(nuxt) {
757
+ const unsupported = [];
758
+ if (nuxt.options.features.inlineStyles) {
759
+ nuxt.options.features.inlineStyles = false;
760
+ unsupported.push("inlined styles (stylesheets are linked instead)");
761
+ }
762
+ if (nuxt.options.experimental.componentIslands && nuxt.options.experimental.componentIslands !== "auto") unsupported.push("server components and islands");
763
+ return unsupported;
764
+ }
765
+ //#endregion
766
+ //#region src/html.ts
767
+ const ESCAPES = {
768
+ "&": "&amp;",
769
+ "<": "&lt;",
770
+ ">": "&gt;",
771
+ "\"": "&quot;"
772
+ };
773
+ function escapeAttr(value) {
774
+ return value.replace(/[&<>"]/g, (c) => ESCAPES[c]);
775
+ }
776
+ function renderAttrs(attrs) {
777
+ let rendered = "";
778
+ for (const key in attrs) {
779
+ const value = attrs[key];
780
+ if (value === void 0 || value === null || value === false) continue;
781
+ rendered += value === true ? ` ${key}` : ` ${key}="${escapeAttr(String(value))}"`;
782
+ }
783
+ return rendered;
784
+ }
785
+ function renderTag(tag, input) {
786
+ const { innerHTML, children, textContent, ...attrs } = input;
787
+ const content = innerHTML ?? children ?? textContent;
788
+ const open = `<${tag}${renderAttrs(attrs)}>`;
789
+ return VOID_TAGS.has(tag) ? open : `${open}${content ?? ""}</${tag}>`;
790
+ }
791
+ const VOID_TAGS = /* @__PURE__ */ new Set([
792
+ "base",
793
+ "link",
794
+ "meta"
795
+ ]);
796
+ /**
797
+ * Renders the head entries configured in `app.head`. Anything registered at runtime
798
+ * (`useHead`, `definePageMeta`) is applied by unhead once the app mounts, so it is
799
+ * intentionally absent from the static shell.
800
+ */
801
+ function renderConfiguredHead(nuxt) {
802
+ const head = nuxt.options.app.head;
803
+ const tags = [];
804
+ if (head.title) tags.push(`<title>${escapeAttr(head.title)}</title>`);
805
+ if (head.base) tags.push(renderTag("base", head.base));
806
+ for (const meta of head.meta || []) tags.push(renderTag("meta", meta));
807
+ for (const link of head.link || []) tags.push(renderTag("link", link));
808
+ for (const style of head.style || []) tags.push(renderTag("style", style));
809
+ for (const script of head.script || []) tags.push(renderTag("script", script));
810
+ for (const noscript of head.noscript || []) tags.push(renderTag("noscript", noscript));
811
+ return tags.join("");
812
+ }
813
+ /**
814
+ * The client reads `window.__NUXT__` for public runtime config and to decide whether
815
+ * it is hydrating server-rendered markup.
816
+ */
817
+ function renderPayload(nuxt) {
818
+ const payload = {
819
+ serverRendered: false,
820
+ config: {
821
+ public: nuxt.options.runtimeConfig.public,
822
+ app: nuxt.options.runtimeConfig.app
823
+ }
824
+ };
825
+ return `<script>window.__NUXT__=${JSON.stringify(payload).replace(/</g, "\\u003C")}<\/script>`;
826
+ }
827
+ /**
828
+ * Renders the SPA document.
829
+ *
830
+ * Only the app entry is linked: stylesheets, module preloads and the transforms of any
831
+ * configured Vite plugin are added by Vite itself, which owns this document as an HTML
832
+ * build input (and, in dev, transforms it on the way out).
833
+ */
834
+ function renderIndexHtml(nuxt, entry, spaLoadingTemplate = "") {
835
+ const app = nuxt.options.app;
836
+ const rootAttrs = renderAttrs(app.rootAttrs);
837
+ const teleportAttrs = renderAttrs(app.teleportAttrs);
838
+ const head = renderConfiguredHead(nuxt);
839
+ const loader = spaLoadingTemplate ? `<${app.spaLoaderTag}${renderAttrs(app.spaLoaderAttrs)}>${spaLoadingTemplate}</${app.spaLoaderTag}>` : "";
840
+ const root = nuxt.options.ssr === false ? "" : "<!--ssr-outlet-->";
841
+ const body = [
842
+ `<${app.rootTag}${rootAttrs}>${root}</${app.rootTag}>`,
843
+ loader,
844
+ `<${app.teleportTag}${teleportAttrs}></${app.teleportTag}>`,
845
+ renderPayload(nuxt),
846
+ `<script type="module" src="${escapeAttr(entry)}"><\/script>`
847
+ ].join("");
848
+ return [
849
+ "<!DOCTYPE html>",
850
+ `<html${renderAttrs(app.head.htmlAttrs || {})}>`,
851
+ `<head>${head}</head>`,
852
+ `<body${renderAttrs(app.head.bodyAttrs || {})}>${body}</body>`,
853
+ "</html>"
854
+ ].join("");
855
+ }
856
+ //#endregion
857
+ //#region src/document.ts
858
+ /**
859
+ * Path of the SPA document, at the root of the Vite project so that it is emitted at the
860
+ * root of the client output. A project may put a real file here to take over the
861
+ * document entirely, as it would in any other Vite app; otherwise it is generated.
862
+ */
863
+ function documentPath(nuxt) {
864
+ return resolve(nuxt.options.vite.root || nuxt.options.srcDir, "index.html");
865
+ }
866
+ async function resolveDocument(nuxt) {
867
+ const path = documentPath(nuxt);
868
+ if (existsSync(path)) return readFile(path, "utf-8");
869
+ const entry = await resolvePath(resolve(nuxt.options.appDir, nuxt.options.dev || nuxt.options.experimental.asyncEntry ? "entry.async" : "entry"));
870
+ return renderIndexHtml(nuxt, nuxt.options.dev ? joinURL("/@fs", entry) : "./" + relative(resolve(path, ".."), entry), await spaLoadingTemplate(nuxt));
871
+ }
872
+ /**
873
+ * Provides the generated document as the contents of the HTML build input, so that
874
+ * nothing has to be written into the project to build it.
875
+ */
876
+ function DocumentPlugin(nuxt) {
877
+ const path = documentPath(nuxt);
878
+ return {
879
+ name: "nuxt:vite-server:document",
880
+ apply: "build",
881
+ applyToEnvironment: (environment) => environment.name === "client",
882
+ resolveId: {
883
+ order: "pre",
884
+ filter: { id: new RegExp(`^${escape(path)}$`) },
885
+ handler: (id) => id
886
+ },
887
+ load: {
888
+ filter: { id: new RegExp(`^${escape(path)}$`) },
889
+ handler: () => resolveDocument(nuxt)
890
+ }
891
+ };
892
+ }
893
+ /**
894
+ * `experimental.entryImportMap` rewrites cross-chunk imports of the entry to the bare
895
+ * `#entry` specifier, so that its hash cannot leak into the chunks importing it. That
896
+ * leaves the document responsible for mapping the specifier to the emitted chunk.
897
+ */
898
+ function EntryImportMapPlugin() {
899
+ return {
900
+ name: "nuxt:vite-server:entry-import-map",
901
+ apply: "build",
902
+ applyToEnvironment: (environment) => environment.name === "client",
903
+ transformIndexHtml: {
904
+ order: "post",
905
+ handler(_html, ctx) {
906
+ for (const chunk of Object.values(ctx.bundle || {})) if (chunk.type === "chunk" && chunk.isEntry && chunk.name === "entry") return [{
907
+ tag: "script",
908
+ attrs: { type: "importmap" },
909
+ children: JSON.stringify({ imports: { "#entry": "./" + chunk.fileName } }),
910
+ injectTo: "head-prepend"
911
+ }];
912
+ }
913
+ }
914
+ };
915
+ }
916
+ /**
917
+ * Builds the client environment, and marks the ssr environment as built when nothing
918
+ * will render on the server, so that the orchestrator skips it.
919
+ *
920
+ * A `buildApp` hook rather than a `builder.buildApp` config option, so that a plugin
921
+ * bringing its own deploy target (`@cloudflare/vite-plugin`, for one) keeps its own
922
+ * orchestration and its own environments: those are built as usual.
923
+ *
924
+ * It runs `pre`, because anything that renders reads the client manifest as it builds, and
925
+ * a target orchestrating with a config-level `builder.buildApp` (which is what such a
926
+ * plugin installs) would otherwise build its own environment first: Vite calls that before
927
+ * the first `post` hook.
928
+ */
929
+ function BuildEnvironmentsPlugin(nuxt) {
930
+ return {
931
+ name: "nuxt:vite-server:build-environments",
932
+ apply: "build",
933
+ buildApp: {
934
+ order: "pre",
935
+ async handler(builder) {
936
+ const ssr = builder.environments.ssr;
937
+ if (ssr && nuxt.options.ssr === false) ssr.isBuilt = true;
938
+ for (const environment of [builder.environments.client, ssr]) if (environment && !environment.isBuilt) await builder.build(environment);
939
+ }
940
+ }
941
+ };
942
+ }
943
+ function escape(path) {
944
+ return path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
945
+ }
946
+ //#endregion
947
+ //#region src/dev.ts
948
+ /**
949
+ * Vite runs in middleware mode, so it creates no HTTP server of its own and leaves
950
+ * `server.httpServer` null. Nuxt does listen, and its middlewares are served from that
951
+ * listener, so the listener is exposed to Vite for other plugins to attach to.
952
+ */
953
+ function DevServerListenerPlugin(nuxt) {
954
+ return {
955
+ name: "nuxt:vite-server:dev-listener",
956
+ enforce: "pre",
957
+ apply: "serve",
958
+ configureServer: {
959
+ order: "pre",
960
+ handler(server) {
961
+ server.httpServer ||= nuxt._devServerListener ?? null;
962
+ }
963
+ }
964
+ };
965
+ }
966
+ function setupDevServer(nuxt, serverEntry) {
967
+ let viteServer;
968
+ nuxt.hook("vite:serverCreated", (server) => {
969
+ viteServer = server;
970
+ });
971
+ const middleware = publicDirs(nuxt).map((dir) => staticMiddleware({ dir }));
972
+ const shell = async (url) => {
973
+ const html = await resolveDocument(nuxt);
974
+ return new Response(viteServer ? await viteServer.transformIndexHtml(url, html) : html, { headers: { "content-type": "text/html;charset=utf-8" } });
975
+ };
976
+ const errorChannel = joinURL(nuxt.options.app.baseURL, nuxt.options.devServer.errorChannel);
977
+ const render = async (request) => {
978
+ const module = await viteServer.ssrLoadModule(serverEntry);
979
+ module.setDevErrorContext?.({
980
+ server: viteServer,
981
+ cwd: nuxt.options.rootDir,
982
+ channel: errorChannel
983
+ });
984
+ return module.fetch(request);
985
+ };
986
+ const respond = (request, url) => {
987
+ return serverEntry && viteServer ? render(request) : shell(url);
988
+ };
989
+ nuxt.server = { handler: async (req, res) => {
990
+ if (viteServer && await handledByVite(viteServer, req, res)) return;
991
+ const request = new NodeRequest({
992
+ req,
993
+ res
994
+ });
995
+ const next = (index) => {
996
+ const handler = middleware[index];
997
+ return handler ? handler(request, () => next(index + 1)) : respond(request, req.url || "/");
998
+ };
999
+ await sendNodeResponse(res, await next(0));
1000
+ } };
1001
+ }
1002
+ function handledByVite(server, req, res) {
1003
+ return new Promise((resolvePromise, reject) => {
1004
+ const originalUrl = req.url;
1005
+ server.middlewares.handle(req, res, (error) => {
1006
+ req.url = originalUrl;
1007
+ if (error) return reject(error);
1008
+ resolvePromise(false);
1009
+ });
1010
+ res.on("close", () => resolvePromise(true));
1011
+ }).then((handled) => handled || res.writableEnded);
1012
+ }
1013
+ //#endregion
1014
+ //#region src/index.ts
1015
+ /**
1016
+ * Experimental server builder implemented with Vite alone.
1017
+ *
1018
+ * It builds the client and emits a document for it: with `ssr: false` that is a complete
1019
+ * static SPA. With SSR enabled it also builds a server from the Nuxt SSR renderer, whose
1020
+ * entry exports a web-standard `{ fetch }` and, on node, serves the static output in
1021
+ * front of it.
1022
+ *
1023
+ * `nuxt generate` crawls that server at the end of the build, leaving the public directory
1024
+ * as the whole deployable.
1025
+ *
1026
+ * Features that need a server runtime are unsupported: server routes and middleware,
1027
+ * route rules an HTTP layer answers (caching, proxying, CORS), and composables that need more of a request than the
1028
+ * platform provides. Modules work to the extent that they do not require one:
1029
+ * `useNitro()` throws and the `nitro:config` / `nitro:init` hooks never fire.
1030
+ */
1031
+ function bundle(nuxt) {
1032
+ if (nuxt.options.builder !== "@nuxt/vite-builder") throw new Error("`@nuxt/vite-server` requires the Vite builder.");
1033
+ const outputDir = resolve(nuxt.options.rootDir, nuxt.options.nitro.output?.dir || ".output");
1034
+ if (nuxt.options.vite.root === nuxt.options.srcDir) nuxt.options.vite.root = nuxt.options.rootDir;
1035
+ nuxt.options.vite.build ||= {};
1036
+ nuxt.options.vite.build.outDir ||= outputDir;
1037
+ const publicDir = resolve(outputDir, "public");
1038
+ const ssr = nuxt.options.ssr !== false;
1039
+ const prerender = isPrerendering(nuxt);
1040
+ const handler = ssr && !nuxt.options.dev ? resolve(outputDir, "server/index.mjs") : void 0;
1041
+ setServerBuild({
1042
+ name: "vite",
1043
+ label: ssr ? "Vite server" : "Vite SPA",
1044
+ output: {
1045
+ dir: () => outputDir,
1046
+ publicDir: () => publicDir
1047
+ },
1048
+ capabilities: {
1049
+ server: ssr && !prerender,
1050
+ dev: true
1051
+ },
1052
+ buildsSeparately: false,
1053
+ runtime: {
1054
+ runtimeConfig: resolve(nuxt.options.buildDir, "vite-server/runtime-config.mjs"),
1055
+ server: ssr ? resolve(nuxt.options.buildDir, "vite-server/server.mjs") : void 0,
1056
+ handler
1057
+ },
1058
+ preview: {
1059
+ command: () => handler && existsSync(handler) ? "node ./server/index.mjs" : void 0,
1060
+ staticDir: () => publicDir
1061
+ }
1062
+ }, nuxt);
1063
+ const server = ssr ? setupSSR(nuxt, outputDir) : void 0;
1064
+ if (server) addServerEntryAlias(nuxt, server.handler);
1065
+ warnExperimental(nuxt, {
1066
+ ssr,
1067
+ unsupported: server?.unsupported ?? []
1068
+ });
1069
+ nuxt.options.experimental.viteEnvironmentApi = true;
1070
+ addTemplate({
1071
+ filename: "vite-server/runtime-config.mjs",
1072
+ write: true,
1073
+ getContents: ({ nuxt }) => [`const config = ${JSON.stringify({
1074
+ app: nuxt.options.runtimeConfig.app,
1075
+ public: nuxt.options.runtimeConfig.public
1076
+ })}`, `export const useRuntimeConfig = () => globalThis.__NUXT__?.config || config`].join("\n")
1077
+ });
1078
+ setupAppManifest(nuxt, prerender);
1079
+ nuxt.options.vite.plugins ||= [];
1080
+ nuxt.options.vite.plugins.push(BuildEnvironmentsPlugin(nuxt), DevServerListenerPlugin(nuxt));
1081
+ if (!nuxt.options.dev) {
1082
+ const client = defu(nuxt.options.vite.$client, { build: {
1083
+ outDir: publicDir,
1084
+ emptyOutDir: true,
1085
+ rolldownOptions: { input: { index: documentPath(nuxt) } }
1086
+ } });
1087
+ if (resolve(nuxt.options.rootDir, client.build.outDir) !== publicDir) {
1088
+ bundlerDiagnostics.NUXT_B7024({
1089
+ outDir: client.build.outDir,
1090
+ publicDir
1091
+ });
1092
+ client.build.outDir = publicDir;
1093
+ }
1094
+ const input = client.build.rolldownOptions.input;
1095
+ if (typeof input !== "object" || Array.isArray(input)) {
1096
+ bundlerDiagnostics.NUXT_B7025({ input: JSON.stringify(input) });
1097
+ client.build.rolldownOptions.input = { index: documentPath(nuxt) };
1098
+ }
1099
+ nuxt.options.vite.$client = client;
1100
+ addVitePlugin(() => [DocumentPlugin(nuxt), EntryImportMapPlugin()], { server: false });
1101
+ }
1102
+ if (nuxt.options.dev) {
1103
+ addPlugin(resolve(nuxt.options.appDir, "plugins/dev-error-overlay.client"));
1104
+ setupDevServer(nuxt, server?.entry);
1105
+ } else nuxt.hook("build:done", async () => {
1106
+ await writeStaticOutput(nuxt, publicDir, {
1107
+ ssr,
1108
+ prerender
1109
+ });
1110
+ if (!prerender || !handler) return;
1111
+ const routes = await prerenderRoutes(nuxt, {
1112
+ publicDir,
1113
+ handler
1114
+ });
1115
+ if (nuxt.options.experimental.appManifest) await writeAppManifest(nuxt, publicDir, routes);
1116
+ await finishStaticOutput(outputDir, publicDir);
1117
+ });
1118
+ return Promise.resolve();
1119
+ }
1120
+ /**
1121
+ * Resolves `#server-entry` to the render as a module for a deploy target's own environment
1122
+ * to build, so that the app is compiled with that target's export conditions and nothing
1123
+ * spells a path inside the build directory.
1124
+ *
1125
+ * In development it resolves to a stub answering every request with a 503: the dev server
1126
+ * serves the app there, and a target rendering would render from a second module graph.
1127
+ */
1128
+ function addServerEntryAlias(nuxt, entry) {
1129
+ nuxt.options.alias["#server-entry"] = nuxt.options.dev ? resolve(distDir, "runtime/dev-handler") : entry;
1130
+ addTypeTemplate({
1131
+ filename: "types/server-entry.d.ts",
1132
+ getContents: () => [
1133
+ `declare module '#server-entry' {`,
1134
+ ` export const fetch: (request: Request) => Promise<Response>`,
1135
+ ` const handler: { fetch: typeof fetch }`,
1136
+ ` export default handler`,
1137
+ `}`,
1138
+ ""
1139
+ ].join("\n")
1140
+ }, {
1141
+ nuxt: true,
1142
+ node: true,
1143
+ shared: true
1144
+ });
1145
+ }
1146
+ /**
1147
+ * The manifest is a static asset naming the prerendered routes, so the client knows which
1148
+ * of them have a payload file. A build that does not prerender has nothing to put in it.
1149
+ */
1150
+ function setupAppManifest(nuxt, prerender) {
1151
+ if (!prerender || !nuxt.options.experimental.appManifest) {
1152
+ nuxt.options.experimental.appManifest = false;
1153
+ nuxt.options.alias["#app-manifest"] = resolveModulePath("mocked-exports/empty", { from: import.meta.url });
1154
+ return;
1155
+ }
1156
+ const buildId = nuxt.options.runtimeConfig.app.buildId ||= nuxt.options.buildId;
1157
+ const { dst } = addTemplate({
1158
+ filename: "vite-server/app-manifest.json",
1159
+ write: true,
1160
+ getContents: () => JSON.stringify({
1161
+ id: buildId,
1162
+ timestamp: manifestTimestamp(nuxt),
1163
+ prerendered: []
1164
+ })
1165
+ });
1166
+ nuxt.options.alias["#app-manifest"] = dst;
1167
+ nuxt.options.prerender.ignore ||= [];
1168
+ nuxt.options.prerender.ignore.push(joinURL(nuxt.options.app.baseURL, nuxt.options.app.buildAssetsDir, "builds"));
1169
+ }
1170
+ /**
1171
+ * Rules a request has to reach a server runtime to be answered: this builder brings none, so
1172
+ * nothing caches, proxies or negotiates CORS for a request it serves.
1173
+ */
1174
+ const SERVER_ONLY_RULES = /* @__PURE__ */ new Set([
1175
+ "cache",
1176
+ "swr",
1177
+ "isr",
1178
+ "proxy",
1179
+ "cors"
1180
+ ]);
1181
+ function warnExperimental(nuxt, build) {
1182
+ const unsupported = [...build.unsupported];
1183
+ if (nuxt.options.serverHandlers.length || getLayerDirectories(nuxt).some((dirs) => existsSync(dirs.server))) unsupported.push("server routes and server middleware");
1184
+ const routeRules = Object.values(nuxt.options.routeRules || {});
1185
+ const ignoredRules = [...new Set(routeRules.flatMap((rules) => Object.keys(rules || {}).filter((key) => SERVER_ONLY_RULES.has(key))))];
1186
+ if (ignoredRules.length) unsupported.push(`route rules needing an HTTP layer (${ignoredRules.sort().map((rule) => `\`${rule}\``).join(", ")})`);
1187
+ const wantsPrerender = nuxt.options.prerender.routes?.length || nuxt.options.prerender.crawlLinks || routeRules.some((rules) => rules?.prerender);
1188
+ if (!isPrerendering(nuxt) && wantsPrerender) unsupported.push("prerendering (run `nuxt generate` for a prerendered build)");
1189
+ if (nuxt.options.dev && nuxt.options.devServerHandlers.length) unsupported.push("dev server handlers");
1190
+ const what = !build.ssr ? "builds a static SPA and ships no server" : isPrerendering(nuxt) ? "prerenders with the Nuxt SSR renderer and ships no server" : "renders with the Nuxt SSR renderer and brings no server runtime of its own";
1191
+ logger.warn([`\`@nuxt/vite-server\` is experimental. It ${what}.`, unsupported.length ? ` Unsupported in this build, and ignored: ${unsupported.join(", ")}.` : ""].join(""));
1192
+ }
1193
+ //#endregion
1194
+ export { bundle };