diff --git a/packages/vinext/src/build/prerender-paths.ts b/packages/vinext/src/build/prerender-paths.ts index b2eef17b3..6695708f7 100644 --- a/packages/vinext/src/build/prerender-paths.ts +++ b/packages/vinext/src/build/prerender-paths.ts @@ -559,13 +559,13 @@ async function collectAppPaths(options: { return { loadingShellPaths, paths }; } -async function resolveAppRscWarmPaths(options: { +async function resolveAppWarmPaths(options: { appDir: string; i18n: ResolvedNextConfig["i18n"]; pagesDir: string | null; pageExtensions: readonly string[]; paths: readonly string[]; -}): Promise<{ loadingShellPaths: string[]; rscPaths: string[] }> { +}): Promise<{ htmlPaths: string[]; loadingShellPaths: string[]; rscPaths: string[] }> { const appRoutes = await appRouter(options.appDir, options.pageExtensions); const [pageRoutes, apiRoutes] = options.pagesDir ? await Promise.all([ @@ -575,6 +575,7 @@ async function resolveAppRscWarmPaths(options: { : [[], []]; const rscPaths: string[] = []; + const htmlPaths: string[] = []; const loadingShellPaths: string[] = []; for (const pathname of options.paths) { const appMatch = matchAppRoute(pathname, appRoutes); @@ -586,6 +587,12 @@ async function resolveAppRscWarmPaths(options: { const appRenderEntryPath = getAppRouteRenderEntryPath(matchedAppRoute); if (!appRenderEntryPath) continue; + if ( + classifyAppRoute(appRenderEntryPath, matchedAppRoute.routePath, matchedAppRoute.isDynamic) + .type === "api" + ) { + continue; + } // Pages Router i18n prefixes are routing metadata rather than part of the // filesystem route. Production strips them before matching Pages/API @@ -603,12 +610,13 @@ async function resolveAppRscWarmPaths(options: { continue; } + htmlPaths.push(pathname); rscPaths.push(pathname); if (appRouteHasMainTreeLoadingBoundary(matchedAppRoute)) { loadingShellPaths.push(pathname); } } - return { loadingShellPaths, rscPaths }; + return { htmlPaths, loadingShellPaths, rscPaths }; } function configuredRouteAffectsWarmPath( @@ -764,20 +772,26 @@ export async function emitPrerenderPathManifest( ? paths.filter((pathname) => configuredRouteAffectsWarmPath(pathname, config)) : [], ); - const warmPaths = paths.filter((pathname) => !excludedWarmPathSet.has(pathname)); - const appOwnedWarmPaths = - options.responseVary && appDir - ? await resolveAppRscWarmPaths({ - appDir, - i18n: config.i18n, - pagesDir, - pageExtensions: config.pageExtensions, - paths: discoveredAppPaths.filter((pathname) => !excludedWarmPathSet.has(pathname)), - }) - : { - loadingShellPaths: discoveredLoadingShellPaths, - rscPaths: discoveredAppPaths, - }; + const configuredPagesWarmPaths = discoveredPagesPaths.filter( + (pathname) => !excludedWarmPathSet.has(pathname), + ); + const appOwnedWarmPaths = appDir + ? await resolveAppWarmPaths({ + appDir, + i18n: config.i18n, + pagesDir, + pageExtensions: config.pageExtensions, + paths: discoveredAppPaths.filter((pathname) => !excludedWarmPathSet.has(pathname)), + }) + : { + htmlPaths: discoveredAppPaths, + loadingShellPaths: discoveredLoadingShellPaths, + rscPaths: discoveredAppPaths, + }; + const warmPathSet = new Set([...appOwnedWarmPaths.htmlPaths, ...configuredPagesWarmPaths]); + const warmPaths = paths.filter( + (pathname) => !excludedWarmPathSet.has(pathname) && warmPathSet.has(pathname), + ); const manifest: PrerenderPathManifest = { ...(config.basePath ? { basePath: config.basePath } : {}), @@ -786,7 +800,7 @@ export async function emitPrerenderPathManifest( ...(config.deploymentId ? { deploymentId: config.deploymentId } : {}), ...(pagesDir ? { - pagesPaths: discoveredPagesPaths.filter((pathname) => !excludedWarmPathSet.has(pathname)), + pagesPaths: configuredPagesWarmPaths, } : {}), ...(excludedWarmPathSet.size > 0 ? { excludedWarmPaths: Array.from(excludedWarmPathSet) } : {}), diff --git a/tests/prerender-paths.test.ts b/tests/prerender-paths.test.ts index 1099d6b8a..7ed1893a4 100644 --- a/tests/prerender-paths.test.ts +++ b/tests/prerender-paths.test.ts @@ -351,7 +351,7 @@ describe("prerender path manifest", () => { expect(manifest?.excludedWarmPaths).toEqual(["/foo"]); }); - it("excludes Pages-owned hybrid paths from App RSC warm discovery", async () => { + it("excludes Pages-owned hybrid paths from App warm discovery", async () => { // Next.js resolves matching Pages and App routes by cross-router specificity: // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/use-params/use-params.test.ts // https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/pages-to-app-routing/pages-to-app-routing.test.ts @@ -387,12 +387,7 @@ describe("prerender path manifest", () => { responseVary: "verbatim", }); - expect(manifest?.paths).toEqual([ - "/pages-dir/static", - "/pages-dir/foobar", - "/api/status", - "/specific/value", - ]); + expect(manifest?.paths).toEqual(["/pages-dir/static", "/specific/value"]); expect(manifest?.rscPaths).toEqual(["/pages-dir/static", "/specific/value"]); expect(manifest?.loadingShellPaths).toEqual(["/specific/value"]); expect(manifest?.pagesPaths).toEqual([]); @@ -412,6 +407,7 @@ describe("prerender path manifest", () => { ); writeFile("app/pages-dir/static/page.tsx", "export default function Page() { return null; }\n"); writeFile("app/specific/[id]/page.tsx", "export default function Page() { return null; }\n"); + writeFile("app/api/status/route.ts", "export function GET() { return new Response('ok'); }\n"); writeFile( "app/specific/[id]/loading.tsx", "export default function Loading() { return null; }\n", @@ -427,9 +423,9 @@ describe("prerender path manifest", () => { expect(manifest?.rscPaths).toEqual([ "/pages-dir/static", "/pages-dir/foobar", - "/api/status", "/specific/value", ]); + expect(manifest?.paths).toEqual(["/pages-dir/static", "/pages-dir/foobar", "/specific/value"]); expect(manifest?.loadingShellPaths).toEqual(["/specific/value"]); });