vinext 0.0.24 → 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 (92) 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 +46 -6
  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 +70 -35
  18. package/dist/deploy.js.map +1 -1
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +172 -18
  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 +238 -114
  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.map +1 -1
  59. package/dist/server/prod-server.js +15 -25
  60. package/dist/server/prod-server.js.map +1 -1
  61. package/dist/shims/cache.d.ts.map +1 -1
  62. package/dist/shims/cache.js +14 -2
  63. package/dist/shims/cache.js.map +1 -1
  64. package/dist/shims/fetch-cache.d.ts.map +1 -1
  65. package/dist/shims/fetch-cache.js +139 -29
  66. package/dist/shims/fetch-cache.js.map +1 -1
  67. package/dist/shims/form.d.ts.map +1 -1
  68. package/dist/shims/form.js +2 -3
  69. package/dist/shims/form.js.map +1 -1
  70. package/dist/shims/layout-segment-context.d.ts +5 -4
  71. package/dist/shims/layout-segment-context.d.ts.map +1 -1
  72. package/dist/shims/layout-segment-context.js +6 -5
  73. package/dist/shims/layout-segment-context.js.map +1 -1
  74. package/dist/shims/link.d.ts.map +1 -1
  75. package/dist/shims/link.js +32 -17
  76. package/dist/shims/link.js.map +1 -1
  77. package/dist/shims/navigation.d.ts +14 -11
  78. package/dist/shims/navigation.d.ts.map +1 -1
  79. package/dist/shims/navigation.js +122 -102
  80. package/dist/shims/navigation.js.map +1 -1
  81. package/dist/shims/router.d.ts.map +1 -1
  82. package/dist/shims/router.js +37 -21
  83. package/dist/shims/router.js.map +1 -1
  84. package/dist/shims/server.d.ts +2 -0
  85. package/dist/shims/server.d.ts.map +1 -1
  86. package/dist/shims/server.js +4 -0
  87. package/dist/shims/server.js.map +1 -1
  88. package/dist/shims/url-utils.d.ts +13 -0
  89. package/dist/shims/url-utils.d.ts.map +1 -0
  90. package/dist/shims/url-utils.js +28 -0
  91. package/dist/shims/url-utils.js.map +1 -0
  92. package/package.json +1 -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
201
  import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, runWithHeadersContext, applyMiddlewareRequestHeaders, getHeadersContext } from "next/headers";
200
- import { NextRequest } from "next/server";
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
 
@@ -1187,12 +1270,6 @@ async function __proxyExternalRequest(request, externalUrl) {
1187
1270
  const headers = new Headers(request.headers);
1188
1271
  headers.set("host", targetUrl.host);
1189
1272
  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
1273
  for (const key of [...headers.keys()]) {
1197
1274
  if (key.startsWith("x-middleware-")) headers.delete(key);
1198
1275
  }
@@ -1282,19 +1359,13 @@ export default async function handler(request) {
1282
1359
  const lk = h.key.toLowerCase();
1283
1360
  if (lk === "vary" || lk === "set-cookie") {
1284
1361
  response.headers.append(h.key, h.value);
1285
- } 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.
1286
1365
  response.headers.set(h.key, h.value);
1287
1366
  }
1288
1367
  }
1289
1368
  }
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
1369
  }
1299
1370
  return response;
1300
1371
  })
@@ -1405,7 +1476,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1405
1476
  mwUrl.pathname = cleanPathname;
1406
1477
  const mwRequest = new Request(mwUrl, request);
1407
1478
  const nextRequest = mwRequest instanceof NextRequest ? mwRequest : new NextRequest(mwRequest);
1408
- const mwResponse = await middlewareFn(nextRequest);
1479
+ const mwFetchEvent = new NextFetchEvent({ page: cleanPathname });
1480
+ const mwResponse = await middlewareFn(nextRequest, mwFetchEvent);
1481
+ mwFetchEvent.drainWaitUntil();
1409
1482
  if (mwResponse) {
1410
1483
  // Check for x-middleware-next (continue)
1411
1484
  if (mwResponse.headers.get("x-middleware-next") === "1") {
@@ -1763,6 +1836,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1763
1836
  if (route.routeHandler) {
1764
1837
  const handler = route.routeHandler;
1765
1838
  const method = request.method.toUpperCase();
1839
+ const revalidateSeconds = typeof handler.revalidate === "number" && handler.revalidate > 0 ? handler.revalidate : null;
1766
1840
 
1767
1841
  // Collect exported HTTP methods for OPTIONS auto-response and Allow header
1768
1842
  const HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
@@ -1797,6 +1871,12 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1797
1871
  try {
1798
1872
  const response = await handlerFn(request, { params });
1799
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
+
1800
1880
  // Collect any Set-Cookie headers from cookies().set()/delete() calls
1801
1881
  const pendingCookies = getAndClearPendingCookies();
1802
1882
  const draftCookie = getDraftModeCookieHeader();
@@ -2018,7 +2098,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2018
2098
  }
2019
2099
  if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
2020
2100
  const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
2021
- const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request);
2101
+ const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, { matchedParams: params });
2022
2102
  if (fallbackResp) return fallbackResp;
2023
2103
  setHeadersContext(null);
2024
2104
  setNavigationContext(null);
@@ -2027,7 +2107,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2027
2107
  }
