vinext 0.0.39 → 0.0.41

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 (100) hide show
  1. package/README.md +1 -1
  2. package/dist/build/standalone.js +7 -0
  3. package/dist/build/standalone.js.map +1 -1
  4. package/dist/check.js +2 -2
  5. package/dist/check.js.map +1 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/entries/app-rsc-entry.d.ts +2 -1
  8. package/dist/entries/app-rsc-entry.js +185 -264
  9. package/dist/entries/app-rsc-entry.js.map +1 -1
  10. package/dist/entries/pages-server-entry.js +205 -199
  11. package/dist/entries/pages-server-entry.js.map +1 -1
  12. package/dist/index.d.ts +32 -1
  13. package/dist/index.js +81 -6
  14. package/dist/index.js.map +1 -1
  15. package/dist/init.d.ts +1 -1
  16. package/dist/init.js +2 -2
  17. package/dist/init.js.map +1 -1
  18. package/dist/plugins/fonts.js +1 -0
  19. package/dist/plugins/fonts.js.map +1 -1
  20. package/dist/plugins/server-externals-manifest.d.ts +11 -1
  21. package/dist/plugins/server-externals-manifest.js +10 -3
  22. package/dist/plugins/server-externals-manifest.js.map +1 -1
  23. package/dist/routing/app-router.d.ts +10 -2
  24. package/dist/routing/app-router.js +37 -22
  25. package/dist/routing/app-router.js.map +1 -1
  26. package/dist/server/app-page-boundary-render.d.ts +1 -0
  27. package/dist/server/app-page-boundary-render.js +1 -0
  28. package/dist/server/app-page-boundary-render.js.map +1 -1
  29. package/dist/server/app-page-render.d.ts +1 -0
  30. package/dist/server/app-page-render.js +2 -0
  31. package/dist/server/app-page-render.js.map +1 -1
  32. package/dist/server/app-page-response.d.ts +4 -1
  33. package/dist/server/app-page-response.js +14 -8
  34. package/dist/server/app-page-response.js.map +1 -1
  35. package/dist/server/app-page-route-wiring.d.ts +79 -0
  36. package/dist/server/app-page-route-wiring.js +167 -0
  37. package/dist/server/app-page-route-wiring.js.map +1 -0
  38. package/dist/server/app-page-stream.d.ts +4 -1
  39. package/dist/server/app-page-stream.js +5 -1
  40. package/dist/server/app-page-stream.js.map +1 -1
  41. package/dist/server/app-route-handler-response.js +6 -2
  42. package/dist/server/app-route-handler-response.js.map +1 -1
  43. package/dist/server/app-router-entry.d.ts +6 -1
  44. package/dist/server/app-router-entry.js +9 -2
  45. package/dist/server/app-router-entry.js.map +1 -1
  46. package/dist/server/app-ssr-entry.d.ts +3 -1
  47. package/dist/server/app-ssr-entry.js +17 -17
  48. package/dist/server/app-ssr-entry.js.map +1 -1
  49. package/dist/server/app-ssr-stream.d.ts +1 -1
  50. package/dist/server/app-ssr-stream.js +4 -4
  51. package/dist/server/app-ssr-stream.js.map +1 -1
  52. package/dist/server/csp.d.ts +12 -0
  53. package/dist/server/csp.js +46 -0
  54. package/dist/server/csp.js.map +1 -0
  55. package/dist/server/dev-server.js +20 -14
  56. package/dist/server/dev-server.js.map +1 -1
  57. package/dist/server/html.d.ts +4 -1
  58. package/dist/server/html.js +11 -1
  59. package/dist/server/html.js.map +1 -1
  60. package/dist/server/middleware-response-headers.d.ts +12 -0
  61. package/dist/server/middleware-response-headers.js +23 -0
  62. package/dist/server/middleware-response-headers.js.map +1 -0
  63. package/dist/server/pages-page-data.d.ts +1 -0
  64. package/dist/server/pages-page-data.js +2 -2
  65. package/dist/server/pages-page-data.js.map +1 -1
  66. package/dist/server/pages-page-response.d.ts +2 -1
  67. package/dist/server/pages-page-response.js +16 -14
  68. package/dist/server/pages-page-response.js.map +1 -1
  69. package/dist/server/prod-server.d.ts +1 -1
  70. package/dist/server/prod-server.js +41 -14
  71. package/dist/server/prod-server.js.map +1 -1
  72. package/dist/server/request-pipeline.d.ts +14 -1
  73. package/dist/server/request-pipeline.js +55 -1
  74. package/dist/server/request-pipeline.js.map +1 -1
  75. package/dist/server/worker-utils.d.ts +4 -1
  76. package/dist/server/worker-utils.js +31 -1
  77. package/dist/server/worker-utils.js.map +1 -1
  78. package/dist/shims/error-boundary.d.ts +14 -5
  79. package/dist/shims/error-boundary.js +23 -3
  80. package/dist/shims/error-boundary.js.map +1 -1
  81. package/dist/shims/head.js.map +1 -1
  82. package/dist/shims/navigation.d.ts +16 -1
  83. package/dist/shims/navigation.js +18 -3
  84. package/dist/shims/navigation.js.map +1 -1
  85. package/dist/shims/router.js +127 -38
  86. package/dist/shims/router.js.map +1 -1
  87. package/dist/shims/script-nonce-context.d.ts +12 -0
  88. package/dist/shims/script-nonce-context.js +17 -0
  89. package/dist/shims/script-nonce-context.js.map +1 -0
  90. package/dist/shims/script.js +41 -10
  91. package/dist/shims/script.js.map +1 -1
  92. package/dist/shims/server.d.ts +17 -4
  93. package/dist/shims/server.js +97 -74
  94. package/dist/shims/server.js.map +1 -1
  95. package/dist/shims/slot.d.ts +28 -0
  96. package/dist/shims/slot.js +49 -0
  97. package/dist/shims/slot.js.map +1 -0
  98. package/dist/shims/url-safety.js +25 -4
  99. package/dist/shims/url-safety.js.map +1 -1
  100. package/package.json +7 -8
