vinext 0.0.23 → 0.0.25

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 (105) hide show
  1. package/README.md +24 -0
  2. package/dist/check.d.ts.map +1 -1
  3. package/dist/check.js +3 -2
  4. package/dist/check.js.map +1 -1
  5. package/dist/client/entry.js +1 -1
  6. package/dist/client/entry.js.map +1 -1
  7. package/dist/config/config-matchers.d.ts +21 -0
  8. package/dist/config/config-matchers.d.ts.map +1 -1
  9. package/dist/config/config-matchers.js +59 -9
  10. package/dist/config/config-matchers.js.map +1 -1
  11. package/dist/config/next-config.d.ts +8 -2
  12. package/dist/config/next-config.d.ts.map +1 -1
  13. package/dist/config/next-config.js +90 -35
  14. package/dist/config/next-config.js.map +1 -1
  15. package/dist/deploy.d.ts +10 -0
  16. package/dist/deploy.d.ts.map +1 -1
  17. package/dist/deploy.js +140 -56
  18. package/dist/deploy.js.map +1 -1
  19. package/dist/index.d.ts +14 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +195 -25
  22. package/dist/index.js.map +1 -1
  23. package/dist/plugins/async-hooks-stub.d.ts +16 -0
  24. package/dist/plugins/async-hooks-stub.d.ts.map +1 -0
  25. package/dist/plugins/async-hooks-stub.js +45 -0
  26. package/dist/plugins/async-hooks-stub.js.map +1 -0
  27. package/dist/routing/app-router.d.ts +12 -6
  28. package/dist/routing/app-router.d.ts.map +1 -1
  29. package/dist/routing/app-router.js +19 -40
  30. package/dist/routing/app-router.js.map +1 -1
  31. package/dist/routing/pages-router.d.ts.map +1 -1
  32. package/dist/routing/pages-router.js +3 -9
  33. package/dist/routing/pages-router.js.map +1 -1
  34. package/dist/routing/utils.d.ts +9 -0
  35. package/dist/routing/utils.d.ts.map +1 -1
  36. package/dist/routing/utils.js +10 -0
  37. package/dist/routing/utils.js.map +1 -1
  38. package/dist/server/api-handler.d.ts.map +1 -1
  39. package/dist/server/api-handler.js +6 -0
  40. package/dist/server/api-handler.js.map +1 -1
  41. package/dist/server/app-dev-server.d.ts +2 -2
  42. package/dist/server/app-dev-server.d.ts.map +1 -1
  43. package/dist/server/app-dev-server.js +294 -133
  44. package/dist/server/app-dev-server.js.map +1 -1
  45. package/dist/server/dev-module-runner.d.ts +84 -0
  46. package/dist/server/dev-module-runner.d.ts.map +1 -0
  47. package/dist/server/dev-module-runner.js +105 -0
  48. package/dist/server/dev-module-runner.js.map +1 -0
  49. package/dist/server/dev-server.js.map +1 -1
  50. package/dist/server/instrumentation.d.ts +52 -9
  51. package/dist/server/instrumentation.d.ts.map +1 -1
  52. package/dist/server/instrumentation.js +52 -15
  53. package/dist/server/instrumentation.js.map +1 -1
  54. package/dist/server/middleware.d.ts +7 -3
  55. package/dist/server/middleware.d.ts.map +1 -1
  56. package/dist/server/middleware.js +16 -6
  57. package/dist/server/middleware.js.map +1 -1
  58. package/dist/server/prod-server.d.ts +8 -2
  59. package/dist/server/prod-server.d.ts.map +1 -1
  60. package/dist/server/prod-server.js +56 -35
  61. package/dist/server/prod-server.js.map +1 -1
  62. package/dist/server/worker-utils.d.ts +15 -0
  63. package/dist/server/worker-utils.d.ts.map +1 -0
  64. package/dist/server/worker-utils.js +41 -0
  65. package/dist/server/worker-utils.js.map +1 -0
  66. package/dist/shims/cache.d.ts.map +1 -1
  67. package/dist/shims/cache.js +14 -2
  68. package/dist/shims/cache.js.map +1 -1
  69. package/dist/shims/fetch-cache.d.ts.map +1 -1
  70. package/dist/shims/fetch-cache.js +139 -29
  71. package/dist/shims/fetch-cache.js.map +1 -1
  72. package/dist/shims/form.d.ts.map +1 -1
  73. package/dist/shims/form.js +2 -3
  74. package/dist/shims/form.js.map +1 -1
  75. package/dist/shims/headers.d.ts +6 -0
  76. package/dist/shims/headers.d.ts.map +1 -1
  77. package/dist/shims/headers.js +8 -0
  78. package/dist/shims/headers.js.map +1 -1
  79. package/dist/shims/layout-segment-context.d.ts +5 -4
  80. package/dist/shims/layout-segment-context.d.ts.map +1 -1
  81. package/dist/shims/layout-segment-context.js +6 -5
  82. package/dist/shims/layout-segment-context.js.map +1 -1
  83. package/dist/shims/link.d.ts.map +1 -1
  84. package/dist/shims/link.js +32 -17
  85. package/dist/shims/link.js.map +1 -1
  86. package/dist/shims/navigation.d.ts +14 -11
  87. package/dist/shims/navigation.d.ts.map +1 -1
  88. package/dist/shims/navigation.js +122 -102
  89. package/dist/shims/navigation.js.map +1 -1
  90. package/dist/shims/router.d.ts.map +1 -1
  91. package/dist/shims/router.js +37 -21
  92. package/dist/shims/router.js.map +1 -1
  93. package/dist/shims/server.d.ts +2 -0
  94. package/dist/shims/server.d.ts.map +1 -1
  95. package/dist/shims/server.js +4 -0
  96. package/dist/shims/server.js.map +1 -1
  97. package/dist/shims/url-utils.d.ts +13 -0
  98. package/dist/shims/url-utils.d.ts.map +1 -0
  99. package/dist/shims/url-utils.js +28 -0
  100. package/dist/shims/url-utils.js.map +1 -0
  101. package/dist/utils/project.d.ts +13 -1
  102. package/dist/utils/project.d.ts.map +1 -1
  103. package/dist/utils/project.js +63 -13
  104. package/dist/utils/project.js.map +1 -1
  105. package/package.json +6 -1
