mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-06 04:22:01 +00:00
[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:
6
.changeset/purple-walls-rescue.md
Normal file
6
.changeset/purple-walls-rescue.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@vercel/next': patch
|
||||
'vercel': patch
|
||||
---
|
||||
|
||||
Support incremental PPR for large applications
|
||||
@@ -1358,6 +1358,11 @@ export const build: BuildV2 = async ({
|
||||
experimentalPPRRoutes.add(route);
|
||||
}
|
||||
|
||||
const isAppPPREnabled = requiredServerFilesManifest
|
||||
? requiredServerFilesManifest.config.experimental?.ppr === true ||
|
||||
requiredServerFilesManifest.config.experimental?.ppr === 'incremental'
|
||||
: false;
|
||||
|
||||
if (requiredServerFilesManifest) {
|
||||
if (!routesManifest) {
|
||||
throw new Error(
|
||||
@@ -1413,6 +1418,7 @@ export const build: BuildV2 = async ({
|
||||
hasIsr500Page,
|
||||
variantsManifest,
|
||||
experimentalPPRRoutes,
|
||||
isAppPPREnabled,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1932,7 +1938,7 @@ export const build: BuildV2 = async ({
|
||||
canUsePreviewMode,
|
||||
bypassToken: prerenderManifest.bypassToken || '',
|
||||
isServerMode,
|
||||
experimentalPPRRoutes,
|
||||
isAppPPREnabled: false,
|
||||
hasActionOutputSupport: false,
|
||||
}).then(arr =>
|
||||
localizeDynamicRoutes(
|
||||
@@ -1963,7 +1969,7 @@ export const build: BuildV2 = async ({
|
||||
canUsePreviewMode,
|
||||
bypassToken: prerenderManifest.bypassToken || '',
|
||||
isServerMode,
|
||||
experimentalPPRRoutes,
|
||||
isAppPPREnabled: false,
|
||||
hasActionOutputSupport: false,
|
||||
}).then(arr =>
|
||||
arr.map(route => {
|
||||
|
||||
@@ -144,6 +144,7 @@ export async function serverBuild({
|
||||
requiredServerFilesManifest,
|
||||
variantsManifest,
|
||||
experimentalPPRRoutes,
|
||||
isAppPPREnabled,
|
||||
}: {
|
||||
appPathRoutesManifest?: Record<string, string>;
|
||||
dynamicPages: string[];
|
||||
@@ -185,7 +186,15 @@ export async function serverBuild({
|
||||
requiredServerFilesManifest: NextRequiredServerFilesManifest;
|
||||
variantsManifest: VariantsManifest | null;
|
||||
experimentalPPRRoutes: ReadonlySet<string>;
|
||||
isAppPPREnabled: boolean;
|
||||
}): Promise<BuildResult> {
|
||||
if (isAppPPREnabled) {
|
||||
debug(
|
||||
'experimentalPPRRoutes',
|
||||
JSON.stringify(Array.from(experimentalPPRRoutes))
|
||||
);
|
||||
}
|
||||
|
||||
lambdaPages = Object.assign({}, lambdaPages, lambdaAppPaths);
|
||||
|
||||
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 appBuildTraces: UnwrapPromise<ReturnType<typeof glob>> = {};
|
||||
let appDir: string | null = null;
|
||||
@@ -230,7 +233,7 @@ export async function serverBuild({
|
||||
if (appPathRoutesManifest) {
|
||||
appDir = path.join(pagesDir, '../app');
|
||||
appBuildTraces = await glob('**/*.js.nft.json', appDir);
|
||||
appRscPrefetches = experimental.ppr
|
||||
appRscPrefetches = isAppPPREnabled
|
||||
? {}
|
||||
: await glob(`**/*${RSC_PREFETCH_SUFFIX}`, appDir);
|
||||
|
||||
@@ -251,7 +254,7 @@ export async function serverBuild({
|
||||
if (rewrite.src && rewrite.dest) {
|
||||
rewrite.src = rewrite.src.replace(
|
||||
/\/?\(\?:\/\)\?/,
|
||||
`(?<rscsuff>${experimental.ppr ? '(\\.prefetch)?' : ''}\\.rsc)?(?:/)?`
|
||||
`(?<rscsuff>${isAppPPREnabled ? '(\\.prefetch)?' : ''}\\.rsc)?(?:/)?`
|
||||
);
|
||||
let destQueryIndex = rewrite.dest.indexOf('?');
|
||||
|
||||
@@ -934,9 +937,6 @@ export async function serverBuild({
|
||||
const appRouterStreamingActionLambdaGroups: LambdaGroup[] = [];
|
||||
|
||||
for (const group of appRouterLambdaGroups) {
|
||||
if (!group.isPrerenders || group.isExperimentalPPR) {
|
||||
group.isStreaming = true;
|
||||
}
|
||||
group.isAppRouter = true;
|
||||
|
||||
// We create a streaming variant of the Prerender lambda group
|
||||
@@ -951,9 +951,6 @@ export async function serverBuild({
|
||||
}
|
||||
|
||||
for (const group of appRouteHandlersLambdaGroups) {
|
||||
if (!group.isPrerenders) {
|
||||
group.isStreaming = true;
|
||||
}
|
||||
group.isAppRouter = true;
|
||||
group.isAppRouteHandler = true;
|
||||
}
|
||||
@@ -984,18 +981,24 @@ export async function serverBuild({
|
||||
apiLambdaGroups: apiLambdaGroups.map(group => ({
|
||||
pages: group.pages,
|
||||
isPrerender: group.isPrerenders,
|
||||
isStreaming: group.isStreaming,
|
||||
isExperimentalPPR: group.isExperimentalPPR,
|
||||
pseudoLayerBytes: group.pseudoLayerBytes,
|
||||
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
|
||||
})),
|
||||
pageLambdaGroups: pageLambdaGroups.map(group => ({
|
||||
pages: group.pages,
|
||||
isPrerender: group.isPrerenders,
|
||||
isStreaming: group.isStreaming,
|
||||
isExperimentalPPR: group.isExperimentalPPR,
|
||||
pseudoLayerBytes: group.pseudoLayerBytes,
|
||||
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
|
||||
})),
|
||||
appRouterLambdaGroups: appRouterLambdaGroups.map(group => ({
|
||||
pages: group.pages,
|
||||
isPrerender: group.isPrerenders,
|
||||
isStreaming: group.isStreaming,
|
||||
isExperimentalPPR: group.isExperimentalPPR,
|
||||
pseudoLayerBytes: group.pseudoLayerBytes,
|
||||
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
|
||||
})),
|
||||
@@ -1003,6 +1006,8 @@ export async function serverBuild({
|
||||
appRouterStreamingActionLambdaGroups.map(group => ({
|
||||
pages: group.pages,
|
||||
isPrerender: group.isPrerenders,
|
||||
isStreaming: group.isStreaming,
|
||||
isExperimentalPPR: group.isExperimentalPPR,
|
||||
pseudoLayerBytes: group.pseudoLayerBytes,
|
||||
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
|
||||
})),
|
||||
@@ -1010,6 +1015,8 @@ export async function serverBuild({
|
||||
group => ({
|
||||
pages: group.pages,
|
||||
isPrerender: group.isPrerenders,
|
||||
isStreaming: group.isStreaming,
|
||||
isExperimentalPPR: group.isExperimentalPPR,
|
||||
pseudoLayerBytes: group.pseudoLayerBytes,
|
||||
uncompressedLayerBytes: group.pseudoLayerUncompressedBytes,
|
||||
})
|
||||
@@ -1189,15 +1196,10 @@ export async function serverBuild({
|
||||
|
||||
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
|
||||
// lambda for the page for revalidation.
|
||||
let revalidate: NodejsLambda | undefined;
|
||||
if (isPPR) {
|
||||
if (group.isExperimentalPPR) {
|
||||
if (!options.isStreaming) {
|
||||
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`.
|
||||
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 `/`.
|
||||
const pagePathname = pagePath === '/index' ? '/' : pagePath;
|
||||
const pagePathname = normalizePage(pageName);
|
||||
|
||||
let isPrerender = prerenderRoutes.has(pagePathname);
|
||||
const isRoutePPREnabled = experimentalPPRRoutes.has(pagePathname);
|
||||
|
||||
if (!isPrerender && routesManifest?.i18n) {
|
||||
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 (isPPR) {
|
||||
if (isRoutePPREnabled) {
|
||||
if (!revalidate) {
|
||||
throw new Error("Invariant: PPR lambda isn't set");
|
||||
}
|
||||
@@ -1320,6 +1319,13 @@ export async function serverBuild({
|
||||
console.timeEnd(lambdaCreationLabel);
|
||||
}
|
||||
|
||||
if (isAppPPREnabled) {
|
||||
debug(
|
||||
'experimentalStreamingLambdaPaths',
|
||||
JSON.stringify(Array.from(experimentalStreamingLambdaPaths))
|
||||
);
|
||||
}
|
||||
|
||||
const prerenderRoute = onPrerenderRoute({
|
||||
appDir,
|
||||
pagesDir,
|
||||
@@ -1407,7 +1413,7 @@ export async function serverBuild({
|
||||
bypassToken: prerenderManifest.bypassToken || '',
|
||||
isServerMode: true,
|
||||
dynamicMiddlewareRouteMap: middleware.dynamicRouteMap,
|
||||
experimentalPPRRoutes,
|
||||
isAppPPREnabled,
|
||||
hasActionOutputSupport,
|
||||
}).then(arr =>
|
||||
localizeDynamicRoutes(
|
||||
@@ -1589,7 +1595,7 @@ export async function serverBuild({
|
||||
if (lambdas[pathname]) {
|
||||
lambdas[`${pathname}.rsc`] = lambdas[pathname];
|
||||
|
||||
if (experimental.ppr) {
|
||||
if (isAppPPREnabled) {
|
||||
lambdas[`${pathname}${RSC_PREFETCH_SUFFIX}`] = lambdas[pathname];
|
||||
}
|
||||
}
|
||||
@@ -1597,7 +1603,7 @@ export async function serverBuild({
|
||||
if (edgeFunctions[pathname]) {
|
||||
edgeFunctions[`${pathname}.rsc`] = edgeFunctions[pathname];
|
||||
|
||||
if (experimental.ppr) {
|
||||
if (isAppPPREnabled) {
|
||||
edgeFunctions[`${pathname}${RSC_PREFETCH_SUFFIX}`] =
|
||||
edgeFunctions[pathname];
|
||||
}
|
||||
@@ -1616,7 +1622,7 @@ export async function serverBuild({
|
||||
'RSC, Next-Router-State-Tree, Next-Router-Prefetch';
|
||||
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'");
|
||||
}
|
||||
|
||||
@@ -1624,7 +1630,7 @@ export async function serverBuild({
|
||||
// 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
|
||||
// routes that have `dynamicParams = false` defined.
|
||||
if (experimental.ppr) {
|
||||
if (isAppPPREnabled) {
|
||||
for (const { srcRoute, dataRoute, experimentalPPR } of Object.values(
|
||||
prerenderManifest.staticRoutes
|
||||
)) {
|
||||
@@ -1907,7 +1913,7 @@ export async function serverBuild({
|
||||
|
||||
...(appDir
|
||||
? [
|
||||
...(rscPrefetchHeader && experimental.ppr
|
||||
...(rscPrefetchHeader && isAppPPREnabled
|
||||
? [
|
||||
{
|
||||
src: `^${path.posix.join('/', entryDirectory, '/')}`,
|
||||
|
||||
@@ -192,8 +192,12 @@ function normalizePage(page: string): string {
|
||||
if (!page.startsWith('/')) {
|
||||
page = `/${page}`;
|
||||
}
|
||||
// remove '/index' from the end
|
||||
page = page.replace(/\/index$/, '/');
|
||||
|
||||
// Replace the `/index` with `/`
|
||||
if (page === '/index') {
|
||||
page = '/';
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -320,8 +324,8 @@ export async function getDynamicRoutes({
|
||||
bypassToken,
|
||||
isServerMode,
|
||||
dynamicMiddlewareRouteMap,
|
||||
experimentalPPRRoutes,
|
||||
hasActionOutputSupport,
|
||||
isAppPPREnabled,
|
||||
}: {
|
||||
entryPath: string;
|
||||
entryDirectory: string;
|
||||
@@ -333,8 +337,8 @@ export async function getDynamicRoutes({
|
||||
bypassToken?: string;
|
||||
isServerMode?: boolean;
|
||||
dynamicMiddlewareRouteMap?: ReadonlyMap<string, RouteWithSrc>;
|
||||
experimentalPPRRoutes: ReadonlySet<string>;
|
||||
hasActionOutputSupport: boolean;
|
||||
isAppPPREnabled: boolean;
|
||||
}): Promise<RouteWithSrc[]> {
|
||||
if (routesManifest) {
|
||||
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');
|
||||
|
||||
if (page === '/' || page === '/index') {
|
||||
@@ -1504,9 +1508,9 @@ export type LambdaGroup = {
|
||||
maxDuration?: number;
|
||||
isAppRouter?: boolean;
|
||||
isAppRouteHandler?: boolean;
|
||||
isStreaming?: boolean;
|
||||
isPrerenders?: boolean;
|
||||
isExperimentalPPR?: boolean;
|
||||
readonly isStreaming: boolean;
|
||||
readonly isPrerenders: boolean;
|
||||
readonly isExperimentalPPR: boolean;
|
||||
isActionLambda?: boolean;
|
||||
isPages?: boolean;
|
||||
isApiLambda: boolean;
|
||||
@@ -1561,6 +1565,7 @@ export async function getPageLambdaGroups({
|
||||
const routeName = normalizePage(page.replace(/\.js$/, ''));
|
||||
const isPrerenderRoute = prerenderRoutes.has(routeName);
|
||||
const isExperimentalPPR = experimentalPPRRoutes?.has(routeName) ?? false;
|
||||
const isStreaming = !isPrerenderRoute || isExperimentalPPR;
|
||||
|
||||
let opts: { memory?: number; maxDuration?: number } = {};
|
||||
|
||||
@@ -1632,6 +1637,7 @@ export async function getPageLambdaGroups({
|
||||
...opts,
|
||||
isPrerenders: isPrerenderRoute,
|
||||
isExperimentalPPR,
|
||||
isStreaming,
|
||||
isApiLambda: !!isApiPage(page),
|
||||
pseudoLayerBytes: initialPseudoLayer.pseudoLayerBytes,
|
||||
pseudoLayerUncompressedBytes: initialPseudoLayerUncompressed,
|
||||
@@ -2219,22 +2225,6 @@ export const onPrerenderRoute =
|
||||
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);
|
||||
|
||||
if (!isAppPathRoute) {
|
||||
@@ -2428,7 +2418,7 @@ export const onPrerenderRoute =
|
||||
// static route first, then try the srcRoute if it doesn't exist. If we
|
||||
// can't find it at all, this constitutes an error.
|
||||
experimentalStreamingLambdaPath = experimentalStreamingLambdaPaths.get(
|
||||
pathnameToOutputName(entryDirectory, routeKey, addedIndexSuffix)
|
||||
pathnameToOutputName(entryDirectory, routeKey)
|
||||
);
|
||||
if (!experimentalStreamingLambdaPath && srcRoute) {
|
||||
experimentalStreamingLambdaPath =
|
||||
@@ -2689,19 +2679,10 @@ export function getNextServerPath(nextVersion: string) {
|
||||
: 'next/dist/next-server/server';
|
||||
}
|
||||
|
||||
function pathnameToOutputName(
|
||||
entryDirectory: string,
|
||||
pathname: string,
|
||||
addedIndexSuffix = false
|
||||
) {
|
||||
function pathnameToOutputName(entryDirectory: string, pathname: string) {
|
||||
if (pathname === '/') {
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user