@@ -24,7 +24,10 @@ const appRouteHandlerCachePath = resolveEntryPath("../server/app-route-handler-c
24
24
  const appPageCachePath = resolveEntryPath("../server/app-page-cache.js", import.meta.url);
25
25
  const appPageExecutionPath = resolveEntryPath("../server/app-page-execution.js", import.meta.url);
26
26
  const appPageBoundaryRenderPath = resolveEntryPath("../server/app-page-boundary-render.js", import.meta.url);
27
+ const appPageRouteWiringPath = resolveEntryPath("../server/app-page-route-wiring.js", import.meta.url);
27
28
  const appPageRenderPath = resolveEntryPath("../server/app-page-render.js", import.meta.url);
29
+ const appPageResponsePath = resolveEntryPath("../server/app-page-response.js", import.meta.url);
30
+ const cspPath = resolveEntryPath("../server/csp.js", import.meta.url);
28
31
  const appPageRequestPath = resolveEntryPath("../server/app-page-request.js", import.meta.url);
29
32
  const appRouteHandlerResponsePath = resolveEntryPath("../server/app-route-handler-response.js", import.meta.url);
30
33
  const routeTriePath = resolveEntryPath("../routing/route-trie.js", import.meta.url);
@@ -50,6 +53,7 @@ function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, global
50
53
  const bodySizeLimit = config?.bodySizeLimit ?? 1 * 1024 * 1024;
51
54
  const i18nConfig = config?.i18n ?? null;
52
55
  const hasPagesDir = config?.hasPagesDir ?? false;
56
+ const publicFiles = config?.publicFiles ?? [];
53
57
  const imports = [];
54
58
  const importMap = /* @__PURE__ */ new Map();
55
59
  let importIdx = 0;
@@ -65,7 +69,7 @@ function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, global
65
69
  if (route.pagePath) getImportVar(route.pagePath);
66
70
  if (route.routePath) getImportVar(route.routePath);
67
71
  for (const layout of route.layouts) getImportVar(layout);
68
- for (const tmpl of route.templates) getImportVar(tmpl);
72
+ for (const tmpl of route.templates) if (tmpl) getImportVar(tmpl);
69
73
  if (route.loadingPath) getImportVar(route.loadingPath);
70
74
  if (route.errorPath) getImportVar(route.errorPath);
71
75
  if (route.layoutErrorPaths) {
@@ -86,7 +90,7 @@ function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, global
86
90
  }
87
91
  const routeEntries = routes.map((route) => {
88
92
  const layoutVars = route.layouts.map((l) => getImportVar(l));
89
- const templateVars = route.templates.map((t) => getImportVar(t));
93
+ const templateVars = route.templates.map((t) => t ? getImportVar(t) : "null");
90
94
  const notFoundVars = (route.notFoundPaths || []).map((nf) => nf ? getImportVar(nf) : "null");
91
95
  const slotEntries = route.parallelSlots.map((slot) => {
92
96
  const interceptEntries = slot.interceptingRoutes.map((ir) => ` {
@@ -102,6 +106,7 @@ function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, global
102
106
  loading: ${slot.loadingPath ? getImportVar(slot.loadingPath) : "null"},
103
107
  error: ${slot.errorPath ? getImportVar(slot.errorPath) : "null"},
104
108
  layoutIndex: ${slot.layoutIndex},
109
+ routeSegments: ${JSON.stringify(slot.routeSegments)},
105
110
  intercepts: [
106
111
  ${interceptEntries.join(",\n")}
107
112
  ],
@@ -204,19 +209,17 @@ function renderToReadableStream(model, options) {
204
209
  }
205
210
  }));
206
211
  }
207
- import { createElement, Suspense, Fragment } from "react";
212
+ import { createElement } from "react";
208
213
  import { setNavigationContext as _setNavigationContextOrig, getNavigationContext as _getNavigationContext } from "next/navigation";
209
214
  import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, applyMiddlewareRequestHeaders, getHeadersContext, setHeadersAccessPhase } from "next/headers";
210
215
  import { NextRequest, NextFetchEvent } from "next/server";
211
- import { ErrorBoundary, NotFoundBoundary } from "vinext/error-boundary";
212
- import { LayoutSegmentProvider } from "vinext/layout-segment-context";
213
- import { MetadataHead, mergeMetadata, resolveModuleMetadata, ViewportHead, mergeViewport, resolveModuleViewport } from "vinext/metadata";
216
+ import { mergeMetadata, resolveModuleMetadata, mergeViewport, resolveModuleViewport } from "vinext/metadata";
214
217
  ${middlewarePath ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};` : ""}
215
218
  ${instrumentationPath ? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};` : ""}
216
219
  ${effectiveMetaRoutes.length > 0 ? `import { sitemapToXml, robotsToText, manifestToJson } from ${JSON.stringify(metadataRoutesPath)};` : ""}
217
220
  import { requestContextFromRequest, normalizeHost, matchRedirect, matchRewrite, matchHeaders, isExternalUrl, proxyExternalRequest, sanitizeDestination } from ${JSON.stringify(configMatchersPath)};
218
221
  import { decodePathParams as __decodePathParams } from ${JSON.stringify(normalizePathModulePath)};
219
- import { validateCsrfOrigin, validateImageUrl, guardProtocolRelativeUrl, hasBasePath, stripBasePath, normalizeTrailingSlash, processMiddlewareHeaders } from ${JSON.stringify(requestPipelinePath)};
222
+ import { validateCsrfOrigin, validateServerActionPayload, validateImageUrl, guardProtocolRelativeUrl, hasBasePath, stripBasePath, normalizeTrailingSlash, processMiddlewareHeaders } from ${JSON.stringify(requestPipelinePath)};
220
223
  import {
221
224
  isKnownDynamicAppRoute as __isKnownDynamicAppRoute,
222
225
  } from ${JSON.stringify(appRouteHandlerRuntimePath)};
@@ -242,9 +245,17 @@ import {
242
245
  renderAppPageErrorBoundary as __renderAppPageErrorBoundary,
243
246
  renderAppPageHttpAccessFallback as __renderAppPageHttpAccessFallback,
244
247
  } from ${JSON.stringify(appPageBoundaryRenderPath)};
248
+ import {
249
+ buildAppPageRouteElement as __buildAppPageRouteElement,
250
+ resolveAppPageChildSegments as __resolveAppPageChildSegments,
251
+ } from ${JSON.stringify(appPageRouteWiringPath)};
245
252
  import {
246
253
  renderAppPageLifecycle as __renderAppPageLifecycle,
247
254
  } from ${JSON.stringify(appPageRenderPath)};
255
+ import {
256
+ mergeMiddlewareResponseHeaders as __mergeMiddlewareResponseHeaders,
257
+ } from ${JSON.stringify(appPageResponsePath)};
258
+ import { getScriptNonceFromHeaderSources as __getScriptNonceFromHeaderSources } from ${JSON.stringify(cspPath)};
248
259
  import {
249
260
  buildAppPageElement as __buildAppPageElement,
250
261
  resolveAppPageIntercept as __resolveAppPageIntercept,
@@ -409,38 +420,6 @@ function makeThenableParams(obj) {
409
420
  return Object.assign(Promise.resolve(plain), plain);
410
421
  }
411
422
 
412
- // Resolve route tree segments to actual values using matched params.
413
- // Dynamic segments like [id] are replaced with param values, catch-all
414
- // segments like [...slug] are joined with "/", and route groups are kept as-is.
415
- function __resolveChildSegments(routeSegments, treePosition, params) {
416
- var raw = routeSegments.slice(treePosition);
417
- var result = [];
418
- for (var j = 0; j < raw.length; j++) {
419
- var seg = raw[j];
420
- // Optional catch-all: [[...param]]
421
- if (seg.indexOf("[[...") === 0 && seg.charAt(seg.length - 1) === "]" && seg.charAt(seg.length - 2) === "]") {
422
- var pn = seg.slice(5, -2);
423
- var v = params[pn];
424
- // Skip empty optional catch-all (e.g., visiting /blog on [[...slug]] route)
425
- if (Array.isArray(v) && v.length === 0) continue;
426
- if (v == null) continue;
427
- result.push(Array.isArray(v) ? v.join("/") : v);
428
- // Catch-all: [...param]
429
- } else if (seg.indexOf("[...") === 0 && seg.charAt(seg.length - 1) === "]") {
430
- var pn2 = seg.slice(4, -1);
431
- var v2 = params[pn2];
432
- result.push(Array.isArray(v2) ? v2.join("/") : (v2 || seg));
433
- // Dynamic: [param]
434
- } else if (seg.charAt(0) === "[" && seg.charAt(seg.length - 1) === "]" && seg.indexOf(".") === -1) {
435
- var pn3 = seg.slice(1, -1);
436
- result.push(params[pn3] || seg);
437
- } else {
438
- result.push(seg);
439
- }
440
- }
441
- return result;
442
- }
443
-
444
423
  // djb2 hash — matches Next.js's stringHash for digest generation.
445
424
  // Produces a stable numeric string from error message + stack.
446
425
  function __errorDigest(str) {
@@ -616,7 +595,7 @@ const rootLayouts = [${rootLayoutVars.join(", ")}];
616
595
  * @param opts.boundaryComponent - Override the boundary component (for layout-level notFound)
617
596
  * @param opts.layouts - Override the layouts to wrap with (for layout-level notFound, excludes the throwing layout)
618
597
  */
619
- async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, opts) {
598
+ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, opts, scriptNonce) {
620
599
  return __renderAppPageHttpAccessFallback({
621
600
  boundaryComponent: opts?.boundaryComponent ?? null,
622
601
  buildFontLinkHeader: __buildAppPageFontLinkHeader,
@@ -640,20 +619,21 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
640
619
  makeThenableParams,
641
620
  matchedParams: opts?.matchedParams ?? route?.params ?? {},
642
621
  requestUrl: request.url,
643
- resolveChildSegments: __resolveChildSegments,
622
+ resolveChildSegments: __resolveAppPageChildSegments,
644
623
  rootForbiddenModule: rootForbiddenModule,
645
624
  rootLayouts: rootLayouts,
646
625
  rootNotFoundModule: rootNotFoundModule,
647
626
  rootUnauthorizedModule: rootUnauthorizedModule,
648
627
  route,
649
628
  renderToReadableStream,
629
+ scriptNonce,
650
630
  statusCode,
651
631
  });
652
632
  }
653
633
 
654
634
  /** Convenience: render a not-found page (404) */
655
- async function renderNotFoundPage(route, isRscRequest, request, matchedParams) {
656
- return renderHTTPAccessFallbackPage(route, 404, isRscRequest, request, { matchedParams });
635
+ async function renderNotFoundPage(route, isRscRequest, request, matchedParams, scriptNonce) {
636
+ return renderHTTPAccessFallbackPage(route, 404, isRscRequest, request, { matchedParams }, scriptNonce);
657
637
  }
658
638
 
659
639
  /**
@@ -663,7 +643,7 @@ async function renderNotFoundPage(route, isRscRequest, request, matchedParams) {
663
643
  * Next.js returns HTTP 200 when error.tsx catches an error (the error is "handled"
664
644
  * by the boundary). This matches that behavior intentionally.
665
645
  */
666
- async function renderErrorBoundaryPage(route, error, isRscRequest, request, matchedParams) {
646
+ async function renderErrorBoundaryPage(route, error, isRscRequest, request, matchedParams, scriptNonce) {
667
647
  return __renderAppPageErrorBoundary({
668
648
  buildFontLinkHeader: __buildAppPageFontLinkHeader,
669
649
  clearRequestContext() {
@@ -686,11 +666,12 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request, matc
686
666
  makeThenableParams,
687
667
  matchedParams: matchedParams ?? route?.params ?? {},
688
668
  requestUrl: request.url,
689
- resolveChildSegments: __resolveChildSegments,
669
+ resolveChildSegments: __resolveAppPageChildSegments,
690
670
  rootLayouts: rootLayouts,
691
671
  route,
692
672
  renderToReadableStream,
693
673
  sanitizeErrorForClient: __sanitizeErrorForClient,
674
+ scriptNonce,
694
675
  });
695
676
  }
696
677
 
@@ -704,6 +685,21 @@ function matchRoute(url) {
704
685
  return _trieMatch(_routeTrie, urlParts);
705
686
  }
706
687
 
688
+ function __createStaticFileSignal(pathname, _mwCtx) {
689
+ const headers = new Headers({
690
+ "x-vinext-static-file": encodeURIComponent(pathname),
691
+ });
692
+ if (_mwCtx.headers) {
693
+ for (const [key, value] of _mwCtx.headers) {
694
+ headers.append(key, value);
695
+ }
696
+ }
697
+ return new Response(null, {
698
+ status: _mwCtx.status ?? 200,
699
+ headers,
700
+ });
701
+ }
702
+
707
703
  // matchPattern is kept for findIntercept (linear scan over small interceptLookup array).
708
704
  function matchPattern(urlParts, patternParts) {
709
705
  const params = Object.create(null);
@@ -852,12 +848,10 @@ async function buildPageElement(route, params, opts, searchParams) {
852
848
  const resolvedMetadata = metadataList.length > 0 ? mergeMetadata(metadataList) : null;
853
849
  const resolvedViewport = mergeViewport(viewportList);
854
850
 
855
- // Build nested layout tree from outermost to innermost.
856
- // Next.js 16 passes params/searchParams as Promises (async pattern)
857
- // but pre-16 code accesses them as plain objects (params.id).
858
- // makeThenableParams() normalises null-prototype + preserves both patterns.
859
- const asyncParams = makeThenableParams(params);
860
- const pageProps = { params: asyncParams };
851
+ // Build the route tree from the leaf page, then delegate the boundary/layout/
852
+ // template/segment wiring to a typed runtime helper so the generated entry
853
+ // stays thin and the wiring logic can be unit tested directly.
854
+ const pageProps = { params: makeThenableParams(params) };
861
855
  if (searchParams) {
862
856
  // Always provide searchParams prop when the URL object is available, even
863
857
  // when the query string is empty -- pages that do "await searchParams" need
@@ -873,192 +867,25 @@ async function buildPageElement(route, params, opts, searchParams) {
873
867
  // dynamic, and this avoids false positives from React internals.
874
868
  if (hasSearchParams) markDynamicUsage();
875
869
  }
876
- let element = createElement(PageComponent, pageProps);
877
-
878
- // Wrap page with empty segment provider so useSelectedLayoutSegments()
879
- // returns [] when called from inside a page component (leaf node).
880
- element = createElement(LayoutSegmentProvider, { segmentMap: { children: [] } }, element);
881
-
882
- // Add metadata + viewport head tags (React 19 hoists title/meta/link to <head>)
883
- // Next.js always injects charset and default viewport even when no metadata/viewport
884
- // is exported. We replicate that by always emitting these essential head elements.
885
- {
886
- const headElements = [];
887
- // Always emit <meta charset="utf-8"> — Next.js includes this on every page
888
- headElements.push(createElement("meta", { charSet: "utf-8" }));
889
- if (resolvedMetadata) headElements.push(createElement(MetadataHead, { metadata: resolvedMetadata }));
890
- headElements.push(createElement(ViewportHead, { viewport: resolvedViewport }));
891
- element = createElement(Fragment, null, ...headElements, element);
892
- }
893
-
894
- // Wrap with loading.tsx Suspense if present
895
- if (route.loading?.default) {
896
- element = createElement(
897
- Suspense,
898
- { fallback: createElement(route.loading.default) },
899
- element,
900
- );
901
- }
902
-
903
- // Wrap with the leaf's error.tsx ErrorBoundary if it's not already covered
904
- // by a per-layout error boundary (i.e., the leaf has error.tsx but no layout).
905
- // Per-layout error boundaries are interleaved with layouts below.
906
- {
907
- const lastLayoutError = route.errors ? route.errors[route.errors.length - 1] : null;
908
- if (route.error?.default && route.error !== lastLayoutError) {
909
- element = createElement(ErrorBoundary, {
910
- fallback: route.error.default,
911
- children: element,
912
- });
913
- }
914
- }
915
-
916
- // Wrap with NotFoundBoundary so client-side notFound() renders not-found.tsx
917
- // instead of crashing the React tree. Must be above ErrorBoundary since
918
- // ErrorBoundary re-throws notFound errors.
919
- // Pre-render the not-found component as a React element since it may be a
920
- // server component (not a client reference) and can't be passed as a function prop.
921
- {
922
- const NotFoundComponent = route.notFound?.default ?? ${rootNotFoundVar ? `${rootNotFoundVar}?.default` : "null"};
923
- if (NotFoundComponent) {
924
- element = createElement(NotFoundBoundary, {
925
- fallback: createElement(NotFoundComponent),
926
- children: element,
927
- });
928
- }
929
- }
930
-
931
- // Wrap with templates (innermost first, then outer)
932
- // Templates are like layouts but re-mount on navigation (client-side concern).
933
- // On the server, they just wrap the content like layouts do.
934
- if (route.templates) {
935
- for (let i = route.templates.length - 1; i >= 0; i--) {
936
- const TemplateComponent = route.templates[i]?.default;
937
- if (TemplateComponent) {
938
- element = createElement(TemplateComponent, { children: element, params });
939
- }
940
- }
941
- }
942
-
943
- // Wrap with layouts (innermost first, then outer).
944
- // At each layout level, first wrap with that level's error boundary (if any)
945
- // so the boundary is inside the layout and catches errors from children.
946
- // This matches Next.js behavior: Layout > ErrorBoundary > children.
947
- // Parallel slots are passed as named props to the innermost layout
948
- // (the layout at the same directory level as the page/slots)
949
- for (let i = route.layouts.length - 1; i >= 0; i--) {
950
- // Wrap with per-layout error boundary before wrapping with layout.
951
- // This places the ErrorBoundary inside the layout, catching errors
952
- // from child segments (matching Next.js per-segment error handling).
953
- if (route.errors && route.errors[i]?.default) {
954
- element = createElement(ErrorBoundary, {
955
- fallback: route.errors[i].default,
956
- children: element,
957
- });
958
- }
959
-
960
- const LayoutComponent = route.layouts[i]?.default;
961
- if (LayoutComponent) {
962
- // Per-layout NotFoundBoundary: wraps this layout's children so that
963
- // notFound() thrown from a child layout is caught here.
964
- // Matches Next.js behavior where each segment has its own boundary.
965
- // The boundary at level N catches errors from Layout[N+1] and below,
966
- // but NOT from Layout[N] itself (which propagates to level N-1).
967
- {
968
- const LayoutNotFound = route.notFounds?.[i]?.default;
969
- if (LayoutNotFound) {
970
- element = createElement(NotFoundBoundary, {
971
- fallback: createElement(LayoutNotFound),
972
- children: element,
973
- });
974
- }
975
- }
976
-
977
- const layoutProps = { children: element, params: makeThenableParams(params) };
978
-
979
- // Add parallel slot elements to the layout that defines them.
980
- // Each slot has a layoutIndex indicating which layout it belongs to.
981
- if (route.slots) {
982
- for (const [slotName, slotMod] of Object.entries(route.slots)) {
983
- // Attach slot to the layout at its layoutIndex, or to the innermost layout if -1
984
- const targetIdx = slotMod.layoutIndex >= 0 ? slotMod.layoutIndex : route.layouts.length - 1;
985
- if (i !== targetIdx) continue;
986
- // Check if this slot has an intercepting route that should activate
987
- let SlotPage = null;
988
- let slotParams = params;
989
-
990
- if (opts && opts.interceptSlot === slotName && opts.interceptPage) {
991
- // Use the intercepting route's page component
992
- SlotPage = opts.interceptPage.default;
993
- slotParams = opts.interceptParams || params;
994
- } else {
995
- SlotPage = slotMod.page?.default || slotMod.default?.default;
996
- }
997
-
998
- if (SlotPage) {
999
- let slotElement = createElement(SlotPage, { params: makeThenableParams(slotParams) });
1000
- // Wrap with slot-specific layout if present.
1001
- // In Next.js, @slot/layout.tsx wraps the slot's page content
1002
- // before it is passed as a prop to the parent layout.
1003
- const SlotLayout = slotMod.layout?.default;
1004
- if (SlotLayout) {
1005
- slotElement = createElement(SlotLayout, {
1006
- children: slotElement,
1007
- params: makeThenableParams(slotParams),
1008
- });
1009
- }
1010
- // Wrap with slot-specific loading if present
1011
- if (slotMod.loading?.default) {
1012
- slotElement = createElement(Suspense,
1013
- { fallback: createElement(slotMod.loading.default) },
1014
- slotElement,
1015
- );
1016
- }
1017
- // Wrap with slot-specific error boundary if present
1018
- if (slotMod.error?.default) {
1019
- slotElement = createElement(ErrorBoundary, {
1020
- fallback: slotMod.error.default,
1021
- children: slotElement,
1022
- });
1023
- }
1024
- layoutProps[slotName] = slotElement;
870
+ return __buildAppPageRouteElement({
871
+ element: createElement(PageComponent, pageProps),
872
+ globalErrorModule: ${globalErrorVar ? globalErrorVar : "null"},
873
+ makeThenableParams,
874
+ matchedParams: params,
875
+ resolvedMetadata,
876
+ resolvedViewport,
877
+ rootNotFoundModule: ${rootNotFoundVar ? rootNotFoundVar : "null"},
878
+ route,
879
+ slotOverrides:
880
+ opts && opts.interceptSlot && opts.interceptPage
881
+ ? {
882
+ [opts.interceptSlot]: {
883
+ pageModule: opts.interceptPage,
884
+ params: opts.interceptParams || params,
885
+ },
1025
886
  }
1026
- }
1027
- }
1028
-
1029
- element = createElement(LayoutComponent, layoutProps);
1030
-
1031
- // Wrap the layout with LayoutSegmentProvider so useSelectedLayoutSegments()
1032
- // called INSIDE this layout gets the correct child segments. We resolve the
1033
- // route tree segments using actual param values and pass them through context.
1034
- // We wrap the layout (not just children) because hooks are called from
1035
- // components rendered inside the layout's own JSX.
1036
- const treePos = route.layoutTreePositions ? route.layoutTreePositions[i] : 0;
1037
- const childSegs = __resolveChildSegments(route.routeSegments || [], treePos, params);
1038
- element = createElement(LayoutSegmentProvider, { segmentMap: { children: childSegs } }, element);
1039
- }
1040
- }
1041
-
1042
- // Wrap with global error boundary if app/global-error.tsx exists.
1043
- // This must be present in both HTML and RSC paths so the component tree
1044
- // structure matches — otherwise React reconciliation on client-side navigation
1045
- // would see a mismatched tree and destroy/recreate the DOM.
1046
- //
1047
- // For RSC requests (client-side nav), this provides error recovery on the client.
1048
- // For HTML requests (initial page load), the ErrorBoundary catches during SSR
1049
- // but produces double <html>/<body> (root layout + global-error). The request
1050
- // handler detects this via the rscOnError flag and re-renders without layouts.
1051
- ${globalErrorVar ? `
1052
- const GlobalErrorComponent = ${globalErrorVar}.default;
1053
- if (GlobalErrorComponent) {
1054
- element = createElement(ErrorBoundary, {
1055
- fallback: GlobalErrorComponent,
1056
- children: element,
1057
- });
1058
- }
1059
- ` : ""}
1060
-
1061
- return element;
887
+ : null,
888
+ });
1062
889
  }
1063
890
 
1064
891
  ${middlewarePath ? generateMiddlewareMatcherCode("modern") : ""}
@@ -1069,6 +896,7 @@ const __i18nConfig = ${JSON.stringify(i18nConfig)};
1069
896
  const __configRedirects = ${JSON.stringify(redirects)};
1070
897
  const __configRewrites = ${JSON.stringify(rewrites)};
1071
898
  const __configHeaders = ${JSON.stringify(headers)};
899
+ const __publicFiles = new Set(${JSON.stringify(publicFiles)});
1072
900
  const __allowedOrigins = ${JSON.stringify(allowedOrigins)};
1073
901
 
1074
902
  ${generateDevOriginCheckCode(config?.allowedDevOrigins)}
@@ -1267,6 +1095,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1267
1095
  let pathname = __normalizePath(decodedUrlPathname);
1268
1096
 
1269
1097
  ${bp ? `
1098
+ if (!hasBasePath(pathname, __basePath) && !pathname.startsWith("/__vinext/")) {
1099
+ return new Response("Not Found", { status: 404 });
1100
+ }
1270
1101
  // Strip basePath prefix
1271
1102
  pathname = stripBasePath(pathname, __basePath);
1272
1103
  ` : ""}
@@ -1510,6 +1341,8 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1510
1341
  }
1511
1342
  ` : ""}
1512
1343
 
1344
+ const _scriptNonce = __getScriptNonceFromHeaderSources(request.headers, _mwCtx.headers);
1345
+
1513
1346
  // Build post-middleware request context for afterFiles/fallback rewrites.
1514
1347
  // These run after middleware in the App Router execution order and should
1515
1348
  // evaluate has/missing conditions against middleware-modified headers.
@@ -1614,6 +1447,18 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1614
1447
  }
1615
1448
  }
1616
1449
 
1450
+ // Serve public/ files as filesystem routes after middleware and before
1451
+ // afterFiles/fallback rewrites, matching Next.js routing semantics.
1452
+ if (
1453
+ (request.method === "GET" || request.method === "HEAD") &&
1454
+ !pathname.endsWith(".rsc") &&
1455
+ __publicFiles.has(cleanPathname)
1456
+ ) {
1457
+ setHeadersContext(null);
1458
+ setNavigationContext(null);
1459
+ return __createStaticFileSignal(cleanPathname, _mwCtx);
1460
+ }
1461
+
1617
1462
  // Set navigation context for Server Components.
1618
1463
  // Note: Headers context is already set by runWithRequestContext in the handler wrapper.
1619
1464
  setNavigationContext({
@@ -1657,6 +1502,12 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1657
1502
  }
1658
1503
  throw sizeErr;
1659
1504
  }
1505
+ const payloadResponse = await validateServerActionPayload(body);
1506
+ if (payloadResponse) {
1507
+ setHeadersContext(null);
1508
+ setNavigationContext(null);
1509
+ return payloadResponse;
1510
+ }
1660
1511
  const temporaryReferences = createTemporaryReferenceSet();
1661
1512
  const args = await decodeReply(body, { temporaryReferences });
1662
1513
  const action = await loadServerAction(actionId);
@@ -1669,7 +1520,10 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1669
1520
  returnValue = { ok: true, data };
1670
1521
  } catch (e) {
1671
1522
  // Detect redirect() / permanentRedirect() called inside the action.
1672
- // These throw errors with digest "NEXT_REDIRECT;replace;url[;status]".
1523
+ // These throw errors with digest "NEXT_REDIRECT;<type>;<url>[;<status>]".
1524
+ // The type field is empty when redirect() was called without an explicit
1525
+ // type argument. In Server Action context, Next.js defaults to "push" so
1526
+ // the Back button works after form submissions.
1673
1527
  // The URL is encodeURIComponent-encoded to prevent semicolons in the URL
1674
1528
  // from corrupting the delimiter-based digest format.
1675
1529
  if (e && typeof e === "object" && "digest" in e) {
@@ -1678,7 +1532,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1678
1532
  const parts = digest.split(";");
1679
1533
  actionRedirect = {
1680
1534
  url: decodeURIComponent(parts[2]),
1681
- type: parts[1] || "replace", // "push" or "replace"
1535
+ type: parts[1] || "push", // Server Action → default "push"
1682
1536
  status: parts[3] ? parseInt(parts[3], 10) : 307,
1683
1537
  };
1684
1538
  returnValue = { ok: true, data: undefined };
@@ -1714,10 +1568,14 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1714
1568
  const redirectHeaders = new Headers({
1715
1569
  "Content-Type": "text/x-component; charset=utf-8",
1716
1570
  "Vary": "RSC, Accept",
1717
- "x-action-redirect": actionRedirect.url,
1718
- "x-action-redirect-type": actionRedirect.type,
1719
- "x-action-redirect-status": String(actionRedirect.status),
1720
1571
  });
1572
+ // Merge middleware headers first so the framework's own redirect control
1573
+ // headers below are always authoritative and cannot be clobbered by
1574
+ // middleware that happens to set x-action-redirect* keys.
1575
+ __mergeMiddlewareResponseHeaders(redirectHeaders, _mwCtx.headers);
1576
+ redirectHeaders.set("x-action-redirect", actionRedirect.url);
1577
+ redirectHeaders.set("x-action-redirect-type", actionRedirect.type);
1578
+ redirectHeaders.set("x-action-redirect-status", String(actionRedirect.status));
1721
1579
  for (const cookie of actionPendingCookies) {
1722
1580
  redirectHeaders.append("Set-Cookie", cookie);
1723
1581
  }
@@ -1737,7 +1595,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1737
1595
  searchParams: url.searchParams,
1738
1596
  params: actionParams,
1739
1597
  });
1740
- element = buildPageElement(actionRoute, actionParams, undefined, url.searchParams);
1598
+ element = await buildPageElement(actionRoute, actionParams, undefined, url.searchParams);
1741
1599
  } else {
1742
1600
  element = createElement("div", null, "Page not found");
1743
1601
  }