@@ -18,7 +18,7 @@ import { isProxyFile } from "./middleware.js";
18
18
  * It matches the incoming request URL to an app route, builds the
19
19
  * nested layout + page tree, and renders it to an RSC stream.
20
20
  */
21
- export function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, globalErrorPath, basePath, trailingSlash, config) {
21
+ export function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, globalErrorPath, basePath, trailingSlash, config, instrumentationPath) {
22
22
  const bp = basePath ?? "";
23
23
  const ts = trailingSlash ?? false;
24
24
  const redirects = config?.redirects ?? [];
@@ -119,7 +119,8 @@ ${interceptEntries.join(",\n")}
119
119
  page: ${route.pagePath ? getImportVar(route.pagePath) : "null"},
120
120
  routeHandler: ${route.routePath ? getImportVar(route.routePath) : "null"},
121
121
  layouts: [${layoutVars.join(", ")}],
122
- layoutSegmentDepths: ${JSON.stringify(route.layoutSegmentDepths)},
122
+ routeSegments: ${JSON.stringify(route.routeSegments)},
123
+ layoutTreePositions: ${JSON.stringify(route.layoutTreePositions)},
123
124
  templates: [${templateVars.join(", ")}],
124
125
  errors: [${layoutErrorVars.join(", ")}],
125
126
  slots: {
@@ -194,14 +195,16 @@ import {
194
195
  loadServerAction,
195
196
  createTemporaryReferenceSet,
196
197
  } from "@vitejs/plugin-rsc/rsc";
198
+ import { AsyncLocalStorage } from "node:async_hooks";
197
199
  import { createElement, Suspense, Fragment } from "react";
198
200
  import { setNavigationContext as _setNavigationContextOrig, getNavigationContext as _getNavigationContext } from "next/navigation";
199
- import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, runWithHeadersContext, applyMiddlewareRequestHeaders } from "next/headers";
200
- import { NextRequest } from "next/server";
201
+ import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, runWithHeadersContext, applyMiddlewareRequestHeaders, getHeadersContext } from "next/headers";
202
+ import { NextRequest, NextFetchEvent } from "next/server";
201
203
  import { ErrorBoundary, NotFoundBoundary } from "vinext/error-boundary";
202
204
  import { LayoutSegmentProvider } from "vinext/layout-segment-context";
203
205
  import { MetadataHead, mergeMetadata, resolveModuleMetadata, ViewportHead, mergeViewport, resolveModuleViewport } from "vinext/metadata";
204
206
  ${middlewarePath ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};` : ""}
207
+ ${instrumentationPath ? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};` : ""}
205
208
  ${effectiveMetaRoutes.length > 0 ? `import { sitemapToXml, robotsToText, manifestToJson } from ${JSON.stringify(fileURLToPath(new URL("./metadata-routes.js", import.meta.url)).replace(/\\/g, "/"))};` : ""}
206
209
  import { _consumeRequestScopedCacheLife, _runWithCacheState } from "next/cache";
207
210
  import { runWithFetchCache } from "vinext/fetch-cache";
