diff --git a/.changeset/purple-walls-rescue.md b/.changeset/purple-walls-rescue.md new file mode 100644 index 000000000..f5e18666a --- /dev/null +++ b/.changeset/purple-walls-rescue.md @@ -0,0 +1,6 @@ +--- +'@vercel/next': patch +'vercel': patch +--- + +Support incremental PPR for large applications diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts index caba9dc91..8a35ced2e 100644 --- a/packages/next/src/index.ts +++ b/packages/next/src/index.ts @@ -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 => { diff --git a/packages/next/src/server-build.ts b/packages/next/src/server-build.ts index ffecd70b8..2428f0fbe 100644 --- a/packages/next/src/server-build.ts +++ b/packages/next/src/server-build.ts @@ -144,6 +144,7 @@ export async function serverBuild({ requiredServerFilesManifest, variantsManifest, experimentalPPRRoutes, + isAppPPREnabled, }: { appPathRoutesManifest?: Record; dynamicPages: string[]; @@ -185,7 +186,15 @@ export async function serverBuild({ requiredServerFilesManifest: NextRequiredServerFilesManifest; variantsManifest: VariantsManifest | null; experimentalPPRRoutes: ReadonlySet; + isAppPPREnabled: boolean; }): Promise { + 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> = {}; let appBuildTraces: UnwrapPromise> = {}; 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( /\/?\(\?:\/\)\?/, - `(?${experimental.ppr ? '(\\.prefetch)?' : ''}\\.rsc)?(?:/)?` + `(?${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, '/')}`, diff --git a/packages/next/src/utils.ts b/packages/next/src/utils.ts index 04021bddf..b31223794 100644 --- a/packages/next/src/utils.ts +++ b/packages/next/src/utils.ts @@ -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; - experimentalPPRRoutes: ReadonlySet; hasActionOutputSupport: boolean; + isAppPPREnabled: boolean; }): Promise { 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); }