vinext 0.0.25 → 0.0.26

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.
Files changed (86) hide show
  1. package/README.md +6 -1
  2. package/dist/check.js +4 -4
  3. package/dist/check.js.map +1 -1
  4. package/dist/cli.js +32 -1
  5. package/dist/cli.js.map +1 -1
  6. package/dist/client/entry.js.map +1 -1
  7. package/dist/client/vinext-next-data.d.ts +22 -0
  8. package/dist/client/vinext-next-data.d.ts.map +1 -0
  9. package/dist/client/vinext-next-data.js +2 -0
  10. package/dist/client/vinext-next-data.js.map +1 -0
  11. package/dist/config/config-matchers.d.ts.map +1 -1
  12. package/dist/config/config-matchers.js +6 -2
  13. package/dist/config/config-matchers.js.map +1 -1
  14. package/dist/config/next-config.d.ts +31 -4
  15. package/dist/config/next-config.d.ts.map +1 -1
  16. package/dist/config/next-config.js +151 -13
  17. package/dist/config/next-config.js.map +1 -1
  18. package/dist/deploy.d.ts +11 -0
  19. package/dist/deploy.d.ts.map +1 -1
  20. package/dist/deploy.js +42 -24
  21. package/dist/deploy.js.map +1 -1
  22. package/dist/entries/app-browser-entry.d.ts +9 -0
  23. package/dist/entries/app-browser-entry.d.ts.map +1 -0
  24. package/dist/entries/app-browser-entry.js +340 -0
  25. package/dist/entries/app-browser-entry.js.map +1 -0
  26. package/dist/{server/app-dev-server.d.ts → entries/app-rsc-entry.d.ts} +4 -17
  27. package/dist/entries/app-rsc-entry.d.ts.map +1 -0
  28. package/dist/{server/app-dev-server.js → entries/app-rsc-entry.js} +360 -1205
  29. package/dist/entries/app-rsc-entry.js.map +1 -0
  30. package/dist/entries/app-ssr-entry.d.ts +8 -0
  31. package/dist/entries/app-ssr-entry.d.ts.map +1 -0
  32. package/dist/entries/app-ssr-entry.js +449 -0
  33. package/dist/entries/app-ssr-entry.js.map +1 -0
  34. package/dist/entries/pages-client-entry.d.ts +4 -0
  35. package/dist/entries/pages-client-entry.d.ts.map +1 -0
  36. package/dist/entries/pages-client-entry.js +94 -0
  37. package/dist/entries/pages-client-entry.js.map +1 -0
  38. package/dist/entries/pages-entry-helpers.d.ts +7 -0
  39. package/dist/entries/pages-entry-helpers.d.ts.map +1 -0
  40. package/dist/entries/pages-entry-helpers.js +18 -0
  41. package/dist/entries/pages-entry-helpers.js.map +1 -0
  42. package/dist/entries/pages-server-entry.d.ts +8 -0
  43. package/dist/entries/pages-server-entry.d.ts.map +1 -0
  44. package/dist/entries/pages-server-entry.js +993 -0
  45. package/dist/entries/pages-server-entry.js.map +1 -0
  46. package/dist/index.d.ts +1 -25
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +206 -1242
  49. package/dist/index.js.map +1 -1
  50. package/dist/server/instrumentation.d.ts +1 -1
  51. package/dist/server/instrumentation.js +1 -1
  52. package/dist/server/instrumentation.js.map +1 -1
  53. package/dist/server/middleware-codegen.d.ts +1 -1
  54. package/dist/server/middleware-codegen.js +1 -1
  55. package/dist/server/middleware-codegen.js.map +1 -1
  56. package/dist/server/prod-server.d.ts.map +1 -1
  57. package/dist/server/prod-server.js +18 -3
  58. package/dist/server/prod-server.js.map +1 -1
  59. package/dist/server/request-pipeline.d.ts +92 -0
  60. package/dist/server/request-pipeline.d.ts.map +1 -0
  61. package/dist/server/request-pipeline.js +202 -0
  62. package/dist/server/request-pipeline.js.map +1 -0
  63. package/dist/shims/constants.d.ts +120 -3
  64. package/dist/shims/constants.d.ts.map +1 -1
  65. package/dist/shims/constants.js +170 -3
  66. package/dist/shims/constants.js.map +1 -1
  67. package/dist/shims/headers.d.ts.map +1 -1
  68. package/dist/shims/headers.js +1 -0
  69. package/dist/shims/headers.js.map +1 -1
  70. package/dist/shims/link.d.ts.map +1 -1
  71. package/dist/shims/link.js +2 -2
  72. package/dist/shims/link.js.map +1 -1
  73. package/dist/shims/metadata.d.ts +7 -1
  74. package/dist/shims/metadata.d.ts.map +1 -1
  75. package/dist/shims/metadata.js +9 -3
  76. package/dist/shims/metadata.js.map +1 -1
  77. package/dist/shims/og.d.ts +6 -6
  78. package/dist/shims/og.js +6 -6
  79. package/dist/shims/og.js.map +1 -1
  80. package/dist/utils/project.d.ts +15 -0
  81. package/dist/utils/project.d.ts.map +1 -1
  82. package/dist/utils/project.js +48 -0
  83. package/dist/utils/project.js.map +1 -1
  84. package/package.json +1 -1
  85. package/dist/server/app-dev-server.d.ts.map +0 -1
  86. package/dist/server/app-dev-server.js.map +0 -1