@@ -214,6 +217,20 @@ import { getSSRFontStyles as _getSSRFontStylesLocal, getSSRFontPreloads as _getS
214
217
  function _getSSRFontStyles() { return [..._getSSRFontStylesGoogle(), ..._getSSRFontStylesLocal()]; }
215
218
  function _getSSRFontPreloads() { return [..._getSSRFontPreloadsGoogle(), ..._getSSRFontPreloadsLocal()]; }
216
219
 
220
+ // ALS used to suppress the expected "Invalid hook call" dev warning when
221
+ // layout/page components are probed outside React's render cycle. Patching
222
+ // console.error once at module load (instead of per-request) avoids the
223
+ // concurrent-request issue where request A's suppression filter could
224
+ // swallow real errors from request B.
225
+ const _suppressHookWarningAls = new AsyncLocalStorage();
226
+ const _origConsoleError = console.error;
227
+ console.error = (...args) => {
228
+ if (_suppressHookWarningAls.getStore() === true &&
229
+ typeof args[0] === "string" &&
230
+ args[0].includes("Invalid hook call")) return;
231
+ _origConsoleError.apply(console, args);
232
+ };
233
+
217
234
  // Set navigation context in the ALS-backed store. "use client" components
218
235
  // rendered during SSR need the pathname/searchParams/params but the SSR
219
236
  // environment has a separate module instance of next/navigation.
@@ -242,6 +259,38 @@ function makeThenableParams(obj) {
242
259
  return Object.assign(Promise.resolve(plain), plain);
243
260
  }
244
261
 
262
+ // Resolve route tree segments to actual values using matched params.
263
+ // Dynamic segments like [id] are replaced with param values, catch-all
264
+ // segments like [...slug] are joined with "/", and route groups are kept as-is.
265
+ function __resolveChildSegments(routeSegments, treePosition, params) {
266
+ var raw = routeSegments.slice(treePosition);
267
+ var result = [];
268
+ for (var j = 0; j < raw.length; j++) {
269
+ var seg = raw[j];
270
+ // Optional catch-all: [[...param]]
271
+ if (seg.indexOf("[[...") === 0 && seg.charAt(seg.length - 1) === "]" && seg.charAt(seg.length - 2) === "]") {
272
+ var pn = seg.slice(5, -2);
273
+ var v = params[pn];
274
+ // Skip empty optional catch-all (e.g., visiting /blog on [[...slug]] route)
275
+ if (Array.isArray(v) && v.length === 0) continue;
276
+ if (v == null) continue;
277
+ result.push(Array.isArray(v) ? v.join("/") : v);
278
+ // Catch-all: [...param]
279
+ } else if (seg.indexOf("[...") === 0 && seg.charAt(seg.length - 1) === "]") {
280
+ var pn2 = seg.slice(4, -1);
281
+ var v2 = params[pn2];
282
+ result.push(Array.isArray(v2) ? v2.join("/") : (v2 || seg));
283
+ // Dynamic: [param]
284
+ } else if (seg.charAt(0) === "[" && seg.charAt(seg.length - 1) === "]" && seg.indexOf(".") === -1) {
285
+ var pn3 = seg.slice(1, -1);
286
+ result.push(params[pn3] || seg);
287
+ } else {
288
+ result.push(seg);
289
+ }
290
+ }
291
+ return result;
292
+ }
293
+
245
294
  // djb2 hash — matches Next.js's stringHash for digest generation.
246
295
  // Produces a stable numeric string from error message + stack.
247
296
  function __errorDigest(str) {
@@ -347,6 +396,23 @@ function rscOnError(error) {
347
396
 
348
397
  ${imports.join("\n")}
349
398
 
399
+ ${instrumentationPath ? `// Run instrumentation register() once at module evaluation time — before any
400
+ // requests are handled. This runs inside the Worker process (or RSC environment),
401
+ // which is exactly where request handling happens. Matches Next.js semantics:
402
+ // register() is called once on startup in the process that handles requests.
403
+ if (typeof _instrumentation.register === "function") {
404
+ await _instrumentation.register();
405
+ }
406
+ // Store the onRequestError handler on globalThis so it is visible to
407
+ // reportRequestError() (imported as _reportRequestError above) regardless
408
+ // of which Vite environment module graph it is called from. With
409
+ // @vitejs/plugin-rsc the RSC and SSR environments run in the same Node.js
410
+ // process and share globalThis. With @cloudflare/vite-plugin everything
411
+ // runs inside the Worker so globalThis is the Worker's global — also correct.
412
+ if (typeof _instrumentation.onRequestError === "function") {
413
+ globalThis.__VINEXT_onRequestErrorHandler__ = _instrumentation.onRequestError;
414
+ }` : ""}
415
+
350
416
  const routes = [
351
417
  ${routeEntries.join(",\n")}
352
418
  ];
@@ -418,13 +484,17 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
418
484
  // We wrap each layout with LayoutSegmentProvider and add GlobalErrorBoundary
419
485
  // to match the wrapping order in buildPageElement(), ensuring smooth
420
486
  // client-side tree reconciliation.
421
- const layoutDepths = route?.layoutSegmentDepths;
487
+ const _treePositions = route?.layoutTreePositions;
488
+ const _routeSegs = route?.routeSegments || [];
489
+ const _fallbackParams = opts?.matchedParams ?? route?.params ?? {};
490
+ const _asyncFallbackParams = makeThenableParams(_fallbackParams);
422
491
  for (let i = layouts.length - 1; i >= 0; i--) {
423
492
  const LayoutComponent = layouts[i]?.default;
424
493
  if (LayoutComponent) {
425
- element = createElement(LayoutComponent, { children: element });
426
- const layoutDepth = layoutDepths ? layoutDepths[i] : 0;
427
- element = createElement(LayoutSegmentProvider, { depth: layoutDepth }, element);
494
+ element = createElement(LayoutComponent, { children: element, params: _asyncFallbackParams });
495
+ const _tp = _treePositions ? _treePositions[i] : 0;
496
+ const _cs = __resolveChildSegments(_routeSegs, _tp, _fallbackParams);
497
+ element = createElement(LayoutSegmentProvider, { childSegments: _cs }, element);
428
498
  }
429
499
  }
430
500
  ${globalErrorVar ? `
@@ -446,10 +516,12 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
446
516
  }
447
517
  // For HTML (full page load) responses, wrap with layouts only (no client-side
448
518
  // wrappers needed since SSR generates the complete HTML document).
519
+ const _fallbackParamsHtml = opts?.matchedParams ?? route?.params ?? {};
520
+ const _asyncFallbackParamsHtml = makeThenableParams(_fallbackParamsHtml);
449
521
  for (let i = layouts.length - 1; i >= 0; i--) {
450
522
  const LayoutComponent = layouts[i]?.default;
451
523
  if (LayoutComponent) {
452
- element = createElement(LayoutComponent, { children: element });
524
+ element = createElement(LayoutComponent, { children: element, params: _asyncFallbackParamsHtml });
453
525
  }
454
526
  }
455
527
  const rscStream = renderToReadableStream(element, { onError: rscOnError });
@@ -473,8 +545,8 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
473
545
  }
474
546
 
475
547
  /** Convenience: render a not-found page (404) */
476
- async function renderNotFoundPage(route, isRscRequest, request) {
477
- return renderHTTPAccessFallbackPage(route, 404, isRscRequest, request);
548
+ async function renderNotFoundPage(route, isRscRequest, request, matchedParams) {
549
+ return renderHTTPAccessFallbackPage(route, 404, isRscRequest, request, { matchedParams });
478
550
  }
479
551
 
480
552
  /**
@@ -484,7 +556,7 @@ async function renderNotFoundPage(route, isRscRequest, request) {
484
556
  * Next.js returns HTTP 200 when error.tsx catches an error (the error is "handled"
485
557
  * by the boundary). This matches that behavior intentionally.
486
558
  */
487
- async function renderErrorBoundaryPage(route, error, isRscRequest, request) {
559
+ async function renderErrorBoundaryPage(route, error, isRscRequest, request, matchedParams) {
488
560
  // Resolve the error boundary component: leaf error.tsx first, then walk per-layout
489
561
  // errors from innermost to outermost (matching ancestor inheritance), then global-error.tsx.
490
562
  let ErrorComponent = route?.error?.default ?? null;
@@ -517,13 +589,17 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request) {
517
589
  // wrappers that buildPageElement() uses (LayoutSegmentProvider, GlobalErrorBoundary).
518
590
  // This ensures React can reconcile the tree without destroying the DOM.
519
591
  // Same rationale as renderHTTPAccessFallbackPage — see comment there.
520
- const layoutDepths = route?.layoutSegmentDepths;
592
+ const _errTreePositions = route?.layoutTreePositions;
593
+ const _errRouteSegs = route?.routeSegments || [];
594
+ const _errParams = matchedParams ?? route?.params ?? {};
595
+ const _asyncErrParams = makeThenableParams(_errParams);
521
596
  for (let i = layouts.length - 1; i >= 0; i--) {
522
597
  const LayoutComponent = layouts[i]?.default;
523
598
  if (LayoutComponent) {
524
- element = createElement(LayoutComponent, { children: element });
525
- const layoutDepth = layoutDepths ? layoutDepths[i] : 0;
526
- element = createElement(LayoutSegmentProvider, { depth: layoutDepth }, element);
599
+ element = createElement(LayoutComponent, { children: element, params: _asyncErrParams });
600
+ const _etp = _errTreePositions ? _errTreePositions[i] : 0;
601
+ const _ecs = __resolveChildSegments(_errRouteSegs, _etp, _errParams);
602
+ element = createElement(LayoutSegmentProvider, { childSegments: _ecs }, element);
527
603
  }
528
604
  }
529
605
  ${globalErrorVar ? `
@@ -544,10 +620,12 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request) {
544
620
  });
545
621
  }
546
622
  // For HTML (full page load) responses, wrap with layouts only.
623
+ const _errParamsHtml = matchedParams ?? route?.params ?? {};
624
+ const _asyncErrParamsHtml = makeThenableParams(_errParamsHtml);
547
625
  for (let i = layouts.length - 1; i >= 0; i--) {
548
626
  const LayoutComponent = layouts[i]?.default;
549
627
  if (LayoutComponent) {
550
- element = createElement(LayoutComponent, { children: element });
628
+ element = createElement(LayoutComponent, { children: element, params: _asyncErrParamsHtml });
551
629
  }
552
630
  }
553
631
  const rscStream = renderToReadableStream(element, { onError: rscOnError });
@@ -703,6 +781,10 @@ async function buildPageElement(route, params, opts, searchParams) {
703
781
  }
704
782
  let element = createElement(PageComponent, pageProps);
705
783
 
784
+ // Wrap page with empty segment provider so useSelectedLayoutSegments()
785
+ // returns [] when called from inside a page component (leaf node).
786
+ element = createElement(LayoutSegmentProvider, { childSegments: [] }, element);
787
+
706
788
  // Add metadata + viewport head tags (React 19 hoists title/meta/link to <head>)
707
789
  // Next.js always injects charset and default viewport even when no metadata/viewport
708
790
  // is exported. We replicate that by always emitting these essential head elements.
@@ -853,12 +935,13 @@ async function buildPageElement(route, params, opts, searchParams) {
853
935
  element = createElement(LayoutComponent, layoutProps);
854
936
 
855
937
  // 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);
938
+ // called INSIDE this layout gets the correct child segments. We resolve the
939
+ // route tree segments using actual param values and pass them through context.
940
+ // We wrap the layout (not just children) because hooks are called from
941
+ // components rendered inside the layout's own JSX.
942
+ const treePos = route.layoutTreePositions ? route.layoutTreePositions[i] : 0;
943
+ const childSegs = __resolveChildSegments(route.routeSegments || [], treePos, params);
944
+ element = createElement(LayoutSegmentProvider, { childSegments: childSegs }, element);
862
945
  }
863
946
  }
