vinext 0.0.24 → 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 (145) hide show
  1. package/README.md +30 -1
  2. package/dist/check.d.ts.map +1 -1
  3. package/dist/check.js +6 -5
  4. package/dist/check.js.map +1 -1
  5. package/dist/cli.js +32 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client/entry.js +1 -1
  8. package/dist/client/entry.js.map +1 -1
  9. package/dist/client/vinext-next-data.d.ts +22 -0
  10. package/dist/client/vinext-next-data.d.ts.map +1 -0
  11. package/dist/client/vinext-next-data.js +2 -0
  12. package/dist/client/vinext-next-data.js.map +1 -0
  13. package/dist/config/config-matchers.d.ts +21 -0
  14. package/dist/config/config-matchers.d.ts.map +1 -1
  15. package/dist/config/config-matchers.js +52 -8
  16. package/dist/config/config-matchers.js.map +1 -1
  17. package/dist/config/next-config.d.ts +39 -6
  18. package/dist/config/next-config.d.ts.map +1 -1
  19. package/dist/config/next-config.js +241 -48
  20. package/dist/config/next-config.js.map +1 -1
  21. package/dist/deploy.d.ts +21 -0
  22. package/dist/deploy.d.ts.map +1 -1
  23. package/dist/deploy.js +94 -41
  24. package/dist/deploy.js.map +1 -1
  25. package/dist/entries/app-browser-entry.d.ts +9 -0
  26. package/dist/entries/app-browser-entry.d.ts.map +1 -0
  27. package/dist/entries/app-browser-entry.js +340 -0
  28. package/dist/entries/app-browser-entry.js.map +1 -0
  29. package/dist/{server/app-dev-server.d.ts → entries/app-rsc-entry.d.ts} +6 -19
  30. package/dist/entries/app-rsc-entry.d.ts.map +1 -0
  31. package/dist/{server/app-dev-server.js → entries/app-rsc-entry.js} +572 -1293
  32. package/dist/entries/app-rsc-entry.js.map +1 -0
  33. package/dist/entries/app-ssr-entry.d.ts +8 -0
  34. package/dist/entries/app-ssr-entry.d.ts.map +1 -0
  35. package/dist/entries/app-ssr-entry.js +449 -0
  36. package/dist/entries/app-ssr-entry.js.map +1 -0
  37. package/dist/entries/pages-client-entry.d.ts +4 -0
  38. package/dist/entries/pages-client-entry.d.ts.map +1 -0
  39. package/dist/entries/pages-client-entry.js +94 -0
  40. package/dist/entries/pages-client-entry.js.map +1 -0
  41. package/dist/entries/pages-entry-helpers.d.ts +7 -0
  42. package/dist/entries/pages-entry-helpers.d.ts.map +1 -0
  43. package/dist/entries/pages-entry-helpers.js +18 -0
  44. package/dist/entries/pages-entry-helpers.js.map +1 -0
  45. package/dist/entries/pages-server-entry.d.ts +8 -0
  46. package/dist/entries/pages-server-entry.d.ts.map +1 -0
  47. package/dist/entries/pages-server-entry.js +993 -0
  48. package/dist/entries/pages-server-entry.js.map +1 -0
  49. package/dist/index.d.ts +4 -25
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +345 -1227
  52. package/dist/index.js.map +1 -1
  53. package/dist/plugins/async-hooks-stub.d.ts +16 -0
  54. package/dist/plugins/async-hooks-stub.d.ts.map +1 -0
  55. package/dist/plugins/async-hooks-stub.js +45 -0
  56. package/dist/plugins/async-hooks-stub.js.map +1 -0
  57. package/dist/routing/app-router.d.ts +12 -6
  58. package/dist/routing/app-router.d.ts.map +1 -1
  59. package/dist/routing/app-router.js +19 -40
  60. package/dist/routing/app-router.js.map +1 -1
  61. package/dist/routing/pages-router.d.ts.map +1 -1
  62. package/dist/routing/pages-router.js +3 -9
  63. package/dist/routing/pages-router.js.map +1 -1
  64. package/dist/routing/utils.d.ts +9 -0
  65. package/dist/routing/utils.d.ts.map +1 -1
  66. package/dist/routing/utils.js +10 -0
  67. package/dist/routing/utils.js.map +1 -1
  68. package/dist/server/api-handler.d.ts.map +1 -1
  69. package/dist/server/api-handler.js +6 -0
  70. package/dist/server/api-handler.js.map +1 -1
  71. package/dist/server/dev-module-runner.d.ts +84 -0
  72. package/dist/server/dev-module-runner.d.ts.map +1 -0
  73. package/dist/server/dev-module-runner.js +105 -0
  74. package/dist/server/dev-module-runner.js.map +1 -0
  75. package/dist/server/dev-server.js.map +1 -1
  76. package/dist/server/instrumentation.d.ts +52 -9
  77. package/dist/server/instrumentation.d.ts.map +1 -1
  78. package/dist/server/instrumentation.js +52 -15
  79. package/dist/server/instrumentation.js.map +1 -1
  80. package/dist/server/middleware-codegen.d.ts +1 -1
  81. package/dist/server/middleware-codegen.js +1 -1
  82. package/dist/server/middleware-codegen.js.map +1 -1
  83. package/dist/server/middleware.d.ts +7 -3
  84. package/dist/server/middleware.d.ts.map +1 -1
  85. package/dist/server/middleware.js +16 -6
  86. package/dist/server/middleware.js.map +1 -1
  87. package/dist/server/prod-server.d.ts.map +1 -1
  88. package/dist/server/prod-server.js +33 -28
  89. package/dist/server/prod-server.js.map +1 -1
  90. package/dist/server/request-pipeline.d.ts +92 -0
  91. package/dist/server/request-pipeline.d.ts.map +1 -0
  92. package/dist/server/request-pipeline.js +202 -0
  93. package/dist/server/request-pipeline.js.map +1 -0
  94. package/dist/shims/cache.d.ts.map +1 -1
  95. package/dist/shims/cache.js +14 -2
  96. package/dist/shims/cache.js.map +1 -1
  97. package/dist/shims/constants.d.ts +120 -3
  98. package/dist/shims/constants.d.ts.map +1 -1
  99. package/dist/shims/constants.js +170 -3
  100. package/dist/shims/constants.js.map +1 -1
  101. package/dist/shims/fetch-cache.d.ts.map +1 -1
  102. package/dist/shims/fetch-cache.js +139 -29
  103. package/dist/shims/fetch-cache.js.map +1 -1
  104. package/dist/shims/form.d.ts.map +1 -1
  105. package/dist/shims/form.js +2 -3
  106. package/dist/shims/form.js.map +1 -1
  107. package/dist/shims/headers.d.ts.map +1 -1
  108. package/dist/shims/headers.js +1 -0
  109. package/dist/shims/headers.js.map +1 -1
  110. package/dist/shims/layout-segment-context.d.ts +5 -4
  111. package/dist/shims/layout-segment-context.d.ts.map +1 -1
  112. package/dist/shims/layout-segment-context.js +6 -5
  113. package/dist/shims/layout-segment-context.js.map +1 -1
  114. package/dist/shims/link.d.ts.map +1 -1
  115. package/dist/shims/link.js +33 -18
  116. package/dist/shims/link.js.map +1 -1
  117. package/dist/shims/metadata.d.ts +7 -1
  118. package/dist/shims/metadata.d.ts.map +1 -1
  119. package/dist/shims/metadata.js +9 -3
  120. package/dist/shims/metadata.js.map +1 -1
  121. package/dist/shims/navigation.d.ts +14 -11
  122. package/dist/shims/navigation.d.ts.map +1 -1
  123. package/dist/shims/navigation.js +122 -102
  124. package/dist/shims/navigation.js.map +1 -1
  125. package/dist/shims/og.d.ts +6 -6
  126. package/dist/shims/og.js +6 -6
  127. package/dist/shims/og.js.map +1 -1
  128. package/dist/shims/router.d.ts.map +1 -1
  129. package/dist/shims/router.js +37 -21
  130. package/dist/shims/router.js.map +1 -1
  131. package/dist/shims/server.d.ts +2 -0
  132. package/dist/shims/server.d.ts.map +1 -1
  133. package/dist/shims/server.js +4 -0
  134. package/dist/shims/server.js.map +1 -1
  135. package/dist/shims/url-utils.d.ts +13 -0
  136. package/dist/shims/url-utils.d.ts.map +1 -0
  137. package/dist/shims/url-utils.js +28 -0
  138. package/dist/shims/url-utils.js.map +1 -0
  139. package/dist/utils/project.d.ts +15 -0
  140. package/dist/utils/project.d.ts.map +1 -1
  141. package/dist/utils/project.js +48 -0
  142. package/dist/utils/project.js.map +1 -1
  143. package/package.json +1 -1
  144. package/dist/server/app-dev-server.d.ts.map +0 -1
  145. package/dist/server/app-dev-server.js.map +0 -1
@@ -1,16 +1,22 @@
1
1
  /**
2
- * App Router dev server handler.
2
+ * App Router RSC entry generator.
3
3
  *
4
- * This module generates virtual entry points for the RSC/SSR/browser
5
- * environments that @vitejs/plugin-rsc manages. The RSC entry does
6
- * route matching and renders the component tree, then delegates to
7
- * the SSR entry for HTML generation.
4
+ * Generates the virtual RSC entry module for the App Router.
5
+ * The RSC entry does route matching and renders the component tree,
6
+ * then delegates to the SSR entry for HTML generation.
7
+ *
8
+ * Previously housed in server/app-dev-server.ts.
8
9
  */
9
10
  import fs from "node:fs";
10
11
  import { fileURLToPath } from "node:url";
11
- import { generateDevOriginCheckCode } from "./dev-origin-check.js";
12
- import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode } from "./middleware-codegen.js";
13
- import { isProxyFile } from "./middleware.js";
12
+ import { generateDevOriginCheckCode } from "../server/dev-origin-check.js";
13
+ import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode } from "../server/middleware-codegen.js";
14
+ import { isProxyFile } from "../server/middleware.js";
15
+ // Pre-computed absolute paths for generated-code imports. The virtual RSC
16
+ // entry can't use relative imports (it has no real file location), so we
17
+ // resolve these at code-generation time and embed them as absolute paths.
18
+ const configMatchersPath = fileURLToPath(new URL("../config/config-matchers.js", import.meta.url)).replace(/\\/g, "/");
19
+ const requestPipelinePath = fileURLToPath(new URL("../server/request-pipeline.js", import.meta.url)).replace(/\\/g, "/");
14
20
  /**
15
21
  * Generate the virtual RSC entry module.
16
22
  *
@@ -18,13 +24,14 @@ import { isProxyFile } from "./middleware.js";
18
24
  * It matches the incoming request URL to an app route, builds the
19
25
  * nested layout + page tree, and renders it to an RSC stream.
20
26
  */
21
- export function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, globalErrorPath, basePath, trailingSlash, config) {
27
+ export function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, globalErrorPath, basePath, trailingSlash, config, instrumentationPath) {
22
28
  const bp = basePath ?? "";
23
29
  const ts = trailingSlash ?? false;
24
30
  const redirects = config?.redirects ?? [];
25
31
  const rewrites = config?.rewrites ?? { beforeFiles: [], afterFiles: [], fallback: [] };
26
32
  const headers = config?.headers ?? [];
27
33
  const allowedOrigins = config?.allowedOrigins ?? [];
34
+ const bodySizeLimit = config?.bodySizeLimit ?? 1 * 1024 * 1024;
28
35
  // Build import map for all page and layout files
29
36
  const imports = [];
30
37
  const importMap = new Map();
@@ -119,7 +126,8 @@ ${interceptEntries.join(",\n")}
119
126
  page: ${route.pagePath ? getImportVar(route.pagePath) : "null"},
120
127
  routeHandler: ${route.routePath ? getImportVar(route.routePath) : "null"},
121
128
  layouts: [${layoutVars.join(", ")}],
122
- layoutSegmentDepths: ${JSON.stringify(route.layoutSegmentDepths)},
129
+ routeSegments: ${JSON.stringify(route.routeSegments)},
130
+ layoutTreePositions: ${JSON.stringify(route.layoutTreePositions)},
123
131
  templates: [${templateVars.join(", ")}],
124
132
  errors: [${layoutErrorVars.join(", ")}],
125
133
  slots: {
@@ -194,15 +202,19 @@ import {
194
202
  loadServerAction,
195
203
  createTemporaryReferenceSet,
196
204
  } from "@vitejs/plugin-rsc/rsc";
205
+ import { AsyncLocalStorage } from "node:async_hooks";
197
206
  import { createElement, Suspense, Fragment } from "react";
198
207
  import { setNavigationContext as _setNavigationContextOrig, getNavigationContext as _getNavigationContext } from "next/navigation";
199
208
  import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, runWithHeadersContext, applyMiddlewareRequestHeaders, getHeadersContext } from "next/headers";
200
- import { NextRequest } from "next/server";
209
+ import { NextRequest, NextFetchEvent } from "next/server";
201
210
  import { ErrorBoundary, NotFoundBoundary } from "vinext/error-boundary";
202
211
  import { LayoutSegmentProvider } from "vinext/layout-segment-context";
203
212
  import { MetadataHead, mergeMetadata, resolveModuleMetadata, ViewportHead, mergeViewport, resolveModuleViewport } from "vinext/metadata";
204
213
  ${middlewarePath ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};` : ""}
205
- ${effectiveMetaRoutes.length > 0 ? `import { sitemapToXml, robotsToText, manifestToJson } from ${JSON.stringify(fileURLToPath(new URL("./metadata-routes.js", import.meta.url)).replace(/\\/g, "/"))};` : ""}
214
+ ${instrumentationPath ? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};` : ""}
215
+ ${effectiveMetaRoutes.length > 0 ? `import { sitemapToXml, robotsToText, manifestToJson } from ${JSON.stringify(fileURLToPath(new URL("../server/metadata-routes.js", import.meta.url)).replace(/\\/g, "/"))};` : ""}
216
+ import { requestContextFromRequest, matchRedirect, matchRewrite, matchHeaders, isExternalUrl, proxyExternalRequest, sanitizeDestination } from ${JSON.stringify(configMatchersPath)};
217
+ import { validateCsrfOrigin, validateImageUrl, guardProtocolRelativeUrl, stripBasePath, normalizeTrailingSlash, processMiddlewareHeaders } from ${JSON.stringify(requestPipelinePath)};
206
218
  import { _consumeRequestScopedCacheLife, _runWithCacheState } from "next/cache";
207
219
  import { runWithFetchCache } from "vinext/fetch-cache";
208
220
  import { runWithPrivateCache as _runWithPrivateCache } from "vinext/cache-runtime";
