vinext 0.0.39 → 0.0.40

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 (50) hide show
  1. package/dist/build/standalone.js +7 -0
  2. package/dist/build/standalone.js.map +1 -1
  3. package/dist/entries/app-rsc-entry.d.ts +2 -1
  4. package/dist/entries/app-rsc-entry.js +131 -245
  5. package/dist/entries/app-rsc-entry.js.map +1 -1
  6. package/dist/index.d.ts +32 -1
  7. package/dist/index.js +80 -6
  8. package/dist/index.js.map +1 -1
  9. package/dist/plugins/server-externals-manifest.d.ts +11 -1
  10. package/dist/plugins/server-externals-manifest.js +10 -3
  11. package/dist/plugins/server-externals-manifest.js.map +1 -1
  12. package/dist/routing/app-router.d.ts +10 -2
  13. package/dist/routing/app-router.js +37 -22
  14. package/dist/routing/app-router.js.map +1 -1
  15. package/dist/server/app-page-response.d.ts +12 -1
  16. package/dist/server/app-page-response.js +26 -7
  17. package/dist/server/app-page-response.js.map +1 -1
  18. package/dist/server/app-page-route-wiring.d.ts +79 -0
  19. package/dist/server/app-page-route-wiring.js +165 -0
  20. package/dist/server/app-page-route-wiring.js.map +1 -0
  21. package/dist/server/app-page-stream.js +3 -0
  22. package/dist/server/app-page-stream.js.map +1 -1
  23. package/dist/server/app-route-handler-response.js +4 -1
  24. package/dist/server/app-route-handler-response.js.map +1 -1
  25. package/dist/server/app-router-entry.d.ts +6 -1
  26. package/dist/server/app-router-entry.js +9 -2
  27. package/dist/server/app-router-entry.js.map +1 -1
  28. package/dist/server/prod-server.d.ts +1 -1
  29. package/dist/server/prod-server.js +37 -11
  30. package/dist/server/prod-server.js.map +1 -1
  31. package/dist/server/worker-utils.d.ts +4 -1
  32. package/dist/server/worker-utils.js +31 -1
  33. package/dist/server/worker-utils.js.map +1 -1
  34. package/dist/shims/error-boundary.d.ts +13 -4
  35. package/dist/shims/error-boundary.js +23 -3
  36. package/dist/shims/error-boundary.js.map +1 -1
  37. package/dist/shims/head.js.map +1 -1
  38. package/dist/shims/navigation.d.ts +16 -1
  39. package/dist/shims/navigation.js +18 -3
  40. package/dist/shims/navigation.js.map +1 -1
  41. package/dist/shims/router.js +127 -38
  42. package/dist/shims/router.js.map +1 -1
  43. package/dist/shims/script.js.map +1 -1
  44. package/dist/shims/server.d.ts +17 -4
  45. package/dist/shims/server.js +91 -73
  46. package/dist/shims/server.js.map +1 -1
  47. package/dist/shims/slot.d.ts +28 -0
  48. package/dist/shims/slot.js +49 -0
  49. package/dist/shims/slot.js.map +1 -0
  50. package/package.json +1 -2
@@ -24,7 +24,9 @@ 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);
28
30
  const appPageRequestPath = resolveEntryPath("../server/app-page-request.js", import.meta.url);
29
31
  const appRouteHandlerResponsePath = resolveEntryPath("../server/app-route-handler-response.js", import.meta.url);
30
32
  const routeTriePath = resolveEntryPath("../routing/route-trie.js", import.meta.url);
@@ -50,6 +52,7 @@ function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, global
50
52
  const bodySizeLimit = config?.bodySizeLimit ?? 1 * 1024 * 1024;
51
53
  const i18nConfig = config?.i18n ?? null;
52
54
  const hasPagesDir = config?.hasPagesDir ?? false;
55
+ const publicFiles = config?.publicFiles ?? [];
53
56
  const imports = [];
54
57
  const importMap = /* @__PURE__ */ new Map();
55
58
  let importIdx = 0;
@@ -65,7 +68,7 @@ function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, global
65
68
  if (route.pagePath) getImportVar(route.pagePath);
66
69
  if (route.routePath) getImportVar(route.routePath);
67
70
  for (const layout of route.layouts) getImportVar(layout);
68
- for (const tmpl of route.templates) getImportVar(tmpl);
71
+ for (const tmpl of route.templates) if (tmpl) getImportVar(tmpl);
69
72
  if (route.loadingPath) getImportVar(route.loadingPath);