864
947
 
@@ -1051,6 +1134,28 @@ function __buildRequestContext(request) {
1051
1134
  };
1052
1135
  }
1053
1136
 
1137
+ /**
1138
+ * Build a request context from the live ALS HeadersContext, which reflects
1139
+ * any x-middleware-request-* header mutations applied by middleware.
1140
+ * Used for afterFiles and fallback rewrite has/missing evaluation — these
1141
+ * run after middleware in the App Router execution order.
1142
+ */
1143
+ function __buildPostMwRequestContext(request) {
1144
+ const url = new URL(request.url);
1145
+ const ctx = getHeadersContext();
1146
+ if (!ctx) return __buildRequestContext(request);
1147
+ // ctx.cookies is a Map<string, string> (HeadersContext), but RequestContext
1148
+ // requires a plain Record<string, string> for has/missing cookie evaluation
1149
+ // (config-matchers.ts uses obj[key] not Map.get()). Convert here.
1150
+ const cookiesRecord = Object.fromEntries(ctx.cookies);
1151
+ return {
1152
+ headers: ctx.headers,
1153
+ cookies: cookiesRecord,
1154
+ query: url.searchParams,
1155
+ host: ctx.headers.get("host") || url.host,
1156
+ };
1157
+ }
1158
+
1054
1159
  function __sanitizeDestination(dest) {
1055
1160
  if (dest.startsWith("http://") || dest.startsWith("https://")) return dest;
1056
1161
  dest = dest.replace(/^[\\\\/]+/, "/");
@@ -1165,12 +1270,6 @@ async function __proxyExternalRequest(request, externalUrl) {
1165
1270
  const headers = new Headers(request.headers);
1166
1271
  headers.set("host", targetUrl.host);
1167
1272
  headers.delete("connection");
1168
- // Strip credentials and internal headers to prevent leaking auth tokens,
1169
- // session cookies, and middleware internals to third-party origins.
1170
- headers.delete("cookie");
1171
- headers.delete("authorization");
1172
- headers.delete("x-api-key");
1173
- headers.delete("proxy-authorization");
1174
1273
  for (const key of [...headers.keys()]) {
1175
1274
  if (key.startsWith("x-middleware-")) headers.delete(key);
1176
1275
  }
@@ -1185,7 +1284,16 @@ async function __proxyExternalRequest(request, externalUrl) {
1185
1284
  console.error("[vinext] External rewrite proxy error:", e); return new Response("Bad Gateway", { status: 502 });
1186
1285
  }
1187
1286
  const respHeaders = new Headers();
1188
- upstream.headers.forEach(function(value, key) { if (!__hopByHopHeaders.has(key.toLowerCase())) respHeaders.append(key, value); });
1287
+ // Node.js fetch() auto-decompresses response bodies, while Workers fetch()
1288
+ // preserves wire encoding. Only strip encoding/length on Node.js to avoid
1289
+ // double-decompression errors without breaking Workers parity.
1290
+ const __isNodeRuntime = typeof process !== "undefined" && !!(process.versions && process.versions.node);
1291
+ upstream.headers.forEach(function(value, key) {
1292
+ var lower = key.toLowerCase();
1293
+ if (__hopByHopHeaders.has(lower)) return;
1294
+ if (__isNodeRuntime && (lower === "content-encoding" || lower === "content-length")) return;
1295
+ respHeaders.append(key, value);
1296
+ });
1189
1297
  return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: respHeaders });