@@ -1760,15 +1618,15 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1760
1618
  const actionPendingCookies = getAndClearPendingCookies();
1761
1619
  const actionDraftCookie = getDraftModeCookieHeader();
1762
1620
 
1763
- const actionHeaders = { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" };
1764
- const actionResponse = new Response(rscStream, { headers: actionHeaders });
1621
+ const actionHeaders = new Headers({ "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" });
1622
+ __mergeMiddlewareResponseHeaders(actionHeaders, _mwCtx.headers);
1765
1623
  if (actionPendingCookies.length > 0 || actionDraftCookie) {
1766
1624
  for (const cookie of actionPendingCookies) {
1767
- actionResponse.headers.append("Set-Cookie", cookie);
1625
+ actionHeaders.append("Set-Cookie", cookie);
1768
1626
  }
1769
- if (actionDraftCookie) actionResponse.headers.append("Set-Cookie", actionDraftCookie);
1627
+ if (actionDraftCookie) actionHeaders.append("Set-Cookie", actionDraftCookie);
1770
1628
  }
1771
- return actionResponse;
1629
+ return new Response(rscStream, { status: _mwCtx.status ?? 200, headers: actionHeaders });
1772
1630
  } catch (err) {
1773
1631
  getAndClearPendingCookies(); // Clear pending cookies on error
1774
1632
  console.error("[vinext] Server action error:", err);
@@ -1846,7 +1704,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1846
1704
  }