70
73
  if (route.errorPath) getImportVar(route.errorPath);
71
74
  if (route.layoutErrorPaths) {
@@ -86,7 +89,7 @@ function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, global
86
89
  }
87
90
  const routeEntries = routes.map((route) => {
88
91
  const layoutVars = route.layouts.map((l) => getImportVar(l));
89
- const templateVars = route.templates.map((t) => getImportVar(t));
92
+ const templateVars = route.templates.map((t) => t ? getImportVar(t) : "null");
90
93
  const notFoundVars = (route.notFoundPaths || []).map((nf) => nf ? getImportVar(nf) : "null");
91
94
  const slotEntries = route.parallelSlots.map((slot) => {
92
95
  const interceptEntries = slot.interceptingRoutes.map((ir) => ` {
@@ -102,6 +105,7 @@ function generateRscEntry(appDir, routes, middlewarePath, metadataRoutes, global
102
105
  loading: ${slot.loadingPath ? getImportVar(slot.loadingPath) : "null"},
103
106
  error: ${slot.errorPath ? getImportVar(slot.errorPath) : "null"},
104
107
  layoutIndex: ${slot.layoutIndex},
108
+ routeSegments: ${JSON.stringify(slot.routeSegments)},
105
109
  intercepts: [
106
110
  ${interceptEntries.join(",\n")}
107
111
  ],
@@ -204,13 +208,11 @@ function renderToReadableStream(model, options) {
204
208
  }
205
209
  }));
206
210
  }
207
- import { createElement, Suspense, Fragment } from "react";
211
+ import { createElement } from "react";
208
212
  import { setNavigationContext as _setNavigationContextOrig, getNavigationContext as _getNavigationContext } from "next/navigation";
209
213
  import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, applyMiddlewareRequestHeaders, getHeadersContext, setHeadersAccessPhase } from "next/headers";
210
214
  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";
215
+ import { mergeMetadata, resolveModuleMetadata, mergeViewport, resolveModuleViewport } from "vinext/metadata";
214
216
  ${middlewarePath ? `import * as middlewareModule from ${JSON.stringify(middlewarePath.replace(/\\/g, "/"))};` : ""}
215
217
  ${instrumentationPath ? `import * as _instrumentation from ${JSON.stringify(instrumentationPath.replace(/\\/g, "/"))};` : ""}
216
218
  ${effectiveMetaRoutes.length > 0 ? `import { sitemapToXml, robotsToText, manifestToJson } from ${JSON.stringify(metadataRoutesPath)};` : ""}
@@ -242,9 +244,16 @@ import {
242
244
  renderAppPageErrorBoundary as __renderAppPageErrorBoundary,
243
245
  renderAppPageHttpAccessFallback as __renderAppPageHttpAccessFallback,
244
246
  } from ${JSON.stringify(appPageBoundaryRenderPath)};
247
+ import {
248
+ buildAppPageRouteElement as __buildAppPageRouteElement,
249
+ resolveAppPageChildSegments as __resolveAppPageChildSegments,
250
+ } from ${JSON.stringify(appPageRouteWiringPath)};
245
251
  import {
246
252
  renderAppPageLifecycle as __renderAppPageLifecycle,
247
253
  } from ${JSON.stringify(appPageRenderPath)};
254
+ import {
255
+ mergeMiddlewareResponseHeaders as __mergeMiddlewareResponseHeaders,
256
+ } from ${JSON.stringify(appPageResponsePath)};
248
257
  import {
249
258
  buildAppPageElement as __buildAppPageElement,
250
259
  resolveAppPageIntercept as __resolveAppPageIntercept,
@@ -409,38 +418,6 @@ function makeThenableParams(obj) {
409
418
  return Object.assign(Promise.resolve(plain), plain);
410
419
  }
411
420
 
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
421
  // djb2 hash — matches Next.js's stringHash for digest generation.
445
422
  // Produces a stable numeric string from error message + stack.
446
423
  function __errorDigest(str) {
@@ -640,7 +617,7 @@ async function renderHTTPAccessFallbackPage(route, statusCode, isRscRequest, req
640
617
  makeThenableParams,
641
618
  matchedParams: opts?.matchedParams ?? route?.params ?? {},
642
619
  requestUrl: request.url,
643
- resolveChildSegments: __resolveChildSegments,
620
+ resolveChildSegments: __resolveAppPageChildSegments,
644
621
  rootForbiddenModule: rootForbiddenModule,
645
622
  rootLayouts: rootLayouts,
646
623
  rootNotFoundModule: rootNotFoundModule,
@@ -686,7 +663,7 @@ async function renderErrorBoundaryPage(route, error, isRscRequest, request, matc
686
663
  makeThenableParams,
687
664
  matchedParams: matchedParams ?? route?.params ?? {},
688
665
  requestUrl: request.url,
689
- resolveChildSegments: __resolveChildSegments,
666
+ resolveChildSegments: __resolveAppPageChildSegments,
690
667
  rootLayouts: rootLayouts,
691
668
  route,
692
669
  renderToReadableStream,
@@ -704,6 +681,21 @@ function matchRoute(url) {
704
681
  return _trieMatch(_routeTrie, urlParts);
705
682
  }
706
683
 
684
+ function __createStaticFileSignal(pathname, _mwCtx) {
685
+ const headers = new Headers({
686
+ "x-vinext-static-file": encodeURIComponent(pathname),
687
+ });
688
+ if (_mwCtx.headers) {
689
+ for (const [key, value] of _mwCtx.headers) {
690
+ headers.append(key, value);
691
+ }
692
+ }
693
+ return new Response(null, {
694
+ status: _mwCtx.status ?? 200,
695
+ headers,
696
+ });
697
+ }
698
+
707
699
  // matchPattern is kept for findIntercept (linear scan over small interceptLookup array).
708
700
  function matchPattern(urlParts, patternParts) {
709
701
  const params = Object.create(null);
@@ -852,12 +844,10 @@ async function buildPageElement(route, params, opts, searchParams) {
852
844
  const resolvedMetadata = metadataList.length > 0 ? mergeMetadata(metadataList) : null;
853
845
  const resolvedViewport = mergeViewport(viewportList);
854
846
 
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 };
847
+ // Build the route tree from the leaf page, then delegate the boundary/layout/
848
+ // template/segment wiring to a typed runtime helper so the generated entry
849
+ // stays thin and the wiring logic can be unit tested directly.
850
+ const pageProps = { params: makeThenableParams(params) };
861
851
  if (searchParams) {
862
852
  // Always provide searchParams prop when the URL object is available, even
863
853
  // when the query string is empty -- pages that do "await searchParams" need
@@ -873,192 +863,25 @@ async function buildPageElement(route, params, opts, searchParams) {
873
863
  // dynamic, and this avoids false positives from React internals.
874
864
  if (hasSearchParams) markDynamicUsage();
875
865
  }
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;
866
+ return __buildAppPageRouteElement({
867
+ element: createElement(PageComponent, pageProps),
868
+ globalErrorModule: ${globalErrorVar ? globalErrorVar : "null"},
869
+ makeThenableParams,
870
+ matchedParams: params,
871
+ resolvedMetadata,
872
+ resolvedViewport,
873
+ rootNotFoundModule: ${rootNotFoundVar ? rootNotFoundVar : "null"},
874
+ route,
875
+ slotOverrides:
876
+ opts && opts.interceptSlot && opts.interceptPage
877
+ ? {
878
+ [opts.interceptSlot]: {
879
+ pageModule: opts.interceptPage,
880
+ params: opts.interceptParams || params,
881
+ },
1025
882
  }
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;
883
+ : null,
884
+ });
1062
885
  }
1063
886
 
1064
887
  ${middlewarePath ? generateMiddlewareMatcherCode("modern") : ""}
@@ -1069,6 +892,7 @@ const __i18nConfig = ${JSON.stringify(i18nConfig)};
1069
892
  const __configRedirects = ${JSON.stringify(redirects)};
1070
893
  const __configRewrites = ${JSON.stringify(rewrites)};
1071
894
  const __configHeaders = ${JSON.stringify(headers)};
895
+ const __publicFiles = new Set(${JSON.stringify(publicFiles)});
1072
896
  const __allowedOrigins = ${JSON.stringify(allowedOrigins)};
1073
897
 
1074
898
  ${generateDevOriginCheckCode(config?.allowedDevOrigins)}
@@ -1267,6 +1091,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1267
1091
  let pathname = __normalizePath(decodedUrlPathname);
1268
1092
 
1269
1093
  ${bp ? `
1094
+ if (!hasBasePath(pathname, __basePath) && !pathname.startsWith("/__vinext/")) {
1095
+ return new Response("Not Found", { status: 404 });
1096
+ }
1270
1097
  // Strip basePath prefix
1271
1098
  pathname = stripBasePath(pathname, __basePath);
1272
1099
  ` : ""}
@@ -1614,6 +1441,18 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1614
1441
  }
1615
1442
  }
1616
1443
 
1444
+ // Serve public/ files as filesystem routes after middleware and before
1445
+ // afterFiles/fallback rewrites, matching Next.js routing semantics.
1446
+ if (
1447
+ (request.method === "GET" || request.method === "HEAD") &&
1448
+ !pathname.endsWith(".rsc") &&
1449
+ __publicFiles.has(cleanPathname)
1450
+ ) {
1451
+ setHeadersContext(null);
1452
+ setNavigationContext(null);
1453
+ return __createStaticFileSignal(cleanPathname, _mwCtx);
1454
+ }
1455
+
1617
1456
  // Set navigation context for Server Components.
1618
1457
  // Note: Headers context is already set by runWithRequestContext in the handler wrapper.
1619
1458
  setNavigationContext({
@@ -1669,7 +1508,10 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1669
1508
  returnValue = { ok: true, data };
1670
1509
  } catch (e) {
1671
1510
  // Detect redirect() / permanentRedirect() called inside the action.
1672
- // These throw errors with digest "NEXT_REDIRECT;replace;url[;status]".
1511
+ // These throw errors with digest "NEXT_REDIRECT;<type>;<url>[;<status>]".
1512
+ // The type field is empty when redirect() was called without an explicit
1513
+ // type argument. In Server Action context, Next.js defaults to "push" so
1514
+ // the Back button works after form submissions.
1673
1515
  // The URL is encodeURIComponent-encoded to prevent semicolons in the URL
1674
1516
  // from corrupting the delimiter-based digest format.
1675
1517
  if (e && typeof e === "object" && "digest" in e) {
@@ -1678,7 +1520,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1678
1520
  const parts = digest.split(";");
1679
1521
  actionRedirect = {
1680
1522
  url: decodeURIComponent(parts[2]),
1681
- type: parts[1] || "replace", // "push" or "replace"
1523
+ type: parts[1] || "push", // Server Action → default "push"
1682
1524
  status: parts[3] ? parseInt(parts[3], 10) : 307,
1683
1525
  };
1684
1526
  returnValue = { ok: true, data: undefined };
@@ -1714,10 +1556,14 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1714
1556
  const redirectHeaders = new Headers({
1715
1557
  "Content-Type": "text/x-component; charset=utf-8",
1716
1558
  "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
1559
  });
1560
+ // Merge middleware headers first so the framework's own redirect control
1561
+ // headers below are always authoritative and cannot be clobbered by
1562
+ // middleware that happens to set x-action-redirect* keys.
1563
+ __mergeMiddlewareResponseHeaders(redirectHeaders, _mwCtx.headers);
1564
+ redirectHeaders.set("x-action-redirect", actionRedirect.url);
1565
+ redirectHeaders.set("x-action-redirect-type", actionRedirect.type);
1566
+ redirectHeaders.set("x-action-redirect-status", String(actionRedirect.status));
1721
1567
  for (const cookie of actionPendingCookies) {
1722
1568
  redirectHeaders.append("Set-Cookie", cookie);
1723
1569
  }
@@ -1737,7 +1583,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1737
1583
  searchParams: url.searchParams,
1738
1584
  params: actionParams,
1739
1585
  });
1740
- element = buildPageElement(actionRoute, actionParams, undefined, url.searchParams);
1586
+ element = await buildPageElement(actionRoute, actionParams, undefined, url.searchParams);
1741
1587
  } else {
1742
1588
  element = createElement("div", null, "Page not found");
1743
1589
  }
@@ -1760,15 +1606,15 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
1760
1606
  const actionPendingCookies = getAndClearPendingCookies();
1761
1607
  const actionDraftCookie = getDraftModeCookieHeader();
1762
1608
 
1763
- const actionHeaders = { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" };
1764
- const actionResponse = new Response(rscStream, { headers: actionHeaders });
1609
+ const actionHeaders = new Headers({ "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" });
1610
+ __mergeMiddlewareResponseHeaders(actionHeaders, _mwCtx.headers);
1765
1611
  if (actionPendingCookies.length > 0 || actionDraftCookie) {
1766
1612
  for (const cookie of actionPendingCookies) {
1767
- actionResponse.headers.append("Set-Cookie", cookie);
1613
+ actionHeaders.append("Set-Cookie", cookie);
1768
1614
  }
1769
- if (actionDraftCookie) actionResponse.headers.append("Set-Cookie", actionDraftCookie);
1615
+ if (actionDraftCookie) actionHeaders.append("Set-Cookie", actionDraftCookie);
1770
1616
  }
1771
- return actionResponse;
1617
+ return new Response(rscStream, { status: _mwCtx.status ?? 200, headers: actionHeaders });
1772
1618
  } catch (err) {
1773
1619
  getAndClearPendingCookies(); // Clear pending cookies on error
1774
1620
  console.error("[vinext] Server action error:", err);
@@ -2148,7 +1994,31 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2148
1994
  },
2149
1995
  isRscRequest,
2150
1996
  matchSourceRouteParams(pattern) {
2151
- return matchRoute(pattern)?.params ?? {};
1997
+ // Extract actual URL param values by prefix-matching the request pathname
1998
+ // against the source route's pattern. This handles all interception conventions:
1999
+ // (.) same-level, (..) one-level-up, and (...) root — the source pattern's
2000
+ // dynamic segments that align with the URL get their real values extracted.
2001
+ // We must NOT use matchRoute(pattern) here: the trie would match the literal
2002
+ // ":param" strings as dynamic segment values, returning e.g. {id: ":id"}.
2003
+ const patternParts = pattern.split("/").filter(Boolean);
2004
+ const urlParts = cleanPathname.split("/").filter(Boolean);
2005
+ const params = Object.create(null);
2006
+ for (let i = 0; i < patternParts.length; i++) {
2007
+ const pp = patternParts[i];
2008
+ if (pp.endsWith("+") || pp.endsWith("*")) {
2009
+ // urlParts.slice(i) safely returns [] when i >= urlParts.length,
2010
+ // which is the correct value for optional catch-all with zero segments.
2011
+ params[pp.slice(1, -1)] = urlParts.slice(i);
2012
+ break;
2013
+ }
2014
+ if (i >= urlParts.length) break;
2015
+ if (pp.startsWith(":")) {
2016
+ params[pp.slice(1)] = urlParts[i];
2017
+ } else if (pp !== urlParts[i]) {
2018
+ break;
2019
+ }
2020
+ }
2021
+ return params;
2152
2022
  },
2153
2023
  renderInterceptResponse(sourceRoute, interceptElement) {
2154
2024
  const interceptOnError = createRscOnErrorHandler(
@@ -2163,8 +2033,11 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2163
2033
  // by the client, and async server components that run during consumption need the
2164
2034
  // context to still be live. The AsyncLocalStorage scope from runWithRequestContext
2165
2035
  // handles cleanup naturally when all async continuations complete.
2036
+ const interceptHeaders = new Headers({ "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" });
2037
+ __mergeMiddlewareResponseHeaders(interceptHeaders, _mwCtx.headers);
2166
2038
  return new Response(interceptStream, {
2167
- headers: { "Content-Type": "text/x-component; charset=utf-8", "Vary": "RSC, Accept" },
2039
+ status: _mwCtx.status ?? 200,
2040
+ headers: interceptHeaders,
2168
2041
  });
2169
2042
  },
2170
2043
  searchParams: url.searchParams,
@@ -2215,6 +2088,19 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2215
2088
  // rscCssTransform — no manual loadCss() call needed.
2216
2089
  const _hasLoadingBoundary = !!(route.loading && route.loading.default);
2217
2090
  const _asyncLayoutParams = makeThenableParams(params);
2091
+ // Convert URLSearchParams to a plain object then wrap in makeThenableParams()
2092
+ // so probePage() passes the same shape that buildPageElement() gives to the
2093
+ // real render. Without this, pages that destructure await-ed searchParams
2094
+ // throw TypeError during probe.
2095
+ const _probeSearchObj = {};
2096
+ url.searchParams.forEach(function(v, k) {
2097
+ if (k in _probeSearchObj) {
2098
+ _probeSearchObj[k] = Array.isArray(_probeSearchObj[k]) ? _probeSearchObj[k].concat(v) : [_probeSearchObj[k], v];
2099
+ } else {
2100
+ _probeSearchObj[k] = v;
2101
+ }
2102
+ });
2103
+ const _asyncSearchParams = makeThenableParams(_probeSearchObj);
2218
2104
  return __renderAppPageLifecycle({
2219
2105
  cleanPathname,
2220
2106
  clearRequestContext() {
@@ -2260,7 +2146,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
2260
2146
  return LayoutComp({ params: _asyncLayoutParams, children: null });
2261
2147
  },
2262
2148
  probePage() {
2263
- return PageComponent({ params });
2149
+ return PageComponent({ params: _asyncLayoutParams, searchParams: _asyncSearchParams });
2264
2150
  },
2265
2151
  revalidateSeconds,
2266
2152
  renderErrorBoundaryResponse(renderErr) {