@@ -0,0 +1,993 @@
1
+ /**
2
+ * Pages Router server entry generator.
3
+ *
4
+ * Generates the virtual SSR server entry module (`virtual:vinext-server-entry`).
5
+ * This is the entry point for `vite build --ssr`. It handles SSR, API routes,
6
+ * middleware, ISR, and i18n for the Pages Router.
7
+ *
8
+ * Extracted from index.ts.
9
+ */
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { pagesRouter, apiRouter } from "../routing/pages-router.js";
13
+ import { isProxyFile } from "../server/middleware.js";
14
+ import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode, } from "../server/middleware-codegen.js";
15
+ import { findFileWithExts } from "./pages-entry-helpers.js";
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ /**
18
+ * Generate the virtual SSR server entry module.
19
+ * This is the entry point for `vite build --ssr`.
20
+ */
21
+ export async function generateServerEntry(pagesDir, nextConfig, fileMatcher, middlewarePath, instrumentationPath) {
22
+ const pageRoutes = await pagesRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
23
+ const apiRoutes = await apiRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
24
+ // Generate import statements using absolute paths since virtual
25
+ // modules don't have a real file location for relative resolution.
26
+ const pageImports = pageRoutes.map((r, i) => {
27
+ const absPath = r.filePath.replace(/\\/g, "/");
28
+ return `import * as page_${i} from ${JSON.stringify(absPath)};`;
29
+ });
30
+ const apiImports = apiRoutes.map((r, i) => {
31
+ const absPath = r.filePath.replace(/\\/g, "/");
32
+ return `import * as api_${i} from ${JSON.stringify(absPath)};`;
33
+ });
34
+ // Build the route table — include filePath for SSR manifest lookup
35
+ const pageRouteEntries = pageRoutes.map((r, i) => {
36
+ const absPath = r.filePath.replace(/\\/g, "/");
37
+ return ` { pattern: ${JSON.stringify(r.pattern)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: page_${i}, filePath: ${JSON.stringify(absPath)} }`;
38
+ });
39
+ const apiRouteEntries = apiRoutes.map((r, i) => {
40
+ return ` { pattern: ${JSON.stringify(r.pattern)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: api_${i} }`;
41
+ });
42
+ // Check for _app and _document
43
+ const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher);
44
+ const docFilePath = findFileWithExts(pagesDir, "_document", fileMatcher);
45
+ const hasApp = appFilePath !== null;
46
+ const hasDoc = docFilePath !== null;
47
+ const appImportCode = hasApp
48
+ ? `import { default as AppComponent } from ${JSON.stringify(appFilePath.replace(/\\/g, "/"))};`
49
+ : `const AppComponent = null;`;
50
+ const docImportCode = hasDoc
51
+ ? `import { default as DocumentComponent } from ${JSON.stringify(docFilePath.replace(/\\/g, "/"))};`
52
+ : `const DocumentComponent = null;`;
53
+ // Serialize i18n config for embedding in the server entry
54
+ const i18nConfigJson = nextConfig?.i18n
55
+ ? JSON.stringify({
56
+ locales: nextConfig.i18n.locales,
57
+ defaultLocale: nextConfig.i18n.defaultLocale,
58
+ localeDetection: nextConfig.i18n.localeDetection,
59
+ })
60
+ : "null";
61
+ // Serialize the full resolved config for the production server.
62
+ // This embeds redirects, rewrites, headers, basePath, trailingSlash
63
+ // so prod-server.ts can apply them without loading next.config.js at runtime.
64
+ const vinextConfigJson = JSON.stringify({
65
+ basePath: nextConfig?.basePath ?? "",
66
+ trailingSlash: nextConfig?.trailingSlash ?? false,
67
+ redirects: nextConfig?.redirects ?? [],
68
+ rewrites: nextConfig?.rewrites ?? { beforeFiles: [], afterFiles: [], fallback: [] },
69
+ headers: nextConfig?.headers ?? [],
70
+ i18n: nextConfig?.i18n ?? null,
71
+ images: {
72
+ deviceSizes: nextConfig?.images?.deviceSizes,
73
+ imageSizes: nextConfig?.images?.imageSizes,
74
+ dangerouslyAllowSVG: nextConfig?.images?.dangerouslyAllowSVG,
75
+ contentDispositionType: nextConfig?.images?.contentDispositionType,
76
+ contentSecurityPolicy: nextConfig?.images?.contentSecurityPolicy,
77
+ },
78
+ });
79
+ // Generate instrumentation code if instrumentation.ts exists.
80
+ // For production (Cloudflare Workers), instrumentation.ts is bundled into the
81
+ // Worker and register() is called as a top-level await at module evaluation time —
82
+ // before any request is handled. This mirrors App Router behavior (generateRscEntry)
83
+ // and matches Next.js semantics: register() runs once on startup in the process
84
+ // that handles requests.
85
+ //
86
+ // The onRequestError handler is stored on globalThis so it is visible across
87
+ // all code within the Worker (same global scope).
88
+ const instrumentationImportCode = instrumentationPath
89
+ ? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};`
90
+ : "";
91
+ const instrumentationInitCode = instrumentationPath
92
+ ? `// Run instrumentation register() once at module evaluation time — before any
93
+ // requests are handled. Matches Next.js semantics: register() is called once
94
+ // on startup in the process that handles requests.
95
+ if (typeof _instrumentation.register === "function") {
96
+ await _instrumentation.register();
97
+ }
98
+ // Store the onRequestError handler on globalThis so it is visible to all
99
+ // code within the Worker (same global scope).
100
+ if (typeof _instrumentation.onRequestError === "function") {
101
+ globalThis.__VINEXT_onRequestErrorHandler__ = _instrumentation.onRequestError;
102
+ }`
103
+ : "";
104
+ // Generate middleware code if middleware.ts exists
105
+ const middlewareImportCode = middlewarePath
106
+ ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};
107
+ import { NextRequest, NextFetchEvent } from "next/server";`
108
+ : "";
109
+ // The matcher config is read from the middleware module at import time.
110
+ // We inline the matching + execution logic so the prod server can call it.
111
+ const middlewareExportCode = middlewarePath
112
+ ? `
113
+ // --- Middleware support (generated from middleware-codegen.ts) ---
114
+ ${generateNormalizePathCode("es5")}
115
+ ${generateSafeRegExpCode("es5")}
116
+ ${generateMiddlewareMatcherCode("es5")}
117
+
118
+ export async function runMiddleware(request, ctx) {
119
+ var isProxy = ${middlewarePath ? JSON.stringify(isProxyFile(middlewarePath)) : "false"};
120
+ var middlewareFn = isProxy
121
+ ? (middlewareModule.proxy ?? middlewareModule.default)
122
+ : (middlewareModule.middleware ?? middlewareModule.default);
123
+ if (typeof middlewareFn !== "function") {
124
+ var fileType = isProxy ? "Proxy" : "Middleware";
125
+ var expectedExport = isProxy ? "proxy" : "middleware";
126
+ throw new Error("The " + fileType + " file must export a function named \`" + expectedExport + "\` or a \`default\` function.");
127
+ }
128
+
129
+ var config = middlewareModule.config;
130
+ var matcher = config && config.matcher;
131
+ var url = new URL(request.url);
132
+
133
+ // Normalize pathname before matching to prevent path-confusion bypasses
134
+ // (percent-encoding like /%61dmin, double slashes like /dashboard//settings).
135
+ var decodedPathname;
136
+ try { decodedPathname = decodeURIComponent(url.pathname); } catch (e) {
137
+ return { continue: false, response: new Response("Bad Request", { status: 400 }) };
138
+ }
139
+ var normalizedPathname = __normalizePath(decodedPathname);
140
+
141
+ if (!matchesMiddleware(normalizedPathname, matcher)) return { continue: true };
142
+
143
+ // Construct a new Request with the decoded + normalized pathname so middleware
144
+ // always sees the same canonical path that the router uses.
145
+ var mwRequest = request;
146
+ if (normalizedPathname !== url.pathname) {
147
+ var mwUrl = new URL(url);
148
+ mwUrl.pathname = normalizedPathname;
149
+ mwRequest = new Request(mwUrl, request);
150
+ }
151
+ var nextRequest = mwRequest instanceof NextRequest ? mwRequest : new NextRequest(mwRequest);
152
+ var fetchEvent = new NextFetchEvent({ page: normalizedPathname });
153
+ var response;
154
+ try { response = await middlewareFn(nextRequest, fetchEvent); }
155
+ catch (e) {
156
+ console.error("[vinext] Middleware error:", e);
157
+ return { continue: false, response: new Response("Internal Server Error", { status: 500 }) };
158
+ }
159
+ if (ctx && typeof ctx.waitUntil === "function") { ctx.waitUntil(fetchEvent.drainWaitUntil()); } else { fetchEvent.drainWaitUntil(); }
160
+
161
+ if (!response) return { continue: true };
162
+
163
+ if (response.headers.get("x-middleware-next") === "1") {
164
+ var rHeaders = new Headers();
165
+ for (var [key, value] of response.headers) {
166
+ // Keep x-middleware-request-* headers so the production server can
167
+ // apply middleware-request header overrides before stripping internals
168
+ // from the final client response.
169
+ if (
170
+ !key.startsWith("x-middleware-") ||
171
+ key.startsWith("x-middleware-request-")
172
+ ) rHeaders.append(key, value);
173
+ }
174
+ return { continue: true, responseHeaders: rHeaders };
175
+ }
176
+
177
+ if (response.status >= 300 && response.status < 400) {
178
+ var location = response.headers.get("Location") || response.headers.get("location");
179
+ if (location) {
180
+ var rdHeaders = new Headers();
181
+ for (var [rk, rv] of response.headers) {
182
+ if (!rk.startsWith("x-middleware-") && rk.toLowerCase() !== "location") rdHeaders.append(rk, rv);
183
+ }
184
+ return { continue: false, redirectUrl: location, redirectStatus: response.status, responseHeaders: rdHeaders };
185
+ }
186
+ }
187
+
188
+ var rewriteUrl = response.headers.get("x-middleware-rewrite");
189
+ if (rewriteUrl) {
190
+ var rwHeaders = new Headers();
191
+ for (var [k, v] of response.headers) {
192
+ if (!k.startsWith("x-middleware-") || k.startsWith("x-middleware-request-")) rwHeaders.append(k, v);
193
+ }
194
+ var rewritePath;
195
+ try { var parsed = new URL(rewriteUrl, request.url); rewritePath = parsed.pathname + parsed.search; }
196
+ catch { rewritePath = rewriteUrl; }
197
+ return { continue: true, rewriteUrl: rewritePath, rewriteStatus: response.status !== 200 ? response.status : undefined, responseHeaders: rwHeaders };
198
+ }
199
+
200
+ return { continue: false, response: response };
201
+ }
202
+ `
203
+ : `
204
+ export async function runMiddleware() { return { continue: true }; }
205
+ `;
206
+ // The server entry is a self-contained module that uses Web-standard APIs
207
+ // (Request/Response, renderToReadableStream) so it runs on Cloudflare Workers.
208
+ return `
209
+ import React from "react";
210
+ import { renderToReadableStream } from "react-dom/server.edge";
211
+ import { resetSSRHead, getSSRHeadHTML } from "next/head";
212
+ import { flushPreloads } from "next/dynamic";
213
+ import { setSSRContext, wrapWithRouterContext } from "next/router";
214
+ import { getCacheHandler } from "next/cache";
215
+ import { runWithFetchCache } from "vinext/fetch-cache";
216
+ import { _runWithCacheState } from "next/cache";
217
+ import { runWithPrivateCache } from "vinext/cache-runtime";
218
+ import { runWithRouterState } from "vinext/router-state";
219
+ import { runWithHeadState } from "vinext/head-state";
220
+ import { safeJsonStringify } from "vinext/html";
221
+ import { getSSRFontLinks as _getSSRFontLinks, getSSRFontStyles as _getSSRFontStylesGoogle, getSSRFontPreloads as _getSSRFontPreloadsGoogle } from "next/font/google";
222
+ import { getSSRFontStyles as _getSSRFontStylesLocal, getSSRFontPreloads as _getSSRFontPreloadsLocal } from "next/font/local";
223
+ import { parseCookies } from ${JSON.stringify(path.resolve(__dirname, "../config/config-matchers.js").replace(/\\/g, "/"))};
224
+ ${instrumentationImportCode}
225
+ ${middlewareImportCode}
226
+
227
+ ${instrumentationInitCode}
228
+
229
+ // i18n config (embedded at build time)
230
+ const i18nConfig = ${i18nConfigJson};
231
+
232
+ // Full resolved config for production server (embedded at build time)
233
+ export const vinextConfig = ${vinextConfigJson};
234
+
235
+ // ISR cache helpers (inlined for the server entry)
236
+ async function isrGet(key) {
237
+ const handler = getCacheHandler();
238
+ const result = await handler.get(key);
239
+ if (!result || !result.value) return null;
240
+ return { value: result, isStale: result.cacheState === "stale" };
241
+ }
242
+ async function isrSet(key, data, revalidateSeconds, tags) {
243
+ const handler = getCacheHandler();
244
+ await handler.set(key, data, { revalidate: revalidateSeconds, tags: tags || [] });
245
+ }
246
+ const pendingRegenerations = new Map();
247
+ function triggerBackgroundRegeneration(key, renderFn) {
248
+ if (pendingRegenerations.has(key)) return;
249
+ const promise = renderFn()
250
+ .catch((err) => console.error("[vinext] ISR regen failed for " + key + ":", err))
251
+ .finally(() => pendingRegenerations.delete(key));
252
+ pendingRegenerations.set(key, promise);
253
+ }
254
+
255
+ async function renderToStringAsync(element) {
256
+ const stream = await renderToReadableStream(element);
257
+ await stream.allReady;
258
+ return new Response(stream).text();
259
+ }
260
+
261
+ ${pageImports.join("\n")}
262
+ ${apiImports.join("\n")}
263
+
264
+ ${appImportCode}
265
+ ${docImportCode}
266
+
267
+ const pageRoutes = [
268
+ ${pageRouteEntries.join(",\n")}
269
+ ];
270
+
271
+ const apiRoutes = [
272
+ ${apiRouteEntries.join(",\n")}
273
+ ];
274
+
275
+ function matchRoute(url, routes) {
276
+ const pathname = url.split("?")[0];
277
+ let normalizedUrl = pathname === "/" ? "/" : pathname.replace(/\\/$/, "");
278
+ // NOTE: Do NOT decodeURIComponent here. The pathname is already decoded at
279
+ // the entry point. Decoding again would create a double-decode vector.
280
+ for (const route of routes) {
281
+ const params = matchPattern(normalizedUrl, route.pattern);
282
+ if (params !== null) return { route, params };
283
+ }
284
+ return null;
285
+ }
286
+
287
+ function matchPattern(url, pattern) {
288
+ const urlParts = url.split("/").filter(Boolean);
289
+ const patternParts = pattern.split("/").filter(Boolean);
290
+ const params = Object.create(null);
291
+ for (let i = 0; i < patternParts.length; i++) {
292
+ const pp = patternParts[i];
293
+ if (pp.endsWith("+")) {
294
+ const paramName = pp.slice(1, -1);
295
+ const remaining = urlParts.slice(i);
296
+ if (remaining.length === 0) return null;
297
+ params[paramName] = remaining;
298
+ return params;
299
+ }
300
+ if (pp.endsWith("*")) {
301
+ const paramName = pp.slice(1, -1);
302
+ params[paramName] = urlParts.slice(i);
303
+ return params;
304
+ }
305
+ if (pp.startsWith(":")) {
306
+ if (i >= urlParts.length) return null;
307
+ params[pp.slice(1)] = urlParts[i];
308
+ continue;
309
+ }
310
+ if (i >= urlParts.length || urlParts[i] !== pp) return null;
311
+ }
312
+ if (urlParts.length !== patternParts.length) return null;
313
+ return params;
314
+ }
315
+
316
+ function parseQuery(url) {
317
+ const qs = url.split("?")[1];
318
+ if (!qs) return {};
319
+ const p = new URLSearchParams(qs);
320
+ const q = {};
321
+ for (const [k, v] of p) {
322
+ if (k in q) {
323
+ q[k] = Array.isArray(q[k]) ? q[k].concat(v) : [q[k], v];
324
+ } else {
325
+ q[k] = v;
326
+ }
327
+ }
328
+ return q;
329
+ }
330
+
331
+ function patternToNextFormat(pattern) {
332
+ return pattern
333
+ .replace(/:([\\w]+)\\*/g, "[[...$1]]")
334
+ .replace(/:([\\w]+)\\+/g, "[...$1]")
335
+ .replace(/:([\\w]+)/g, "[$1]");
336
+ }
337
+
338
+ function collectAssetTags(manifest, moduleIds) {
339
+ // Fall back to embedded manifest (set by vinext:cloudflare-build for Workers)
340
+ const m = (manifest && Object.keys(manifest).length > 0)
341
+ ? manifest
342
+ : (typeof globalThis !== "undefined" && globalThis.__VINEXT_SSR_MANIFEST__) || null;
343
+ const tags = [];
344
+ const seen = new Set();
345
+
346
+ // Load the set of lazy chunk filenames (only reachable via dynamic imports).
347
+ // These should NOT get <link rel="modulepreload"> or <script type="module">
348
+ // tags — they are fetched on demand when the dynamic import() executes (e.g.
349
+ // chunks behind React.lazy() or next/dynamic boundaries).
350
+ var lazyChunks = (typeof globalThis !== "undefined" && globalThis.__VINEXT_LAZY_CHUNKS__) || null;
351
+ var lazySet = lazyChunks && lazyChunks.length > 0 ? new Set(lazyChunks) : null;
352
+
353
+ // Inject the client entry script if embedded by vinext:cloudflare-build
354
+ if (typeof globalThis !== "undefined" && globalThis.__VINEXT_CLIENT_ENTRY__) {
355
+ const entry = globalThis.__VINEXT_CLIENT_ENTRY__;
356
+ seen.add(entry);
357
+ tags.push('<link rel="modulepreload" href="/' + entry + '" />');
358
+ tags.push('<script type="module" src="/' + entry + '" crossorigin></script>');
359
+ }
360
+ if (m) {
361
+ // Always inject shared chunks (framework, vinext runtime, entry) and
362
+ // page-specific chunks. The manifest maps module file paths to their
363
+ // associated JS/CSS assets.
364
+ //
365
+ // For page-specific injection, the module IDs may be absolute paths
366
+ // while the manifest uses relative paths. Try both the original ID
367
+ // and a suffix match to find the correct manifest entry.
368
+ var allFiles = [];
369
+
370
+ if (moduleIds && moduleIds.length > 0) {
371
+ // Collect assets for the requested page modules
372
+ for (var mi = 0; mi < moduleIds.length; mi++) {
373
+ var id = moduleIds[mi];
374
+ var files = m[id];
375
+ if (!files) {
376
+ // Absolute path didn't match — try matching by suffix.
377
+ // Manifest keys are relative (e.g. "pages/about.tsx") while
378
+ // moduleIds may be absolute (e.g. "/home/.../pages/about.tsx").
379
+ for (var mk in m) {
380
+ if (id.endsWith("/" + mk) || id === mk) {
381
+ files = m[mk];
382
+ break;
383
+ }
384
+ }
385
+ }
386
+ if (files) {
387
+ for (var fi = 0; fi < files.length; fi++) allFiles.push(files[fi]);
388
+ }
389
+ }
390
+
391
+ // Also inject shared chunks that every page needs: framework,
392
+ // vinext runtime, and the entry bootstrap. These are identified
393
+ // by scanning all manifest values for chunk filenames containing
394
+ // known prefixes.
395
+ for (var key in m) {
396
+ var vals = m[key];
397
+ if (!vals) continue;
398
+ for (var vi = 0; vi < vals.length; vi++) {
399
+ var file = vals[vi];
400
+ var basename = file.split("/").pop() || "";
401
+ if (
402
+ basename.startsWith("framework-") ||
403
+ basename.startsWith("vinext-") ||
404
+ basename.includes("vinext-client-entry") ||
405
+ basename.includes("vinext-app-browser-entry")
406
+ ) {
407
+ allFiles.push(file);
408
+ }
409
+ }
410
+ }
411
+ } else {
412
+ // No specific modules — include all assets from manifest
413
+ for (var akey in m) {
414
+ var avals = m[akey];
415
+ if (avals) {
416
+ for (var ai = 0; ai < avals.length; ai++) allFiles.push(avals[ai]);
417
+ }
418
+ }
419
+ }
420
+
421
+ for (var ti = 0; ti < allFiles.length; ti++) {
422
+ var tf = allFiles[ti];
423
+ // Normalize: Vite's SSR manifest values include a leading '/'
424
+ // (from base path), but we prepend '/' ourselves when building
425
+ // href/src attributes. Strip any existing leading slash to avoid
426
+ // producing protocol-relative URLs like "//assets/chunk.js".
427
+ // This also ensures consistent keys for the seen-set dedup and
428
+ // lazySet.has() checks (which use values without leading slash).
429
+ if (tf.charAt(0) === '/') tf = tf.slice(1);
430
+ if (seen.has(tf)) continue;
431
+ seen.add(tf);
432
+ if (tf.endsWith(".css")) {
433
+ tags.push('<link rel="stylesheet" href="/' + tf + '" />');
434
+ } else if (tf.endsWith(".js")) {
435
+ // Skip lazy chunks — they are behind dynamic import() boundaries
436
+ // (React.lazy, next/dynamic) and should only be fetched on demand.
437
+ if (lazySet && lazySet.has(tf)) continue;
438
+ tags.push('<link rel="modulepreload" href="/' + tf + '" />');
439
+ tags.push('<script type="module" src="/' + tf + '" crossorigin></script>');
440
+ }
441
+ }
442
+ }
443
+ return tags.join("\\n ");
444
+ }
445
+
446
+ // i18n helpers
447
+ function extractLocale(url) {
448
+ if (!i18nConfig) return { locale: undefined, url, hadPrefix: false };
449
+ const pathname = url.split("?")[0];
450
+ const parts = pathname.split("/").filter(Boolean);
451
+ const query = url.includes("?") ? url.slice(url.indexOf("?")) : "";
452
+ if (parts.length > 0 && i18nConfig.locales.includes(parts[0])) {
453
+ const locale = parts[0];
454
+ const rest = "/" + parts.slice(1).join("/");
455
+ return { locale, url: (rest || "/") + query, hadPrefix: true };
456
+ }
457
+ return { locale: i18nConfig.defaultLocale, url, hadPrefix: false };
458
+ }
459
+
460
+ function detectLocaleFromHeaders(headers) {
461
+ if (!i18nConfig) return null;
462
+ const acceptLang = headers.get("accept-language");
463
+ if (!acceptLang) return null;
464
+ const langs = acceptLang.split(",").map(function(part) {
465
+ const pieces = part.trim().split(";");
466
+ const q = pieces[1] ? parseFloat(pieces[1].replace("q=", "")) : 1;
467
+ return { lang: pieces[0].trim().toLowerCase(), q: q };
468
+ }).sort(function(a, b) { return b.q - a.q; });
469
+ for (let k = 0; k < langs.length; k++) {
470
+ const lang = langs[k].lang;
471
+ for (let j = 0; j < i18nConfig.locales.length; j++) {
472
+ if (i18nConfig.locales[j].toLowerCase() === lang) return i18nConfig.locales[j];
473
+ }
474
+ const prefix = lang.split("-")[0];
475
+ for (let j = 0; j < i18nConfig.locales.length; j++) {
476
+ const loc = i18nConfig.locales[j].toLowerCase();
477
+ if (loc === prefix || loc.startsWith(prefix + "-")) return i18nConfig.locales[j];
478
+ }
479
+ }
480
+ return null;
481
+ }
482
+
483
+ function parseCookieLocaleFromHeader(cookieHeader) {
484
+ if (!i18nConfig || !cookieHeader) return null;
485
+ const match = cookieHeader.match(/(?:^|;\\s*)NEXT_LOCALE=([^;]*)/);
486
+ if (!match) return null;
487
+ var value;
488
+ try { value = decodeURIComponent(match[1].trim()); } catch (e) { return null; }
489
+ if (i18nConfig.locales.indexOf(value) !== -1) return value;
490
+ return null;
491
+ }
492
+
493
+ // Lightweight req/res facade for getServerSideProps and API routes.
494
+ // Next.js pages expect ctx.req/ctx.res with Node-like shapes.
495
+ function createReqRes(request, url, query, body) {
496
+ const headersObj = {};
497
+ for (const [k, v] of request.headers) headersObj[k.toLowerCase()] = v;
498
+
499
+ const req = {
500
+ method: request.method,
501
+ url: url,
502
+ headers: headersObj,
503
+ query: query,
504
+ body: body,
505
+ cookies: parseCookies(request.headers.get("cookie")),
506
+ };
507
+
508
+ let resStatusCode = 200;
509
+ const resHeaders = {};
510
+ // set-cookie needs array support (multiple Set-Cookie headers are common)
511
+ const setCookieHeaders = [];
512
+ let resBody = null;
513
+ let ended = false;
514
+ let resolveResponse;
515
+ const responsePromise = new Promise(function(r) { resolveResponse = r; });
516
+
517
+ const res = {
518
+ get statusCode() { return resStatusCode; },
519
+ set statusCode(code) { resStatusCode = code; },
520
+ writeHead: function(code, headers) {
521
+ resStatusCode = code;
522
+ if (headers) {
523
+ for (const [k, v] of Object.entries(headers)) {
524
+ if (k.toLowerCase() === "set-cookie") {
525
+ if (Array.isArray(v)) { for (const c of v) setCookieHeaders.push(c); }
526
+ else { setCookieHeaders.push(v); }
527
+ } else {
528
+ resHeaders[k] = v;
529
+ }
530
+ }
531
+ }
532
+ return res;
533
+ },
534
+ setHeader: function(name, value) {
535
+ if (name.toLowerCase() === "set-cookie") {
536
+ if (Array.isArray(value)) { for (const c of value) setCookieHeaders.push(c); }
537
+ else { setCookieHeaders.push(value); }
538
+ } else {
539
+ resHeaders[name.toLowerCase()] = value;
540
+ }
541
+ return res;
542
+ },
543
+ getHeader: function(name) {
544
+ if (name.toLowerCase() === "set-cookie") return setCookieHeaders.length > 0 ? setCookieHeaders : undefined;
545
+ return resHeaders[name.toLowerCase()];
546
+ },
547
+ end: function(data) {
548
+ if (ended) return;
549
+ ended = true;
550
+ if (data !== undefined && data !== null) resBody = data;
551
+ const h = new Headers(resHeaders);
552
+ for (const c of setCookieHeaders) h.append("set-cookie", c);
553
+ resolveResponse(new Response(resBody, { status: resStatusCode, headers: h }));
554
+ },
555
+ status: function(code) { resStatusCode = code; return res; },
556
+ json: function(data) {
557
+ resHeaders["content-type"] = "application/json";
558
+ res.end(JSON.stringify(data));
559
+ },
560
+ send: function(data) {
561
+ if (typeof data === "object" && data !== null) { res.json(data); }
562
+ else { if (!resHeaders["content-type"]) resHeaders["content-type"] = "text/plain"; res.end(String(data)); }
563
+ },
564
+ redirect: function(statusOrUrl, url2) {
565
+ if (typeof statusOrUrl === "string") { res.writeHead(307, { Location: statusOrUrl }); }
566
+ else { res.writeHead(statusOrUrl, { Location: url2 }); }
567
+ res.end();
568
+ },
569
+ getHeaders: function() {
570
+ var h = Object.assign({}, resHeaders);
571
+ if (setCookieHeaders.length > 0) h["set-cookie"] = setCookieHeaders;
572
+ return h;
573
+ },
574
+ get headersSent() { return ended; },
575
+ };
576
+
577
+ return { req, res, responsePromise };
578
+ }
579
+
580
+ /**
581
+ * Read request body as text with a size limit.
582
+ * Throws if the body exceeds maxBytes. This prevents DoS via chunked
583
+ * transfer encoding where Content-Length is absent or spoofed.
584
+ */
585
+ async function readBodyWithLimit(request, maxBytes) {
586
+ if (!request.body) return "";
587
+ var reader = request.body.getReader();
588
+ var decoder = new TextDecoder();
589
+ var chunks = [];
590
+ var totalSize = 0;
591
+ for (;;) {
592
+ var result = await reader.read();
593
+ if (result.done) break;
594
+ totalSize += result.value.byteLength;
595
+ if (totalSize > maxBytes) {
596
+ reader.cancel();
597
+ throw new Error("Request body too large");
598
+ }
599
+ chunks.push(decoder.decode(result.value, { stream: true }));
600
+ }
601
+ chunks.push(decoder.decode());
602
+ return chunks.join("");
603
+ }
604
+
605
+ export async function renderPage(request, url, manifest) {
606
+ const localeInfo = extractLocale(url);
607
+ const locale = localeInfo.locale;
608
+ const routeUrl = localeInfo.url;
609
+ const cookieHeader = request.headers.get("cookie") || "";
610
+
611
+ // i18n redirect: check NEXT_LOCALE cookie first, then Accept-Language
612
+ if (i18nConfig && !localeInfo.hadPrefix) {
613
+ const cookieLocale = parseCookieLocaleFromHeader(cookieHeader);
614
+ if (cookieLocale && cookieLocale !== i18nConfig.defaultLocale) {
615
+ return new Response(null, { status: 307, headers: { Location: "/" + cookieLocale + routeUrl } });
616
+ }
617
+ if (!cookieLocale && i18nConfig.localeDetection !== false) {
618
+ const detected = detectLocaleFromHeaders(request.headers);
619
+ if (detected && detected !== i18nConfig.defaultLocale) {
620
+ return new Response(null, { status: 307, headers: { Location: "/" + detected + routeUrl } });
621
+ }
622
+ }
623
+ }
624
+
625
+ const match = matchRoute(routeUrl, pageRoutes);
626
+ if (!match) {
627
+ return new Response("<!DOCTYPE html><html><body><h1>404 - Page not found</h1></body></html>",
628
+ { status: 404, headers: { "Content-Type": "text/html" } });
629
+ }
630
+
631
+ const { route, params } = match;
632
+ return runWithRouterState(() =>
633
+ runWithHeadState(() =>
634
+ _runWithCacheState(() =>
635
+ runWithPrivateCache(() =>
636
+ runWithFetchCache(async () => {
637
+ try {
638
+ if (typeof setSSRContext === "function") {
639
+ setSSRContext({
640
+ pathname: routeUrl.split("?")[0],
641
+ query: { ...params, ...parseQuery(routeUrl) },
642
+ asPath: routeUrl,
643
+ locale: locale,
644
+ locales: i18nConfig ? i18nConfig.locales : undefined,
645
+ defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
646
+ });
647
+ }
648
+
649
+ if (i18nConfig) {
650
+ globalThis.__VINEXT_LOCALE__ = locale;
651
+ globalThis.__VINEXT_LOCALES__ = i18nConfig.locales;
652
+ globalThis.__VINEXT_DEFAULT_LOCALE__ = i18nConfig.defaultLocale;
653
+ }
654
+
655
+ const pageModule = route.module;
656
+ const PageComponent = pageModule.default;
657
+ if (!PageComponent) {
658
+ return new Response("Page has no default export", { status: 500 });
659
+ }
660
+
661
+ // Handle getStaticPaths for dynamic routes
662
+ if (typeof pageModule.getStaticPaths === "function" && route.isDynamic) {
663
+ const pathsResult = await pageModule.getStaticPaths({
664
+ locales: i18nConfig ? i18nConfig.locales : [],
665
+ defaultLocale: i18nConfig ? i18nConfig.defaultLocale : "",
666
+ });
667
+ const fallback = pathsResult && pathsResult.fallback !== undefined ? pathsResult.fallback : false;
668
+
669
+ if (fallback === false) {
670
+ const paths = pathsResult && pathsResult.paths ? pathsResult.paths : [];
671
+ const isValidPath = paths.some(function(p) {
672
+ return Object.entries(p.params).every(function(entry) {
673
+ var key = entry[0], val = entry[1];
674
+ var actual = params[key];
675
+ if (Array.isArray(val)) {
676
+ return Array.isArray(actual) && val.join("/") === actual.join("/");
677
+ }
678
+ return String(val) === String(actual);
679
+ });
680
+ });
681
+ if (!isValidPath) {
682
+ return new Response("<!DOCTYPE html><html><body><h1>404 - Page not found</h1></body></html>",
683
+ { status: 404, headers: { "Content-Type": "text/html" } });
684
+ }
685
+ }
686
+ }
687
+
688
+ let pageProps = {};
689
+ var gsspRes = null;
690
+ if (typeof pageModule.getServerSideProps === "function") {
691
+ const { req, res, responsePromise } = createReqRes(request, routeUrl, parseQuery(routeUrl), undefined);
692
+ const ctx = {
693
+ params, req, res,
694
+ query: parseQuery(routeUrl),
695
+ resolvedUrl: routeUrl,
696
+ locale: locale,
697
+ locales: i18nConfig ? i18nConfig.locales : undefined,
698
+ defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
699
+ };
700
+ const result = await pageModule.getServerSideProps(ctx);
701
+ // If gSSP called res.end() directly (short-circuit), return that response.
702
+ if (res.headersSent) {
703
+ return await responsePromise;
704
+ }
705
+ if (result && result.props) pageProps = result.props;
706
+ if (result && result.redirect) {
707
+ var gsspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
708
+ return new Response(null, { status: gsspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
709
+ }
710
+ if (result && result.notFound) {
711
+ return new Response("404", { status: 404 });
712
+ }
713
+ // Preserve the res object so headers/status/cookies set by gSSP
714
+ // can be merged into the final HTML response.
715
+ gsspRes = res;
716
+ }
717
+ // Build font Link header early so it's available for ISR cached responses too.
718
+ // Font preloads are module-level state populated at import time and persist across requests.
719
+ var _fontLinkHeader = "";
720
+ var _allFp = [];
721
+ try {
722
+ var _fpGoogle = typeof _getSSRFontPreloadsGoogle === "function" ? _getSSRFontPreloadsGoogle() : [];
723
+ var _fpLocal = typeof _getSSRFontPreloadsLocal === "function" ? _getSSRFontPreloadsLocal() : [];
724
+ _allFp = _fpGoogle.concat(_fpLocal);
725
+ if (_allFp.length > 0) {
726
+ _fontLinkHeader = _allFp.map(function(p) { return "<" + p.href + ">; rel=preload; as=font; type=" + p.type + "; crossorigin"; }).join(", ");
727
+ }
728
+ } catch (e) { /* font preloads not available */ }
729
+
730
+ let isrRevalidateSeconds = null;
731
+ if (typeof pageModule.getStaticProps === "function") {
732
+ const pathname = routeUrl.split("?")[0];
733
+ const cacheKey = "pages:" + (pathname === "/" ? "/" : pathname.replace(/\\/$/, ""));
734
+ const cached = await isrGet(cacheKey);
735
+
736
+ if (cached && !cached.isStale && cached.value.value && cached.value.value.kind === "PAGES") {
737
+ var _hitHeaders = {
738
+ "Content-Type": "text/html", "X-Vinext-Cache": "HIT",
739
+ "Cache-Control": "s-maxage=" + (cached.value.value.revalidate || 60) + ", stale-while-revalidate",
740
+ };
741
+ if (_fontLinkHeader) _hitHeaders["Link"] = _fontLinkHeader;
742
+ return new Response(cached.value.value.html, { status: 200, headers: _hitHeaders });
743
+ }
744
+
745
+ if (cached && cached.isStale && cached.value.value && cached.value.value.kind === "PAGES") {
746
+ triggerBackgroundRegeneration(cacheKey, async function() {
747
+ const freshResult = await pageModule.getStaticProps({ params });
748
+ if (freshResult && freshResult.props && typeof freshResult.revalidate === "number" && freshResult.revalidate > 0) {
749
+ await isrSet(cacheKey, { kind: "PAGES", html: cached.value.value.html, pageData: freshResult.props, headers: undefined, status: undefined }, freshResult.revalidate);
750
+ }
751
+ });
752
+ var _staleHeaders = {
753
+ "Content-Type": "text/html", "X-Vinext-Cache": "STALE",
754
+ "Cache-Control": "s-maxage=0, stale-while-revalidate",
755
+ };
756
+ if (_fontLinkHeader) _staleHeaders["Link"] = _fontLinkHeader;
757
+ return new Response(cached.value.value.html, { status: 200, headers: _staleHeaders });
758
+ }
759
+
760
+ const ctx = {
761
+ params,
762
+ locale: locale,
763
+ locales: i18nConfig ? i18nConfig.locales : undefined,
764
+ defaultLocale: i18nConfig ? i18nConfig.defaultLocale : undefined,
765
+ };
766
+ const result = await pageModule.getStaticProps(ctx);
767
+ if (result && result.props) pageProps = result.props;
768
+ if (result && result.redirect) {
769
+ var gspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
770
+ return new Response(null, { status: gspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
771
+ }
772
+ if (result && result.notFound) {
773
+ return new Response("404", { status: 404 });
774
+ }
775
+ if (typeof result.revalidate === "number" && result.revalidate > 0) {
776
+ isrRevalidateSeconds = result.revalidate;
777
+ }
778
+ }
779
+
780
+ let element;
781
+ if (AppComponent) {
782
+ element = React.createElement(AppComponent, { Component: PageComponent, pageProps });
783
+ } else {
784
+ element = React.createElement(PageComponent, pageProps);
785
+ }
786
+ element = wrapWithRouterContext(element);
787
+
788
+ if (typeof resetSSRHead === "function") resetSSRHead();
789
+ if (typeof flushPreloads === "function") await flushPreloads();
790
+
791
+ const ssrHeadHTML = typeof getSSRHeadHTML === "function" ? getSSRHeadHTML() : "";
792
+
793
+ // Collect SSR font data (Google Font links, font preloads, font-face styles)
794
+ var fontHeadHTML = "";
795
+ function _escAttr(s) { return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;"); }
796
+ try {
797
+ var fontLinks = typeof _getSSRFontLinks === "function" ? _getSSRFontLinks() : [];
798
+ for (var fl of fontLinks) { fontHeadHTML += '<link rel="stylesheet" href="' + _escAttr(fl) + '" />\\n '; }
799
+ } catch (e) { /* next/font/google not used */ }
800
+ // Emit <link rel="preload"> for all font files (reuse _allFp collected earlier for Link header)
801
+ for (var fp of _allFp) { fontHeadHTML += '<link rel="preload" href="' + _escAttr(fp.href) + '" as="font" type="' + _escAttr(fp.type) + '" crossorigin />\\n '; }
802
+ try {
803
+ var allFontStyles = [];
804
+ if (typeof _getSSRFontStylesGoogle === "function") allFontStyles.push(..._getSSRFontStylesGoogle());
805
+ if (typeof _getSSRFontStylesLocal === "function") allFontStyles.push(..._getSSRFontStylesLocal());
806
+ if (allFontStyles.length > 0) { fontHeadHTML += '<style data-vinext-fonts>' + allFontStyles.join("\\n") + '</style>\\n '; }
807
+ } catch (e) { /* font styles not available */ }
808
+
809
+ const pageModuleIds = route.filePath ? [route.filePath] : [];
810
+ const assetTags = collectAssetTags(manifest, pageModuleIds);
811
+ const nextDataPayload = {
812
+ props: { pageProps }, page: patternToNextFormat(route.pattern), query: params, isFallback: false,
813
+ };
814
+ if (i18nConfig) {
815
+ nextDataPayload.locale = locale;
816
+ nextDataPayload.locales = i18nConfig.locales;
817
+ nextDataPayload.defaultLocale = i18nConfig.defaultLocale;
818
+ }
819
+ const localeGlobals = i18nConfig
820
+ ? ";window.__VINEXT_LOCALE__=" + safeJsonStringify(locale) +
821
+ ";window.__VINEXT_LOCALES__=" + safeJsonStringify(i18nConfig.locales) +
822
+ ";window.__VINEXT_DEFAULT_LOCALE__=" + safeJsonStringify(i18nConfig.defaultLocale)
823
+ : "";
824
+ const nextDataScript = "<script>window.__NEXT_DATA__ = " + safeJsonStringify(nextDataPayload) + localeGlobals + "</script>";
825
+
826
+ // Build the document shell with a placeholder for the streamed body
827
+ var BODY_MARKER = "<!--VINEXT_STREAM_BODY-->";
828
+ var shellHtml;
829
+ if (DocumentComponent) {
830
+ const docElement = React.createElement(DocumentComponent);
831
+ shellHtml = await renderToStringAsync(docElement);
832
+ shellHtml = shellHtml.replace("__NEXT_MAIN__", BODY_MARKER);
833
+ if (ssrHeadHTML || assetTags || fontHeadHTML) {
834
+ shellHtml = shellHtml.replace("</head>", " " + fontHeadHTML + ssrHeadHTML + "\\n " + assetTags + "\\n</head>");
835
+ }
836
+ shellHtml = shellHtml.replace("<!-- __NEXT_SCRIPTS__ -->", nextDataScript);
837
+ if (!shellHtml.includes("__NEXT_DATA__")) {
838
+ shellHtml = shellHtml.replace("</body>", " " + nextDataScript + "\\n</body>");
839
+ }
840
+ } else {
841
+ shellHtml = "<!DOCTYPE html>\\n<html>\\n<head>\\n <meta charset=\\"utf-8\\" />\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1\\" />\\n " + fontHeadHTML + ssrHeadHTML + "\\n " + assetTags + "\\n</head>\\n<body>\\n <div id=\\"__next\\">" + BODY_MARKER + "</div>\\n " + nextDataScript + "\\n</body>\\n</html>";
842
+ }
843
+
844
+ if (typeof setSSRContext === "function") setSSRContext(null);
845
+
846
+ // Split the shell at the body marker
847
+ var markerIdx = shellHtml.indexOf(BODY_MARKER);
848
+ var shellPrefix = shellHtml.slice(0, markerIdx);
849
+ var shellSuffix = shellHtml.slice(markerIdx + BODY_MARKER.length);
850
+
851
+ // Start the React body stream — progressive SSR (no allReady wait)
852
+ var bodyStream = await renderToReadableStream(element);
853
+ var encoder = new TextEncoder();
854
+
855
+ // Create a composite stream: prefix + body + suffix
856
+ var compositeStream = new ReadableStream({
857
+ async start(controller) {
858
+ controller.enqueue(encoder.encode(shellPrefix));
859
+ var reader = bodyStream.getReader();
860
+ try {
861
+ for (;;) {
862
+ var chunk = await reader.read();
863
+ if (chunk.done) break;
864
+ controller.enqueue(chunk.value);
865
+ }
866
+ } finally {
867
+ reader.releaseLock();
868
+ }
869
+ controller.enqueue(encoder.encode(shellSuffix));
870
+ controller.close();
871
+ }
872
+ });
873
+
874
+ // Cache the rendered HTML for ISR (needs the full string — re-render synchronously)
875
+ if (isrRevalidateSeconds !== null && isrRevalidateSeconds > 0) {
876
+ // Tee the stream so we can cache and respond simultaneously would be ideal,
877
+ // but ISR responses are rare on first hit. Re-render to get complete HTML for cache.
878
+ var isrElement;
879
+ if (AppComponent) {
880
+ isrElement = React.createElement(AppComponent, { Component: PageComponent, pageProps });
881
+ } else {
882
+ isrElement = React.createElement(PageComponent, pageProps);
883
+ }
884
+ isrElement = wrapWithRouterContext(isrElement);
885
+ var isrHtml = await renderToStringAsync(isrElement);
886
+ var fullHtml = shellPrefix + isrHtml + shellSuffix;
887
+ var isrPathname = url.split("?")[0];
888
+ var isrCacheKey = "pages:" + (isrPathname === "/" ? "/" : isrPathname.replace(/\\/$/, ""));
889
+ await isrSet(isrCacheKey, { kind: "PAGES", html: fullHtml, pageData: pageProps, headers: undefined, status: undefined }, isrRevalidateSeconds);
890
+ }
891
+
892
+ // Merge headers/status/cookies set by getServerSideProps on the res object.
893
+ // gSSP commonly uses res.setHeader("Set-Cookie", ...) or res.status(304).
894
+ var finalStatus = 200;
895
+ const responseHeaders = new Headers({ "Content-Type": "text/html" });
896
+ if (gsspRes) {
897
+ finalStatus = gsspRes.statusCode;
898
+ var gsspHeaders = gsspRes.getHeaders();
899
+ for (var hk of Object.keys(gsspHeaders)) {
900
+ var hv = gsspHeaders[hk];
901
+ if (hk === "set-cookie" && Array.isArray(hv)) {
902
+ for (var sc of hv) responseHeaders.append("set-cookie", sc);
903
+ } else if (hv != null) {
904
+ responseHeaders.set(hk, String(hv));
905
+ }
906
+ }
907
+ // Ensure Content-Type stays text/html (gSSP shouldn't override it for page renders)
908
+ responseHeaders.set("Content-Type", "text/html");
909
+ }
910
+ if (isrRevalidateSeconds) {
911
+ responseHeaders.set("Cache-Control", "s-maxage=" + isrRevalidateSeconds + ", stale-while-revalidate");
912
+ responseHeaders.set("X-Vinext-Cache", "MISS");
913
+ }
914
+ // Set HTTP Link header for font preloading
915
+ if (_fontLinkHeader) {
916
+ responseHeaders.set("Link", _fontLinkHeader);
917
+ }
918
+ return new Response(compositeStream, { status: finalStatus, headers: responseHeaders });
919
+ } catch (e) {
920
+ console.error("[vinext] SSR error:", e);
921
+ return new Response("Internal Server Error", { status: 500 });
922
+ }
923
+ }) // end runWithFetchCache
924
+ ) // end runWithPrivateCache
925
+ ) // end _runWithCacheState
926
+ ) // end runWithHeadState
927
+ ); // end runWithRouterState
928
+ }
929
+
930
+ export async function handleApiRoute(request, url) {
931
+ const match = matchRoute(url, apiRoutes);
932
+ if (!match) {
933
+ return new Response("404 - API route not found", { status: 404 });
934
+ }
935
+
936
+ const { route, params } = match;
937
+ const handler = route.module.default;
938
+ if (typeof handler !== "function") {
939
+ return new Response("API route does not export a default function", { status: 500 });
940
+ }
941
+
942
+ const query = { ...params };
943
+ const qs = url.split("?")[1];
944
+ if (qs) {
945
+ for (const [k, v] of new URLSearchParams(qs)) {
946
+ if (k in query) {
947
+ // Multi-value: promote to array (Next.js returns string[] for duplicate keys)
948
+ query[k] = Array.isArray(query[k]) ? query[k].concat(v) : [query[k], v];
949
+ } else {
950
+ query[k] = v;
951
+ }
952
+ }
953
+ }
954
+
955
+ // Parse request body (enforce 1MB limit to prevent memory exhaustion,
956
+ // matching Next.js default bodyParser sizeLimit).
957
+ // Check Content-Length first as a fast path, then enforce on the actual
958
+ // stream to prevent bypasses via chunked transfer encoding.
959
+ const contentLength = parseInt(request.headers.get("content-length") || "0", 10);
960
+ if (contentLength > 1 * 1024 * 1024) {
961
+ return new Response("Request body too large", { status: 413 });
962
+ }
963
+ let body;
964
+ const ct = request.headers.get("content-type") || "";
965
+ let rawBody;
966
+ try { rawBody = await readBodyWithLimit(request, 1 * 1024 * 1024); }
967
+ catch { return new Response("Request body too large", { status: 413 }); }
968
+ if (!rawBody) {
969
+ body = undefined;
970
+ } else if (ct.includes("application/json")) {
971
+ try { body = JSON.parse(rawBody); } catch { body = rawBody; }
972
+ } else {
973
+ body = rawBody;
974
+ }
975
+
976
+ const { req, res, responsePromise } = createReqRes(request, url, query, body);
977
+
978
+ try {
979
+ await handler(req, res);
980
+ // If handler didn't call res.end(), end it now.
981
+ // The end() method is idempotent — safe to call twice.
982
+ res.end();
983
+ return await responsePromise;
984
+ } catch (e) {
985
+ console.error("[vinext] API error:", e);
986
+ return new Response("Internal Server Error", { status: 500 });
987
+ }
988
+ }
989
+
990
+ ${middlewareExportCode}
991
+ `;
992
+ }
993
+ //# sourceMappingURL=pages-server-entry.js.map