1190
1298
  }
1191
1299
 
@@ -1251,19 +1359,13 @@ export default async function handler(request) {
1251
1359
  const lk = h.key.toLowerCase();
1252
1360
  if (lk === "vary" || lk === "set-cookie") {
1253
1361
  response.headers.append(h.key, h.value);
1254
- } else {
1362
+ } else if (!response.headers.has(lk)) {
1363
+ // Middleware headers take precedence: skip config keys already
1364
+ // set by middleware so middleware headers always win.
1255
1365
  response.headers.set(h.key, h.value);
1256
1366
  }
1257
1367
  }
1258
1368
  }
1259
- // Merge middleware response headers into the final response.
1260
- // This runs at the top level so every response path (route
1261
- // handlers, server actions, metadata, errors, etc.) gets them.
1262
- if (_mwCtx.headers) {
1263
- for (const [key, value] of _mwCtx.headers) {
1264
- response.headers.append(key, value);
1265
- }
1266
- }
1267
1369
  }
1268
1370
  return response;
1269
1371
  })
@@ -1344,21 +1446,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1344
1446
  }
1345
1447
  }
1346
1448
 
1347
- // ── Apply beforeFiles rewrites from next.config.js ────────────────────
1348
- if (__configRewrites.beforeFiles && __configRewrites.beforeFiles.length) {
1349
- // Strip .rsc suffix before matching rewrite rules — same reason as redirects above.
1350
- const __rewritePathname = pathname.endsWith(".rsc") ? pathname.slice(0, -4) : pathname;
1351
- const __rewritten = __applyConfigRewrites(__rewritePathname, __configRewrites.beforeFiles, __reqCtx);
1352
- if (__rewritten) {
1353
- if (__isExternalUrl(__rewritten)) {
1354
- setHeadersContext(null);
1355
- setNavigationContext(null);
1356
- return __proxyExternalRequest(request, __rewritten);
1357
- }
1358
- pathname = __rewritten;
1359
- }
1360
- }
1361
-
1362
1449
  const isRscRequest = pathname.endsWith(".rsc") || request.headers.get("accept")?.includes("text/x-component");
1363
1450
  let cleanPathname = pathname.replace(/\\.rsc$/, "");
1364
1451
 
@@ -1389,7 +1476,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1389
1476
  mwUrl.pathname = cleanPathname;
1390
1477
  const mwRequest = new Request(mwUrl, request);
1391
1478
  const nextRequest = mwRequest instanceof NextRequest ? mwRequest : new NextRequest(mwRequest);