2028
2108
  }
2029
2109
  // Non-special error (e.g. generateMetadata() threw) — render error.tsx if available
2030
- const errorBoundaryResp = await renderErrorBoundaryPage(route, buildErr, isRscRequest, request);
2110
+ const errorBoundaryResp = await renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params);
2031
2111
  if (errorBoundaryResp) return errorBoundaryResp;
2032
2112
  throw buildErr;
2033
2113
  }
@@ -2049,7 +2129,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2049
2129
  }
2050
2130
  if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;")) {
2051
2131
  const statusCode = digest === "NEXT_NOT_FOUND" ? 404 : parseInt(digest.split(";")[1], 10);
2052
- const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request);
2132
+ const fallbackResp = await renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, { matchedParams: params });
2053
2133
  if (fallbackResp) return fallbackResp;
2054
2134
  setHeadersContext(null);
2055
2135
  setNavigationContext(null);
@@ -2074,54 +2154,61 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2074
2154
  // layouts itself throws notFound() during the fallback rendering (causing a 500).
2075
2155
  if (route.layouts && route.layouts.length > 0) {
2076
2156
  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;
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
+ }
2105
2190
  }
2106
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 });
2107
2204
  }
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
2205
  }
2206
+ // Not a special error — let it propagate through normal RSC rendering
2121
2207
  }
2122
- // Not a special error — let it propagate through normal RSC rendering
2123
2208
  }
2124
- }
2209
+ return null;
2210
+ });
2211
+ if (_layoutProbeResult instanceof Response) return _layoutProbeResult;
2125
2212
  }
2126
2213
 
2127
2214
  // Pre-render the page component to catch redirect()/notFound() thrown synchronously.
@@ -2134,37 +2221,35 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2134
2221
  // would be hit before the RSC stream even starts).
2135
2222
  //
2136
2223
  // 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.
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.
2138
2226
  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(() => {});
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
+ }
2156
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.
2157
2249
  }
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
- }
2250
+ return null;
2251
+ });
2252
+ if (_pageProbeResult instanceof Response) return _pageProbeResult;
2168
2253
 
2169
2254
  // Mark end of compile phase: route matching, middleware, tree building are done.
2170
2255
  if (process.env.NODE_ENV !== "production") __compileEnd = performance.now();
@@ -2191,7 +2276,37 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2191
2276
  } else if (revalidateSeconds) {
2192
2277
  responseHeaders["Cache-Control"] = "s-maxage=" + revalidateSeconds + ", stale-while-revalidate";
2193
2278
  }
2194
- // 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
+ }
2195
2310
  // Attach internal timing header so the dev server middleware can log it.
2196
2311
  // Format: "handlerStart,compileMs,renderMs"
2197
2312
  // handlerStart - absolute performance.now() when _handleRequest began,
@@ -2239,7 +2354,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2239
2354
  const specialResponse = await handleRenderError(ssrErr);
2240
2355
  if (specialResponse) return specialResponse;
2241
2356
  // Non-special error during SSR — render error.tsx if available
2242
- const errorBoundaryResp = await renderErrorBoundaryPage(route, ssrErr, isRscRequest, request);
2357
+ const errorBoundaryResp = await renderErrorBoundaryPage(route, ssrErr, isRscRequest, request, params);
2243
2358
  if (errorBoundaryResp) return errorBoundaryResp;
2244
2359
  throw ssrErr;
2245
2360
  }
@@ -2259,7 +2374,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2259
2374
  if (fontLinkHeader) {
2260
2375
  response.headers.set("Link", fontLinkHeader);
2261
2376
  }
2262
- // 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
+ }
2263
2387
  // Attach internal timing header so the dev server middleware can log it.
2264
2388
  // Format: "handlerStart,compileMs,renderMs"
2265
2389
  // handlerStart - absolute performance.now() when _handleRequest began,