Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/vinext/src/entries/app-rsc-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@ export default createAppRscHandler({
renderMode,
observeMetadataSearchParamsAccess: buildOptions?.observeMetadataSearchParamsAccess === true,
observePageSearchParamsAccess: buildOptions?.observePageSearchParamsAccess === true,
serveStreamingMetadata: buildOptions?.serveStreamingMetadata,
}, layoutParamAccess, displayPathname);
},
clientReuseManifest,
Expand Down
35 changes: 23 additions & 12 deletions packages/vinext/src/server/app-page-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ export type DispatchAppPageOptions<TRoute extends AppPageDispatchRoute> = {
options?: {
observeMetadataSearchParamsAccess?: boolean;
observePageSearchParamsAccess?: boolean;
serveStreamingMetadata?: boolean;
},
) => Promise<AppPageElement>;
clientReuseManifest?: ClientReuseManifestParseResult;
Expand Down Expand Up @@ -565,6 +566,16 @@ async function dispatchAppPageInner<TRoute extends AppPageDispatchRoute>(
const isForceStatic = dynamicConfig === "force-static";
const isDynamicError = dynamicConfig === "error";
const isForceDynamic = dynamicConfig === "force-dynamic";
const isPrerender = process.env.VINEXT_PRERENDER === "1";
const serveStreamingMetadata = shouldServeStreamingMetadata(
options.request.headers.get("user-agent") ?? "",
options.htmlLimitedBots,
);
// Full static artifacts resolve generated metadata into <head>. PPR fallback
// shells keep request-time placement until metadata staticness can be tracked
// independently from the route's staticness.
const placeGeneratedMetadataInBody =
(!isPrerender || options.pprFallbackShell !== undefined) && serveStreamingMetadata;
const isPrefetchDynamicShell = options.renderMode === APP_RSC_RENDER_MODE_PREFETCH_DYNAMIC_SHELL;
const isDraftMode = isDraftModeRequest(options.request, options.draftModeSecret);
const requestHeadersContext = getHeadersContext();
Expand Down Expand Up @@ -718,6 +729,9 @@ async function dispatchAppPageInner<TRoute extends AppPageDispatchRoute>(
{
observeMetadataSearchParamsAccess: revalidationDynamicConfig !== "force-static",
observePageSearchParamsAccess: revalidationDynamicConfig !== "force-static",
// Cache regeneration produces a complete static artifact, so metadata
// must be resolved into <head> before the artifact is stored.
serveStreamingMetadata: false,
},
);
const revalidatedOnError = options.createRscOnErrorHandler(
Expand Down Expand Up @@ -847,6 +861,7 @@ async function dispatchAppPageInner<TRoute extends AppPageDispatchRoute>(
{
observeMetadataSearchParamsAccess: sourceDynamicConfig !== "force-static",
observePageSearchParamsAccess: sourceDynamicConfig !== "force-static",
serveStreamingMetadata: placeGeneratedMetadataInBody,
},
);
},
Expand Down Expand Up @@ -919,6 +934,7 @@ async function dispatchAppPageInner<TRoute extends AppPageDispatchRoute>(
{
observeMetadataSearchParamsAccess: !isForceStatic,
observePageSearchParamsAccess: !isForceStatic,
serveStreamingMetadata: placeGeneratedMetadataInBody,
},
);
},
Expand All @@ -938,7 +954,7 @@ async function dispatchAppPageInner<TRoute extends AppPageDispatchRoute>(
return options.renderErrorBoundaryPage(buildError);
},
renderSpecialError(specialError) {
return renderPageSpecialError(options, specialError);
return renderPageSpecialError(options, specialError, serveStreamingMetadata);
},
resolveSpecialError: resolveAppPageSpecialError,
});
Expand Down Expand Up @@ -982,7 +998,6 @@ async function dispatchAppPageInner<TRoute extends AppPageDispatchRoute>(
const activeFallbackShellState = options.pprRuntime?.getState() ?? null;
const pprFallbackShellSignal = activeFallbackShellState?.abortController.signal;
const pprFallbackShellReactSignal = activeFallbackShellState?.reactAbortController.signal;
const isPrerender = process.env.VINEXT_PRERENDER === "1";
const isSpeculativePrerender =
isPrerender && options.request.headers.get(VINEXT_PRERENDER_SPECULATIVE_HEADER) === "1";

Expand Down Expand Up @@ -1098,10 +1113,10 @@ async function dispatchAppPageInner<TRoute extends AppPageDispatchRoute>(
return options.renderErrorBoundaryPage(renderError, errorOrigin);
},
renderLayoutSpecialError(specialError, layoutIndex) {
return renderLayoutSpecialError(options, specialError, layoutIndex);
return renderLayoutSpecialError(options, specialError, layoutIndex, serveStreamingMetadata);
},
renderPageSpecialError(specialError) {
return renderPageSpecialError(options, specialError);
return renderPageSpecialError(options, specialError, serveStreamingMetadata);
},
renderToReadableStream: options.renderToReadableStream,
hasCustomGlobalError: options.hasCustomGlobalError,
Expand All @@ -1121,6 +1136,7 @@ async function renderLayoutSpecialError<TRoute extends AppPageDispatchRoute>(
options: DispatchAppPageOptions<TRoute>,
specialError: AppPageSpecialError,
layoutIndex: number,
serveStreamingMetadata: boolean,
): Promise<Response> {
return buildAppPageSpecialErrorResponse({
basePath: options.basePath,
Expand All @@ -1131,10 +1147,7 @@ async function renderLayoutSpecialError<TRoute extends AppPageDispatchRoute>(
}),
clearRequestContext: options.clearRequestContext,
getAndClearPendingCookies,
serveStreamingMetadata: shouldServeStreamingMetadata(
options.request.headers.get("user-agent") ?? "",
options.htmlLimitedBots,
),
serveStreamingMetadata,
isEdgeRuntime: options.isEdgeRuntime,
isRscRequest: options.isRscRequest,
middlewareContext: options.middlewareContext,
Expand Down Expand Up @@ -1167,6 +1180,7 @@ async function renderLayoutSpecialError<TRoute extends AppPageDispatchRoute>(
async function renderPageSpecialError<TRoute extends AppPageDispatchRoute>(
options: DispatchAppPageOptions<TRoute>,
specialError: AppPageSpecialError,
serveStreamingMetadata: boolean,
): Promise<Response> {
return buildAppPageSpecialErrorResponse({
basePath: options.basePath,
Expand All @@ -1177,10 +1191,7 @@ async function renderPageSpecialError<TRoute extends AppPageDispatchRoute>(
}),
clearRequestContext: options.clearRequestContext,
getAndClearPendingCookies,
serveStreamingMetadata: shouldServeStreamingMetadata(
options.request.headers.get("user-agent") ?? "",
options.htmlLimitedBots,
),
serveStreamingMetadata,
isEdgeRuntime: options.isEdgeRuntime,
isRscRequest: options.isRscRequest,
middlewareContext: options.middlewareContext,
Expand Down
12 changes: 8 additions & 4 deletions packages/vinext/src/server/app-page-element-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export type AppPagePageRequest<TModule extends AppPageModule = AppPageModule> =
observePageSearchParamsAccess?: boolean;
/** Observe page metadata `searchParams` access for cache-safety classification. */
observeMetadataSearchParamsAccess?: boolean;
/** Whether generated metadata may stream into the response body. */
serveStreamingMetadata?: boolean;
};

export type BuildPageElementsOptions<
Expand Down Expand Up @@ -198,6 +200,7 @@ export async function buildPageElements<
renderMode = APP_RSC_RENDER_MODE_NAVIGATION,
observeMetadataSearchParamsAccess = false,
observePageSearchParamsAccess = false,
serveStreamingMetadata,
} = pageRequest;

const pageModule: AppPageModule | null | undefined = route.page;
Expand Down Expand Up @@ -360,10 +363,11 @@ export async function buildPageElements<
const slotOverrides = buildSlotOverrides(route, params, routePath, opts);
const metadataPlacement =
hasDynamicMetadata &&
shouldServeStreamingMetadata(
pageRequest.request.headers.get("user-agent") ?? "",
options.htmlLimitedBots,
)
(serveStreamingMetadata ??
shouldServeStreamingMetadata(
pageRequest.request.headers.get("user-agent") ?? "",
options.htmlLimitedBots,
))
? "body"
: "head";

Expand Down
51 changes: 49 additions & 2 deletions tests/app-page-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ type CreateDispatchOptionsOverrides = {
mountedSlotsHeader?: string | null;
params?: Record<string, string | string[]>;
pprFallbackCacheShells?: DispatchOptions["pprFallbackCacheShells"];
pprFallbackShell?: DispatchOptions["pprFallbackShell"];
pprRuntime?: DispatchOptions["pprRuntime"];
probeLayoutAt?: DispatchOptions["probeLayoutAt"];
probePage?: DispatchOptions["probePage"];
Expand Down Expand Up @@ -386,6 +387,7 @@ function createDispatchOptions(overrides: CreateDispatchOptionsOverrides = {}) {
mountedSlotsHeader: overrides.mountedSlotsHeader,
params,
pprFallbackCacheShells: overrides.pprFallbackCacheShells,
pprFallbackShell: overrides.pprFallbackShell,
pprRuntime: overrides.pprRuntime,
probeLayoutAt: overrides.probeLayoutAt ?? createLayoutParamProbe(route, params, []),
probePage: overrides.probePage ?? (() => null),
Expand Down Expand Up @@ -569,6 +571,42 @@ function createLayoutParamProbe(
}

describe("app page dispatch", () => {
it("disables streaming metadata while producing prerendered HTML", async () => {
vi.stubEnv("VINEXT_PRERENDER", "1");
const buildPageElement = vi.fn<DispatchOptions["buildPageElement"]>(async () =>
React.createElement("main", null, "page"),
);
const { options } = createDispatchOptions({ buildPageElement });

const response = await dispatchAppPage(options);

expect(response.status).toBe(200);
expect(buildPageElement.mock.calls[0]?.[5]).toMatchObject({
serveStreamingMetadata: false,
});
});

it("preserves request-time metadata placement for PPR fallback-shell prerenders", async () => {
vi.stubEnv("VINEXT_PRERENDER", "1");
const buildPageElement = vi.fn<DispatchOptions["buildPageElement"]>(async () =>
React.createElement("main", null, "page"),
);
const { options } = createDispatchOptions({
buildPageElement,
pprFallbackShell: {
fallbackParamNames: ["slug"],
routePattern: "/posts/[slug]",
},
});

const response = await dispatchAppPage(options);

expect(response.status).toBe(200);
expect(buildPageElement.mock.calls[0]?.[5]).toMatchObject({
serveStreamingMetadata: true,
});
});

it("does not run a speculative connection() page probe for HTML renders", async () => {
const probePage = vi.fn(async () => {
await connection();
Expand Down Expand Up @@ -2407,7 +2445,11 @@ describe("app page dispatch", () => {

await expect(response.text()).resolves.toBe("popular");
expect(buildOptions).toEqual([
{ observeMetadataSearchParamsAccess: true, observePageSearchParamsAccess: true },
{
observeMetadataSearchParamsAccess: true,
observePageSearchParamsAccess: true,
serveStreamingMetadata: true,
},
]);
});

Expand Down Expand Up @@ -2474,9 +2516,13 @@ describe("app page dispatch", () => {
};
let capturedWaitForAllReady: boolean | undefined;
let capturedFallbackToErrorDocument: boolean | undefined;
let capturedServeStreamingMetadata: boolean | undefined;
const isrSet = vi.fn(async () => {});
const { options } = createDispatchOptions({
buildPageElement: async () => React.createElement("main", null, "fresh"),
buildPageElement: async (_route, _params, _opts, _searchParams, _layout, buildOptions) => {
capturedServeStreamingMetadata = buildOptions?.serveStreamingMetadata;
return React.createElement("main", null, "fresh");
},
cleanPathname: "/posts/hello",
isProduction: true,
isrGet: vi.fn(async () =>
Expand Down Expand Up @@ -2517,6 +2563,7 @@ describe("app page dispatch", () => {
await scheduledRender();

expect(capturedWaitForAllReady).toBe(true);
expect(capturedServeStreamingMetadata).toBe(false);
expect(capturedFallbackToErrorDocument).toBeUndefined();
expect(isrSet).toHaveBeenCalled();
});
Expand Down
Loading
Loading