[ppr] Fix PPR detection for groups (#11625)

Previously, PPR was either all on, or all off. As we've added support for incremental adoption of PPR, the logic for determining if there's a grouping issue had to be adjusted.

This now only considers a group as supporting PPR if a route in the group supports PPR. As we also use this as a grouping key, this still ensures we won't have conflicts.
This commit is contained in:
Wyatt Johnson
2024-05-22 22:06:12 -06:00
committed by GitHub
parent 464cb26255
commit 73e558913a
4 changed files with 68 additions and 69 deletions

View File

@@ -0,0 +1,6 @@
---
'@vercel/next': patch
'vercel': patch
---
Support incremental PPR for large applications

View File

@@ -1358,6 +1358,11 @@ export const build: BuildV2 = async ({
experimentalPPRRoutes.add(route); experimentalPPRRoutes.add(route);
} }
const isAppPPREnabled = requiredServerFilesManifest
? requiredServerFilesManifest.config.experimental?.ppr === true ||
requiredServerFilesManifest.config.experimental?.ppr === 'incremental'
: false;
if (requiredServerFilesManifest) { if (requiredServerFilesManifest) {
if (!routesManifest) { if (!routesManifest) {
throw new Error( throw new Error(
@@ -1413,6 +1418,7 @@ export const build: BuildV2 = async ({
hasIsr500Page, hasIsr500Page,
variantsManifest, variantsManifest,
experimentalPPRRoutes, experimentalPPRRoutes,
isAppPPREnabled,
}); });
} }
@@ -1932,7 +1938,7 @@ export const build: BuildV2 = async ({
canUsePreviewMode, canUsePreviewMode,
bypassToken: prerenderManifest.bypassToken || '', bypassToken: prerenderManifest.bypassToken || '',
isServerMode, isServerMode,
experimentalPPRRoutes, isAppPPREnabled: false,
hasActionOutputSupport: false, hasActionOutputSupport: false,
}).then(arr => }).then(arr =>
localizeDynamicRoutes( localizeDynamicRoutes(
@@ -1963,7 +1969,7 @@ export const build: BuildV2 = async ({
canUsePreviewMode, canUsePreviewMode,
bypassToken: prerenderManifest.bypassToken || '', bypassToken: prerenderManifest.bypassToken || '',
isServerMode, isServerMode,
experimentalPPRRoutes, isAppPPREnabled: false,
hasActionOutputSupport: false, hasActionOutputSupport: false,
}).then(arr => }).then(arr =>
arr.map(route => { arr.map(route => {

View File

@@ -144,6 +144,7 @@ export async function serverBuild({
requiredServerFilesManifest, requiredServerFilesManifest,
variantsManifest, variantsManifest,
experimentalPPRRoutes, experimentalPPRRoutes,
isAppPPREnabled,
}: { }: {
appPathRoutesManifest?: Record<string, string>; appPathRoutesManifest?: Record<string, string>;
dynamicPages: string[]; dynamicPages: string[];
@@ -185,7 +186,15 @@ export async function serverBuild({
requiredServerFilesManifest: NextRequiredServerFilesManifest; requiredServerFilesManifest: NextRequiredServerFilesManifest;
variantsManifest: VariantsManifest | null; variantsManifest: VariantsManifest | null;
experimentalPPRRoutes: ReadonlySet<string>; experimentalPPRRoutes: ReadonlySet<string>;
isAppPPREnabled: boolean;
}): Promise<BuildResult> { }): Promise<BuildResult> {
if (isAppPPREnabled) {
debug(
'experimentalPPRRoutes',
JSON.stringify(Array.from(experimentalPPRRoutes))
);
}
lambdaPages = Object.assign({}, lambdaPages, lambdaAppPaths); lambdaPages = Object.assign({}, lambdaPages, lambdaAppPaths);
const experimentalAllowBundling = Boolean( const experimentalAllowBundling = Boolean(
@@ -217,12 +226,6 @@ export async function serverBuild({
} }
} }
const experimental = {
ppr:
requiredServerFilesManifest.config.experimental?.ppr === true ||
requiredServerFilesManifest.config.experimental?.ppr === 'incremental',
};
let appRscPrefetches: UnwrapPromise<ReturnType<typeof glob>> = {}; let appRscPrefetches: UnwrapPromise<ReturnType<typeof glob>> = {};
let appBuildTraces: UnwrapPromise<ReturnType<typeof glob>> = {}; let appBuildTraces: UnwrapPromise<ReturnType<typeof glob>> = {};
let appDir: string | null = null; let appDir: string | null = null;
@@ -230,7 +233,7 @@ export async function serverBuild({
if (appPathRoutesManifest) { if (appPathRoutesManifest) {
appDir = path.join(pagesDir, '../app'); appDir = path.join(pagesDir, '../app');
appBuildTraces = await glob('**/*.js.nft.json', appDir); appBuildTraces = await glob('**/*.js.nft.json', appDir);
appRscPrefetches = experimental.ppr appRscPrefetches = isAppPPREnabled
? {} ? {}
: await glob(`**/*${RSC_PREFETCH_SUFFIX}`, appDir); : await glob(`**/*${RSC_PREFETCH_SUFFIX}`, appDir);
@@ -251,7 +254,7 @@ export async function serverBuild({
if (rewrite.src && rewrite.dest) { if (rewrite.src && rewrite.dest) {
rewrite.src = rewrite.src.replace( rewrite.src = rewrite.src.replace(
/\/?\(\?:\/\)\?/, /\/?\(\?:\/\)\?/,
`(?<rscsuff>${experimental.ppr ? '(\\.prefetch)?' : ''}\\.rsc)?(?:/)?` `(?<rscsuff>${isAppPPREnabled ? '(\\.prefetch)?' : ''}\\.rsc)?(?:/)?`
); );
let destQueryIndex = rewrite.dest.indexOf('?'); let destQueryIndex = rewrite.dest.indexOf('?');
@@ -934,9 +937,6 @@ export async function serverBuild({
const appRouterStreamingActionLambdaGroups: LambdaGroup[] = []; const appRouterStreamingActionLambdaGroups: LambdaGroup[] = [];
for (const group of appRouterLambdaGroups) { for (const group of appRouterLambdaGroups) {
if (!group.isPrerenders || group.isExperimentalPPR) {
group.isStreaming = true;
}
group.isAppRouter = true; group.isAppRouter = true;
// We create a streaming variant of the Prerender lambda group // We create a streaming variant of the Prerender lambda group
@@ -951,9 +951,6 @@ export async function serverBuild({
} }
for (const group of appRouteHandlersLambdaGroups) { for (const group of appRouteHandlersLambdaGroups) {
if (!group.isPrerenders) {
group.isStreaming = true;
}
group.isAppRouter = true; group.isAppRouter = true;
group.isAppRouteHandler = true; group.isAppRouteHandler = true;
} }
@@ -984,18 +981,24 @@ export async function serverBuild({
apiLambdaGroups: apiLambdaGroups.map(group => ({ apiLambdaGroups: apiLambdaGroups.map(group => ({
pages: group.pages, pages: group.pages,
isPrerender: group.isPrerenders, isPrerender: group.isPrerenders,
isStreaming: group.isStreaming,
isExperimentalPPR: group.isExperimentalPPR,
pseudoLayerBytes: group.pseudoLayerBytes, pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes, uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})), })),
pageLambdaGroups: pageLambdaGroups.map(group => ({ pageLambdaGroups: pageLambdaGroups.map(group => ({
pages: group.pages, pages: group.pages,
isPrerender: group.isPrerenders, isPrerender: group.isPrerenders,
isStreaming: group.isStreaming,
isExperimentalPPR: group.isExperimentalPPR,
pseudoLayerBytes: group.pseudoLayerBytes, pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes, uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})), })),
appRouterLambdaGroups: appRouterLambdaGroups.map(group => ({ appRouterLambdaGroups: appRouterLambdaGroups.map(group => ({
pages: group.pages, pages: group.pages,
isPrerender: group.isPrerenders, isPrerender: group.isPrerenders,
isStreaming: group.isStreaming,
isExperimentalPPR: group.isExperimentalPPR,
pseudoLayerBytes: group.pseudoLayerBytes, pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes, uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})), })),
@@ -1003,6 +1006,8 @@ export async function serverBuild({
appRouterStreamingActionLambdaGroups.map(group => ({ appRouterStreamingActionLambdaGroups.map(group => ({
pages: group.pages, pages: group.pages,
isPrerender: group.isPrerenders, isPrerender: group.isPrerenders,
isStreaming: group.isStreaming,
isExperimentalPPR: group.isExperimentalPPR,
pseudoLayerBytes: group.pseudoLayerBytes, pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes, uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
})), })),
@@ -1010,6 +1015,8 @@ export async function serverBuild({
group => ({ group => ({
pages: group.pages, pages: group.pages,
isPrerender: group.isPrerenders, isPrerender: group.isPrerenders,
isStreaming: group.isStreaming,
isExperimentalPPR: group.isExperimentalPPR,
pseudoLayerBytes: group.pseudoLayerBytes, pseudoLayerBytes: group.pseudoLayerBytes,
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes, uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
}) })
@@ -1189,15 +1196,10 @@ export async function serverBuild({
const lambda = await createLambdaFromPseudoLayers(options); const lambda = await createLambdaFromPseudoLayers(options);
// This is a PPR lambda if it's an App Page with the PPR experimental flag
// enabled.
const isPPR =
experimental.ppr && group.isAppRouter && !group.isAppRouteHandler;
// If PPR is enabled and this is an App Page, create the non-streaming // If PPR is enabled and this is an App Page, create the non-streaming
// lambda for the page for revalidation. // lambda for the page for revalidation.
let revalidate: NodejsLambda | undefined; let revalidate: NodejsLambda | undefined;
if (isPPR) { if (group.isExperimentalPPR) {
if (!options.isStreaming) { if (!options.isStreaming) {
throw new Error("Invariant: PPR lambda isn't streaming"); throw new Error("Invariant: PPR lambda isn't streaming");
} }
@@ -1214,14 +1216,11 @@ export async function serverBuild({
// This is the name of the page, where the root is `index`. // This is the name of the page, where the root is `index`.
const pageName = pageFilename.replace(/\.js$/, ''); const pageName = pageFilename.replace(/\.js$/, '');
// This is the name of the page prefixed with a `/`, where the root is
// `/index`.
const pagePath = path.posix.join('/', pageName);
// This is the routable pathname for the page, where the root is `/`. // This is the routable pathname for the page, where the root is `/`.
const pagePathname = pagePath === '/index' ? '/' : pagePath; const pagePathname = normalizePage(pageName);
let isPrerender = prerenderRoutes.has(pagePathname); let isPrerender = prerenderRoutes.has(pagePathname);
const isRoutePPREnabled = experimentalPPRRoutes.has(pagePathname);
if (!isPrerender && routesManifest?.i18n) { if (!isPrerender && routesManifest?.i18n) {
isPrerender = routesManifest.i18n.locales.some(locale => { isPrerender = routesManifest.i18n.locales.some(locale => {
@@ -1239,7 +1238,7 @@ export async function serverBuild({
} }
// If this is a PPR page, then we should prefix the output name. // If this is a PPR page, then we should prefix the output name.
if (isPPR) { if (isRoutePPREnabled) {
if (!revalidate) { if (!revalidate) {
throw new Error("Invariant: PPR lambda isn't set"); throw new Error("Invariant: PPR lambda isn't set");
} }
@@ -1320,6 +1319,13 @@ export async function serverBuild({
console.timeEnd(lambdaCreationLabel); console.timeEnd(lambdaCreationLabel);
} }
if (isAppPPREnabled) {
debug(
'experimentalStreamingLambdaPaths',
JSON.stringify(Array.from(experimentalStreamingLambdaPaths))
);
}
const prerenderRoute = onPrerenderRoute({ const prerenderRoute = onPrerenderRoute({
appDir, appDir,
pagesDir, pagesDir,
@@ -1407,7 +1413,7 @@ export async function serverBuild({
bypassToken: prerenderManifest.bypassToken || '', bypassToken: prerenderManifest.bypassToken || '',
isServerMode: true, isServerMode: true,
dynamicMiddlewareRouteMap: middleware.dynamicRouteMap, dynamicMiddlewareRouteMap: middleware.dynamicRouteMap,
experimentalPPRRoutes, isAppPPREnabled,
hasActionOutputSupport, hasActionOutputSupport,
}).then(arr => }).then(arr =>
localizeDynamicRoutes( localizeDynamicRoutes(
@@ -1589,7 +1595,7 @@ export async function serverBuild({
if (lambdas[pathname]) { if (lambdas[pathname]) {
lambdas[`${pathname}.rsc`] = lambdas[pathname]; lambdas[`${pathname}.rsc`] = lambdas[pathname];
if (experimental.ppr) { if (isAppPPREnabled) {
lambdas[`${pathname}${RSC_PREFETCH_SUFFIX}`] = lambdas[pathname]; lambdas[`${pathname}${RSC_PREFETCH_SUFFIX}`] = lambdas[pathname];
} }
} }
@@ -1597,7 +1603,7 @@ export async function serverBuild({
if (edgeFunctions[pathname]) { if (edgeFunctions[pathname]) {
edgeFunctions[`${pathname}.rsc`] = edgeFunctions[pathname]; edgeFunctions[`${pathname}.rsc`] = edgeFunctions[pathname];
if (experimental.ppr) { if (isAppPPREnabled) {
edgeFunctions[`${pathname}${RSC_PREFETCH_SUFFIX}`] = edgeFunctions[`${pathname}${RSC_PREFETCH_SUFFIX}`] =
edgeFunctions[pathname]; edgeFunctions[pathname];
} }
@@ -1616,7 +1622,7 @@ export async function serverBuild({
'RSC, Next-Router-State-Tree, Next-Router-Prefetch'; 'RSC, Next-Router-State-Tree, Next-Router-Prefetch';
const appNotFoundPath = path.posix.join('.', entryDirectory, '_not-found'); const appNotFoundPath = path.posix.join('.', entryDirectory, '_not-found');
if (experimental.ppr && !rscPrefetchHeader) { if (isAppPPREnabled && !rscPrefetchHeader) {
throw new Error("Invariant: cannot use PPR without 'rsc.prefetchHeader'"); throw new Error("Invariant: cannot use PPR without 'rsc.prefetchHeader'");
} }
@@ -1624,7 +1630,7 @@ export async function serverBuild({
// all the routes that support it (and are listed) have configured lambdas. // all the routes that support it (and are listed) have configured lambdas.
// This only applies to routes that do not have fallbacks enabled (these are // This only applies to routes that do not have fallbacks enabled (these are
// routes that have `dynamicParams = false` defined. // routes that have `dynamicParams = false` defined.
if (experimental.ppr) { if (isAppPPREnabled) {
for (const { srcRoute, dataRoute, experimentalPPR } of Object.values( for (const { srcRoute, dataRoute, experimentalPPR } of Object.values(
prerenderManifest.staticRoutes prerenderManifest.staticRoutes
)) { )) {
@@ -1907,7 +1913,7 @@ export async function serverBuild({
...(appDir ...(appDir
? [ ? [
...(rscPrefetchHeader && experimental.ppr ...(rscPrefetchHeader && isAppPPREnabled
? [ ? [
{ {
src: `^${path.posix.join('/', entryDirectory, '/')}`, src: `^${path.posix.join('/', entryDirectory, '/')}`,

View File

@@ -192,8 +192,12 @@ function normalizePage(page: string): string {
if (!page.startsWith('/')) { if (!page.startsWith('/')) {
page = `/${page}`; page = `/${page}`;
} }
// remove '/index' from the end
page = page.replace(/\/index$/, '/'); // Replace the `/index` with `/`
if (page === '/index') {
page = '/';
}
return page; return page;
} }
@@ -320,8 +324,8 @@ export async function getDynamicRoutes({
bypassToken, bypassToken,
isServerMode, isServerMode,
dynamicMiddlewareRouteMap, dynamicMiddlewareRouteMap,
experimentalPPRRoutes,
hasActionOutputSupport, hasActionOutputSupport,
isAppPPREnabled,
}: { }: {
entryPath: string; entryPath: string;
entryDirectory: string; entryDirectory: string;
@@ -333,8 +337,8 @@ export async function getDynamicRoutes({
bypassToken?: string; bypassToken?: string;
isServerMode?: boolean; isServerMode?: boolean;
dynamicMiddlewareRouteMap?: ReadonlyMap<string, RouteWithSrc>; dynamicMiddlewareRouteMap?: ReadonlyMap<string, RouteWithSrc>;
experimentalPPRRoutes: ReadonlySet<string>;
hasActionOutputSupport: boolean; hasActionOutputSupport: boolean;
isAppPPREnabled: boolean;
}): Promise<RouteWithSrc[]> { }): Promise<RouteWithSrc[]> {
if (routesManifest) { if (routesManifest) {
switch (routesManifest.version) { switch (routesManifest.version) {
@@ -408,7 +412,7 @@ export async function getDynamicRoutes({
]; ];
} }
if (experimentalPPRRoutes.has(page)) { if (isAppPPREnabled) {
let dest = route.dest?.replace(/($|\?)/, '.prefetch.rsc$1'); let dest = route.dest?.replace(/($|\?)/, '.prefetch.rsc$1');
if (page === '/' || page === '/index') { if (page === '/' || page === '/index') {
@@ -1504,9 +1508,9 @@ export type LambdaGroup = {
maxDuration?: number; maxDuration?: number;
isAppRouter?: boolean; isAppRouter?: boolean;
isAppRouteHandler?: boolean; isAppRouteHandler?: boolean;
isStreaming?: boolean; readonly isStreaming: boolean;
isPrerenders?: boolean; readonly isPrerenders: boolean;
isExperimentalPPR?: boolean; readonly isExperimentalPPR: boolean;
isActionLambda?: boolean; isActionLambda?: boolean;
isPages?: boolean; isPages?: boolean;
isApiLambda: boolean; isApiLambda: boolean;
@@ -1561,6 +1565,7 @@ export async function getPageLambdaGroups({
const routeName = normalizePage(page.replace(/\.js$/, '')); const routeName = normalizePage(page.replace(/\.js$/, ''));
const isPrerenderRoute = prerenderRoutes.has(routeName); const isPrerenderRoute = prerenderRoutes.has(routeName);
const isExperimentalPPR = experimentalPPRRoutes?.has(routeName) ?? false; const isExperimentalPPR = experimentalPPRRoutes?.has(routeName) ?? false;
const isStreaming = !isPrerenderRoute || isExperimentalPPR;
let opts: { memory?: number; maxDuration?: number } = {}; let opts: { memory?: number; maxDuration?: number } = {};
@@ -1632,6 +1637,7 @@ export async function getPageLambdaGroups({
...opts, ...opts,
isPrerenders: isPrerenderRoute, isPrerenders: isPrerenderRoute,
isExperimentalPPR, isExperimentalPPR,
isStreaming,
isApiLambda: !!isApiPage(page), isApiLambda: !!isApiPage(page),
pseudoLayerBytes: initialPseudoLayer.pseudoLayerBytes, pseudoLayerBytes: initialPseudoLayer.pseudoLayerBytes,
pseudoLayerUncompressedBytes: initialPseudoLayerUncompressed, pseudoLayerUncompressedBytes: initialPseudoLayerUncompressed,
@@ -2219,22 +2225,6 @@ export const onPrerenderRoute =
initialStatus = 404; initialStatus = 404;
} }
/**
* If the route key had an `/index` suffix added, we need to note it so we
* can remove it from the output path later accurately.
*/
let addedIndexSuffix = false;
if (isAppPathRoute) {
// for literal index routes we need to append an additional /index
// due to the proxy's normalizing for /index routes
if (routeKey !== '/index' && routeKey.endsWith('/index')) {
routeKey = `${routeKey}/index`;
routeFileNoExt = routeKey;
addedIndexSuffix = true;
}
}
let outputPathPage = path.posix.join(entryDirectory, routeFileNoExt); let outputPathPage = path.posix.join(entryDirectory, routeFileNoExt);
if (!isAppPathRoute) { if (!isAppPathRoute) {
@@ -2428,7 +2418,7 @@ export const onPrerenderRoute =
// static route first, then try the srcRoute if it doesn't exist. If we // static route first, then try the srcRoute if it doesn't exist. If we
// can't find it at all, this constitutes an error. // can't find it at all, this constitutes an error.
experimentalStreamingLambdaPath = experimentalStreamingLambdaPaths.get( experimentalStreamingLambdaPath = experimentalStreamingLambdaPaths.get(
pathnameToOutputName(entryDirectory, routeKey, addedIndexSuffix) pathnameToOutputName(entryDirectory, routeKey)
); );
if (!experimentalStreamingLambdaPath && srcRoute) { if (!experimentalStreamingLambdaPath && srcRoute) {
experimentalStreamingLambdaPath = experimentalStreamingLambdaPath =
@@ -2689,19 +2679,10 @@ export function getNextServerPath(nextVersion: string) {
: 'next/dist/next-server/server'; : 'next/dist/next-server/server';
} }
function pathnameToOutputName( function pathnameToOutputName(entryDirectory: string, pathname: string) {
entryDirectory: string,
pathname: string,
addedIndexSuffix = false
) {
if (pathname === '/') { if (pathname === '/') {
pathname = '/index'; pathname = '/index';
} }
// If the `/index` was added for a route that ended in `/index` we need to
// strip the second one off before joining it with the entryDirectory.
else if (addedIndexSuffix) {
pathname = pathname.replace(/\/index$/, '');
}
return path.posix.join(entryDirectory, pathname); return path.posix.join(entryDirectory, pathname);
} }