1847
1705
  ` : ""}
1848
1706
  // Render custom not-found page if available, otherwise plain 404
1849
- const notFoundResponse = await renderNotFoundPage(null, isRscRequest, request);
1707
+ const notFoundResponse = await renderNotFoundPage(null, isRscRequest, request, undefined, _scriptNonce);
1850
1708
  if (notFoundResponse) return notFoundResponse;
1851
1709
  setHeadersContext(null);
1852
1710
  setNavigationContext(null);
@@ -2059,6 +1917,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2059
1917
  if (
2060
1918
  process.env.NODE_ENV === "production" &&
2061
1919
  !isForceDynamic &&
1920
+ (isRscRequest || !_scriptNonce) &&
2062
1921
  revalidateSeconds !== null && revalidateSeconds > 0 && revalidateSeconds !== Infinity
2063
1922
  ) {
2064
1923
  const __cachedPageResponse = await __readAppPageCacheResponse({
@@ -2148,7 +2007,31 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2148
2007
  },
2149
2008
  isRscRequest,
2150
2009
  matchSourceRouteParams(pattern) {
2151
- return matchRoute(pattern)?.params ?? {};
2010
+ // Extract actual URL param values by prefix-matching the request pathname
2011
+ // against the source route's pattern. This handles all interception conventions:
2012
+ // (.) same-level, (..) one-level-up, and (...) root — the source pattern's
2013
+ // dynamic segments that align with the URL get their real values extracted.
2014
+ // We must NOT use matchRoute(pattern) here: the trie would match the literal
2015
+ // ":param" strings as dynamic segment values, returning e.g. {id: ":id"}.
2016
+ const patternParts = pattern.split("/").filter(Boolean);
2017
+ const urlParts = cleanPathname.split("/").filter(Boolean);
2018
+ const params = Object.create(null);
2019
+ for (let i = 0; i < patternParts.length; i++) {
2020
+ const pp = patternParts[i];
2021
+ if (pp.endsWith("+") || pp.endsWith("*")) {
2022
+ // urlParts.slice(i) safely returns [] when i >= urlParts.length,
2023
+ // which is the correct value for optional catch-all with zero segments.
2024
+ params[pp.slice(1, -1)] = urlParts.slice(i);
2025
+ break;
2026
+ }
2027
+ if (i >= urlParts.length) break;
2028
+ if (pp.startsWith(":")) {
2029
+ params[pp.slice(1)] = urlParts[i];
2030
+ } else if (pp !== urlParts[i]) {
2031
+ break;
2032
+ }
2033
+ }
2034
+ return params;
2152
2035
  },
2153
2036
  renderInterceptResponse(sourceRoute, interceptElement) {
2154
2037
  const interceptOnError = createRscOnErrorHandler(
@@ -2163,8 +2046,11 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2163
2046
  // by the client, and async server components that run during consumption need the
2164
2047
  // context to still be live. The AsyncLocalStorage scope from runWithRequestContext
2165
2048
  // handles cleanup naturally when all async continuations complete.
2049
+ const interceptHeaders = new Headers({ "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" });
2050
+ __mergeMiddlewareResponseHeaders(interceptHeaders, _mwCtx.headers);
2166
2051
  return new Response(interceptStream, {
2167
- headers: { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" },
2052
+ status: _mwCtx.status ?? 200,
2053
+ headers: interceptHeaders,
2168
2054
  });
2169
2055
  },
2170
2056
  searchParams: url.searchParams,
@@ -2187,7 +2073,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2187
2073
  return buildPageElement(route, params, interceptOpts, url.searchParams);
2188
2074
  },
2189
2075
  renderErrorBoundaryPage(buildErr) {
2190
- return renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params);
2076
+ return renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params, _scriptNonce);
2191
2077
  },
2192
2078
  renderSpecialError(__buildSpecialError) {
2193
2079
  return __buildAppPageSpecialErrorResponse({
@@ -2196,9 +2082,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2196
2082
  setNavigationContext(null);
2197
2083
  },
2198
2084
  renderFallbackPage(statusCode) {
2199
- return renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, {
2200
- matchedParams: params,
2201
- });
2085
+ return renderHTTPAccessFallbackPage(
2086
+ route,
2087
+ statusCode,
2088
+ isRscRequest,
2089
+ request,
2090
+ {
2091
+ matchedParams: params,
2092
+ },
2093
+ _scriptNonce,
2094
+ );
2202
2095
  },
2203
2096
  requestUrl: request.url,
2204
2097
  specialError: __buildSpecialError,
@@ -2215,6 +2108,19 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2215
2108
  // rscCssTransform — no manual loadCss() call needed.
2216
2109
  const _hasLoadingBoundary = !!(route.loading && route.loading.default);
2217
2110
  const _asyncLayoutParams = makeThenableParams(params);
2111
+ // Convert URLSearchParams to a plain object then wrap in makeThenableParams()
2112
+ // so probePage() passes the same shape that buildPageElement() gives to the
2113
+ // real render. Without this, pages that destructure await-ed searchParams
2114
+ // throw TypeError during probe.
2115
+ const _probeSearchObj = {};
2116
+ url.searchParams.forEach(function(v, k) {
2117
+ if (k in _probeSearchObj) {
2118
+ _probeSearchObj[k] = Array.isArray(_probeSearchObj[k]) ? _probeSearchObj[k].concat(v) : [_probeSearchObj[k], v];
2119
+ } else {
2120
+ _probeSearchObj[k] = v;
2121
+ }
2122
+ });
2123
+ const _asyncSearchParams = makeThenableParams(_probeSearchObj);
2218
2124
  return __renderAppPageLifecycle({
2219
2125
  cleanPathname,
2220
2126
  clearRequestContext() {
@@ -2260,11 +2166,11 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2260
2166
  return LayoutComp({ params: _asyncLayoutParams, children: null });
2261
2167
  },
2262
2168
  probePage() {
2263
- return PageComponent({ params });
2169
+ return PageComponent({ params: _asyncLayoutParams, searchParams: _asyncSearchParams });
2264
2170
  },
2265
2171
  revalidateSeconds,
2266
2172
  renderErrorBoundaryResponse(renderErr) {
2267
- return renderErrorBoundaryPage(route, renderErr, isRscRequest, request, params);
2173
+ return renderErrorBoundaryPage(route, renderErr, isRscRequest, request, params, _scriptNonce);
2268
2174
  },
2269
2175
  async renderLayoutSpecialError(__layoutSpecialError, li) {
2270
2176
  return __buildAppPageSpecialErrorResponse({
@@ -2287,11 +2193,18 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2287
2193
  }
2288
2194
  if (!parentNotFound) parentNotFound = ${rootNotFoundVar ? `${rootNotFoundVar}?.default` : "null"};
2289
2195
  const parentLayouts = route.layouts.slice(0, li);
2290
- return renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, {
2291
- boundaryComponent: parentNotFound,
2292
- layouts: parentLayouts,
2293
- matchedParams: params,
2294
- });
2196
+ return renderHTTPAccessFallbackPage(
2197
+ route,
2198
+ statusCode,
2199
+ isRscRequest,
2200
+ request,
2201
+ {
2202
+ boundaryComponent: parentNotFound,
2203
+ layouts: parentLayouts,
2204
+ matchedParams: params,
2205
+ },
2206
+ _scriptNonce,
2207
+ );
2295
2208
  },
2296
2209
  requestUrl: request.url,
2297
2210
  specialError: __layoutSpecialError,
@@ -2304,9 +2217,16 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2304
2217
  setNavigationContext(null);
2305
2218
  },
2306
2219
  renderFallbackPage(statusCode) {
2307
- return renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, request, {
2308
- matchedParams: params,
2309
- });
2220
+ return renderHTTPAccessFallbackPage(
2221
+ route,
2222
+ statusCode,
2223
+ isRscRequest,
2224
+ request,
2225
+ {
2226
+ matchedParams: params,
2227
+ },
2228
+ _scriptNonce,
2229
+ );
2310
2230
  },
2311
2231
  requestUrl: request.url,
2312
2232
  specialError,
@@ -2321,6 +2241,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2321
2241
  // each have their own ALS store and are unaffected.
2322
2242
  return _suppressHookWarningAls.run(true, probe);
2323
2243
  },
2244
+ scriptNonce: _scriptNonce,
2324
2245
  waitUntil(__cachePromise) {
2325
2246
  _getRequestExecutionContext()?.waitUntil(__cachePromise);
2326
2247
  },