1392
- const mwResponse = await middlewareFn(nextRequest);
1479
+ const mwFetchEvent = new NextFetchEvent({ page: cleanPathname });
1480
+ const mwResponse = await middlewareFn(nextRequest, mwFetchEvent);
1481
+ mwFetchEvent.drainWaitUntil();
1393
1482
  if (mwResponse) {
1394
1483
  // Check for x-middleware-next (continue)
1395
1484
  if (mwResponse.headers.get("x-middleware-next") === "1") {
@@ -1452,6 +1541,27 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1452
1541
  }
1453
1542
  ` : ""}
1454
1543
 
1544
+ // Build post-middleware request context for afterFiles/fallback rewrites.
1545
+ // These run after middleware in the App Router execution order and should
1546
+ // evaluate has/missing conditions against middleware-modified headers.
1547
+ // When no middleware is present, this falls back to __buildRequestContext.
1548
+ const __postMwReqCtx = __buildPostMwRequestContext(request);
1549
+
1550
+ // ── Apply beforeFiles rewrites from next.config.js ────────────────────
1551
+ // In App Router execution order, beforeFiles runs after middleware so that
1552
+ // has/missing conditions can evaluate against middleware-modified headers.
1553
+ if (__configRewrites.beforeFiles && __configRewrites.beforeFiles.length) {
1554
+ const __rewritten = __applyConfigRewrites(cleanPathname, __configRewrites.beforeFiles, __postMwReqCtx);
1555
+ if (__rewritten) {
1556
+ if (__isExternalUrl(__rewritten)) {
1557
+ setHeadersContext(null);
1558
+ setNavigationContext(null);
1559
+ return __proxyExternalRequest(request, __rewritten);
1560
+ }
1561
+ cleanPathname = __rewritten;
1562
+ }
1563
+ }
1564
+
1455
1565
  // ── Image optimization passthrough (dev mode — no transformation) ───────
1456
1566
  if (cleanPathname === "/_vinext/image") {
1457
1567
  const __rawImgUrl = url.searchParams.get("url");
@@ -1677,7 +1787,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1677
1787
 
1678
1788
  // ── Apply afterFiles rewrites from next.config.js ──────────────────────
1679
1789
  if (__configRewrites.afterFiles && __configRewrites.afterFiles.length) {
1680
- const __afterRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.afterFiles, __reqCtx);
1790
+ const __afterRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.afterFiles, __postMwReqCtx);
1681
1791
  if (__afterRewritten) {
1682
1792
  if (__isExternalUrl(__afterRewritten)) {
1683
1793
  setHeadersContext(null);
@@ -1692,7 +1802,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1692
1802
 
1693
1803
  // ── Fallback rewrites from next.config.js (if no route matched) ───────
1694
1804
  if (!match && __configRewrites.fallback && __configRewrites.fallback.length) {
1695
- const __fallbackRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.fallback, __reqCtx);
1805
+ const __fallbackRewritten = __applyConfigRewrites(cleanPathname, __configRewrites.fallback, __postMwReqCtx);
1696
1806
  if (__fallbackRewritten) {
1697
1807
  if (__isExternalUrl(__fallbackRewritten)) {
1698
1808
  setHeadersContext(null);
@@ -1726,6 +1836,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1726
1836
  if (route.routeHandler) {
1727
1837
  const handler = route.routeHandler;
1728
1838
  const method = request.method.toUpperCase();
1839
+ const revalidateSeconds = typeof handler.revalidate === "number" && handler.revalidate > 0 ? handler.revalidate : null;
1729
1840
 
1730
1841
  // Collect exported HTTP methods for OPTIONS auto-response and Allow header
1731
1842
  const HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
@@ -1760,6 +1871,12 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1760
1871
  try {
1761
1872
  const response = await handlerFn(request, { params });
1762
1873
 
1874
+ // Apply Cache-Control from route segment config (export const revalidate = N).
1875
+ // Next.js sets s-maxage on GET route handlers with a numeric revalidate value.
1876
+ if (revalidateSeconds !== null && (method === "GET" || isAutoHead) && !response.headers.has("cache-control")) {
1877
+ response.headers.set("cache-control", "s-maxage=" + revalidateSeconds + ", stale-while-revalidate");
1878
+ }
1879
+
1763
1880
  // Collect any Set-Cookie headers from cookies().set()/delete() calls
1764
1881
  const pendingCookies = getAndClearPendingCookies();
1765
1882
  const draftCookie = getDraftModeCookieHeader();
@@ -1981,7 +2098,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1981
2098
  }
1982
2099
  if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
1983
2100
  const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
1984
- const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request);
2101
+ const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, { matchedParams: params });
1985
2102
  if (fallbackResp) return fallbackResp;
1986
2103
  setHeadersContext(null);
1987
2104
  setNavigationContext(null);
@@ -1990,7 +2107,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1990
2107
  }
1991
2108
  }
1992
2109
  // Non-special error (e.g. generateMetadata() threw) — render error.tsx if available
1993
- const errorBoundaryResp = await renderErrorBoundaryPage(route, buildErr, isRscRequest, request);
2110
+ const errorBoundaryResp = await renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params);
1994
2111
  if (errorBoundaryResp) return errorBoundaryResp;
1995
2112
  throw buildErr;
1996
2113
  }
@@ -2012,7 +2129,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2012
2129
  }
2013
2130
  if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
2014
2131
  const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
2015
- const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request);
2132
+ const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, { matchedParams: params });
2016
2133
  if (fallbackResp) return fallbackResp;
2017
2134
  setHeadersContext(null);
2018
2135
  setNavigationContext(null);
@@ -2037,54 +2154,61 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2037
2154
  // layouts itself throws notFound() during the fallback rendering (causing a 500).
2038
2155
  if (route.layouts && route.layouts.length > 0) {
2039
2156
  const asyncParams = makeThenableParams(params);
2040
- for (let li = route.layouts.length - 1; li >= 0; li--) {
2041
- const LayoutComp = route.layouts[li]?.default;
2042
- if (!LayoutComp) continue;
2043
- try {
2044
- const lr = LayoutComp({ params: asyncParams, children: null });
2045
- if (lr && typeof lr === "object" && typeof lr.then === "function") await lr;
2046
- } catch (layoutErr) {
2047
- if (layoutErr && typeof layoutErr === "object" && "digest" in layoutErr) {
2048
- const digest = String(layoutErr.digest);
2049
- if (digest.startsWith("NEXT_REDIRECT;")) {
2050
- const parts = digest.split(";");
2051
- const redirectUrl = decodeURIComponent(parts[2]);
2052
- const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
2053
- setHeadersContext(null);
2054
- setNavigationContext(null);
2055
- return Response.redirect(new URL(redirectUrl, request.url), statusCode);
2056
- }
2057
- if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
2058
- const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
2059
- // Find the not-found component from the parent level (the boundary that
2060
- // would catch this in Next.js). Walk up from the throwing layout to find
2061
- // the nearest not-found at a parent layout's directory.
2062
- let parentNotFound = null;
2063
- if (route.notFounds) {
2064
- for (let pi = li - 1; pi >= 0; pi--) {
2065
- if (route.notFounds[pi]?.default) {
2066
- parentNotFound = route.notFounds[pi].default;
2067
- break;
2157
+ // Run inside ALS context so the module-level console.error patch suppresses
2158
+ // "Invalid hook call" only for this request's probe — concurrent requests
2159
+ // each have their own ALS store and are unaffected.
2160
+ const _layoutProbeResult = await _suppressHookWarningAls.run(true, async () => {
2161
+ for (let li = route.layouts.length - 1; li >= 0; li--) {
2162
+ const LayoutComp = route.layouts[li]?.default;
2163
+ if (!LayoutComp) continue;
2164
+ try {
2165
+ const lr = LayoutComp({ params: asyncParams, children: null });
2166
+ if (lr && typeof lr === "object" && typeof lr.then === "function") await lr;
2167
+ } catch (layoutErr) {
2168
+ if (layoutErr && typeof layoutErr === "object" && "digest" in layoutErr) {
2169
+ const digest = String(layoutErr.digest);
2170
+ if (digest.startsWith("NEXT_REDIRECT;")) {
2171
+ const parts = digest.split(";");
2172
+ const redirectUrl = decodeURIComponent(parts[2]);
2173
+ const statusCode = parts[3] ? parseInt(parts[3], 10) : 307;
2174
+ setHeadersContext(null);
2175
+ setNavigationContext(null);
2176
+ return Response.redirect(new URL(redirectUrl, request.url), statusCode);
2177
+ }
2178
+ if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
2179
+ const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
2180
+ // Find the not-found component from the parent level (the boundary that
2181
+ // would catch this in Next.js). Walk up from the throwing layout to find
2182
+ // the nearest not-found at a parent layout's directory.
2183
+ let parentNotFound = null;
2184
+ if (route.notFounds) {
2185
+ for (let pi = li - 1; pi >= 0; pi--) {
2186
+ if (route.notFounds[pi]?.default) {
2187
+ parentNotFound = route.notFounds[pi].default;
2188
+ break;
2189
+ }
2068
2190
  }
2069
2191
  }
2192
+ if (!parentNotFound) parentNotFound = ${rootNotFoundVar ? `${rootNotFoundVar}?.default` : "null"};
2193
+ // Wrap in only the layouts above the throwing one
2194
+ const parentLayouts = route.layouts.slice(0, li);
2195
+ const fallbackResp = await renderHTTPAccessFallbackPage(
2196
+ route, statusCode, isRscRequest, request,
2197
+ { boundaryComponent: parentNotFound, layouts: parentLayouts, matchedParams: params }
2198
+ );
2199
+ if (fallbackResp) return fallbackResp;
2200
+ setHeadersContext(null);
2201
+ setNavigationContext(null);
2202
+ const statusText = statusCode === 403 ? "Forbidden" : statusCode === 401 ? "Unauthorized" : "Not Found";
2203
+ return new Response(statusText, { status: statusCode });
2070
2204
  }
2071
- if (!parentNotFound) parentNotFound = ${rootNotFoundVar ? `${rootNotFoundVar}?.default` : "null"};
2072
- // Wrap in only the layouts above the throwing one
2073
- const parentLayouts = route.layouts.slice(0, li);
2074
- const fallbackResp = await renderHTTPAccessFallbackPage(
2075
- route, statusCode, isRscRequest, request,
2076
- { boundaryComponent: parentNotFound, layouts: parentLayouts }
2077
- );
2078
- if (fallbackResp) return fallbackResp;
2079
- setHeadersContext(null);
2080
- setNavigationContext(null);
2081
- const statusText = statusCode === 403 ? "Forbidden" : statusCode === 401 ? "Unauthorized" : "Not Found";
2082
- return new Response(statusText, { status: statusCode });
2083
2205
  }
2206
+ // Not a special error — let it propagate through normal RSC rendering
2084
2207
  }
2085
- // Not a special error — let it propagate through normal RSC rendering
2086
2208
  }
2087
- }
2209
+ return null;
2210
+ });
2211
+ if (_layoutProbeResult instanceof Response) return _layoutProbeResult;
2088
2212
  }
2089
2213
 
2090
2214
  // Pre-render the page component to catch redirect()/notFound() thrown synchronously.
@@ -2097,37 +2221,35 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2097
2221
  // would be hit before the RSC stream even starts).
2098
2222
  //
2099
2223
  // Because this calls the component outside React's render cycle, hooks like use()
2100
- // trigger "Invalid hook call" console.error in dev. Suppress that expected warning.
2224
+ // trigger "Invalid hook call" console.error in dev. The module-level ALS patch
2225
+ // suppresses the warning only within this request's execution context.
2101
2226
  const _hasLoadingBoundary = !!(route.loading && route.loading.default);
2102
- const _origConsoleError = console.error;
2103
- console.error = (...args) => {
2104
- if (typeof args[0] === "string" && args[0].includes("Invalid hook call")) return;
2105
- _origConsoleError.apply(console, args);
2106
- };
2107
- try {
2108
- const testResult = PageComponent({ params });
2109
- // If it's a promise (async component), only await if there's no loading boundary.
2110
- // With a loading boundary, the Suspense streaming pipeline handles async resolution
2111
- // and any redirect/notFound errors via rscOnError.
2112
- if (testResult && typeof testResult === "object" && typeof testResult.then === "function") {
2113
- if (!_hasLoadingBoundary) {
2114
- await testResult;
2115
- } else {
2116
- // Suppress unhandled promise rejection — with a loading boundary,
2117
- // redirect/notFound errors are handled by rscOnError during streaming.
2118
- testResult.catch(() => {});
2227
+ const _pageProbeResult = await _suppressHookWarningAls.run(true, async () => {
2228
+ try {
2229
+ const testResult = PageComponent({ params });
2230
+ // If it's a promise (async component), only await if there's no loading boundary.
2231
+ // With a loading boundary, the Suspense streaming pipeline handles async resolution
2232
+ // and any redirect/notFound errors via rscOnError.
2233
+ if (testResult && typeof testResult === "object" && typeof testResult.then === "function") {
2234
+ if (!_hasLoadingBoundary) {
2235
+ await testResult;
2236
+ } else {
2237
+ // Suppress unhandled promise rejection with a loading boundary,
2238
+ // redirect/notFound errors are handled by rscOnError during streaming.
2239
+ testResult.catch(() => {});
2240
+ }
2119
2241
  }
2242
+ } catch (preRenderErr) {
2243
+ const specialResponse = await handleRenderError(preRenderErr);
2244
+ if (specialResponse) return specialResponse;
2245
+ // Non-special errors from the pre-render test are expected (e.g. use() hook
2246
+ // fails outside React's render cycle, client references can't execute on server).
2247
+ // Only redirect/notFound/forbidden/unauthorized are actionable here — other
2248
+ // errors will be properly caught during actual RSC/SSR rendering below.
2120
2249
  }
2121
- } catch (preRenderErr) {
2122
- const specialResponse = await handleRenderError(preRenderErr);
2123
- if (specialResponse) return specialResponse;
2124
- // Non-special errors from the pre-render test are expected (e.g. use() hook
2125
- // fails outside React's render cycle, client references can't execute on server).
2126
- // Only redirect/notFound/forbidden/unauthorized are actionable here — other
2127
- // errors will be properly caught during actual RSC/SSR rendering below.
2128
- } finally {
2129
- console.error = _origConsoleError;
2130
- }
2250
+ return null;
2251
+ });
2252
+ if (_pageProbeResult instanceof Response) return _pageProbeResult;
2131
2253
 
2132
2254
  // Mark end of compile phase: route matching, middleware, tree building are done.
2133
2255
  if (process.env.NODE_ENV !== "production") __compileEnd = performance.now();
@@ -2154,7 +2276,37 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2154
2276
  } else if (revalidateSeconds) {
2155
2277
  responseHeaders["Cache-Control"] = "s-maxage=" + revalidateSeconds + ", stale-while-revalidate";
2156
2278
  }
2157
- // Middleware response headers are merged by the handler() wrapper.
2279
+ // Merge middleware response headers into the RSC response.
2280
+ // set-cookie and vary are accumulated to preserve existing values
2281
+ // (e.g. "Vary: RSC, Accept" set above); all other keys use plain
2282
+ // assignment so middleware headers win over config headers, which
2283
+ // the outer handler applies afterward and skips keys already present.
2284
+ if (_mwCtx.headers) {
2285
+ for (const [key, value] of _mwCtx.headers) {
2286
+ const lk = key.toLowerCase();
2287
+ if (lk === "set-cookie") {
2288
+ const existing = responseHeaders[lk];
2289
+ if (Array.isArray(existing)) {
2290
+ existing.push(value);
2291
+ } else if (existing) {
2292
+ responseHeaders[lk] = [existing, value];
2293
+ } else {
2294
+ responseHeaders[lk] = [value];
2295
+ }
2296
+ } else if (lk === "vary") {
2297
+ // Accumulate Vary values to preserve the existing "RSC, Accept" entry.
2298
+ const existing = responseHeaders["Vary"] ?? responseHeaders["vary"];
2299
+ if (existing) {
2300
+ responseHeaders["Vary"] = existing + ", " + value;
2301
+ if (responseHeaders["vary"] !== undefined) delete responseHeaders["vary"];
2302
+ } else {
2303
+ responseHeaders[key] = value;
2304
+ }
2305
+ } else {
2306
+ responseHeaders[key] = value;
2307
+ }
2308
+ }
2309
+ }
2158
2310
  // Attach internal timing header so the dev server middleware can log it.
2159
2311
  // Format: "handlerStart,compileMs,renderMs"
2160
2312
  // handlerStart - absolute performance.now() when _handleRequest began,
@@ -2202,7 +2354,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2202
2354
  const specialResponse = await handleRenderError(ssrErr);
2203
2355
  if (specialResponse) return specialResponse;
2204
2356
  // Non-special error during SSR — render error.tsx if available
2205
- const errorBoundaryResp = await renderErrorBoundaryPage(route, ssrErr, isRscRequest, request);
2357
+ const errorBoundaryResp = await renderErrorBoundaryPage(route, ssrErr, isRscRequest, request, params);
2206
2358
  if (errorBoundaryResp) return errorBoundaryResp;
2207
2359
  throw ssrErr;
2208
2360
  }
@@ -2222,7 +2374,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2222
2374
  if (fontLinkHeader) {
2223
2375
  response.headers.set("Link", fontLinkHeader);
2224
2376
  }
2225
- // Middleware response headers are merged by the handler() wrapper.
2377
+ // Merge middleware response headers into the final response.
2378
+ // The response is freshly constructed above (new Response(htmlStream, {...})),
2379
+ // so set() and append() are equivalent — there are no same-key conflicts yet.
2380
+ // Precedence over config headers is handled by the outer handler, which
2381
+ // skips config keys that middleware already placed on the response.
2382
+ if (_mwCtx.headers) {
2383
+ for (const [key, value] of _mwCtx.headers) {
2384
+ response.headers.append(key, value);
2385
+ }
2386
+ }
2226
2387
  // Attach internal timing header so the dev server middleware can log it.
2227
2388
  // Format: "handlerStart,compileMs,renderMs"
2228
2389
  // handlerStart - absolute performance.now() when _handleRequest began,