@@ -60,6 +60,7 @@ const appRouteHandlerCachePath = resolveEntryPath(
6060const implicitTagsPath = resolveEntryPath ( "../server/implicit-tags.js" , import . meta. url ) ;
6161const appPageCachePath = resolveEntryPath ( "../server/app-page-cache.js" , import . meta. url ) ;
6262const appPageExecutionPath = resolveEntryPath ( "../server/app-page-execution.js" , import . meta. url ) ;
63+ const appPageBoundaryPath = resolveEntryPath ( "../server/app-page-boundary.js" , import . meta. url ) ;
6364const appPageBoundaryRenderPath = resolveEntryPath (
6465 "../server/app-page-boundary-render.js" ,
6566 import . meta. url ,
@@ -69,6 +70,8 @@ const appPageRouteWiringPath = resolveEntryPath(
6970 "../server/app-page-route-wiring.js" ,
7071 import . meta. url ,
7172) ;
73+ const appPageHeadPath = resolveEntryPath ( "../server/app-page-head.js" , import . meta. url ) ;
74+ const appPageParamsPath = resolveEntryPath ( "../server/app-page-params.js" , import . meta. url ) ;
7275const appPageRenderPath = resolveEntryPath ( "../server/app-page-render.js" , import . meta. url ) ;
7376const appPageResponsePath = resolveEntryPath ( "../server/app-page-response.js" , import . meta. url ) ;
7477const cspPath = resolveEntryPath ( "../server/csp.js" , import . meta. url ) ;
@@ -166,16 +169,23 @@ export function generateRscEntry(
166169 for ( const tmpl of route . templates ) getImportVar ( tmpl ) ;
167170 if ( route . loadingPath ) getImportVar ( route . loadingPath ) ;
168171 if ( route . errorPath ) getImportVar ( route . errorPath ) ;
169- if ( route . layoutErrorPaths )
172+ if ( route . layoutErrorPaths ) {
170173 for ( const ep of route . layoutErrorPaths ) {
171174 if ( ep ) getImportVar ( ep ) ;
172175 }
176+ }
173177 if ( route . notFoundPath ) getImportVar ( route . notFoundPath ) ;
174178 for ( const nfp of route . notFoundPaths || [ ] ) {
175179 if ( nfp ) getImportVar ( nfp ) ;
176180 }
177181 if ( route . forbiddenPath ) getImportVar ( route . forbiddenPath ) ;
182+ for ( const fp of route . forbiddenPaths || [ ] ) {
183+ if ( fp ) getImportVar ( fp ) ;
184+ }
178185 if ( route . unauthorizedPath ) getImportVar ( route . unauthorizedPath ) ;
186+ for ( const up of route . unauthorizedPaths || [ ] ) {
187+ if ( up ) getImportVar ( up ) ;
188+ }
179189 // Register parallel slot modules
180190 for ( const slot of route . parallelSlots ) {
181191 if ( slot . pagePath ) getImportVar ( slot . pagePath ) ;
@@ -198,6 +208,12 @@ export function generateRscEntry(
198208 const layoutVars = route . layouts . map ( ( l ) => getImportVar ( l ) ) ;
199209 const templateVars = route . templates . map ( ( t ) => getImportVar ( t ) ) ;
200210 const notFoundVars = ( route . notFoundPaths || [ ] ) . map ( ( nf ) => ( nf ? getImportVar ( nf ) : "null" ) ) ;
211+ const forbiddenVars = ( route . forbiddenPaths || [ ] ) . map ( ( fp ) =>
212+ fp ? getImportVar ( fp ) : "null" ,
213+ ) ;
214+ const unauthorizedVars = ( route . unauthorizedPaths || [ ] ) . map ( ( up ) =>
215+ up ? getImportVar ( up ) : "null" ,
216+ ) ;
201217 const slotEntries = route . parallelSlots . map ( ( slot ) => {
202218 const interceptEntries = slot . interceptingRoutes . map (
203219 ( ir ) => ` {
@@ -249,7 +265,9 @@ ${slotEntries.join(",\n")}
249265 notFound: ${ route . notFoundPath ? getImportVar ( route . notFoundPath ) : "null" } ,
250266 notFounds: [${ notFoundVars . join ( ", " ) } ],
251267 forbidden: ${ route . forbiddenPath ? getImportVar ( route . forbiddenPath ) : "null" } ,
268+ forbiddens: [${ forbiddenVars . join ( ", " ) } ],
252269 unauthorized: ${ route . unauthorizedPath ? getImportVar ( route . unauthorizedPath ) : "null" } ,
270+ unauthorizeds: [${ unauthorizedVars . join ( ", " ) } ],
253271 }` ;
254272 } ) ;
255273
@@ -374,7 +392,6 @@ import { createElement } from "react";
374392import { setNavigationContext as _setNavigationContextOrig, getNavigationContext as _getNavigationContext } from "next/navigation";
375393import { setHeadersContext, headersContextFromRequest, getDraftModeCookieHeader, getAndClearPendingCookies, consumeDynamicUsage, markDynamicUsage, applyMiddlewareRequestHeaders, getHeadersContext, setHeadersAccessPhase } from "next/headers";
376394import { NextRequest, NextFetchEvent } from "next/server";
377- import { mergeMetadata, resolveModuleMetadata, mergeViewport, resolveModuleViewport } from "vinext/metadata";
378395${ middlewarePath ? `import * as middlewareModule from ${ JSON . stringify ( middlewarePath . replace ( / \\ / g, "/" ) ) } ;` : "" }
379396${ instrumentationPath ? `import * as _instrumentation from ${ JSON . stringify ( instrumentationPath . replace ( / \\ / g, "/" ) ) } ;` : "" }
380397${ effectiveMetaRoutes . length > 0 ? `import { sitemapToXml, robotsToText, manifestToJson } from ${ JSON . stringify ( metadataRoutesPath ) } ;` : "" }
@@ -406,6 +423,9 @@ import {
406423 resolveAppPageSpecialError as __resolveAppPageSpecialError,
407424 teeAppPageRscStreamForCapture as __teeAppPageRscStreamForCapture,
408425} from ${ JSON . stringify ( appPageExecutionPath ) } ;
426+ import {
427+ resolveAppPageParentHttpAccessBoundaryModule as __resolveAppPageParentHttpAccessBoundaryModule,
428+ } from ${ JSON . stringify ( appPageBoundaryPath ) } ;
409429import {
410430 renderAppPageErrorBoundary as __renderAppPageErrorBoundary,
411431 renderAppPageHttpAccessFallback as __renderAppPageHttpAccessFallback,
@@ -419,6 +439,13 @@ import {
419439 createAppPageTreePath as __createAppPageTreePath,
420440 resolveAppPageChildSegments as __resolveAppPageChildSegments,
421441} from ${ JSON . stringify ( appPageRouteWiringPath ) } ;
442+ import {
443+ resolveAppPageSegmentParams as __resolveAppPageSegmentParams,
444+ } from ${ JSON . stringify ( appPageParamsPath ) } ;
445+ import {
446+ collectAppPageSearchParams as __collectAppPageSearchParams,
447+ resolveAppPageHead as __resolveAppPageHead,
448+ } from ${ JSON . stringify ( appPageHeadPath ) } ;
422449import {
423450 renderAppPageLifecycle as __renderAppPageLifecycle,
424451} from ${ JSON . stringify ( appPageRenderPath ) } ;
@@ -1020,83 +1047,18 @@ async function buildPageElements(route, params, routePath, pageRequest) {
10201047 };
10211048 }
10221049
1023- // Resolve metadata and viewport from layouts and page.
1024- //
1025- // generateMetadata() accepts a "parent" (Promise of ResolvedMetadata) as its
1026- // second argument (Next.js 13+). The parent resolves to the accumulated
1027- // merged metadata of all ancestor segments, enabling patterns like:
1028- //
1029- // const previousImages = (await parent).openGraph?.images ?? []
1030- // return { openGraph: { images: ['/new-image.jpg', ...previousImages] } }
1031- //
1032- // Next.js uses an eager-execution-with-serial-resolution approach:
1033- // all generateMetadata() calls are kicked off concurrently, but each
1034- // segment's "parent" promise resolves only after the preceding segment's
1035- // metadata is resolved and merged. This preserves concurrency for I/O-bound
1036- // work while guaranteeing that parent data is available when needed.
1037- //
1038- // We build a chain: layoutParentPromises[0] = Promise.resolve({}) (no parent
1039- // for root layout), layoutParentPromises[i+1] resolves to merge(layouts[0..i]),
1040- // and pageParentPromise resolves to merge(all layouts).
1041- //
1042- // IMPORTANT: Layout metadata errors are swallowed (.catch(() => null)) because
1043- // a layout's generateMetadata() failing should not crash the page.
1044- // Page metadata errors are NOT swallowed — if the page's generateMetadata()
1045- // throws, the error propagates out of buildPageElement() so the caller can
1046- // route it to the nearest error.tsx boundary (or global-error.tsx).
1047- const layoutMods = route.layouts.filter(Boolean);
1048-
1049- // Convert URLSearchParams → plain object for page generateMetadata() and
1050- // pageProps.searchParams. Built before the layout loop so the page metadata
1051- // call (below) and pageProps can reference the same object.
1052- // NOTE: Layouts do NOT receive searchParams in generateMetadata() — only
1053- // pages do. This matches Next.js behavior (resolve-metadata.ts:777).
1054- const spObj = Object.create(null);
1055- let hasSearchParams = false;
1056- if (searchParams && searchParams.forEach) {
1057- searchParams.forEach(function(v, k) {
1058- hasSearchParams = true;
1059- if (k in spObj) {
1060- spObj[k] = Array.isArray(spObj[k]) ? spObj[k].concat(v) : [spObj[k], v];
1061- } else {
1062- spObj[k] = v;
1063- }
1064- });
1065- }
1066-
1067- // Build the parent promise chain and kick off metadata resolution in one pass.
1068- // Each layout module is called exactly once. layoutMetaPromises[i] is the
1069- // promise for layout[i]'s own metadata result.
1070- //
1071- // All calls are kicked off immediately (concurrent I/O), but each layout's
1072- // "parent" promise only resolves after the preceding layout's metadata is done.
1073- const layoutMetaPromises = [];
1074- let accumulatedMetaPromise = Promise.resolve({});
1075- for (let i = 0; i < layoutMods.length; i++) {
1076- const parentForThisLayout = accumulatedMetaPromise;
1077- // Kick off this layout's metadata resolution now (concurrent with others).
1078- const metaPromise = resolveModuleMetadata(layoutMods[i], params, undefined, parentForThisLayout)
1079- .catch((err) => { console.error("[vinext] Layout generateMetadata() failed:", err); return null; });
1080- layoutMetaPromises.push(metaPromise);
1081- // Advance accumulator: resolves to merged(layouts[0..i]) once layout[i] is done.
1082- accumulatedMetaPromise = metaPromise.then(async (result) =>
1083- result ? mergeMetadata([await parentForThisLayout, result]) : await parentForThisLayout
1084- );
1085- }
1086- // Page's parent is the fully-accumulated layout metadata.
1087- const pageParentPromise = accumulatedMetaPromise;
1088-
1089- const [layoutMetaResults, layoutVpResults, pageMeta, pageVp] = await Promise.all([
1090- Promise.all(layoutMetaPromises),
1091- Promise.all(layoutMods.map((mod) => resolveModuleViewport(mod, params).catch((err) => { console.error("[vinext] Layout generateViewport() failed:", err); return null; }))),
1092- route.page ? resolveModuleMetadata(route.page, params, spObj, pageParentPromise) : Promise.resolve(null),
1093- route.page ? resolveModuleViewport(route.page, params) : Promise.resolve(null),
1094- ]);
1095-
1096- const metadataList = [...layoutMetaResults.filter(Boolean), ...(pageMeta ? [pageMeta] : [])];
1097- const viewportList = [...layoutVpResults.filter(Boolean), ...(pageVp ? [pageVp] : [])];
1098- const resolvedMetadata = metadataList.length > 0 ? mergeMetadata(metadataList) : null;
1099- const resolvedViewport = mergeViewport(viewportList);
1050+ const __headResult = await __resolveAppPageHead({
1051+ layoutModules: route.layouts,
1052+ layoutTreePositions: route.layoutTreePositions,
1053+ pageModule: route.page,
1054+ params,
1055+ routeSegments: route.routeSegments,
1056+ searchParams,
1057+ });
1058+ const spObj = __headResult.searchParamsObject;
1059+ const hasSearchParams = __headResult.hasSearchParams;
1060+ const resolvedMetadata = __headResult.metadata;
1061+ const resolvedViewport = __headResult.viewport;
11001062
11011063 // Build the route tree from the leaf page, then delegate the boundary/layout/
11021064 // template/segment wiring to a typed runtime helper so the generated entry
@@ -2534,7 +2496,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
25342496 // Note: CSS is automatically injected by @vitejs/plugin-rsc's
25352497 // rscCssTransform — no manual loadCss() call needed.
25362498 const _hasLoadingBoundary = !!(route.loading && route.loading.default);
2537- const _asyncLayoutParams = makeThenableParams(params);
2499+ const _asyncRouteParams = makeThenableParams(params);
25382500 return __renderAppPageLifecycle({
25392501 cleanPathname,
25402502 clearRequestContext() {
@@ -2576,22 +2538,21 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
25762538 probeLayoutAt(li) {
25772539 const LayoutComp = route.layouts[li]?.default;
25782540 if (!LayoutComp) return null;
2579- return LayoutComp({ params: _asyncLayoutParams, children: null });
2541+ return LayoutComp({
2542+ params: makeThenableParams(__resolveAppPageSegmentParams(
2543+ route.routeSegments,
2544+ route.layoutTreePositions?.[li] ?? 0,
2545+ params,
2546+ )),
2547+ children: null,
2548+ });
25802549 },
25812550 probePage() {
25822551 if (!PageComponent) return null;
2583- const _probeSearchObj = {};
2584- url.searchParams.forEach(function(v, k) {
2585- if (k in _probeSearchObj) {
2586- _probeSearchObj[k] = Array.isArray(_probeSearchObj[k])
2587- ? _probeSearchObj[k].concat(v)
2588- : [_probeSearchObj[k], v];
2589- } else {
2590- _probeSearchObj[k] = v;
2591- }
2592- });
2593- const _asyncSearchParams = makeThenableParams(_probeSearchObj);
2594- return PageComponent({ params: _asyncLayoutParams, searchParams: _asyncSearchParams });
2552+ const _asyncSearchParams = makeThenableParams(
2553+ __collectAppPageSearchParams(url.searchParams).searchParamsObject,
2554+ );
2555+ return PageComponent({ params: _asyncRouteParams, searchParams: _asyncSearchParams });
25952556 },
25962557 classification: {
25972558 getLayoutId(index) {
@@ -2625,27 +2586,24 @@ async function _handleRequest(request, __reqCtx, _mwCtx) {
26252586 },
26262587 middlewareContext: _mwCtx,
26272588 renderFallbackPage(statusCode) {
2628- // Find the not-found component from the parent level (the boundary that
2629- // would catch this in Next.js). Walk up from the throwing layout to find
2630- // the nearest not-found at a parent layout's directory.
2631- let parentNotFound = null;
2632- if (route.notFounds) {
2633- for (let pi = li - 1; pi >= 0; pi--) {
2634- if (route.notFounds[pi]?.default) {
2635- parentNotFound = route.notFounds[pi].default;
2636- break;
2637- }
2638- }
2639- }
2640- if (!parentNotFound) parentNotFound = ${ rootNotFoundVar ? `${ rootNotFoundVar } ?.default` : "null" } ;
2589+ const parentBoundary = __resolveAppPageParentHttpAccessBoundaryModule({
2590+ layoutIndex: li,
2591+ rootForbiddenModule: ${ rootForbiddenVar ?? "null" } ,
2592+ rootNotFoundModule: ${ rootNotFoundVar ?? "null" } ,
2593+ rootUnauthorizedModule: ${ rootUnauthorizedVar ?? "null" } ,
2594+ routeForbiddenModules: route.forbiddens,
2595+ routeNotFoundModules: route.notFounds,
2596+ routeUnauthorizedModules: route.unauthorizeds,
2597+ statusCode,
2598+ })?.default ?? null;
26412599 const parentLayouts = route.layouts.slice(0, li);
26422600 return renderHTTPAccessFallbackPage(
26432601 route,
26442602 statusCode,
26452603 isRscRequest,
26462604 request,
26472605 {
2648- boundaryComponent: parentNotFound ,
2606+ boundaryComponent: parentBoundary ,
26492607 layouts: parentLayouts,
26502608 matchedParams: params,
26512609 },
0 commit comments