@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/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.mjs +1194 -0
- package/dist/runtime/dev-error.mjs +92 -0
- package/dist/runtime/dev-handler.mjs +9 -0
- package/dist/runtime/event.mjs +16 -0
- package/dist/runtime/node.mjs +51 -0
- package/dist/runtime/prerender.mjs +58 -0
- package/dist/runtime/renderer.mjs +220 -0
- package/dist/runtime/server.mjs +15 -0
- package/package.json +47 -3
- package/dist/.gitkeep +0 -0
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
|
+
"<": "<",
|
|
273
|
+
">": ">",
|
|
274
|
+
"&": "&",
|
|
275
|
+
"'": "'",
|
|
276
|
+
""": "\""
|
|
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
|
+
"&": "&",
|
|
769
|
+
"<": "<",
|
|
770
|
+
">": ">",
|
|
771
|
+
"\"": """
|
|
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 };
|