@@ -214,6 +226,20 @@ import { getSSRFontStyles as _getSSRFontStylesLocal, getSSRFontPreloads as _getS
214
226
  function _getSSRFontStyles() { return [..._getSSRFontStylesGoogle(), ..._getSSRFontStylesLocal()]; }
215
227
  function _getSSRFontPreloads() { return [..._getSSRFontPreloadsGoogle(), ..._getSSRFontPreloadsLocal()]; }
216
228
 
229
+ // ALS used to suppress the expected "Invalid hook call" dev warning when
230
+ // layout/page components are probed outside React's render cycle. Patching
231
+ // console.error once at module load (instead of per-request) avoids the
232
+ // concurrent-request issue where request A's suppression filter could
233
+ // swallow real errors from request B.
234
+ const _suppressHookWarningAls = new AsyncLocalStorage();
235
+ const _origConsoleError = console.error;
236
+ console.error = (...args) => {
237
+ if (_suppressHookWarningAls.getStore() === true &&
238
+ typeof args[0] === "string" &&
239
+ args[0].includes("Invalid hook call")) return;
240
+ _origConsoleError.apply(console, args);
241
+ };
242
+
217
243
  // Set navigation context in the ALS-backed store. "use client" components
218
244
  // rendered during SSR need the pathname/searchParams/params but the SSR
219
245
  // environment has a separate module instance of next/navigation.
@@ -242,6 +268,38 @@ function makeThenableParams(obj) {
242
268
  return Object.assign(Promise.resolve(plain), plain);
243
269
  }
244
270
 
271
+ // Resolve route tree segments to actual values using matched params.
272
+ // Dynamic segments like [id] are replaced with param values, catch-all
273
+ // segments like [...slug] are joined with "/", and route groups are kept as-is.
274
+ function __resolveChildSegments(routeSegments, treePosition, params) {
275
+ var raw = routeSegments.slice(treePosition);
276
+ var result = [];
277
+ for (var j = 0; j < raw.length; j++) {
278
+ var seg = raw[j];
279
+ // Optional catch-all: [[...param]]
280
+ if (seg.indexOf("[[...") === 0 && seg.charAt(seg.length - 1) === "]" && seg.charAt(seg.length - 2) === "]") {
281
+ var pn = seg.slice(5, -2);
282
+ var v = params[pn];
283
+ // Skip empty optional catch-all (e.g., visiting /blog on [[...slug]] route)
284
+ if (Array.isArray(v) && v.length === 0) continue;
285
+ if (v == null) continue;
286
+ result.push(Array.isArray(v) ? v.join("/") : v);
287
+ // Catch-all: [...param]
288
+ } else if (seg.indexOf("[...") === 0 && seg.charAt(seg.length - 1) === "]") {
289
+ var pn2 = seg.slice(4, -1);
290
+ var v2 = params[pn2];
291
+ result.push(Array.isArray(v2) ? v2.join("/") : (v2 || seg));
292
+ // Dynamic: [param]
293
+ } else if (seg.charAt(0) === "[" && seg.charAt(seg.length - 1) === "]" && seg.indexOf(".") === -1) {
294
+ var pn3 = seg.slice(1, -1);
295
+ result.push(params[pn3] || seg);
296
+ } else {
297
+ result.push(seg);
298
+ }
299
+ }
300
+ return result;
301
+ }
302
+
245
303
  // djb2 hash — matches Next.js's stringHash for digest generation.
246
304
  // Produces a stable numeric string from error message + stack.
247
305
  function __errorDigest(str) {
@@ -290,7 +348,7 @@ function __sanitizeErrorForClient(error) {
290
348
  // thrown during RSC streaming (e.g. inside Suspense boundaries).
291
349
  // For non-navigation errors in production, generates a digest hash so the
292
350
  // error can be correlated with server logs without leaking details.
293
- function rscOnError(error) {
351
+ function rscOnError(error, requestInfo, errorContext) {
294
352
  if (error && typeof error === "object" && "digest" in error) {
295
353
  return String(error.digest);
296
354
  }
@@ -336,6 +394,16 @@ function rscOnError(error) {
336
394
  return undefined;
337
395
  }
338
396
 
397
+ if (requestInfo && errorContext && error) {
398
+ _reportRequestError(
399
+ error instanceof Error ? error : new Error(String(error)),
400
+ requestInfo,
401
+ errorContext,
402
+ ).catch((reportErr) => {
403
+ console.error("[vinext] Failed to report render error:", reportErr);
404
+ });
405
+ }
406
+
339
407
  // In production, generate a digest hash for non-navigation errors
340
408
  if (process.env.NODE_ENV === "production" && error) {
341
409
  const msg = error instanceof Error ? error.message : String(error);
@@ -345,8 +413,55 @@ function rscOnError(error) {
345
413
  return undefined;
346
414
  }
347
415
 
416
+ function createRscOnErrorHandler(request, pathname, routePath) {
417
+ const requestInfo = {
418
+ path: pathname,
419
+ method: request.method,
420
+ headers: Object.fromEntries(request.headers.entries()),
421
+ };
422
+ const errorContext = {
423
+ routerKind: "App Router",
424
+ routePath: routePath || pathname,
425
+ routeType: "render",
426
+ };
427
+ return function(error) {
428
+ return rscOnError(error, requestInfo, errorContext);
429
+ };
430
+ }
431
+
348
432
  ${imports.join("\n")}
349
433
 
434
+ ${instrumentationPath ? `// Run instrumentation register() exactly once, lazily on the first request.
435
+ // Previously this was a top-level await, which blocked the entire module graph
436
+ // from finishing initialization until register() resolved — adding that latency
437
+ // to every cold start. Moving it here preserves the "runs before any request is
438
+ // handled" guarantee while not blocking V8 isolate initialization.
439
+ // On Cloudflare Workers, module evaluation happens synchronously in the isolate
440
+ // startup phase; a top-level await extends that phase and increases cold-start
441
+ // wall time for all requests, not just the first.
442
+ let __instrumentationInitialized = false;
443
+ let __instrumentationInitPromise = null;
444
+ async function __ensureInstrumentation() {
445
+ if (__instrumentationInitialized) return;
446
+ if (__instrumentationInitPromise) return __instrumentationInitPromise;
447
+ __instrumentationInitPromise = (async () => {
448
+ if (typeof _instrumentation.register === "function") {
449
+ await _instrumentation.register();
450
+ }
451
+ // Store the onRequestError handler on globalThis so it is visible to
452
+ // reportRequestError() (imported as _reportRequestError above) regardless
453
+ // of which Vite environment module graph it is called from. With
454
+ // @vitejs/plugin-rsc the RSC and SSR environments run in the same Node.js
455
+ // process and share globalThis. With @cloudflare/vite-plugin everything
456
+ // runs inside the Worker so globalThis is the Worker's global — also correct.
457
+ if (typeof _instrumentation.onRequestError === "function") {
458
+ globalThis.__VINEXT_onRequestErrorHandler__ = _instrumentation.onRequestError;
459
+ }
460
+ __instrumentationInitialized = true;
461
+ })();
462
+ return __instrumentationInitPromise;
463
+ }` : ""}
464
+
350
465
  const routes = [
351
466
  ${routeEntries.join(",\n")}
352
467
  ];
@@ -386,16 +501,26 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
386
501
 
387
502
  // Resolve metadata and viewport from parent layouts so that not-found/error
388
503
  // pages inherit title, description, OG tags etc. — matching Next.js behavior.
389
- const metadataList = [];
390
- const viewportList = [];
391
- for (const layoutMod of layouts) {
392
- if (layoutMod) {
393
- const meta = await resolveModuleMetadata(layoutMod);
394
- if (meta) metadataList.push(meta);
395
- const vp = await resolveModuleViewport(layoutMod);
396
- if (vp) viewportList.push(vp);
397
- }
504
+ // Build the serial parent chain for layout metadata (same as buildPageElement).
505
+ const _filteredLayouts = layouts.filter(Boolean);
506
+ const _fallbackParams = opts?.matchedParams ?? route?.params ?? {};
507
+ const _layoutMetaPromises = [];
508
+ let _accumulatedMeta = Promise.resolve({});
509
+ for (let _i = 0; _i < _filteredLayouts.length; _i++) {
510
+ const _parentForLayout = _accumulatedMeta;
511
+ const _metaP = resolveModuleMetadata(_filteredLayouts[_i], _fallbackParams, undefined, _parentForLayout)
512
+ .catch((err) => { console.error("[vinext] Layout generateMetadata() failed:", err); return null; });
513
+ _layoutMetaPromises.push(_metaP);
514
+ _accumulatedMeta = _metaP.then(async (_r) =>
515
+ _r ? mergeMetadata([await _parentForLayout, _r]) : await _parentForLayout
516
+ );
398
517
  }
518
+ const [_metaResults, _vpResults] = await Promise.all([
519
+ Promise.all(_layoutMetaPromises),
520
+ Promise.all(_filteredLayouts.map((mod) => resolveModuleViewport(mod, _fallbackParams).catch((err) => { console.error("[vinext] Layout generateViewport() failed:", err); return null; }))),
521
+ ]);
522
+ const metadataList = _metaResults.filter(Boolean);
523
+ const viewportList = _vpResults.filter(Boolean);
399
524
  const resolvedMetadata = metadataList.length > 0 ? mergeMetadata(metadataList) : null;
400
525
  const resolvedViewport = mergeViewport(viewportList);
401
526
 
@@ -418,13 +543,17 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
418
543
  // We wrap each layout with LayoutSegmentProvider and add GlobalErrorBoundary
419
544
  // to match the wrapping order in buildPageElement(), ensuring smooth
420
545
  // client-side tree reconciliation.
421
- const layoutDepths = route?.layoutSegmentDepths;
546
+ const _treePositions = route?.layoutTreePositions;
547
+ const _routeSegs = route?.routeSegments || [];
548
+ const _fallbackParams = opts?.matchedParams ?? route?.params ?? {};
549
+ const _asyncFallbackParams = makeThenableParams(_fallbackParams);
422
550
  for (let i = layouts.length - 1; i >= 0; i--) {
423
551
  const LayoutComponent = layouts[i]?.default;
424
552
  if (LayoutComponent) {
425
- element = createElement(LayoutComponent, { children: element });
426
- const layoutDepth = layoutDepths ? layoutDepths[i] : 0;
427
- element = createElement(LayoutSegmentProvider, { depth: layoutDepth }, element);
553
+ element = createElement(LayoutComponent, { children: element, params: _asyncFallbackParams });
554
+ const _tp = _treePositions ? _treePositions[i] : 0;
555
+ const _cs = __resolveChildSegments(_routeSegs, _tp, _fallbackParams);
556
+ element = createElement(LayoutSegmentProvider, { childSegments: _cs }, element);
428
557
  }
429
558
  }
430
559
  ${globalErrorVar ? `
@@ -436,9 +565,19 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
436
565
  });
437
566
  }
438
567
  ` : ""}
439
- const rscStream = renderToReadableStream(element, { onError: rscOnError });
440
- setHeadersContext(null);
441
- setNavigationContext(null);
568
+ const _pathname = new URL(request.url).pathname;
569
+ const onRenderError = createRscOnErrorHandler(
570
+ request,
571
+ _pathname,
572
+ route?.pattern ?? _pathname,
573
+ );
574
+ const rscStream = renderToReadableStream(element, { onError: onRenderError });
575
+ // Do NOT clear context here — the RSC stream is consumed lazily by the client.
576
+ // Clearing context now would cause async server components (e.g. NextIntlClientProviderServer)
577
+ // that run during stream consumption to see null headers/navigation context and throw,
578
+ // resulting in missing provider context on the client (e.g. next-intl useTranslations fails
579
+ // with "context from NextIntlClientProvider was not found").
580
+ // Context is cleared naturally when the ALS scope from runWithHeadersContext unwinds.
442
581
  return new Response(rscStream, {
443
582
  status: statusCode,
444
583
  headers: { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" },
@@ -446,13 +585,21 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
446
585
  }
447
586
  // For HTML (full page load) responses, wrap with layouts only (no client-side
448
587
  // wrappers needed since SSR generates the complete HTML document).
588
+ const _fallbackParamsHtml = opts?.matchedParams ?? route?.params ?? {};
589
+ const _asyncFallbackParamsHtml = makeThenableParams(_fallbackParamsHtml);
449
590
  for (let i = layouts.length - 1; i >= 0; i--) {
450
591
  const LayoutComponent = layouts[i]?.default;
451
592
  if (LayoutComponent) {
452
- element = createElement(LayoutComponent, { children: element });
593
+ element = createElement(LayoutComponent, { children: element, params: _asyncFallbackParamsHtml });
453
594
  }
454
595
  }
455
- const rscStream = renderToReadableStream(element, { onError: rscOnError });
596
+ const _pathname = new URL(request.url).pathname;
597
+ const onRenderError = createRscOnErrorHandler(
598
+ request,
599
+ _pathname,
600
+ route?.pattern ?? _pathname,
601
+ );
602
+ const rscStream = renderToReadableStream(element, { onError: onRenderError });
456
603
  // Collect font data from RSC environment
457
604
  const fontData = {
458
605
  links: _getSSRFontLinks(),
@@ -473,8 +620,8 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
473
620
  }
474
621
 
475
622
  /** Convenience: render a not-found page (404) */
476
- async function renderNotFoundPage(route, isRscRequest, request) {
477
- return renderHTTPAccessFallbackPage(route, 404, isRscRequest, request);
623
+ async function renderNotFoundPage(route, isRscRequest, request, matchedParams) {
624
+ return renderHTTPAccessFallbackPage(route, 404, isRscRequest, request, { matchedParams });
478
625
  }
479
626
 
480
627
  /**
@@ -484,10 +631,11 @@ async function renderNotFoundPage(route, isRscRequest, request) {
484
631
  * Next.js returns HTTP 200 when error.tsx catches an error (the error is "handled"
485
632
  * by the boundary). This matches that behavior intentionally.
486
633
  */
487
- async function renderErrorBoundaryPage(route, error, isRscRequest, request) {
634
+ async function renderErrorBoundaryPage(route, error, isRscRequest, request, matchedParams) {
488
635
  // Resolve the error boundary component: leaf error.tsx first, then walk per-layout
489
636
  // errors from innermost to outermost (matching ancestor inheritance), then global-error.tsx.
490
637
  let ErrorComponent = route?.error?.default ?? null;
638
+ let _isGlobalError = false;
491
639
  if (!ErrorComponent && route?.errors) {
492
640
  for (let i = route.errors.length - 1; i >= 0; i--) {
493
641
  if (route.errors[i]?.default) {
@@ -496,7 +644,12 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request) {
496
644
  }
497
645
  }
498
646
  }
499
- ErrorComponent = ErrorComponent${globalErrorVar ? ` ?? ${globalErrorVar}?.default` : ""};
647
+ ${globalErrorVar ? `
648
+ if (!ErrorComponent) {
649
+ ErrorComponent = ${globalErrorVar}?.default ?? null;
650
+ _isGlobalError = !!ErrorComponent;
651
+ }
652
+ ` : ""}
500
653
  if (!ErrorComponent) return null;
501
654
 
502
655
  const rawError = error instanceof Error ? error : new Error(String(error));
@@ -511,46 +664,74 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request) {
511
664
  let element = createElement(ErrorComponent, {
512
665
  error: errorObj,
513
666
  });
514
- const layouts = route?.layouts ?? rootLayouts;
515
- if (isRscRequest) {
516
- // For RSC requests (client-side navigation), wrap with the same component
517
- // wrappers that buildPageElement() uses (LayoutSegmentProvider, GlobalErrorBoundary).
518
- // This ensures React can reconcile the tree without destroying the DOM.
519
- // Same rationale as renderHTTPAccessFallbackPage — see comment there.
520
- const layoutDepths = route?.layoutSegmentDepths;
521
- for (let i = layouts.length - 1; i >= 0; i--) {
522
- const LayoutComponent = layouts[i]?.default;
523
- if (LayoutComponent) {
524
- element = createElement(LayoutComponent, { children: element });
525
- const layoutDepth = layoutDepths ? layoutDepths[i] : 0;
526
- element = createElement(LayoutSegmentProvider, { depth: layoutDepth }, element);
667
+
668
+ // global-error.tsx provides its own <html> and <body> (it replaces the root
669
+ // layout). Skip layout wrapping when rendering it to avoid double <html> tags.
670
+ if (!_isGlobalError) {
671
+ const layouts = route?.layouts ?? rootLayouts;
672
+ if (isRscRequest) {
673
+ // For RSC requests (client-side navigation), wrap with the same component
674
+ // wrappers that buildPageElement() uses (LayoutSegmentProvider, GlobalErrorBoundary).
675
+ // This ensures React can reconcile the tree without destroying the DOM.
676
+ // Same rationale as renderHTTPAccessFallbackPage — see comment there.
677
+ const _errTreePositions = route?.layoutTreePositions;
678
+ const _errRouteSegs = route?.routeSegments || [];
679
+ const _errParams = matchedParams ?? route?.params ?? {};
680
+ const _asyncErrParams = makeThenableParams(_errParams);
681
+ for (let i = layouts.length - 1; i >= 0; i--) {
682
+ const LayoutComponent = layouts[i]?.default;
683
+ if (LayoutComponent) {
684
+ element = createElement(LayoutComponent, { children: element, params: _asyncErrParams });
685
+ const _etp = _errTreePositions ? _errTreePositions[i] : 0;
686
+ const _ecs = __resolveChildSegments(_errRouteSegs, _etp, _errParams);
687
+ element = createElement(LayoutSegmentProvider, { childSegments: _ecs }, element);
688
+ }
689
+ }
690
+ ${globalErrorVar ? `
691
+ const _ErrGlobalComponent = ${globalErrorVar}.default;
692
+ if (_ErrGlobalComponent) {
693
+ element = createElement(ErrorBoundary, {
694
+ fallback: _ErrGlobalComponent,
695
+ children: element,
696
+ });
697
+ }
698
+ ` : ""}
699
+ } else {
700
+ // For HTML (full page load) responses, wrap with layouts only.
701
+ const _errParamsHtml = matchedParams ?? route?.params ?? {};
702
+ const _asyncErrParamsHtml = makeThenableParams(_errParamsHtml);
703
+ for (let i = layouts.length - 1; i >= 0; i--) {
704
+ const LayoutComponent = layouts[i]?.default;
705
+ if (LayoutComponent) {
706
+ element = createElement(LayoutComponent, { children: element, params: _asyncErrParamsHtml });
707
+ }
527
708
  }
528
709
  }
529
- ${globalErrorVar ? `
530
- const _ErrGlobalComponent = ${globalErrorVar}.default;
531
- if (_ErrGlobalComponent) {
532
- element = createElement(ErrorBoundary, {
533
- fallback: _ErrGlobalComponent,
534
- children: element,
535
- });
536
- }
537
- ` : ""}
538
- const rscStream = renderToReadableStream(element, { onError: rscOnError });
539
- setHeadersContext(null);
540
- setNavigationContext(null);
710
+ }
711
+
712
+ const _pathname = new URL(request.url).pathname;
713
+ const onRenderError = createRscOnErrorHandler(
714
+ request,
715
+ _pathname,
716
+ route?.pattern ?? _pathname,
717
+ );
718
+
719
+ if (isRscRequest) {
720
+ const rscStream = renderToReadableStream(element, { onError: onRenderError });
721
+ // Do NOT clear context here — the RSC stream is consumed lazily by the client.
722
+ // Clearing context now would cause async server components (e.g. NextIntlClientProviderServer)
723
+ // that run during stream consumption to see null headers/navigation context and throw,
724
+ // resulting in missing provider context on the client (e.g. next-intl useTranslations fails
725
+ // with "context from NextIntlClientProvider was not found").
726
+ // Context is cleared naturally when the ALS scope from runWithHeadersContext unwinds.
541
727
  return new Response(rscStream, {
542
728
  status: 200,
543
729
  headers: { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" },
544
730
  });
545
731
  }
546
- // For HTML (full page load) responses, wrap with layouts only.
547
- for (let i = layouts.length - 1; i >= 0; i--) {
548
- const LayoutComponent = layouts[i]?.default;
549
- if (LayoutComponent) {
550
- element = createElement(LayoutComponent, { children: element });
551
- }
552
- }
553
- const rscStream = renderToReadableStream(element, { onError: rscOnError });
732
+
733
+ // HTML (full page load) response render through RSC SSR pipeline
734
+ const rscStream = renderToReadableStream(element, { onError: onRenderError });
554
735
  // Collect font data from RSC environment so error pages include font styles
555
736
  const fontData = {
556
737
  links: _getSSRFontLinks(),
@@ -652,23 +833,79 @@ async function buildPageElement(route, params, opts, searchParams) {
652
833
  return createElement("div", null, "Page has no default export");
653
834
  }
654
835
 
655
- // Resolve metadata and viewport from layouts and page
656
- const metadataList = [];
657
- const viewportList = [];
658
- for (const layoutMod of route.layouts) {
659
- if (layoutMod) {
660
- const meta = await resolveModuleMetadata(layoutMod, params);
661
- if (meta) metadataList.push(meta);
662
- const vp = await resolveModuleViewport(layoutMod, params);
663
- if (vp) viewportList.push(vp);
664
- }
836
+ // Resolve metadata and viewport from layouts and page.
837
+ //
838
+ // generateMetadata() accepts a "parent" (Promise of ResolvedMetadata) as its
839
+ // second argument (Next.js 13+). The parent resolves to the accumulated
840
+ // merged metadata of all ancestor segments, enabling patterns like:
841
+ //
842
+ // const previousImages = (await parent).openGraph?.images ?? []
843
+ // return { openGraph: { images: ['/new-image.jpg', ...previousImages] } }
844
+ //
845
+ // Next.js uses an eager-execution-with-serial-resolution approach:
846
+ // all generateMetadata() calls are kicked off concurrently, but each
847
+ // segment's "parent" promise resolves only after the preceding segment's
848
+ // metadata is resolved and merged. This preserves concurrency for I/O-bound
849
+ // work while guaranteeing that parent data is available when needed.
850
+ //
851
+ // We build a chain: layoutParentPromises[0] = Promise.resolve({}) (no parent
852
+ // for root layout), layoutParentPromises[i+1] resolves to merge(layouts[0..i]),
853
+ // and pageParentPromise resolves to merge(all layouts).
854
+ //
855
+ // IMPORTANT: Layout metadata errors are swallowed (.catch(() => null)) because
856
+ // a layout's generateMetadata() failing should not crash the page.
857
+ // Page metadata errors are NOT swallowed — if the page's generateMetadata()
858
+ // throws, the error propagates out of buildPageElement() so the caller can
859
+ // route it to the nearest error.tsx boundary (or global-error.tsx).
860
+ const layoutMods = route.layouts.filter(Boolean);
861
+
862
+ // Build the parent promise chain and kick off metadata resolution in one pass.
863
+ // Each layout module is called exactly once. layoutMetaPromises[i] is the
864
+ // promise for layout[i]'s own metadata result.
865
+ //
866
+ // All calls are kicked off immediately (concurrent I/O), but each layout's
867
+ // "parent" promise only resolves after the preceding layout's metadata is done.
868
+ const layoutMetaPromises = [];
869
+ let accumulatedMetaPromise = Promise.resolve({});
870
+ for (let i = 0; i < layoutMods.length; i++) {
871
+ const parentForThisLayout = accumulatedMetaPromise;
872
+ // Kick off this layout's metadata resolution now (concurrent with others).
873
+ const metaPromise = resolveModuleMetadata(layoutMods[i], params, undefined, parentForThisLayout)
874
+ .catch((err) => { console.error("[vinext] Layout generateMetadata() failed:", err); return null; });
875
+ layoutMetaPromises.push(metaPromise);
876
+ // Advance accumulator: resolves to merged(layouts[0..i]) once layout[i] is done.
877
+ accumulatedMetaPromise = metaPromise.then(async (result) =>
878
+ result ? mergeMetadata([await parentForThisLayout, result]) : await parentForThisLayout
879
+ );
665
880
  }
666
- if (route.page) {
667
- const pageMeta = await resolveModuleMetadata(route.page, params);
668
- if (pageMeta) metadataList.push(pageMeta);
669
- const pageVp = await resolveModuleViewport(route.page, params);
670
- if (pageVp) viewportList.push(pageVp);
881
+ // Page's parent is the fully-accumulated layout metadata.
882
+ const pageParentPromise = accumulatedMetaPromise;
883
+
884
+ // Convert URLSearchParams plain object so we can pass it to
885
+ // resolveModuleMetadata (which expects Record<string, string | string[]>).
886
+ // This same object is reused for pageProps.searchParams below.
887
+ const spObj = {};
888
+ let hasSearchParams = false;
889
+ if (searchParams && searchParams.forEach) {
890
+ searchParams.forEach(function(v, k) {
891
+ hasSearchParams = true;
892
+ if (k in spObj) {
893
+ spObj[k] = Array.isArray(spObj[k]) ? spObj[k].concat(v) : [spObj[k], v];
894
+ } else {
895
+ spObj[k] = v;
896
+ }
897
+ });
671
898
  }
899
+
900
+ const [layoutMetaResults, layoutVpResults, pageMeta, pageVp] = await Promise.all([
901
+ Promise.all(layoutMetaPromises),
902
+ Promise.all(layoutMods.map((mod) => resolveModuleViewport(mod, params).catch((err) => { console.error("[vinext] Layout generateViewport() failed:", err); return null; }))),
903
+ route.page ? resolveModuleMetadata(route.page, params, spObj, pageParentPromise) : Promise.resolve(null),
904
+ route.page ? resolveModuleViewport(route.page, params) : Promise.resolve(null),
905
+ ]);
906
+
907
+ const metadataList = [...layoutMetaResults.filter(Boolean), ...(pageMeta ? [pageMeta] : [])];
908
+ const viewportList = [...layoutVpResults.filter(Boolean), ...(pageVp ? [pageVp] : [])];
672
909
  const resolvedMetadata = metadataList.length > 0 ? mergeMetadata(metadataList) : null;
673
910
  const resolvedViewport = mergeViewport(viewportList);
674
911
 
@@ -679,17 +916,10 @@ async function buildPageElement(route, params, opts, searchParams) {
679
916
  const asyncParams = makeThenableParams(params);
680
917
  const pageProps = { params: asyncParams };
681
918
  if (searchParams) {
682
- const spObj = {};
683
- let hasSearchParams = false;
684
- if (searchParams.forEach) searchParams.forEach(function(v, k) {
685
- hasSearchParams = true;
686
- if (k in spObj) {
687
- // Multi-value: promote to array (Next.js returns string[] for duplicate keys)
688
- spObj[k] = Array.isArray(spObj[k]) ? spObj[k].concat(v) : [spObj[k], v];
689
- } else {
690
- spObj[k] = v;
691
- }
692
- });
919
+ // Always provide searchParams prop when the URL object is available, even
920
+ // when the query string is empty -- pages that do "await searchParams" need
921
+ // it to be a thenable rather than undefined.
922
+ pageProps.searchParams = makeThenableParams(spObj);
693
923
  // If the URL has query parameters, mark the page as dynamic.
694
924
  // In Next.js, only accessing the searchParams prop signals dynamic usage,
695
925
  // but a Proxy-based approach doesn't work here because React's RSC debug
@@ -699,10 +929,13 @@ async function buildPageElement(route, params, opts, searchParams) {
699
929
  // approximation: pages with query params in the URL are almost always
700
930
  // dynamic, and this avoids false positives from React internals.
701
931
  if (hasSearchParams) markDynamicUsage();
702
- pageProps.searchParams = makeThenableParams(spObj);
703
932
  }
704
933
  let element = createElement(PageComponent, pageProps);
705
934
 
935
+ // Wrap page with empty segment provider so useSelectedLayoutSegments()
936
+ // returns [] when called from inside a page component (leaf node).
937
+ element = createElement(LayoutSegmentProvider, { childSegments: [] }, element);
938
+
706
939
  // Add metadata + viewport head tags (React 19 hoists title/meta/link to <head>)
707
940
  // Next.js always injects charset and default viewport even when no metadata/viewport
708
941
  // is exported. We replicate that by always emitting these essential head elements.
@@ -853,17 +1086,25 @@ async function buildPageElement(route, params, opts, searchParams) {
853
1086
  element = createElement(LayoutComponent, layoutProps);
854
1087
 
855
1088
  // Wrap the layout with LayoutSegmentProvider so useSelectedLayoutSegments()
856
- // called INSIDE this layout knows its URL segment depth. The depth tells the
857
- // hook how many URL segments are above this layout, so it returns only the
858
- // segments below. We wrap the layout (not just children) because hooks are
859
- // called from components rendered inside the layout's own JSX.
860
- const layoutDepth = route.layoutSegmentDepths ? route.layoutSegmentDepths[i] : 0;
861
- element = createElement(LayoutSegmentProvider, { depth: layoutDepth }, element);
1089
+ // called INSIDE this layout gets the correct child segments. We resolve the
1090
+ // route tree segments using actual param values and pass them through context.
1091
+ // We wrap the layout (not just children) because hooks are called from
1092
+ // components rendered inside the layout's own JSX.
1093
+ const treePos = route.layoutTreePositions ? route.layoutTreePositions[i] : 0;
1094
+ const childSegs = __resolveChildSegments(route.routeSegments || [], treePos, params);
1095
+ element = createElement(LayoutSegmentProvider, { childSegments: childSegs }, element);
862
1096
  }
863
1097
  }
864
1098
 
865
1099
  // Wrap with global error boundary if app/global-error.tsx exists.
866
- // This catches errors in the root layout itself.
1100
+ // This must be present in both HTML and RSC paths so the component tree
1101
+ // structure matches — otherwise React reconciliation on client-side navigation
1102
+ // would see a mismatched tree and destroy/recreate the DOM.
1103
+ //
1104
+ // For RSC requests (client-side nav), this provides error recovery on the client.
1105
+ // For HTML requests (initial page load), the ErrorBoundary catches during SSR
1106
+ // but produces double <html>/<body> (root layout + global-error). The request
1107
+ // handler detects this via the rscOnError flag and re-renders without layouts.
867
1108
  ${globalErrorVar ? `
868
1109
  const GlobalErrorComponent = ${globalErrorVar}.default;
869
1110
  if (GlobalErrorComponent) {
@@ -888,168 +1129,18 @@ const __allowedOrigins = ${JSON.stringify(allowedOrigins)};
888
1129
 
889
1130
  ${generateDevOriginCheckCode(config?.allowedDevOrigins)}
890
1131
 
891
- // ── CSRF origin validation for server actions ───────────────────────────
892
- // Matches Next.js behavior: compare the Origin header against the Host header.
893
- // If they don't match, the request is rejected with 403 unless the origin is
894
- // in the allowedOrigins list (from experimental.serverActions.allowedOrigins).
895
- function __isOriginAllowed(origin, allowed) {
896
- for (const pattern of allowed) {
897
- if (pattern.startsWith("*.")) {
898
- // Wildcard: *.example.com matches sub.example.com, a.b.example.com
899
- const suffix = pattern.slice(1); // ".example.com"
900
- if (origin === pattern.slice(2) || origin.endsWith(suffix)) return true;
901
- } else if (origin === pattern) {
902
- return true;
903
- }
904
- }
905
- return false;
906
- }
907
-
908
- function __validateCsrfOrigin(request) {
909
- const originHeader = request.headers.get("origin");
910
- // If there's no Origin header, allow the request — same-origin requests
911
- // from non-fetch navigations (e.g. SSR) may lack an Origin header.
912
- // The x-rsc-action custom header already provides protection against simple
913
- // form-based CSRF since custom headers can't be set by cross-origin forms.
914
- if (!originHeader || originHeader === "null") return null;
915
-
916
- let originHost;
917
- try {
918
- originHost = new URL(originHeader).host.toLowerCase();
919
- } catch {
920
- return new Response("Forbidden", { status: 403, headers: { "Content-Type": "text/plain" } });
921
- }
922
-
923
- // Only use the Host header for origin comparison — never trust
924
- // X-Forwarded-Host here, since it can be freely set by the client
925
- // and would allow the check to be bypassed if it matched a spoofed
926
- // Origin. The prod server's resolveHost() handles trusted proxy
927
- // scenarios separately.
928
- const hostHeader = (
929
- request.headers.get("host") ||
930
- ""
931
- ).split(",")[0].trim().toLowerCase();
932
-
933
- if (!hostHeader) return null;
934
-
935
- // Same origin — allow
936
- if (originHost === hostHeader) return null;
937
-
938
- // Check allowedOrigins from next.config.js
939
- if (__allowedOrigins.length > 0 && __isOriginAllowed(originHost, __allowedOrigins)) return null;
940
-
941
- console.warn(
942
- \`[vinext] CSRF origin mismatch: origin "\${originHost}" does not match host "\${hostHeader}". Blocking server action request.\`
943
- );
944
- return new Response("Forbidden", { status: 403, headers: { "Content-Type": "text/plain" } });
945
- }
946
-
947
- // ── ReDoS-safe regex compilation ────────────────────────────────────────
1132
+ // ── ReDoS-safe regex compilation (still needed for middleware matching) ──
948
1133
  ${generateSafeRegExpCode("modern")}
949
1134
 
950
1135
  // ── Path normalization ──────────────────────────────────────────────────
951
1136
  ${generateNormalizePathCode("modern")}
952
1137
 
953
- // ── Config pattern matching (redirects, rewrites, headers) ──────────────
954
- function __matchConfigPattern(pathname, pattern) {
955
- if (pattern.includes("(") || pattern.includes("\\\\") || /:[\\w-]+[*+][^/]/.test(pattern) || /:[\\w-]+\\./.test(pattern)) {
956
- try {
957
- const paramNames = [];
958
- const regexStr = pattern
959
- .replace(/\\./g, "\\\\.")
960
- .replace(/:([\\w-]+)\\*(?:\\(([^)]+)\\))?/g, (_, name, c) => { paramNames.push(name); return c ? "(" + c + ")" : "(.*)"; })
961
- .replace(/:([\\w-]+)\\+(?:\\(([^)]+)\\))?/g, (_, name, c) => { paramNames.push(name); return c ? "(" + c + ")" : "(.+)"; })
962
- .replace(/:([\\w-]+)\\(([^)]+)\\)/g, (_, name, c) => { paramNames.push(name); return "(" + c + ")"; })
963
- .replace(/:([\\w-]+)/g, (_, name) => { paramNames.push(name); return "([^/]+)"; });
964
- const re = __safeRegExp("^" + regexStr + "$");
965
- if (!re) return null;
966
- const match = re.exec(pathname);
967
- if (!match) return null;
968
- const params = Object.create(null);
969
- for (let i = 0; i < paramNames.length; i++) params[paramNames[i]] = match[i + 1] || "";
970
- return params;
971
- } catch { /* fall through */ }
972
- }
973
- const catchAllMatch = pattern.match(/:([\\w-]+)(\\*|\\+)$/);
974
- if (catchAllMatch) {
975
- const prefix = pattern.slice(0, pattern.lastIndexOf(":"));
976
- const paramName = catchAllMatch[1];
977
- const isPlus = catchAllMatch[2] === "+";
978
- if (!pathname.startsWith(prefix.replace(/\\/$/, ""))) return null;
979
- const rest = pathname.slice(prefix.replace(/\\/$/, "").length);
980
- if (isPlus && (!rest || rest === "/")) return null;
981
- let restValue = rest.startsWith("/") ? rest.slice(1) : rest;
982
- // NOTE: Do NOT decodeURIComponent here. The pathname is already decoded at
983
- // the request entry point. Decoding again would produce incorrect param values.
984
- return { [paramName]: restValue };
985
- }
986
- const parts = pattern.split("/");
987
- const pathParts = pathname.split("/");
988
- if (parts.length !== pathParts.length) return null;
989
- const params = Object.create(null);
990
- for (let i = 0; i < parts.length; i++) {
991
- if (parts[i].startsWith(":")) params[parts[i].slice(1)] = pathParts[i];
992
- else if (parts[i] !== pathParts[i]) return null;
993
- }
994
- return params;
995
- }
996
-
997
- function __parseCookies(cookieHeader) {
998
- if (!cookieHeader) return {};
999
- const cookies = {};
1000
- for (const part of cookieHeader.split(";")) {
1001
- const eq = part.indexOf("=");
1002
- if (eq === -1) continue;
1003
- const key = part.slice(0, eq).trim();
1004
- const value = part.slice(eq + 1).trim();
1005
- if (key) cookies[key] = value;
1006
- }
1007
- return cookies;
1008
- }
1009
-
1010
- function __checkSingleCondition(condition, ctx) {
1011
- switch (condition.type) {
1012
- case "header": {
1013
- const v = ctx.headers.get(condition.key);
1014
- if (v === null) return false;
1015
- if (condition.value !== undefined) { const re = __safeRegExp(condition.value); return re ? re.test(v) : v === condition.value; }
1016
- return true;
1017
- }
1018
- case "cookie": {
1019
- const v = ctx.cookies[condition.key];
1020
- if (v === undefined) return false;
1021
- if (condition.value !== undefined) { const re = __safeRegExp(condition.value); return re ? re.test(v) : v === condition.value; }
1022
- return true;
1023
- }
1024
- case "query": {
1025
- const v = ctx.query.get(condition.key);
1026
- if (v === null) return false;
1027
- if (condition.value !== undefined) { const re = __safeRegExp(condition.value); return re ? re.test(v) : v === condition.value; }
1028
- return true;
1029
- }
1030
- case "host": {
1031
- if (condition.value !== undefined) { const re = __safeRegExp(condition.value); return re ? re.test(ctx.host) : ctx.host === condition.value; }
1032
- return ctx.host === condition.key;
1033
- }
1034
- default: return false;
1035
- }
1036
- }
1037
-
1038
- function __checkHasConditions(has, missing, ctx) {
1039
- if (has) { for (const c of has) { if (!__checkSingleCondition(c, ctx)) return false; } }
1040
- if (missing) { for (const c of missing) { if (__checkSingleCondition(c, ctx)) return false; } }
1041
- return true;
1042
- }
1043
-
1044
- function __buildRequestContext(request) {
1045
- const url = new URL(request.url);
1046
- return {
1047
- headers: request.headers,
1048
- cookies: __parseCookies(request.headers.get("cookie")),
1049
- query: url.searchParams,
1050
- host: request.headers.get("host") || url.host,
1051
- };
1052
- }
1138
+ // ── Config pattern matching, redirects, rewrites, headers, CSRF validation,
1139
+ // external URL proxy, cookie parsing, and request context are imported from
1140
+ // config-matchers.ts and request-pipeline.ts (see import statements above).
1141
+ // This eliminates ~250 lines of duplicated inline code and ensures the
1142
+ // single-pass tokenizer in config-matchers.ts is used consistently
1143
+ // (fixing the chained .replace() divergence flagged by CodeQL).
1053
1144
 
1054
1145
  /**
1055
1146
  * Build a request context from the live ALS HeadersContext, which reflects
@@ -1060,7 +1151,7 @@ function __buildRequestContext(request) {
1060
1151
  function __buildPostMwRequestContext(request) {
1061
1152
  const url = new URL(request.url);
1062
1153
  const ctx = getHeadersContext();
1063
- if (!ctx) return __buildRequestContext(request);
1154
+ if (!ctx) return requestContextFromRequest(request);
1064
1155
  // ctx.cookies is a Map<string, string> (HeadersContext), but RequestContext
1065
1156
  // requires a plain Record<string, string> for has/missing cookie evaluation
1066
1157
  // (config-matchers.ts uses obj[key] not Map.get()). Convert here.
@@ -1073,51 +1164,14 @@ function __buildPostMwRequestContext(request) {
1073
1164
  };
1074
1165
  }
1075
1166
 
1076
- function __sanitizeDestination(dest) {
1077
- if (dest.startsWith("http://") || dest.startsWith("https://")) return dest;
1078
- dest = dest.replace(/^[\\\\/]+/, "/");
1079
- return dest;
1080
- }
1081
-
1082
- function __applyConfigRedirects(pathname, ctx) {
1083
- for (const rule of __configRedirects) {
1084
- const params = __matchConfigPattern(pathname, rule.source);
1085
- if (params) {
1086
- if (ctx && (rule.has || rule.missing)) { if (!__checkHasConditions(rule.has, rule.missing, ctx)) continue; }
1087
- let dest = rule.destination;
1088
- for (const [key, value] of Object.entries(params)) { dest = dest.replace(":" + key + "*", value); dest = dest.replace(":" + key + "+", value); dest = dest.replace(":" + key, value); }
1089
- dest = __sanitizeDestination(dest);
1090
- return { destination: dest, permanent: rule.permanent };
1091
- }
1092
- }
1093
- return null;
1094
- }
1095
-
1096
- function __applyConfigRewrites(pathname, rules, ctx) {
1097
- for (const rule of rules) {
1098
- const params = __matchConfigPattern(pathname, rule.source);
1099
- if (params) {
1100
- if (ctx && (rule.has || rule.missing)) { if (!__checkHasConditions(rule.has, rule.missing, ctx)) continue; }
1101
- let dest = rule.destination;
1102
- for (const [key, value] of Object.entries(params)) { dest = dest.replace(":" + key + "*", value); dest = dest.replace(":" + key + "+", value); dest = dest.replace(":" + key, value); }
1103
- dest = __sanitizeDestination(dest);
1104
- return dest;
1105
- }
1106
- }
1107
- return null;
1108
- }
1109
-
1110
- function __isExternalUrl(url) {
1111
- return /^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith("//");
1112
- }
1113
-
1114
1167
  /**
1115
- * Maximum server-action request body size (1 MB).
1116
- * Matches the Next.js default for serverActions.bodySizeLimit.
1168
+ * Maximum server-action request body size.
1169
+ * Configurable via experimental.serverActions.bodySizeLimit in next.config.
1170
+ * Defaults to 1MB, matching the Next.js default.
1117
1171
  * @see https://nextjs.org/docs/app/api-reference/config/next-config-js/serverActions#bodysizelimit
1118
1172
  * Prevents unbounded request body buffering.
1119
1173
  */
1120
- var __MAX_ACTION_BODY_SIZE = 1 * 1024 * 1024;
1174
+ var __MAX_ACTION_BODY_SIZE = ${JSON.stringify(bodySizeLimit)};
1121
1175
 
1122
1176
  /**
1123
1177
  * Read a request body as text with a size limit.
@@ -1176,77 +1230,11 @@ async function __readFormDataWithLimit(request, maxBytes) {
1176
1230
  return new Response(combined, { headers: { "Content-Type": contentType } }).formData();
1177
1231
  }
1178
1232
 
1179
- const __hopByHopHeaders = new Set(["connection","keep-alive","proxy-authenticate","proxy-authorization","te","trailers","transfer-encoding","upgrade"]);
1180
-
1181
- async function __proxyExternalRequest(request, externalUrl) {
1182
- const originalUrl = new URL(request.url);
1183
- const targetUrl = new URL(externalUrl);
1184
- for (const [key, value] of originalUrl.searchParams) {
1185
- if (!targetUrl.searchParams.has(key)) targetUrl.searchParams.set(key, value);
1186
- }
1187
- const headers = new Headers(request.headers);
1188
- headers.set("host", targetUrl.host);
1189
- headers.delete("connection");
1190
- // Strip credentials and internal headers to prevent leaking auth tokens,
1191
- // session cookies, and middleware internals to third-party origins.
1192
- headers.delete("cookie");
1193
- headers.delete("authorization");
1194
- headers.delete("x-api-key");
1195
- headers.delete("proxy-authorization");
1196
- for (const key of [...headers.keys()]) {
1197
- if (key.startsWith("x-middleware-")) headers.delete(key);
1198
- }
1199
- const method = request.method;
1200
- const hasBody = method !== "GET" && method !== "HEAD";
1201
- const init = { method, headers, redirect: "manual", signal: AbortSignal.timeout(30000) };
1202
- if (hasBody && request.body) { init.body = request.body; init.duplex = "half"; }
1203
- let upstream;
1204
- try { upstream = await fetch(targetUrl.href, init); }
1205
- catch (e) {
1206
- if (e && e.name === "TimeoutError") return new Response("Gateway Timeout", { status: 504 });
1207
- console.error("[vinext] External rewrite proxy error:", e); return new Response("Bad Gateway", { status: 502 });
1208
- }
1209
- const respHeaders = new Headers();
1210
- // Node.js fetch() auto-decompresses response bodies, while Workers fetch()
1211
- // preserves wire encoding. Only strip encoding/length on Node.js to avoid
1212
- // double-decompression errors without breaking Workers parity.
1213
- const __isNodeRuntime = typeof process !== "undefined" && !!(process.versions && process.versions.node);
1214
- upstream.headers.forEach(function(value, key) {
1215
- var lower = key.toLowerCase();
1216
- if (__hopByHopHeaders.has(lower)) return;
1217
- if (__isNodeRuntime && (lower === "content-encoding" || lower === "content-length")) return;
1218
- respHeaders.append(key, value);
1219
- });
1220
- return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: respHeaders });
1221
- }
1222
-
1223
- function __applyConfigHeaders(pathname, ctx) {
1224
- const result = [];
1225
- for (const rule of __configHeaders) {
1226
- const groups = [];
1227
- const withPlaceholders = rule.source.replace(/\\(([^)]+)\\)/g, (_, inner) => {
1228
- groups.push(inner);
1229
- return "___GROUP_" + (groups.length - 1) + "___";
1230
- });
1231
- const escaped = withPlaceholders
1232
- .replace(/\\./g, "\\\\.")
1233
- .replace(/\\+/g, "\\\\+")
1234
- .replace(/\\?/g, "\\\\?")
1235
- .replace(/\\*/g, ".*")
1236
- .replace(/:[\\w-]+/g, "[^/]+")
1237
- .replace(/___GROUP_(\\d+)___/g, (_, idx) => "(" + groups[Number(idx)] + ")");
1238
- const sourceRegex = __safeRegExp("^" + escaped + "$");
1239
- if (sourceRegex && sourceRegex.test(pathname)) {
1240
- if (ctx && (rule.has || rule.missing)) {
1241
- if (!__checkHasConditions(rule.has, rule.missing, ctx)) continue;
1242
- }
1243
- result.push(...rule.headers);
1244
- }
1245
- }
1246
- return result;
1247
- }
1248
-
1249
1233
  export default async function handler(request) {
1234
+ ${instrumentationPath ? `// Ensure instrumentation.register() has run before handling the first request.
1235
+ // This is a no-op after the first call (guarded by __instrumentationInitialized).
1236
+ await __ensureInstrumentation();
1237
+ ` : ""}
1250
1238
  // Wrap the entire request in nested AsyncLocalStorage.run() scopes to ensure
1251
1239
  // per-request isolation for all state modules. Each runWith*() creates an
1252
1240
  // ALS scope that propagates through all async continuations (including RSC
@@ -1258,7 +1246,7 @@ export default async function handler(request) {
1258
1246
  _runWithCacheState(() =>
1259
1247
  _runWithPrivateCache(() =>
1260
1248
  runWithFetchCache(async () => {
1261
- const __reqCtx = __buildRequestContext(request);
1249
+ const __reqCtx = requestContextFromRequest(request);
1262
1250
  // Per-request container for middleware state. Passed into
1263
1251
  // _handleRequest which fills in .headers and .status;
1264
1252
  // avoids module-level variables that race on Workers.
@@ -1273,7 +1261,7 @@ export default async function handler(request) {
1273
1261
  let pathname;
1274
1262
  try { pathname = __normalizePath(decodeURIComponent(url.pathname)); } catch { pathname = url.pathname; }
1275
1263
  ${bp ? `if (pathname.startsWith(${JSON.stringify(bp)})) pathname = pathname.slice(${JSON.stringify(bp)}.length) || "/";` : ""}
1276
- const extraHeaders = __applyConfigHeaders(pathname, __reqCtx);
1264
+ const extraHeaders = matchHeaders(pathname, __configHeaders, __reqCtx);
1277
1265
  for (const h of extraHeaders) {
1278
1266
  // Use append() for headers where multiple values must coexist
1279
1267
  // (Vary, Set-Cookie). Using set() on these would destroy
@@ -1282,19 +1270,13 @@ export default async function handler(request) {
1282
1270
  const lk = h.key.toLowerCase();
1283
1271
  if (lk === "vary" || lk === "set-cookie") {
1284
1272
  response.headers.append(h.key, h.value);
1285
- } else {
1273
+ } else if (!response.headers.has(lk)) {
1274
+ // Middleware headers take precedence: skip config keys already
1275
+ // set by middleware so middleware headers always win.
1286
1276
  response.headers.set(h.key, h.value);
1287
1277
  }
1288
1278
  }
1289
1279
  }
1290
- // Merge middleware response headers into the final response.
1291
- // This runs at the top level so every response path (route
1292
- // handlers, server actions, metadata, errors, etc.) gets them.
1293
- if (_mwCtx.headers) {
1294
- for (const [key, value] of _mwCtx.headers) {
1295
- response.headers.append(key, value);
1296
- }
1297
- }
1298
1280
  }
1299
1281
  return response;
1300
1282
  })
@@ -1313,22 +1295,18 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1313
1295
  // Format: "handlerStart,compileMs,renderMs" - all as integers (ms). Dev-only.
1314
1296
  const url = new URL(request.url);
1315
1297
 
1316
- // ── Cross-origin request protection ─────────────────────────────────
1298
+ // ── Cross-origin request protection (dev only) ─────────────────────
1317
1299
  // Block requests from non-localhost origins to prevent data exfiltration.
1318
- const __originBlock = __validateDevRequestOrigin(request);
1319
- if (__originBlock) return __originBlock;
1320
-
1321
- // Guard against protocol-relative URL open redirects.
1322
- // Paths like //example.com/ would be redirected to //example.com by the
1323
- // trailing-slash normalizer, which browsers interpret as http://example.com.
1324
- // Backslashes are equivalent to forward slashes in the URL spec
1325
- // (e.g. /\\evil.com is treated as //evil.com by browsers and the URL constructor).
1326
- // Next.js returns 404 for these paths. Check the RAW pathname before
1327
- // normalization so the guard fires before normalizePath collapses //.
1328
- if (url.pathname.replaceAll("\\\\", "/").startsWith("//")) {
1329
- return new Response("404 Not Found", { status: 404 });
1300
+ // Skipped in production — Vite replaces NODE_ENV at build time.
1301
+ if (process.env.NODE_ENV !== "production") {
1302
+ const __originBlock = __validateDevRequestOrigin(request);
1303
+ if (__originBlock) return __originBlock;
1330
1304
  }
1331
1305
 
1306
+ // Guard against protocol-relative URL open redirects (see request-pipeline.ts).
1307
+ const __protoGuard = guardProtocolRelativeUrl(url.pathname);
1308
+ if (__protoGuard) return __protoGuard;
1309
+
1332
1310
  // Decode percent-encoding and normalize pathname to canonical form.
1333
1311
  // decodeURIComponent prevents /%61dmin from bypassing /admin matchers.
1334
1312
  // __normalizePath collapses //foo///bar → /foo/bar, resolves . and .. segments.
@@ -1340,20 +1318,12 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1340
1318
 
1341
1319
  ${bp ? `
1342
1320
  // Strip basePath prefix
1343
- if (__basePath && pathname.startsWith(__basePath)) {
1344
- pathname = pathname.slice(__basePath.length) || "/";
1345
- }
1321
+ pathname = stripBasePath(pathname, __basePath);
1346
1322
  ` : ""}
1347
1323
 
1348
1324
  // Trailing slash normalization (redirect to canonical form)
1349
- if (pathname !== "/" && !pathname.startsWith("/api")) {
1350
- const hasTrailing = pathname.endsWith("/");
1351
- if (__trailingSlash && !hasTrailing && !pathname.endsWith(".rsc")) {
1352
- return Response.redirect(new URL(__basePath + pathname + "/" + url.search, request.url), 308);
1353
- } else if (!__trailingSlash && hasTrailing) {
1354
- return Response.redirect(new URL(__basePath + pathname.replace(/\\/+$/, "") + url.search, request.url), 308);
1355
- }
1356
- }
1325
+ const __tsRedirect = normalizeTrailingSlash(pathname, __basePath, __trailingSlash, url.search);
1326
+ if (__tsRedirect) return __tsRedirect;
1357
1327
 
1358
1328
  // ── Apply redirects from next.config.js ───────────────────────────────
1359
1329
  if (__configRedirects.length) {
@@ -1361,9 +1331,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1361
1331
  // arrive as /some/path.rsc but redirect patterns are defined without it (e.g.
1362
1332
  // /some/path). Without this, soft-nav fetches bypass all config redirects.
1363
1333
  const __redirPathname = pathname.endsWith(".rsc") ? pathname.slice(0, -4) : pathname;
1364
- const __redir = __applyConfigRedirects(__redirPathname, __reqCtx);
1334
+ const __redir = matchRedirect(__redirPathname, __configRedirects, __reqCtx);
1365
1335
  if (__redir) {
1366
- const __redirDest = __sanitizeDestination(
1336
+ const __redirDest = sanitizeDestination(
1367
1337
  __basePath && !__redir.destination.startsWith(__basePath)
1368
1338
  ? __basePath + __redir.destination
1369
1339
  : __redir.destination
@@ -1405,7 +1375,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1405
1375
  mwUrl.pathname = cleanPathname;
1406
1376
  const mwRequest = new Request(mwUrl, request);
1407
1377
  const nextRequest = mwRequest instanceof NextRequest ? mwRequest : new NextRequest(mwRequest);
1408
- const mwResponse = await middlewareFn(nextRequest);
1378
+ const mwFetchEvent = new NextFetchEvent({ page: cleanPathname });
1379
+ const mwResponse = await middlewareFn(nextRequest, mwFetchEvent);
1380
+ mwFetchEvent.drainWaitUntil();
1409
1381
  if (mwResponse) {
1410
1382
  // Check for x-middleware-next (continue)
1411
1383
  if (mwResponse.headers.get("x-middleware-next") === "1") {
@@ -1460,30 +1432,26 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1460
1432
  // internal routing signals and must never reach clients.
1461
1433
  if (_mwCtx.headers) {
1462
1434
  applyMiddlewareRequestHeaders(_mwCtx.headers);
1463
- for (const key of [..._mwCtx.headers.keys()]) {
1464
- if (key.startsWith("x-middleware-")) {
1465
- _mwCtx.headers.delete(key);
1466
- }
1467
- }
1435
+ processMiddlewareHeaders(_mwCtx.headers);
1468
1436
  }
1469
1437
  ` : ""}
1470
1438
 
1471
1439
  // Build post-middleware request context for afterFiles/fallback rewrites.
1472
1440
  // These run after middleware in the App Router execution order and should
1473
1441
  // evaluate has/missing conditions against middleware-modified headers.
1474
- // When no middleware is present, this falls back to __buildRequestContext.
1442
+ // When no middleware is present, this falls back to requestContextFromRequest.
1475
1443
  const __postMwReqCtx = __buildPostMwRequestContext(request);
1476
1444
 
1477
1445
  // ── Apply beforeFiles rewrites from next.config.js ────────────────────
1478
1446
  // In App Router execution order, beforeFiles runs after middleware so that
1479
1447
  // has/missing conditions can evaluate against middleware-modified headers.
1480
1448
  if (__configRewrites.beforeFiles && __configRewrites.beforeFiles.length) {
1481
- const __rewritten = __applyConfigRewrites(cleanPathname, __configRewrites.beforeFiles, __postMwReqCtx);
1449
+ const __rewritten = matchRewrite(cleanPathname, __configRewrites.beforeFiles, __postMwReqCtx);
1482
1450
  if (__rewritten) {
1483
- if (__isExternalUrl(__rewritten)) {
1451
+ if (isExternalUrl(__rewritten)) {
1484
1452
  setHeadersContext(null);
1485
1453
  setNavigationContext(null);
1486
- return __proxyExternalRequest(request, __rewritten);
1454
+ return proxyExternalRequest(request, __rewritten);
1487
1455
  }
1488
1456
  cleanPathname = __rewritten;
1489
1457
  }
@@ -1491,22 +1459,10 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1491
1459
 
1492
1460
  // ── Image optimization passthrough (dev mode — no transformation) ───────
1493
1461
  if (cleanPathname === "/_vinext/image") {
1494
- const __rawImgUrl = url.searchParams.get("url");
1495
- // Normalize backslashes: browsers and the URL constructor treat
1496
- // /\\evil.com as protocol-relative (//evil.com), bypassing the // check.
1497
- const __imgUrl = __rawImgUrl?.replaceAll("\\\\", "/") ?? null;
1498
- // Allowlist: must start with "/" but not "//" — blocks absolute URLs,
1499
- // protocol-relative, backslash variants, and exotic schemes.
1500
- if (!__imgUrl || !__imgUrl.startsWith("/") || __imgUrl.startsWith("//")) {
1501
- return new Response(!__rawImgUrl ? "Missing url parameter" : "Only relative URLs allowed", { status: 400 });
1502
- }
1503
- // Validate the constructed URL's origin hasn't changed (defense in depth).
1504
- const __resolvedImg = new URL(__imgUrl, request.url);
1505
- if (__resolvedImg.origin !== url.origin) {
1506
- return new Response("Only relative URLs allowed", { status: 400 });
1507
- }
1462
+ const __imgResult = validateImageUrl(url.searchParams.get("url"), request.url);
1463
+ if (__imgResult instanceof Response) return __imgResult;
1508
1464
  // In dev, redirect to the original asset URL so Vite's static serving handles it.
1509
- return Response.redirect(__resolvedImg.href, 302);
1465
+ return Response.redirect(new URL(__imgResult, url.origin).href, 302);
1510
1466
  }
1511
1467
 
1512
1468
  // Handle metadata routes (sitemap.xml, robots.txt, manifest.webmanifest, etc.)
@@ -1562,7 +1518,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1562
1518
  // ── CSRF protection ─────────────────────────────────────────────────
1563
1519
  // Verify that the Origin header matches the Host header to prevent
1564
1520
  // cross-site request forgery, matching Next.js server action behavior.
1565
- const csrfResponse = __validateCsrfOrigin(request);
1521
+ const csrfResponse = validateCsrfOrigin(request, __allowedOrigins);
1566
1522
  if (csrfResponse) return csrfResponse;
1567
1523
 
1568
1524
  // ── Body size limit ─────────────────────────────────────────────────
@@ -1671,16 +1627,23 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1671
1627
  element = createElement("div", null, "Page not found");
1672
1628
  }
1673
1629
 
1630
+ const onRenderError = createRscOnErrorHandler(
1631
+ request,
1632
+ cleanPathname,
1633
+ match ? match.route.pattern : cleanPathname,
1634
+ );
1674
1635
  const rscStream = renderToReadableStream(
1675
1636
  { root: element, returnValue },
1676
- { temporaryReferences, onError: rscOnError },
1637
+ { temporaryReferences, onError: onRenderError },
1677
1638
  );
1678
1639
 
1679
- // Collect cookies set during the action
1640
+ // Collect cookies set during the action synchronously (before stream is consumed).
1641
+ // Do NOT clear headers/navigation context here — the RSC stream is consumed lazily
1642
+ // by the client, and async server components that run during consumption need the
1643
+ // context to still be live. The AsyncLocalStorage scope from runWithHeadersContext
1644
+ // handles cleanup naturally when all async continuations complete.
1680
1645
  const actionPendingCookies = getAndClearPendingCookies();
1681
1646
  const actionDraftCookie = getDraftModeCookieHeader();
1682
- setHeadersContext(null);
1683
- setNavigationContext(null);
1684
1647
 
1685
1648
  const actionHeaders = { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" };
1686
1649
  const actionResponse = new Response(rscStream, { headers: actionHeaders });
@@ -1714,12 +1677,12 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1714
1677
 
1715
1678
  // ── Apply afterFiles rewrites from next.config.js ──────────────────────
1716
1679
  if (__configRewrites.afterFiles && __configRewrites.afterFiles.length) {
1717
- const __afterRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.afterFiles, __postMwReqCtx);
1680
+ const __afterRewritten = matchRewrite(cleanPathname, __configRewrites.afterFiles, __postMwReqCtx);
1718
1681
  if (__afterRewritten) {
1719
- if (__isExternalUrl(__afterRewritten)) {
1682
+ if (isExternalUrl(__afterRewritten)) {
1720
1683
  setHeadersContext(null);
1721
1684
  setNavigationContext(null);
1722
- return __proxyExternalRequest(request, __afterRewritten);
1685
+ return proxyExternalRequest(request, __afterRewritten);
1723
1686
  }
1724
1687
  cleanPathname = __afterRewritten;
1725
1688
  }
@@ -1729,12 +1692,12 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1729
1692
 
1730
1693
  // ── Fallback rewrites from next.config.js (if no route matched) ───────
1731
1694
  if (!match && __configRewrites.fallback && __configRewrites.fallback.length) {
1732
- const __fallbackRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.fallback, __postMwReqCtx);
1695
+ const __fallbackRewritten = matchRewrite(cleanPathname, __configRewrites.fallback, __postMwReqCtx);
1733
1696
  if (__fallbackRewritten) {
1734
- if (__isExternalUrl(__fallbackRewritten)) {
1697
+ if (isExternalUrl(__fallbackRewritten)) {
1735
1698
  setHeadersContext(null);
1736
1699
  setNavigationContext(null);
1737
- return __proxyExternalRequest(request, __fallbackRewritten);
1700
+ return proxyExternalRequest(request, __fallbackRewritten);
1738
1701
  }
1739
1702
  cleanPathname = __fallbackRewritten;
1740
1703
  match = matchRoute(cleanPathname, routes);
@@ -1763,6 +1726,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1763
1726
  if (route.routeHandler) {
1764
1727
  const handler = route.routeHandler;
1765
1728
  const method = request.method.toUpperCase();
1729
+ const revalidateSeconds = typeof handler.revalidate === "number" && handler.revalidate > 0 ? handler.revalidate : null;
1766
1730
 
1767
1731
  // Collect exported HTTP methods for OPTIONS auto-response and Allow header
1768
1732
  const HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
@@ -1797,6 +1761,12 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1797
1761
  try {
1798
1762
  const response = await handlerFn(request, { params });
1799
1763
 
1764
+ // Apply Cache-Control from route segment config (export const revalidate = N).
1765
+ // Next.js sets s-maxage on GET route handlers with a numeric revalidate value.
1766
+ if (revalidateSeconds !== null && (method === "GET" || isAutoHead) && !response.headers.has("cache-control")) {
1767
+ response.headers.set("cache-control", "s-maxage=" + revalidateSeconds + ", stale-while-revalidate");
1768
+ }
1769
+
1800
1770
  // Collect any Set-Cookie headers from cookies().set()/delete() calls
1801
1771
  const pendingCookies = getAndClearPendingCookies();
1802
1772
  const draftCookie = getDraftModeCookieHeader();
@@ -1985,9 +1955,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1985
1955
  interceptPage: intercept.page,
1986
1956
  interceptParams: intercept.matchedParams,
1987
1957
  }, url.searchParams);
1988
- const interceptStream = renderToReadableStream(interceptElement, { onError: rscOnError });
1989
- setHeadersContext(null);
1990
- setNavigationContext(null);
1958
+ const interceptOnError = createRscOnErrorHandler(
1959
+ request,
1960
+ cleanPathname,
1961
+ sourceRoute.pattern,
1962
+ );
1963
+ const interceptStream = renderToReadableStream(interceptElement, { onError: interceptOnError });
1964
+ // Do NOT clear headers/navigation context here — the RSC stream is consumed lazily
1965
+ // by the client, and async server components that run during consumption need the
1966
+ // context to still be live. The AsyncLocalStorage scope from runWithHeadersContext
1967
+ // handles cleanup naturally when all async continuations complete.
1991
1968
  return new Response(interceptStream, {
1992
1969
  headers: { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" },
1993
1970
  });
@@ -2018,7 +1995,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2018
1995
  }
2019
1996
  if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
2020
1997
  const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
2021
- const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request);
1998
+ const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, { matchedParams: params });
2022
1999
  if (fallbackResp) return fallbackResp;
2023
2000
  setHeadersContext(null);
2024
2001
  setNavigationContext(null);
@@ -2027,7 +2004,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2027
2004
  }
2028
2005
  }
2029
2006
  // Non-special error (e.g. generateMetadata() threw) — render error.tsx if available
2030
- const errorBoundaryResp = await renderErrorBoundaryPage(route, buildErr, isRscRequest, request);
2007
+ const errorBoundaryResp = await renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params);
2031
2008
  if (errorBoundaryResp) return errorBoundaryResp;
2032
2009
  throw buildErr;
2033
2010
  }
@@ -2049,7 +2026,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2049
2026
  }
2050
2027
  if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
2051
2028
  const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
2052
- const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request);
2029
+ const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, { matchedParams: params });
2053
2030
  if (fallbackResp) return fallbackResp;
2054
2031
  setHeadersContext(null);
2055
2032
  setNavigationContext(null);
@@ -2074,54 +2051,61 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2074
2051
  // layouts itself throws notFound() during the fallback rendering (causing a 500).
2075
2052
  if (route.layouts && route.layouts.length > 0) {
2076
2053
  const asyncParams = makeThenableParams(params);
2077
- for (let li = route.layouts.length - 1; li >= 0; li--) {
2078
- const LayoutComp = route.layouts[li]?.default;
2079
- if (!LayoutComp) continue;
2080
- try {
2081
- const lr = LayoutComp({ params: asyncParams, children: null });
2082
- if (lr && typeof lr === "object" && typeof lr.then === "function") await lr;
2083
- } catch (layoutErr) {
2084
- if (layoutErr && typeof layoutErr === "object" && "digest" in layoutErr) {
2085
- const digest = String(layoutErr.digest);
2086
- if (digest.startsWith("NEXT_REDIRECT;")) {
2087
- const parts = digest.split(";");
2088
- const redirectUrl = decodeURIComponent(parts[2]);
2089
- const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
2090
- setHeadersContext(null);
2091
- setNavigationContext(null);
2092
- return Response.redirect(new URL(redirectUrl, request.url), statusCode);
2093
- }
2094
- if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
2095
- const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
2096
- // Find the not-found component from the parent level (the boundary that
2097
- // would catch this in Next.js). Walk up from the throwing layout to find
2098
- // the nearest not-found at a parent layout's directory.
2099
- let parentNotFound = null;
2100
- if (route.notFounds) {
2101
- for (let pi = li - 1; pi >= 0; pi--) {
2102
- if (route.notFounds[pi]?.default) {
2103
- parentNotFound = route.notFounds[pi].default;
2104
- break;
2054
+ // Run inside ALS context so the module-level console.error patch suppresses
2055
+ // "Invalid hook call" only for this request's probe — concurrent requests
2056
+ // each have their own ALS store and are unaffected.
2057
+ const _layoutProbeResult = await _suppressHookWarningAls.run(true, async () => {
2058
+ for (let li = route.layouts.length - 1; li >= 0; li--) {
2059
+ const LayoutComp = route.layouts[li]?.default;
2060
+ if (!LayoutComp) continue;
2061
+ try {
2062
+ const lr = LayoutComp({ params: asyncParams, children: null });
2063
+ if (lr && typeof lr === "object" && typeof lr.then === "function") await lr;
2064
+ } catch (layoutErr) {
2065
+ if (layoutErr && typeof layoutErr === "object" && "digest" in layoutErr) {
2066
+ const digest = String(layoutErr.digest);
2067
+ if (digest.startsWith("NEXT_REDIRECT;")) {
2068
+ const parts = digest.split(";");
2069
+ const redirectUrl = decodeURIComponent(parts[2]);
2070
+ const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
2071
+ setHeadersContext(null);
2072
+ setNavigationContext(null);
2073
+ return Response.redirect(new URL(redirectUrl, request.url), statusCode);
2074
+ }
2075
+ if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
2076
+ const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
2077
+ // Find the not-found component from the parent level (the boundary that
2078
+ // would catch this in Next.js). Walk up from the throwing layout to find
2079
+ // the nearest not-found at a parent layout's directory.
2080
+ let parentNotFound = null;
2081
+ if (route.notFounds) {
2082
+ for (let pi = li - 1; pi >= 0; pi--) {
2083
+ if (route.notFounds[pi]?.default) {
2084
+ parentNotFound = route.notFounds[pi].default;
2085
+ break;
2086
+ }
2105
2087
  }
2106
2088
  }
2089
+ if (!parentNotFound) parentNotFound = ${rootNotFoundVar ? `${rootNotFoundVar}?.default` : "null"};
2090
+ // Wrap in only the layouts above the throwing one
2091
+ const parentLayouts = route.layouts.slice(0, li);
2092
+ const fallbackResp = await renderHTTPAccessFallbackPage(
2093
+ route, statusCode, isRscRequest, request,
2094
+ { boundaryComponent: parentNotFound, layouts: parentLayouts, matchedParams: params }
2095
+ );
2096
+ if (fallbackResp) return fallbackResp;
2097
+ setHeadersContext(null);
2098
+ setNavigationContext(null);
2099
+ const statusText = statusCode === 403 ? "Forbidden" : statusCode === 401 ? "Unauthorized" : "Not Found";
2100
+ return new Response(statusText, { status: statusCode });
2107
2101
  }
2108
- if (!parentNotFound) parentNotFound = ${rootNotFoundVar ? `${rootNotFoundVar}?.default` : "null"};
2109
- // Wrap in only the layouts above the throwing one
2110
- const parentLayouts = route.layouts.slice(0, li);
2111
- const fallbackResp = await renderHTTPAccessFallbackPage(
2112
- route, statusCode, isRscRequest, request,
2113
- { boundaryComponent: parentNotFound, layouts: parentLayouts }
2114
- );
2115
- if (fallbackResp) return fallbackResp;
2116
- setHeadersContext(null);
2117
- setNavigationContext(null);
2118
- const statusText = statusCode === 403 ? "Forbidden" : statusCode === 401 ? "Unauthorized" : "Not Found";
2119
- return new Response(statusText, { status: statusCode });
2120
2102
  }
2103
+ // Not a special error — let it propagate through normal RSC rendering
2121
2104
  }
2122
- // Not a special error — let it propagate through normal RSC rendering
2123
2105
  }
2124
- }
2106
+ return null;
2107
+ });
2108
+ if (_layoutProbeResult instanceof Response) return _layoutProbeResult;
2125
2109
  }
2126
2110
 
2127
2111
  // Pre-render the page component to catch redirect()/notFound() thrown synchronously.
@@ -2134,43 +2118,52 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2134
2118
  // would be hit before the RSC stream even starts).
2135
2119
  //
2136
2120
  // Because this calls the component outside React's render cycle, hooks like use()
2137
- // trigger "Invalid hook call" console.error in dev. Suppress that expected warning.
2121
+ // trigger "Invalid hook call" console.error in dev. The module-level ALS patch
2122
+ // suppresses the warning only within this request's execution context.
2138
2123
  const _hasLoadingBoundary = !!(route.loading && route.loading.default);
2139
- const _origConsoleError = console.error;
2140
- console.error = (...args) => {
2141
- if (typeof args[0] === "string" && args[0].includes("Invalid hook call")) return;
2142
- _origConsoleError.apply(console, args);
2143
- };
2144
- try {
2145
- const testResult = PageComponent({ params });
2146
- // If it's a promise (async component), only await if there's no loading boundary.
2147
- // With a loading boundary, the Suspense streaming pipeline handles async resolution
2148
- // and any redirect/notFound errors via rscOnError.
2149
- if (testResult && typeof testResult === "object" && typeof testResult.then === "function") {
2150
- if (!_hasLoadingBoundary) {
2151
- await testResult;
2152
- } else {
2153
- // Suppress unhandled promise rejection — with a loading boundary,
2154
- // redirect/notFound errors are handled by rscOnError during streaming.
2155
- testResult.catch(() => {});
2124
+ const _pageProbeResult = await _suppressHookWarningAls.run(true, async () => {
2125
+ try {
2126
+ const testResult = PageComponent({ params });
2127
+ // If it's a promise (async component), only await if there's no loading boundary.
2128
+ // With a loading boundary, the Suspense streaming pipeline handles async resolution
2129
+ // and any redirect/notFound errors via rscOnError.
2130
+ if (testResult && typeof testResult === "object" && typeof testResult.then === "function") {
2131
+ if (!_hasLoadingBoundary) {
2132
+ await testResult;
2133
+ } else {
2134
+ // Suppress unhandled promise rejection with a loading boundary,
2135
+ // redirect/notFound errors are handled by rscOnError during streaming.
2136
+ testResult.catch(() => {});
2137
+ }
2156
2138
  }
2139
+ } catch (preRenderErr) {
2140
+ const specialResponse = await handleRenderError(preRenderErr);
2141
+ if (specialResponse) return specialResponse;
2142
+ // Non-special errors from the pre-render test are expected (e.g. use() hook
2143
+ // fails outside React's render cycle, client references can't execute on server).
2144
+ // Only redirect/notFound/forbidden/unauthorized are actionable here — other
2145
+ // errors will be properly caught during actual RSC/SSR rendering below.
2157
2146
  }
2158
- } catch (preRenderErr) {
2159
- const specialResponse = await handleRenderError(preRenderErr);
2160
- if (specialResponse) return specialResponse;
2161
- // Non-special errors from the pre-render test are expected (e.g. use() hook
2162
- // fails outside React's render cycle, client references can't execute on server).
2163
- // Only redirect/notFound/forbidden/unauthorized are actionable here — other
2164
- // errors will be properly caught during actual RSC/SSR rendering below.
2165
- } finally {
2166
- console.error = _origConsoleError;
2167
- }
2147
+ return null;
2148
+ });
2149
+ if (_pageProbeResult instanceof Response) return _pageProbeResult;
2168
2150
 
2169
2151
  // Mark end of compile phase: route matching, middleware, tree building are done.
2170
2152
  if (process.env.NODE_ENV !== "production") __compileEnd = performance.now();
2171
2153
 
2172
- // Render to RSC stream
2173
- const rscStream = renderToReadableStream(element, { onError: rscOnError });
2154
+ // Render to RSC stream.
2155
+ // Track non-navigation RSC errors so we can detect when the in-tree global
2156
+ // ErrorBoundary catches during SSR (producing double <html>/<body>) and
2157
+ // re-render with renderErrorBoundaryPage (which skips layouts for global-error).
2158
+ let _rscErrorForRerender = null;
2159
+ const _baseOnError = createRscOnErrorHandler(request, cleanPathname, route.pattern);
2160
+ const onRenderError = function(error, requestInfo, errorContext) {
2161
+ if (!(error && typeof error === "object" && "digest" in error)) {
2162
+ _rscErrorForRerender = error;
2163
+ }
2164
+ return _baseOnError(error, requestInfo, errorContext);
2165
+ };
2166
+ const rscStream = renderToReadableStream(element, { onError: onRenderError });
2174
2167
 
2175
2168
  if (isRscRequest) {
2176
2169
  // Direct RSC stream response (for client-side navigation)
@@ -2191,7 +2184,37 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2191
2184
  } else if (revalidateSeconds) {
2192
2185
  responseHeaders["Cache-Control"] = "s-maxage=" + revalidateSeconds + ", stale-while-revalidate";
2193
2186
  }
2194
- // Middleware response headers are merged by the handler() wrapper.
2187
+ // Merge middleware response headers into the RSC response.
2188
+ // set-cookie and vary are accumulated to preserve existing values
2189
+ // (e.g. "Vary: RSC, Accept" set above); all other keys use plain
2190
+ // assignment so middleware headers win over config headers, which
2191
+ // the outer handler applies afterward and skips keys already present.
2192
+ if (_mwCtx.headers) {
2193
+ for (const [key, value] of _mwCtx.headers) {
2194
+ const lk = key.toLowerCase();
2195
+ if (lk === "set-cookie") {
2196
+ const existing = responseHeaders[lk];
2197
+ if (Array.isArray(existing)) {
2198
+ existing.push(value);
2199
+ } else if (existing) {
2200
+ responseHeaders[lk] = [existing, value];
2201
+ } else {
2202
+ responseHeaders[lk] = [value];
2203
+ }
2204
+ } else if (lk === "vary") {
2205
+ // Accumulate Vary values to preserve the existing "RSC, Accept" entry.
2206
+ const existing = responseHeaders["Vary"] ?? responseHeaders["vary"];
2207
+ if (existing) {
2208
+ responseHeaders["Vary"] = existing + ", " + value;
2209
+ if (responseHeaders["vary"] !== undefined) delete responseHeaders["vary"];
2210
+ } else {
2211
+ responseHeaders[key] = value;
2212
+ }
2213
+ } else {
2214
+ responseHeaders[key] = value;
2215
+ }
2216
+ }
2217
+ }
2195
2218
  // Attach internal timing header so the dev server middleware can log it.
2196
2219
  // Format: "handlerStart,compileMs,renderMs"
2197
2220
  // handlerStart - absolute performance.now() when _handleRequest began,
@@ -2239,11 +2262,25 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2239
2262
  const specialResponse = await handleRenderError(ssrErr);
2240
2263
  if (specialResponse) return specialResponse;
2241
2264
  // Non-special error during SSR — render error.tsx if available
2242
- const errorBoundaryResp = await renderErrorBoundaryPage(route, ssrErr, isRscRequest, request);
2265
+ const errorBoundaryResp = await renderErrorBoundaryPage(route, ssrErr, isRscRequest, request, params);
2243
2266
  if (errorBoundaryResp) return errorBoundaryResp;
2244
2267
  throw ssrErr;
2245
2268
  }
2246
2269
 
2270
+ // If an RSC error was caught by the in-tree global ErrorBoundary during SSR,
2271
+ // the HTML output has double <html>/<body> (root layout + global-error.tsx).
2272
+ // Discard it and re-render using renderErrorBoundaryPage which skips layouts
2273
+ // when the error falls through to global-error.tsx.
2274
+ ${globalErrorVar ? `
2275
+ if (_rscErrorForRerender && !isRscRequest) {
2276
+ const _hasLocalBoundary = !!(route?.error?.default) || !!(route?.errors && route.errors.some(function(e) { return e?.default; }));
2277
+ if (!_hasLocalBoundary) {
2278
+ const cleanResp = await renderErrorBoundaryPage(route, _rscErrorForRerender, false, request, params);
2279
+ if (cleanResp) return cleanResp;
2280
+ }
2281
+ }
2282
+ ` : ""}
2283
+
2247
2284
  // Check for draftMode Set-Cookie header (from draftMode().enable()/disable())
2248
2285
  const draftCookie = getDraftModeCookieHeader();
2249
2286
 
@@ -2259,7 +2296,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2259
2296
  if (fontLinkHeader) {
2260
2297
  response.headers.set("Link", fontLinkHeader);
2261
2298
  }
2262
- // Middleware response headers are merged by the handler() wrapper.
2299
+ // Merge middleware response headers into the final response.
2300
+ // The response is freshly constructed above (new Response(htmlStream, {...})),
2301
+ // so set() and append() are equivalent — there are no same-key conflicts yet.
2302
+ // Precedence over config headers is handled by the outer handler, which
2303
+ // skips config keys that middleware already placed on the response.
2304
+ if (_mwCtx.headers) {
2305
+ for (const [key, value] of _mwCtx.headers) {
2306
+ response.headers.append(key, value);
2307
+ }
2308
+ }
2263
2309
  // Attach internal timing header so the dev server middleware can log it.
2264
2310
  // Format: "handlerStart,compileMs,renderMs"
2265
2311
  // handlerStart - absolute performance.now() when _handleRequest began,
@@ -2358,771 +2404,4 @@ if (import.meta.hot) {
2358
2404
  }
2359
2405
  `;
2360
2406
  }
2361
- /**
2362
- * Generate the virtual SSR entry module.
2363
- *
2364
- * This runs in the `ssr` Vite environment. It receives an RSC stream,
2365
- * deserializes it to a React tree, and renders to HTML.
2366
- */
2367
- export function generateSsrEntry() {
2368
- return `
2369
- import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
2370
- import { renderToReadableStream, renderToStaticMarkup } from "react-dom/server.edge";
2371
- import { setNavigationContext, ServerInsertedHTMLContext } from "next/navigation";
2372
- import { runWithNavigationContext as _runWithNavCtx } from "vinext/navigation-state";
2373
- import { safeJsonStringify } from "vinext/html";
2374
- import { createElement as _ssrCE } from "react";
2375
-
2376
- /**
2377
- * Collect all chunks from a ReadableStream into an array of text strings.
2378
- * Used to capture the RSC payload for embedding in HTML.
2379
- * The RSC flight protocol is text-based (line-delimited key:value pairs),
2380
- * so we decode to text strings instead of byte arrays — this is dramatically
2381
- * more compact when JSON-serialized into inline <script> tags.
2382
- */
2383
- async function collectStreamChunks(stream) {
2384
- const reader = stream.getReader();
2385
- const decoder = new TextDecoder();
2386
- const chunks = [];
2387
- while (true) {
2388
- const { done, value } = await reader.read();
2389
- if (done) break;
2390
- // Decode Uint8Array to text string for compact JSON serialization
2391
- chunks.push(decoder.decode(value, { stream: true }));
2392
- }
2393
- return chunks;
2394
- }
2395
-
2396
- // React 19 dev-mode workaround (see VinextFlightRoot in handleSsr):
2397
- //
2398
- // In dev, Flight error decoding in react-server-dom-webpack/client.edge
2399
- // can hit resolveErrorDev() which (via React's dev error stack capture)
2400
- // expects a non-null hooks dispatcher.
2401
- //
2402
- // Vinext previously called createFromReadableStream() outside of any React render.
2403
- // When an RSC stream contains an error, dev-mode decoding could crash with:
2404
- // - "Invalid hook call"
2405
- // - "Cannot read properties of null (reading 'useContext')"
2406
- //
2407
- // Fix: call createFromReadableStream() lazily inside a React component render.
2408
- // This mirrors Next.js behavior and ensures the dispatcher is set.
2409
-
2410
- /**
2411
- * Create a TransformStream that appends RSC chunks as inline <script> tags
2412
- * to the HTML stream. This allows progressive hydration — the browser receives
2413
- * RSC data incrementally as Suspense boundaries resolve, rather than waiting
2414
- * for the entire RSC payload before hydration can begin.
2415
- *
2416
- * Each chunk is written as:
2417
- * <script>self.__VINEXT_RSC_CHUNKS__=self.__VINEXT_RSC_CHUNKS__||[];self.__VINEXT_RSC_CHUNKS__.push("...")</script>
2418
- *
2419
- * Chunks are embedded as text strings (not byte arrays) since the RSC flight
2420
- * protocol is text-based. The browser entry encodes them back to Uint8Array.
2421
- * This is ~3x more compact than the previous byte-array format.
2422
- */
2423
- function createRscEmbedTransform(embedStream) {
2424
- const reader = embedStream.getReader();
2425
- const _decoder = new TextDecoder();
2426
- let done = false;
2427
- let pendingChunks = [];
2428
- let reading = false;
2429
-
2430
- // Fix invalid preload "as" values in RSC Flight hint lines before
2431
- // they reach the client. React Flight emits HL hints with
2432
- // as="stylesheet" for CSS, but the HTML spec requires as="style"
2433
- // for <link rel="preload">. The fixPreloadAs() below only fixes the
2434
- // server-rendered HTML stream; this fixes the raw Flight data that
2435
- // gets embedded as __VINEXT_RSC_CHUNKS__ and processed client-side.
2436
- function fixFlightHints(text) {
2437
- // Flight hint format: <id>:HL["url","stylesheet"] or with options
2438
- return text.replace(/(\\d+:HL\\[.*?),"stylesheet"(\\]|,)/g, '$1,"style"$2');
2439
- }
2440
-
2441
- // Start reading RSC chunks in the background, accumulating them as text strings.
2442
- // The RSC flight protocol is text-based, so decoding to strings and embedding
2443
- // as JSON strings is ~3x more compact than the byte-array format.
2444
- async function pumpReader() {
2445
- if (reading) return;
2446
- reading = true;
2447
- try {
2448
- while (true) {
2449
- const result = await reader.read();
2450
- if (result.done) {
2451
- done = true;
2452
- break;
2453
- }
2454
- const text = _decoder.decode(result.value, { stream: true });
2455
- pendingChunks.push(fixFlightHints(text));
2456
- }
2457
- } catch (err) {
2458
- if (process.env.NODE_ENV !== "production") {
2459
- console.warn("[vinext] RSC embed stream read error:", err);
2460
- }
2461
- done = true;
2462
- }
2463
- reading = false;
2464
- }
2465
-
2466
- // Fire off the background reader immediately
2467
- const pumpPromise = pumpReader();
2468
-
2469
- return {
2470
- /**
2471
- * Flush any accumulated RSC chunks as <script> tags.
2472
- * Called after each HTML chunk is enqueued.
2473
- */
2474
- flush() {
2475
- if (pendingChunks.length === 0) return "";
2476
- const chunks = pendingChunks;
2477
- pendingChunks = [];
2478
- let scripts = "";
2479
- for (const chunk of chunks) {
2480
- scripts += "<script>self.__VINEXT_RSC_CHUNKS__=self.__VINEXT_RSC_CHUNKS__||[];self.__VINEXT_RSC_CHUNKS__.push(" + safeJsonStringify(chunk) + ")</script>";
2481
- }
2482
- return scripts;
2483
- },
2484
-
2485
- /**
2486
- * Wait for the RSC stream to fully complete and return any final
2487
- * script tags plus the closing signal.
2488
- */
2489
- async finalize() {
2490
- await pumpPromise;
2491
- let scripts = this.flush();
2492
- // Signal that all RSC chunks have been sent.
2493
- // Params are already embedded in <head> — no need to include here.
2494
- scripts += "<script>self.__VINEXT_RSC_DONE__=true</script>";
2495
- return scripts;
2496
- },
2497
- };
2498
- }
2499
-
2500
- /**
2501
- * Render the RSC stream to HTML.
2502
- *
2503
- * @param rscStream - The RSC payload stream from the RSC environment
2504
- * @param navContext - Navigation context for client component SSR hooks.
2505
- * "use client" components like those using usePathname() need the current
2506
- * request URL during SSR, and they run in this SSR environment (separate
2507
- * from the RSC environment where the context was originally set).
2508
- * @param fontData - Font links and styles collected from the RSC environment.
2509
- * Fonts are loaded during RSC rendering (when layout calls Geist() etc.),
2510
- * and the data needs to be passed to SSR since they're separate module instances.
2511
- */
2512
- export async function handleSsr(rscStream, navContext, fontData) {
2513
- // Wrap in a navigation ALS scope for per-request isolation in the SSR
2514
- // environment. The SSR environment has separate module instances from RSC,
2515
- // so it needs its own ALS scope.
2516
- return _runWithNavCtx(async () => {
2517
- // Set navigation context so hooks like usePathname() work during SSR
2518
- // of "use client" components
2519
- if (navContext) {
2520
- setNavigationContext(navContext);
2521
- }
2522
-
2523
- // Clear any stale callbacks from previous requests
2524
- const { clearServerInsertedHTML, flushServerInsertedHTML, useServerInsertedHTML: _addInsertedHTML } = await import("next/navigation");
2525
- clearServerInsertedHTML();
2526
-
2527
- try {
2528
- // Tee the RSC stream - one for SSR rendering, one for embedding in HTML.
2529
- // This ensures the browser uses the SAME RSC payload for hydration that
2530
- // was used to generate the HTML, avoiding hydration mismatches (React #418).
2531
- const [ssrStream, embedStream] = rscStream.tee();
2532
-
2533
- // Create the progressive RSC embed helper — it reads the embed stream
2534
- // in the background and provides script tags to inject into the HTML stream.
2535
- const rscEmbed = createRscEmbedTransform(embedStream);
2536
-
2537
- // Deserialize RSC stream back to React VDOM.
2538
- // IMPORTANT: Do NOT await this — createFromReadableStream returns a thenable
2539
- // that React's renderToReadableStream can consume progressively. By passing
2540
- // the unresolved thenable, React will render Suspense fallbacks (loading.tsx)
2541
- // immediately in the HTML shell, then stream in resolved content as RSC
2542
- // chunks arrive. Awaiting here would block until all async server components
2543
- // complete, collapsing the streaming behavior.
2544
- // Lazily create the Flight root inside render so React's hook dispatcher is set
2545
- // (avoids React 19 dev-mode resolveErrorDev() crash). VinextFlightRoot returns
2546
- // a thenable (not a ReactNode), which React 19 consumes via its internal
2547
- // thenable-as-child suspend/resume behavior. This matches Next.js's approach.
2548
- let flightRoot;
2549
- function VinextFlightRoot() {
2550
- if (!flightRoot) {
2551
- flightRoot = createFromReadableStream(ssrStream);
2552
- }
2553
- return flightRoot;
2554
- }
2555
- const root = _ssrCE(VinextFlightRoot);
2556
-
2557
- // Wrap with ServerInsertedHTMLContext.Provider so libraries that use
2558
- // useContext(ServerInsertedHTMLContext) (Apollo Client, styled-components,
2559
- // etc.) get a working callback registration function during SSR.
2560
- // The provider value is useServerInsertedHTML — same function that direct
2561
- // callers use — so both paths push to the same ALS-backed callback array.
2562
- const ssrRoot = ServerInsertedHTMLContext
2563
- ? _ssrCE(ServerInsertedHTMLContext.Provider, { value: _addInsertedHTML }, root)
2564
- : root;
2565
-
2566
- // Get the bootstrap script content for the browser entry
2567
- const bootstrapScriptContent =
2568
- await import.meta.viteRsc.loadBootstrapScriptContent("index");
2569
-
2570
- // djb2 hash for digest generation in the SSR environment.
2571
- // Matches the RSC environment's __errorDigest function.
2572
- function ssrErrorDigest(str) {
2573
- let hash = 5381;
2574
- for (let i = str.length - 1; i >= 0; i--) {
2575
- hash = (hash * 33) ^ str.charCodeAt(i);
2576
- }
2577
- return (hash >>> 0).toString();
2578
- }
2579
-
2580
- // Render HTML (streaming SSR)
2581
- // useServerInsertedHTML callbacks are registered during this render.
2582
- // The onError callback preserves the digest for Next.js navigation errors
2583
- // (redirect, notFound, forbidden, unauthorized) thrown inside Suspense
2584
- // boundaries during RSC streaming. Without this, React's default onError
2585
- // returns undefined and the digest is lost in the $RX() call, preventing
2586
- // client-side error boundaries from identifying the error type.
2587
- // In production, non-navigation errors also get a digest hash so they
2588
- // can be correlated with server logs without leaking details to clients.
2589
- const htmlStream = await renderToReadableStream(ssrRoot, {
2590
- bootstrapScriptContent,
2591
- onError(error) {
2592
- if (error && typeof error === "object" && "digest" in error) {
2593
- return String(error.digest);
2594
- }
2595
- // In production, generate a digest hash for non-navigation errors
2596
- if (process.env.NODE_ENV === "production" && error) {
2597
- const msg = error instanceof Error ? error.message : String(error);
2598
- const stack = error instanceof Error ? (error.stack || "") : "";
2599
- return ssrErrorDigest(msg + stack);
2600
- }
2601
- return undefined;
2602
- },
2603
- });
2604
-
2605
-
2606
- // Flush useServerInsertedHTML callbacks (CSS-in-JS style injection)
2607
- const insertedElements = flushServerInsertedHTML();
2608
-
2609
- // Render the inserted elements to HTML strings
2610
- const { Fragment } = await import("react");
2611
- let insertedHTML = "";
2612
- for (const el of insertedElements) {
2613
- try {
2614
- insertedHTML += renderToStaticMarkup(_ssrCE(Fragment, null, el));
2615
- } catch {
2616
- // Skip elements that can't be rendered
2617
- }
2618
- }
2619
-
2620
- // Escape HTML attribute values (defense-in-depth for font URLs/types).
2621
- function _escAttr(s) { return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;"); }
2622
-
2623
- // Build font HTML from data passed from RSC environment
2624
- // (Fonts are loaded during RSC rendering, and RSC/SSR are separate module instances)
2625
- let fontHTML = "";
2626
- if (fontData) {
2627
- if (fontData.links && fontData.links.length > 0) {
2628
- for (const url of fontData.links) {
2629
- fontHTML += '<link rel="stylesheet" href="' + _escAttr(url) + '" />\\n';
2630
- }
2631
- }
2632
- // Emit <link rel="preload"> for local font files
2633
- if (fontData.preloads && fontData.preloads.length > 0) {
2634
- for (const preload of fontData.preloads) {
2635
- fontHTML += '<link rel="preload" href="' + _escAttr(preload.href) + '" as="font" type="' + _escAttr(preload.type) + '" crossorigin />\\n';
2636
- }
2637
- }
2638
- if (fontData.styles && fontData.styles.length > 0) {
2639
- fontHTML += '<style data-vinext-fonts>' + fontData.styles.join("\\n") + '</style>\\n';
2640
- }
2641
- }
2642
-
2643
- // Extract client entry module URL from bootstrapScriptContent to emit
2644
- // a <link rel="modulepreload"> hint. The RSC plugin formats bootstrap
2645
- // content as: import("URL") — we extract the URL so the browser can
2646
- // speculatively fetch and parse the JS module while still processing
2647
- // the HTML body, instead of waiting until it reaches the inline script.
2648
- let modulePreloadHTML = "";
2649
- if (bootstrapScriptContent) {
2650
- const m = bootstrapScriptContent.match(/import\\("([^"]+)"\\)/);
2651
- if (m && m[1]) {
2652
- modulePreloadHTML = '<link rel="modulepreload" href="' + _escAttr(m[1]) + '" />\\n';
2653
- }
2654
- }
2655
-
2656
- // Head-injected HTML: server-inserted HTML, font HTML, route params,
2657
- // and modulepreload hints.
2658
- // RSC payload is now embedded progressively via script tags in the body stream.
2659
- // Params are embedded eagerly in <head> so they're available before client
2660
- // hydration starts, avoiding the need for polling on the client.
2661
- const paramsScript = '<script>self.__VINEXT_RSC_PARAMS__=' + safeJsonStringify(navContext?.params || {}) + '</script>';
2662
- const injectHTML = paramsScript + modulePreloadHTML + insertedHTML + fontHTML;
2663
-
2664
- // Inject the collected HTML before </head> and progressively embed RSC
2665
- // chunks as script tags throughout the HTML body stream.
2666
- const decoder = new TextDecoder();
2667
- const encoder = new TextEncoder();
2668
- let injected = false;
2669
-
2670
- // Fix invalid preload "as" values in server-rendered HTML.
2671
- // React Fizz emits <link rel="preload" as="stylesheet"> for CSS,
2672
- // but the HTML spec requires as="style" for <link rel="preload">.
2673
- // Note: fixFlightHints() in createRscEmbedTransform handles the
2674
- // complementary case — fixing the raw Flight stream data before
2675
- // it's embedded as __VINEXT_RSC_CHUNKS__ for client-side processing.
2676
- // See: https://html.spec.whatwg.org/multipage/links.html#link-type-preload
2677
- function fixPreloadAs(html) {
2678
- // Match <link ...rel="preload"... as="stylesheet"...> in any attribute order
2679
- return html.replace(/<link(?=[^>]*\\srel="preload")[^>]*>/g, function(tag) {
2680
- return tag.replace(' as="stylesheet"', ' as="style"');
2681
- });
2682
- }
2683
-
2684
- // Tick-buffered RSC script injection.
2685
- //
2686
- // React's renderToReadableStream (Fizz) flushes chunks synchronously
2687
- // within one microtask — all chunks from a single flushCompletedQueues
2688
- // call arrive in the same macrotask. We buffer HTML chunks as they
2689
- // arrive, then use setTimeout(0) to defer emitting them plus any
2690
- // accumulated RSC scripts to the next macrotask. This guarantees we
2691
- // never inject <script> tags between partial HTML chunks (which would
2692
- // corrupt split elements like "<linearGradi" + "ent>"), while still
2693
- // delivering RSC data progressively as Suspense boundaries resolve.
2694
- //
2695
- // Reference: rsc-html-stream by Devon Govett (credited by Next.js)
2696
- // https://github.com/devongovett/rsc-html-stream
2697
- let buffered = [];
2698
- let timeoutId = null;
2699
-
2700
- const transform = new TransformStream({
2701
- transform(chunk, controller) {
2702
- const text = decoder.decode(chunk, { stream: true });
2703
- const fixed = fixPreloadAs(text);
2704
- buffered.push(fixed);
2705
-
2706
- if (timeoutId !== null) return;
2707
-
2708
- timeoutId = setTimeout(() => {
2709
- // Flush all buffered HTML chunks from this React flush cycle
2710
- for (const buf of buffered) {
2711
- if (!injected) {
2712
- const headEnd = buf.indexOf("</head>");
2713
- if (headEnd !== -1) {
2714
- const before = buf.slice(0, headEnd);
2715
- const after = buf.slice(headEnd);
2716
- controller.enqueue(encoder.encode(before + injectHTML + after));
2717
- injected = true;
2718
- continue;
2719
- }
2720
- }
2721
- controller.enqueue(encoder.encode(buf));
2722
- }
2723
- buffered = [];
2724
-
2725
- // Now safe to inject any accumulated RSC scripts — we're between
2726
- // React flush cycles, so no partial HTML chunks can follow until
2727
- // the next macrotask.
2728
- const rscScripts = rscEmbed.flush();
2729
- if (rscScripts) {
2730
- controller.enqueue(encoder.encode(rscScripts));
2731
- }
2732
-
2733
- timeoutId = null;
2734
- }, 0);
2735
- },
2736
- async flush(controller) {
2737
- // Cancel any pending setTimeout callback — flush() drains
2738
- // everything itself, so the callback would be a no-op but
2739
- // cancelling makes the code obviously correct.
2740
- if (timeoutId !== null) {
2741
- clearTimeout(timeoutId);
2742
- timeoutId = null;
2743
- }
2744
-
2745
- // Flush any remaining buffered HTML chunks
2746
- for (const buf of buffered) {
2747
- if (!injected) {
2748
- const headEnd = buf.indexOf("</head>");
2749
- if (headEnd !== -1) {
2750
- const before = buf.slice(0, headEnd);
2751
- const after = buf.slice(headEnd);
2752
- controller.enqueue(encoder.encode(before + injectHTML + after));
2753
- injected = true;
2754
- continue;
2755
- }
2756
- }
2757
- controller.enqueue(encoder.encode(buf));
2758
- }
2759
- buffered = [];
2760
-
2761
- if (!injected && injectHTML) {
2762
- controller.enqueue(encoder.encode(injectHTML));
2763
- }
2764
- // Finalize: wait for the RSC stream to complete and emit remaining
2765
- // chunks plus the __VINEXT_RSC_DONE__ signal.
2766
- const finalScripts = await rscEmbed.finalize();
2767
- if (finalScripts) {
2768
- controller.enqueue(encoder.encode(finalScripts));
2769
- }
2770
- },
2771
- });
2772
-
2773
- return htmlStream.pipeThrough(transform);
2774
- } finally {
2775
- // Clean up so we don't leak context between requests
2776
- setNavigationContext(null);
2777
- clearServerInsertedHTML();
2778
- }
2779
- }); // end _runWithNavCtx
2780
- }
2781
-
2782
- export default {
2783
- async fetch(request) {
2784
- const url = new URL(request.url);
2785
- if (url.pathname.startsWith("//")) {
2786
- return new Response("404 Not Found", { status: 404 });
2787
- }
2788
- const rscModule = await import.meta.viteRsc.loadModule("rsc", "index");
2789
- const result = await rscModule.default(request);
2790
- if (result instanceof Response) {
2791
- return result;
2792
- }
2793
- if (result === null || result === undefined) {
2794
- return new Response("Not Found", { status: 404 });
2795
- }
2796
- return new Response(String(result), { status: 200 });
2797
- },
2798
- };
2799
- `;
2800
- }
2801
- /**
2802
- * Generate the virtual browser entry module.
2803
- *
2804
- * This runs in the client (browser). It hydrates the page from the
2805
- * embedded RSC payload and handles client-side navigation by re-fetching
2806
- * RSC streams.
2807
- */
2808
- export function generateBrowserEntry() {
2809
- return `
2810
- import {
2811
- createFromReadableStream,
2812
- createFromFetch,
2813
- setServerCallback,
2814
- encodeReply,
2815
- createTemporaryReferenceSet,
2816
- } from "@vitejs/plugin-rsc/browser";
2817
- import { hydrateRoot } from "react-dom/client";
2818
- import { flushSync } from "react-dom";
2819
- import { setClientParams, toRscUrl, getPrefetchCache, getPrefetchedUrls, PREFETCH_CACHE_TTL } from "next/navigation";
2820
-
2821
- let reactRoot;
2822
-
2823
- /**
2824
- * Convert the embedded RSC chunks back to a ReadableStream.
2825
- * Each chunk is a text string that needs to be encoded back to Uint8Array.
2826
- */
2827
- function chunksToReadableStream(chunks) {
2828
- const encoder = new TextEncoder();
2829
- return new ReadableStream({
2830
- start(controller) {
2831
- for (const chunk of chunks) {
2832
- controller.enqueue(encoder.encode(chunk));
2833
- }
2834
- controller.close();
2835
- }
2836
- });
2837
- }
2838
-
2839
- /**
2840
- * Create a ReadableStream from progressively-embedded RSC chunks.
2841
- * The server injects RSC data as <script> tags that push to
2842
- * self.__VINEXT_RSC_CHUNKS__ throughout the HTML stream, and sets
2843
- * self.__VINEXT_RSC_DONE__ = true when complete.
2844
- *
2845
- * Instead of polling with setTimeout, we monkey-patch the array's
2846
- * push() method so new chunks are delivered immediately when the
2847
- * server's <script> tags execute. This eliminates unnecessary
2848
- * wakeups and reduces latency — same pattern Next.js uses with
2849
- * __next_f. The stream closes on DOMContentLoaded (when all
2850
- * server-injected scripts have executed) or when __VINEXT_RSC_DONE__
2851
- * is set, whichever comes first.
2852
- */
2853
- function createProgressiveRscStream() {
2854
- const encoder = new TextEncoder();
2855
- return new ReadableStream({
2856
- start(controller) {
2857
- const chunks = self.__VINEXT_RSC_CHUNKS__ || [];
2858
-
2859
- // Deliver any chunks that arrived before this code ran
2860
- // (from <script> tags that executed before the browser entry loaded)
2861
- for (const chunk of chunks) {
2862
- controller.enqueue(encoder.encode(chunk));
2863
- }
2864
-
2865
- // If the stream is already complete, close immediately
2866
- if (self.__VINEXT_RSC_DONE__) {
2867
- controller.close();
2868
- return;
2869
- }
2870
-
2871
- // Monkey-patch push() so future chunks stream in immediately
2872
- // when the server's <script> tags execute
2873
- let closed = false;
2874
- function closeOnce() {
2875
- if (!closed) {
2876
- closed = true;
2877
- controller.close();
2878
- }
2879
- }
2880
-
2881
- const arr = self.__VINEXT_RSC_CHUNKS__ = self.__VINEXT_RSC_CHUNKS__ || [];
2882
- arr.push = function(chunk) {
2883
- Array.prototype.push.call(this, chunk);
2884
- if (!closed) {
2885
- controller.enqueue(encoder.encode(chunk));
2886
- if (self.__VINEXT_RSC_DONE__) {
2887
- closeOnce();
2888
- }
2889
- }
2890
- return this.length;
2891
- };
2892
-
2893
- // Safety net: if the server crashes mid-stream and __VINEXT_RSC_DONE__
2894
- // never arrives, close the stream when all server-injected scripts
2895
- // have executed (DOMContentLoaded). Without this, a truncated response
2896
- // leaves the ReadableStream open forever, hanging hydration.
2897
- if (typeof document !== "undefined") {
2898
- if (document.readyState === "loading") {
2899
- document.addEventListener("DOMContentLoaded", closeOnce);
2900
- } else {
2901
- // Document already loaded — close immediately if not already done
2902
- closeOnce();
2903
- }
2904
- }
2905
- }
2906
- });
2907
- }
2908
-
2909
- // Register the server action callback — React calls this internally
2910
- // when a "use server" function is invoked from client code.
2911
- setServerCallback(async (id, args) => {
2912
- const temporaryReferences = createTemporaryReferenceSet();
2913
- const body = await encodeReply(args, { temporaryReferences });
2914
-
2915
- const fetchResponse = await fetch(toRscUrl(window.location.pathname + window.location.search), {
2916
- method: "POST",
2917
- headers: { "x-rsc-action": id },
2918
- body,
2919
- });
2920
-
2921
- // Check for redirect signal from server action that called redirect()
2922
- const actionRedirect = fetchResponse.headers.get("x-action-redirect");
2923
- if (actionRedirect) {
2924
- // External URLs (different origin) need a hard redirect — client-side
2925
- // RSC navigation only works for same-origin paths.
2926
- try {
2927
- const redirectUrl = new URL(actionRedirect, window.location.origin);
2928
- if (redirectUrl.origin !== window.location.origin) {
2929
- window.location.href = actionRedirect;
2930
- return undefined;
2931
- }
2932
- } catch {
2933
- // If URL parsing fails, fall through to client-side navigation
2934
- }
2935
-
2936
- // Navigate to the redirect target using client-side navigation
2937
- const redirectType = fetchResponse.headers.get("x-action-redirect-type") || "replace";
2938
- if (redirectType === "push") {
2939
- window.history.pushState(null, "", actionRedirect);
2940
- } else {
2941
- window.history.replaceState(null, "", actionRedirect);
2942
- }
2943
- // Trigger RSC navigation to the redirect target
2944
- if (typeof window.__VINEXT_RSC_NAVIGATE__ === "function") {
2945
- window.__VINEXT_RSC_NAVIGATE__(actionRedirect);
2946
- }
2947
- return undefined;
2948
- }
2949
-
2950
- const result = await createFromFetch(Promise.resolve(fetchResponse), { temporaryReferences });
2951
-
2952
- // The RSC response for actions contains { root, returnValue }.
2953
- // Re-render the page with the updated tree.
2954
- if (result && typeof result === "object" && "root" in result) {
2955
- reactRoot.render(result.root);
2956
- // Return the action's return value to the caller
2957
- if (result.returnValue) {
2958
- if (!result.returnValue.ok) throw result.returnValue.data;
2959
- return result.returnValue.data;
2960
- }
2961
- return undefined;
2962
- }
2963
-
2964
- // Fallback: render the entire result as the tree
2965
- reactRoot.render(result);
2966
- return result;
2967
- });
2968
-
2969
- async function main() {
2970
- let rscStream;
2971
-
2972
- // Use embedded RSC data for initial hydration if available.
2973
- // This ensures we use the SAME RSC payload that generated the HTML,
2974
- // avoiding hydration mismatches (React error #418).
2975
- //
2976
- // The server embeds RSC chunks progressively as <script> tags that push
2977
- // to self.__VINEXT_RSC_CHUNKS__. When complete, self.__VINEXT_RSC_DONE__
2978
- // is set and self.__VINEXT_RSC_PARAMS__ contains route params.
2979
- // For backwards compat, also check the legacy self.__VINEXT_RSC__ format.
2980
- if (self.__VINEXT_RSC_CHUNKS__ || self.__VINEXT_RSC_DONE__ || self.__VINEXT_RSC__) {
2981
- if (self.__VINEXT_RSC__) {
2982
- // Legacy format: single object with all chunks
2983
- const embedData = self.__VINEXT_RSC__;
2984
- delete self.__VINEXT_RSC__;
2985
- if (embedData.params) {
2986
- setClientParams(embedData.params);
2987
- }
2988
- rscStream = chunksToReadableStream(embedData.rsc);
2989
- } else {
2990
- // Progressive format: chunks arrive incrementally via script tags.
2991
- // Params are embedded in <head> so they're always available by this point.
2992
- if (self.__VINEXT_RSC_PARAMS__) {
2993
- setClientParams(self.__VINEXT_RSC_PARAMS__);
2994
- }
2995
- rscStream = createProgressiveRscStream();
2996
- }
2997
- } else {
2998
- // Fallback: fetch fresh RSC (shouldn't happen on initial page load)
2999
- const rscResponse = await fetch(toRscUrl(window.location.pathname + window.location.search));
3000
-
3001
- // Hydrate useParams() with route params from the server before React hydration
3002
- const paramsHeader = rscResponse.headers.get("X-Vinext-Params");
3003
- if (paramsHeader) {
3004
- try { setClientParams(JSON.parse(paramsHeader)); } catch (_e) { /* ignore */ }
3005
- }
3006
-
3007
- rscStream = rscResponse.body;
3008
- }
3009
-
3010
- const root = await createFromReadableStream(rscStream);
3011
-
3012
- // Hydrate the document
3013
- // In development, suppress Vite's error overlay for errors caught by React error
3014
- // boundaries. Without this, React re-throws caught errors to the global handler,
3015
- // which triggers Vite's overlay even though the error was handled by an error.tsx.
3016
- // In production, preserve React's default onCaughtError (console.error) so
3017
- // boundary-caught errors remain visible to error monitoring.
3018
- reactRoot = hydrateRoot(document, root, import.meta.env.DEV ? {
3019
- onCaughtError: function() {},
3020
- } : undefined);
3021
-
3022
- // Store for client-side navigation
3023
- window.__VINEXT_RSC_ROOT__ = reactRoot;
3024
-
3025
- // Client-side navigation handler
3026
- // Checks the prefetch cache (populated by <Link> IntersectionObserver and
3027
- // router.prefetch()) before making a network request. This makes navigation
3028
- // near-instant for prefetched routes.
3029
- window.__VINEXT_RSC_NAVIGATE__ = async function navigateRsc(href, __redirectDepth) {
3030
- if ((__redirectDepth || 0) > 10) {
3031
- console.error("[vinext] Too many RSC redirects — aborting navigation to prevent infinite loop.");
3032
- window.location.href = href;
3033
- return;
3034
- }
3035
- try {
3036
- const url = new URL(href, window.location.origin);
3037
- const rscUrl = toRscUrl(url.pathname + url.search);
3038
-
3039
- // Check the in-memory prefetch cache first
3040
- let navResponse;
3041
- const prefetchCache = getPrefetchCache();
3042
- const cached = prefetchCache.get(rscUrl);
3043
- if (cached && (Date.now() - cached.timestamp) < PREFETCH_CACHE_TTL) {
3044
- navResponse = cached.response;
3045
- prefetchCache.delete(rscUrl); // Consume the cached entry (one-time use)
3046
- getPrefetchedUrls().delete(rscUrl); // Allow re-prefetch when link is visible again
3047
- } else if (cached) {
3048
- prefetchCache.delete(rscUrl); // Expired, clean up
3049
- getPrefetchedUrls().delete(rscUrl);
3050
- }
3051
-
3052
- // Fallback to network fetch if not in cache
3053
- if (!navResponse) {
3054
- navResponse = await fetch(rscUrl, {
3055
- headers: { Accept: "text/x-component" },
3056
- credentials: "include",
3057
- });
3058
- }
3059
-
3060
- // Detect if fetch followed a redirect: compare the final response URL to
3061
- // what we requested. If they differ, the server issued a 3xx — push the
3062
- // canonical destination URL into history before rendering.
3063
- const __finalUrl = new URL(navResponse.url);
3064
- const __requestedUrl = new URL(rscUrl, window.location.origin);
3065
- if (__finalUrl.pathname !== __requestedUrl.pathname) {
3066
- // Strip .rsc suffix from the final URL to get the page path for history.
3067
- // Use replaceState instead of pushState: the caller (navigateImpl) already
3068
- // pushed the pre-redirect URL; replacing it avoids a stale history entry.
3069
- const __destPath = __finalUrl.pathname.replace(/\\.rsc$/, "") + __finalUrl.search;
3070
- window.history.replaceState(null, "", __destPath);
3071
- return window.__VINEXT_RSC_NAVIGATE__(__destPath, (__redirectDepth || 0) + 1);
3072
- }
3073
-
3074
- // Update useParams() with route params from the server before re-rendering
3075
- const navParamsHeader = navResponse.headers.get("X-Vinext-Params");
3076
- if (navParamsHeader) {
3077
- try { setClientParams(JSON.parse(navParamsHeader)); } catch (_e) { /* ignore */ }
3078
- } else {
3079
- setClientParams({});
3080
- }
3081
-
3082
- const rscPayload = await createFromFetch(Promise.resolve(navResponse));
3083
- // Use flushSync to guarantee React commits the new tree to the DOM
3084
- // synchronously before this function returns. Callers scroll to top
3085
- // after awaiting, so the new content must be painted first.
3086
- flushSync(function () { reactRoot.render(rscPayload); });
3087
- } catch (err) {
3088
- console.error("[vinext] RSC navigation error:", err);
3089
- // Fallback to full page load
3090
- window.location.href = href;
3091
- }
3092
- };
3093
-
3094
- // Handle popstate (browser back/forward)
3095
- // Store the navigation promise on a well-known property so that
3096
- // restoreScrollPosition (in navigation.ts) can await it before scrolling.
3097
- // This prevents a flash where the old content is visible at the restored
3098
- // scroll position before the new RSC payload has rendered.
3099
- window.addEventListener("popstate", () => {
3100
- const p = window.__VINEXT_RSC_NAVIGATE__(window.location.href);
3101
- window.__VINEXT_RSC_PENDING__ = p;
3102
- p.finally(() => {
3103
- // Clear once settled so stale promises aren't awaited later
3104
- if (window.__VINEXT_RSC_PENDING__ === p) {
3105
- window.__VINEXT_RSC_PENDING__ = null;
3106
- }
3107
- });
3108
- });
3109
-
3110
- // HMR: re-render on server module updates
3111
- if (import.meta.hot) {
3112
- import.meta.hot.on("rsc:update", async () => {
3113
- try {
3114
- const rscPayload = await createFromFetch(
3115
- fetch(toRscUrl(window.location.pathname + window.location.search))
3116
- );
3117
- reactRoot.render(rscPayload);
3118
- } catch (err) {
3119
- console.error("[vinext] RSC HMR error:", err);
3120
- }
3121
- });
3122
- }
3123
- }
3124
-
3125
- main();
3126
- `;
3127
- }
3128
- //# sourceMappingURL=app-dev-server.js.map
2407
+ //# sourceMappingURL=app-rsc-entry.js.map