From 431df188271795f7801293488ddd860d289c4512 Mon Sep 17 00:00:00 2001 From: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:03:27 +1000 Subject: [PATCH] refactor(app-rsc-entry): centralize request-derived page inputs Reshape buildPageElements to accept a single pageRequest object bundling opts, searchParams, isRscRequest, request, and mountedSlotsHeader. Lift the mounted-slots header read to the handler scope so every call site shares one source of truth and a future refactor cannot silently drift one read path out of the other. --- packages/vinext/src/entries/app-rsc-entry.ts | 71 ++- .../entry-templates.test.ts.snap | 426 ++++++++++++------ 2 files changed, 343 insertions(+), 154 deletions(-) diff --git a/packages/vinext/src/entries/app-rsc-entry.ts b/packages/vinext/src/entries/app-rsc-entry.ts index 29be20f8c..e40deeaec 100644 --- a/packages/vinext/src/entries/app-rsc-entry.ts +++ b/packages/vinext/src/entries/app-rsc-entry.ts @@ -935,7 +935,14 @@ function findIntercept(pathname) { return null; } -async function buildPageElements(route, params, routePath, opts, searchParams, isRscRequest, request) { +async function buildPageElements(route, params, routePath, pageRequest) { + const { + opts, + searchParams, + isRscRequest, + request, + mountedSlotsHeader, + } = pageRequest; const PageComponent = route.page?.default; if (!PageComponent) { const _interceptionContext = opts?.interceptionContext ?? null; @@ -1052,11 +1059,12 @@ async function buildPageElements(route, params, routePath, opts, searchParams, i // dynamic, and this avoids false positives from React internals. if (hasSearchParams) markDynamicUsage(); } - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request?.headers?.get("x-vinext-mounted-slots"), - ); - const mountedSlotIds = __mountedSlotsHeader - ? new Set(__mountedSlotsHeader.split(" ")) + // mountedSlotsHeader is threaded through from the handler scope so every + // call site shares one source of truth for request-derived values. Reading + // the same header in two places invites silent drift when a future refactor + // changes only one of them. + const mountedSlotIds = mountedSlotsHeader + ? new Set(mountedSlotsHeader.split(" ")) : null; return __buildAppPageElements({ @@ -1412,6 +1420,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { } const isRscRequest = pathname.endsWith(".rsc") || request.headers.get("accept")?.includes("text/x-component"); + // Read mounted-slots header once at the handler scope and thread it through + // every buildPageElements call site. Previously both the handler and + // buildPageElements read and normalized it independently, which invited + // silent drift if a future refactor changed only one path. + const __mountedSlotsHeader = __normalizeMountedSlotsHeader( + request.headers.get("x-vinext-mounted-slots"), + ); const interceptionContextHeader = request.headers.get("X-Vinext-Interception-Context")?.replaceAll("\0", "") || null; let cleanPathname = pathname.replace(/\\.rsc$/, ""); @@ -1815,10 +1830,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { actionRoute, actionParams, cleanPathname, - undefined, - url.searchParams, - isRscRequest, - request, + { + opts: undefined, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); } else { const _actionRouteId = __createAppPayloadRouteId(cleanPathname, null); @@ -2153,9 +2171,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { // force-dynamic: set no-store Cache-Control const isForceDynamic = dynamicConfig === "force-dynamic"; - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request.headers.get("x-vinext-mounted-slots"), - ); // ── ISR cache read (production only) ───────────────────────────────────── // Read from cache BEFORE generateStaticParams and all rendering work. @@ -2211,10 +2226,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { route, params, cleanPathname, - undefined, - new URLSearchParams(), - isRscRequest, - request, + { + opts: undefined, + searchParams: new URLSearchParams(), + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); const __revalOnError = createRscOnErrorHandler(request, cleanPathname, route.pattern); const __revalRscStream = renderToReadableStream(__revalElement, { onError: __revalOnError }); @@ -2269,10 +2287,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { interceptRoute, interceptParams, cleanPathname, - interceptOpts, - interceptSearchParams, - isRscRequest, - request, + { + opts: interceptOpts, + searchParams: interceptSearchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); }, cleanPathname, @@ -2326,7 +2347,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { const __pageBuildResult = await __buildAppPageElement({ buildPageElement() { - return buildPageElements(route, params, cleanPathname, interceptOpts, url.searchParams, isRscRequest, request); + return buildPageElements(route, params, cleanPathname, { + opts: interceptOpts, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }); }, renderErrorBoundaryPage(buildErr) { return renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params, _scriptNonce); diff --git a/tests/__snapshots__/entry-templates.test.ts.snap b/tests/__snapshots__/entry-templates.test.ts.snap index b5fc7a232..924bfd492 100644 --- a/tests/__snapshots__/entry-templates.test.ts.snap +++ b/tests/__snapshots__/entry-templates.test.ts.snap @@ -689,7 +689,14 @@ function findIntercept(pathname) { return null; } -async function buildPageElements(route, params, routePath, opts, searchParams, isRscRequest, request) { +async function buildPageElements(route, params, routePath, pageRequest) { + const { + opts, + searchParams, + isRscRequest, + request, + mountedSlotsHeader, + } = pageRequest; const PageComponent = route.page?.default; if (!PageComponent) { const _interceptionContext = opts?.interceptionContext ?? null; @@ -806,11 +813,12 @@ async function buildPageElements(route, params, routePath, opts, searchParams, i // dynamic, and this avoids false positives from React internals. if (hasSearchParams) markDynamicUsage(); } - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request?.headers?.get("x-vinext-mounted-slots"), - ); - const mountedSlotIds = __mountedSlotsHeader - ? new Set(__mountedSlotsHeader.split(" ")) + // mountedSlotsHeader is threaded through from the handler scope so every + // call site shares one source of truth for request-derived values. Reading + // the same header in two places invites silent drift when a future refactor + // changes only one of them. + const mountedSlotIds = mountedSlotsHeader + ? new Set(mountedSlotsHeader.split(" ")) : null; return __buildAppPageElements({ @@ -1273,6 +1281,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { } const isRscRequest = pathname.endsWith(".rsc") || request.headers.get("accept")?.includes("text/x-component"); + // Read mounted-slots header once at the handler scope and thread it through + // every buildPageElements call site. Previously both the handler and + // buildPageElements read and normalized it independently, which invited + // silent drift if a future refactor changed only one path. + const __mountedSlotsHeader = __normalizeMountedSlotsHeader( + request.headers.get("x-vinext-mounted-slots"), + ); const interceptionContextHeader = request.headers.get("X-Vinext-Interception-Context")?.replaceAll("", "") || null; let cleanPathname = pathname.replace(/\\.rsc$/, ""); @@ -1534,10 +1549,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { actionRoute, actionParams, cleanPathname, - undefined, - url.searchParams, - isRscRequest, - request, + { + opts: undefined, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); } else { const _actionRouteId = __createAppPayloadRouteId(cleanPathname, null); @@ -1830,9 +1848,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { // force-dynamic: set no-store Cache-Control const isForceDynamic = dynamicConfig === "force-dynamic"; - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request.headers.get("x-vinext-mounted-slots"), - ); // ── ISR cache read (production only) ───────────────────────────────────── // Read from cache BEFORE generateStaticParams and all rendering work. @@ -1888,10 +1903,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { route, params, cleanPathname, - undefined, - new URLSearchParams(), - isRscRequest, - request, + { + opts: undefined, + searchParams: new URLSearchParams(), + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); const __revalOnError = createRscOnErrorHandler(request, cleanPathname, route.pattern); const __revalRscStream = renderToReadableStream(__revalElement, { onError: __revalOnError }); @@ -1946,10 +1964,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { interceptRoute, interceptParams, cleanPathname, - interceptOpts, - interceptSearchParams, - isRscRequest, - request, + { + opts: interceptOpts, + searchParams: interceptSearchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); }, cleanPathname, @@ -2003,7 +2024,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { const __pageBuildResult = await __buildAppPageElement({ buildPageElement() { - return buildPageElements(route, params, cleanPathname, interceptOpts, url.searchParams, isRscRequest, request); + return buildPageElements(route, params, cleanPathname, { + opts: interceptOpts, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }); }, renderErrorBoundaryPage(buildErr) { return renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params, _scriptNonce); @@ -2873,7 +2900,14 @@ function findIntercept(pathname) { return null; } -async function buildPageElements(route, params, routePath, opts, searchParams, isRscRequest, request) { +async function buildPageElements(route, params, routePath, pageRequest) { + const { + opts, + searchParams, + isRscRequest, + request, + mountedSlotsHeader, + } = pageRequest; const PageComponent = route.page?.default; if (!PageComponent) { const _interceptionContext = opts?.interceptionContext ?? null; @@ -2990,11 +3024,12 @@ async function buildPageElements(route, params, routePath, opts, searchParams, i // dynamic, and this avoids false positives from React internals. if (hasSearchParams) markDynamicUsage(); } - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request?.headers?.get("x-vinext-mounted-slots"), - ); - const mountedSlotIds = __mountedSlotsHeader - ? new Set(__mountedSlotsHeader.split(" ")) + // mountedSlotsHeader is threaded through from the handler scope so every + // call site shares one source of truth for request-derived values. Reading + // the same header in two places invites silent drift when a future refactor + // changes only one of them. + const mountedSlotIds = mountedSlotsHeader + ? new Set(mountedSlotsHeader.split(" ")) : null; return __buildAppPageElements({ @@ -3463,6 +3498,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { } const isRscRequest = pathname.endsWith(".rsc") || request.headers.get("accept")?.includes("text/x-component"); + // Read mounted-slots header once at the handler scope and thread it through + // every buildPageElements call site. Previously both the handler and + // buildPageElements read and normalized it independently, which invited + // silent drift if a future refactor changed only one path. + const __mountedSlotsHeader = __normalizeMountedSlotsHeader( + request.headers.get("x-vinext-mounted-slots"), + ); const interceptionContextHeader = request.headers.get("X-Vinext-Interception-Context")?.replaceAll("", "") || null; let cleanPathname = pathname.replace(/\\.rsc$/, ""); @@ -3724,10 +3766,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { actionRoute, actionParams, cleanPathname, - undefined, - url.searchParams, - isRscRequest, - request, + { + opts: undefined, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); } else { const _actionRouteId = __createAppPayloadRouteId(cleanPathname, null); @@ -4020,9 +4065,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { // force-dynamic: set no-store Cache-Control const isForceDynamic = dynamicConfig === "force-dynamic"; - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request.headers.get("x-vinext-mounted-slots"), - ); // ── ISR cache read (production only) ───────────────────────────────────── // Read from cache BEFORE generateStaticParams and all rendering work. @@ -4078,10 +4120,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { route, params, cleanPathname, - undefined, - new URLSearchParams(), - isRscRequest, - request, + { + opts: undefined, + searchParams: new URLSearchParams(), + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); const __revalOnError = createRscOnErrorHandler(request, cleanPathname, route.pattern); const __revalRscStream = renderToReadableStream(__revalElement, { onError: __revalOnError }); @@ -4136,10 +4181,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { interceptRoute, interceptParams, cleanPathname, - interceptOpts, - interceptSearchParams, - isRscRequest, - request, + { + opts: interceptOpts, + searchParams: interceptSearchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); }, cleanPathname, @@ -4193,7 +4241,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { const __pageBuildResult = await __buildAppPageElement({ buildPageElement() { - return buildPageElements(route, params, cleanPathname, interceptOpts, url.searchParams, isRscRequest, request); + return buildPageElements(route, params, cleanPathname, { + opts: interceptOpts, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }); }, renderErrorBoundaryPage(buildErr) { return renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params, _scriptNonce); @@ -5064,7 +5118,14 @@ function findIntercept(pathname) { return null; } -async function buildPageElements(route, params, routePath, opts, searchParams, isRscRequest, request) { +async function buildPageElements(route, params, routePath, pageRequest) { + const { + opts, + searchParams, + isRscRequest, + request, + mountedSlotsHeader, + } = pageRequest; const PageComponent = route.page?.default; if (!PageComponent) { const _interceptionContext = opts?.interceptionContext ?? null; @@ -5181,11 +5242,12 @@ async function buildPageElements(route, params, routePath, opts, searchParams, i // dynamic, and this avoids false positives from React internals. if (hasSearchParams) markDynamicUsage(); } - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request?.headers?.get("x-vinext-mounted-slots"), - ); - const mountedSlotIds = __mountedSlotsHeader - ? new Set(__mountedSlotsHeader.split(" ")) + // mountedSlotsHeader is threaded through from the handler scope so every + // call site shares one source of truth for request-derived values. Reading + // the same header in two places invites silent drift when a future refactor + // changes only one of them. + const mountedSlotIds = mountedSlotsHeader + ? new Set(mountedSlotsHeader.split(" ")) : null; return __buildAppPageElements({ @@ -5648,6 +5710,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { } const isRscRequest = pathname.endsWith(".rsc") || request.headers.get("accept")?.includes("text/x-component"); + // Read mounted-slots header once at the handler scope and thread it through + // every buildPageElements call site. Previously both the handler and + // buildPageElements read and normalized it independently, which invited + // silent drift if a future refactor changed only one path. + const __mountedSlotsHeader = __normalizeMountedSlotsHeader( + request.headers.get("x-vinext-mounted-slots"), + ); const interceptionContextHeader = request.headers.get("X-Vinext-Interception-Context")?.replaceAll("", "") || null; let cleanPathname = pathname.replace(/\\.rsc$/, ""); @@ -5909,10 +5978,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { actionRoute, actionParams, cleanPathname, - undefined, - url.searchParams, - isRscRequest, - request, + { + opts: undefined, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); } else { const _actionRouteId = __createAppPayloadRouteId(cleanPathname, null); @@ -6205,9 +6277,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { // force-dynamic: set no-store Cache-Control const isForceDynamic = dynamicConfig === "force-dynamic"; - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request.headers.get("x-vinext-mounted-slots"), - ); // ── ISR cache read (production only) ───────────────────────────────────── // Read from cache BEFORE generateStaticParams and all rendering work. @@ -6263,10 +6332,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { route, params, cleanPathname, - undefined, - new URLSearchParams(), - isRscRequest, - request, + { + opts: undefined, + searchParams: new URLSearchParams(), + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); const __revalOnError = createRscOnErrorHandler(request, cleanPathname, route.pattern); const __revalRscStream = renderToReadableStream(__revalElement, { onError: __revalOnError }); @@ -6321,10 +6393,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { interceptRoute, interceptParams, cleanPathname, - interceptOpts, - interceptSearchParams, - isRscRequest, - request, + { + opts: interceptOpts, + searchParams: interceptSearchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); }, cleanPathname, @@ -6378,7 +6453,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { const __pageBuildResult = await __buildAppPageElement({ buildPageElement() { - return buildPageElements(route, params, cleanPathname, interceptOpts, url.searchParams, isRscRequest, request); + return buildPageElements(route, params, cleanPathname, { + opts: interceptOpts, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }); }, renderErrorBoundaryPage(buildErr) { return renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params, _scriptNonce); @@ -7278,7 +7359,14 @@ function findIntercept(pathname) { return null; } -async function buildPageElements(route, params, routePath, opts, searchParams, isRscRequest, request) { +async function buildPageElements(route, params, routePath, pageRequest) { + const { + opts, + searchParams, + isRscRequest, + request, + mountedSlotsHeader, + } = pageRequest; const PageComponent = route.page?.default; if (!PageComponent) { const _interceptionContext = opts?.interceptionContext ?? null; @@ -7395,11 +7483,12 @@ async function buildPageElements(route, params, routePath, opts, searchParams, i // dynamic, and this avoids false positives from React internals. if (hasSearchParams) markDynamicUsage(); } - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request?.headers?.get("x-vinext-mounted-slots"), - ); - const mountedSlotIds = __mountedSlotsHeader - ? new Set(__mountedSlotsHeader.split(" ")) + // mountedSlotsHeader is threaded through from the handler scope so every + // call site shares one source of truth for request-derived values. Reading + // the same header in two places invites silent drift when a future refactor + // changes only one of them. + const mountedSlotIds = mountedSlotsHeader + ? new Set(mountedSlotsHeader.split(" ")) : null; return __buildAppPageElements({ @@ -7865,6 +7954,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { } const isRscRequest = pathname.endsWith(".rsc") || request.headers.get("accept")?.includes("text/x-component"); + // Read mounted-slots header once at the handler scope and thread it through + // every buildPageElements call site. Previously both the handler and + // buildPageElements read and normalized it independently, which invited + // silent drift if a future refactor changed only one path. + const __mountedSlotsHeader = __normalizeMountedSlotsHeader( + request.headers.get("x-vinext-mounted-slots"), + ); const interceptionContextHeader = request.headers.get("X-Vinext-Interception-Context")?.replaceAll("", "") || null; let cleanPathname = pathname.replace(/\\.rsc$/, ""); @@ -8126,10 +8222,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { actionRoute, actionParams, cleanPathname, - undefined, - url.searchParams, - isRscRequest, - request, + { + opts: undefined, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); } else { const _actionRouteId = __createAppPayloadRouteId(cleanPathname, null); @@ -8422,9 +8521,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { // force-dynamic: set no-store Cache-Control const isForceDynamic = dynamicConfig === "force-dynamic"; - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request.headers.get("x-vinext-mounted-slots"), - ); // ── ISR cache read (production only) ───────────────────────────────────── // Read from cache BEFORE generateStaticParams and all rendering work. @@ -8480,10 +8576,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { route, params, cleanPathname, - undefined, - new URLSearchParams(), - isRscRequest, - request, + { + opts: undefined, + searchParams: new URLSearchParams(), + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); const __revalOnError = createRscOnErrorHandler(request, cleanPathname, route.pattern); const __revalRscStream = renderToReadableStream(__revalElement, { onError: __revalOnError }); @@ -8538,10 +8637,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { interceptRoute, interceptParams, cleanPathname, - interceptOpts, - interceptSearchParams, - isRscRequest, - request, + { + opts: interceptOpts, + searchParams: interceptSearchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); }, cleanPathname, @@ -8595,7 +8697,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { const __pageBuildResult = await __buildAppPageElement({ buildPageElement() { - return buildPageElements(route, params, cleanPathname, interceptOpts, url.searchParams, isRscRequest, request); + return buildPageElements(route, params, cleanPathname, { + opts: interceptOpts, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }); }, renderErrorBoundaryPage(buildErr) { return renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params, _scriptNonce); @@ -9472,7 +9580,14 @@ function findIntercept(pathname) { return null; } -async function buildPageElements(route, params, routePath, opts, searchParams, isRscRequest, request) { +async function buildPageElements(route, params, routePath, pageRequest) { + const { + opts, + searchParams, + isRscRequest, + request, + mountedSlotsHeader, + } = pageRequest; const PageComponent = route.page?.default; if (!PageComponent) { const _interceptionContext = opts?.interceptionContext ?? null; @@ -9589,11 +9704,12 @@ async function buildPageElements(route, params, routePath, opts, searchParams, i // dynamic, and this avoids false positives from React internals. if (hasSearchParams) markDynamicUsage(); } - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request?.headers?.get("x-vinext-mounted-slots"), - ); - const mountedSlotIds = __mountedSlotsHeader - ? new Set(__mountedSlotsHeader.split(" ")) + // mountedSlotsHeader is threaded through from the handler scope so every + // call site shares one source of truth for request-derived values. Reading + // the same header in two places invites silent drift when a future refactor + // changes only one of them. + const mountedSlotIds = mountedSlotsHeader + ? new Set(mountedSlotsHeader.split(" ")) : null; return __buildAppPageElements({ @@ -10056,6 +10172,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { } const isRscRequest = pathname.endsWith(".rsc") || request.headers.get("accept")?.includes("text/x-component"); + // Read mounted-slots header once at the handler scope and thread it through + // every buildPageElements call site. Previously both the handler and + // buildPageElements read and normalized it independently, which invited + // silent drift if a future refactor changed only one path. + const __mountedSlotsHeader = __normalizeMountedSlotsHeader( + request.headers.get("x-vinext-mounted-slots"), + ); const interceptionContextHeader = request.headers.get("X-Vinext-Interception-Context")?.replaceAll("", "") || null; let cleanPathname = pathname.replace(/\\.rsc$/, ""); @@ -10317,10 +10440,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { actionRoute, actionParams, cleanPathname, - undefined, - url.searchParams, - isRscRequest, - request, + { + opts: undefined, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); } else { const _actionRouteId = __createAppPayloadRouteId(cleanPathname, null); @@ -10613,9 +10739,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { // force-dynamic: set no-store Cache-Control const isForceDynamic = dynamicConfig === "force-dynamic"; - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request.headers.get("x-vinext-mounted-slots"), - ); // ── ISR cache read (production only) ───────────────────────────────────── // Read from cache BEFORE generateStaticParams and all rendering work. @@ -10671,10 +10794,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { route, params, cleanPathname, - undefined, - new URLSearchParams(), - isRscRequest, - request, + { + opts: undefined, + searchParams: new URLSearchParams(), + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); const __revalOnError = createRscOnErrorHandler(request, cleanPathname, route.pattern); const __revalRscStream = renderToReadableStream(__revalElement, { onError: __revalOnError }); @@ -10729,10 +10855,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { interceptRoute, interceptParams, cleanPathname, - interceptOpts, - interceptSearchParams, - isRscRequest, - request, + { + opts: interceptOpts, + searchParams: interceptSearchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); }, cleanPathname, @@ -10786,7 +10915,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { const __pageBuildResult = await __buildAppPageElement({ buildPageElement() { - return buildPageElements(route, params, cleanPathname, interceptOpts, url.searchParams, isRscRequest, request); + return buildPageElements(route, params, cleanPathname, { + opts: interceptOpts, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }); }, renderErrorBoundaryPage(buildErr) { return renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params, _scriptNonce); @@ -11656,7 +11791,14 @@ function findIntercept(pathname) { return null; } -async function buildPageElements(route, params, routePath, opts, searchParams, isRscRequest, request) { +async function buildPageElements(route, params, routePath, pageRequest) { + const { + opts, + searchParams, + isRscRequest, + request, + mountedSlotsHeader, + } = pageRequest; const PageComponent = route.page?.default; if (!PageComponent) { const _interceptionContext = opts?.interceptionContext ?? null; @@ -11773,11 +11915,12 @@ async function buildPageElements(route, params, routePath, opts, searchParams, i // dynamic, and this avoids false positives from React internals. if (hasSearchParams) markDynamicUsage(); } - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request?.headers?.get("x-vinext-mounted-slots"), - ); - const mountedSlotIds = __mountedSlotsHeader - ? new Set(__mountedSlotsHeader.split(" ")) + // mountedSlotsHeader is threaded through from the handler scope so every + // call site shares one source of truth for request-derived values. Reading + // the same header in two places invites silent drift when a future refactor + // changes only one of them. + const mountedSlotIds = mountedSlotsHeader + ? new Set(mountedSlotsHeader.split(" ")) : null; return __buildAppPageElements({ @@ -12469,6 +12612,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { } const isRscRequest = pathname.endsWith(".rsc") || request.headers.get("accept")?.includes("text/x-component"); + // Read mounted-slots header once at the handler scope and thread it through + // every buildPageElements call site. Previously both the handler and + // buildPageElements read and normalized it independently, which invited + // silent drift if a future refactor changed only one path. + const __mountedSlotsHeader = __normalizeMountedSlotsHeader( + request.headers.get("x-vinext-mounted-slots"), + ); const interceptionContextHeader = request.headers.get("X-Vinext-Interception-Context")?.replaceAll("", "") || null; let cleanPathname = pathname.replace(/\\.rsc$/, ""); @@ -12868,10 +13018,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { actionRoute, actionParams, cleanPathname, - undefined, - url.searchParams, - isRscRequest, - request, + { + opts: undefined, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); } else { const _actionRouteId = __createAppPayloadRouteId(cleanPathname, null); @@ -13164,9 +13317,6 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { // force-dynamic: set no-store Cache-Control const isForceDynamic = dynamicConfig === "force-dynamic"; - const __mountedSlotsHeader = __normalizeMountedSlotsHeader( - request.headers.get("x-vinext-mounted-slots"), - ); // ── ISR cache read (production only) ───────────────────────────────────── // Read from cache BEFORE generateStaticParams and all rendering work. @@ -13222,10 +13372,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { route, params, cleanPathname, - undefined, - new URLSearchParams(), - isRscRequest, - request, + { + opts: undefined, + searchParams: new URLSearchParams(), + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); const __revalOnError = createRscOnErrorHandler(request, cleanPathname, route.pattern); const __revalRscStream = renderToReadableStream(__revalElement, { onError: __revalOnError }); @@ -13280,10 +13433,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { interceptRoute, interceptParams, cleanPathname, - interceptOpts, - interceptSearchParams, - isRscRequest, - request, + { + opts: interceptOpts, + searchParams: interceptSearchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }, ); }, cleanPathname, @@ -13337,7 +13493,13 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { const __pageBuildResult = await __buildAppPageElement({ buildPageElement() { - return buildPageElements(route, params, cleanPathname, interceptOpts, url.searchParams, isRscRequest, request); + return buildPageElements(route, params, cleanPathname, { + opts: interceptOpts, + searchParams: url.searchParams, + isRscRequest, + request, + mountedSlotsHeader: __mountedSlotsHeader, + }); }, renderErrorBoundaryPage(buildErr) { return renderErrorBoundaryPage(route, buildErr, isRscRequest, request, params, _scriptNonce);