diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 11a7398978052..8c28f45f2d053 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -2278,10 +2278,7 @@ export default async function build( }) } - if ( - workerResult.prerenderedRoutes && - workerResult.prerenderedRoutes.length > 0 - ) { + if (workerResult.prerenderedRoutes) { staticPaths.set( originalAppPath, workerResult.prerenderedRoutes diff --git a/packages/next/src/build/segment-config/app/app-segments.ts b/packages/next/src/build/segment-config/app/app-segments.ts index 02538bddc62ea..84c3828717722 100644 --- a/packages/next/src/build/segment-config/app/app-segments.ts +++ b/packages/next/src/build/segment-config/app/app-segments.ts @@ -21,6 +21,7 @@ import { import { isClientReference } from '../../../lib/client-reference' import { getSegmentParam } from '../../../server/app-render/get-segment-param' import { getLayoutOrPageModule } from '../../../server/lib/app-dir-module' +import { PAGE_SEGMENT_KEY } from '../../../shared/lib/segment' type GenerateStaticParams = (options: { params?: Params }) => Promise @@ -75,13 +76,16 @@ export type AppSegment = { async function collectAppPageSegments(routeModule: AppPageRouteModule) { const segments: AppSegment[] = [] - let current = routeModule.userland.loaderTree - while (current) { - const [name, parallelRoutes] = current - const { mod: userland, filePath } = await getLayoutOrPageModule(current) + // Helper function to process a loader tree path + async function processLoaderTree( + loaderTree: any, + currentSegments: AppSegment[] = [] + ): Promise { + const [name, parallelRoutes] = loaderTree + const { mod: userland, filePath } = await getLayoutOrPageModule(loaderTree) const isClientComponent: boolean = userland && isClientReference(userland) - const isDynamicSegment = /^\[.*\]$/.test(name) + const isDynamicSegment = /\[.*\]$/.test(name) const param = isDynamicSegment ? getSegmentParam(name)?.param : undefined const segment: AppSegment = { @@ -100,12 +104,22 @@ async function collectAppPageSegments(routeModule: AppPageRouteModule) { attach(segment, userland, routeModule.definition.pathname) } - segments.push(segment) + currentSegments.push(segment) + + // If this is a page segment, we know we've reached a leaf node associated with the + // page we're collecting segments for. We can add the collected segments to our final result. + if (name === PAGE_SEGMENT_KEY) { + segments.push(...currentSegments) + } - // Use this route's parallel route children as the next segment. - current = parallelRoutes.children + // Recursively process parallel routes + for (const parallelRouteKey in parallelRoutes) { + const parallelRoute = parallelRoutes[parallelRouteKey] + await processLoaderTree(parallelRoute, [...currentSegments]) + } } + await processLoaderTree(routeModule.userland.loaderTree) return segments } diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 15648b13107bb..9780d3d3bb1fa 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -1382,6 +1382,7 @@ export async function buildAppStaticPaths({ } ) + let lastDynamicSegmentHadGenerateStaticParams = false for (const segment of segments) { // Check to see if there are any missing params for segments that have // dynamicParams set to false. @@ -1402,6 +1403,15 @@ export async function buildAppStaticPaths({ ) } } + + if ( + segment.isDynamicSegment && + typeof segment.generateStaticParams !== 'function' + ) { + lastDynamicSegmentHadGenerateStaticParams = false + } else if (typeof segment.generateStaticParams === 'function') { + lastDynamicSegmentHadGenerateStaticParams = true + } } // Determine if all the segments have had their parameters provided. If there @@ -1437,7 +1447,9 @@ export async function buildAppStaticPaths({ let result: PartialStaticPathsResult = { fallbackMode, - prerenderedRoutes: undefined, + prerenderedRoutes: lastDynamicSegmentHadGenerateStaticParams + ? [] + : undefined, } if (hadAllParamsGenerated && fallbackMode) { diff --git a/test/production/app-dir/empty-generate-static-params/app/[slug]/page.tsx b/test/production/app-dir/empty-generate-static-params/app/[slug]/page.tsx new file mode 100644 index 0000000000000..08ff0ab06ba61 --- /dev/null +++ b/test/production/app-dir/empty-generate-static-params/app/[slug]/page.tsx @@ -0,0 +1,24 @@ +import { Suspense } from 'react' + +export default async function Page({ + params, +}: { + params: Promise<{ slug: string }> +}) { + return ( +
+ Hello World +
+ +
+
+ ) +} + +async function Params({ params }: { params: Promise<{ slug: string }> }) { + return {(await params).slug} +} + +export async function generateStaticParams() { + return [] +} diff --git a/test/production/app-dir/empty-generate-static-params/app/layout.tsx b/test/production/app-dir/empty-generate-static-params/app/layout.tsx new file mode 100644 index 0000000000000..00098bb74ebdc --- /dev/null +++ b/test/production/app-dir/empty-generate-static-params/app/layout.tsx @@ -0,0 +1,10 @@ +import { ReactNode, Suspense } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ) +} diff --git a/test/production/app-dir/empty-generate-static-params/app/page.tsx b/test/production/app-dir/empty-generate-static-params/app/page.tsx new file mode 100644 index 0000000000000..ff7159d9149fe --- /dev/null +++ b/test/production/app-dir/empty-generate-static-params/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/production/app-dir/empty-generate-static-params/empty-generate-static-params.test.ts b/test/production/app-dir/empty-generate-static-params/empty-generate-static-params.test.ts new file mode 100644 index 0000000000000..09920a5229a06 --- /dev/null +++ b/test/production/app-dir/empty-generate-static-params/empty-generate-static-params.test.ts @@ -0,0 +1,34 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('empty-generate-static-params', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + }) + + if (skipped) return + + it('should mark the page with empty generateStaticParams as SSG in build output', async () => { + const isPPREnabled = process.env.__NEXT_EXPERIMENTAL_PPR === 'true' + expect(next.cliOutput).toContain(`${isPPREnabled ? '◐' : '●'} /[slug]`) + }) + + it('should be a cache miss on the initial render followed by a HIT after being generated', async () => { + const firstResponse = await next.fetch('/foo') + expect(firstResponse.status).toBe(200) + + // With PPR enabled, the initial request doesn't send back a cache header + const isPPREnabled = process.env.__NEXT_EXPERIMENTAL_PPR === 'true' + + expect(firstResponse.headers.get('x-nextjs-cache')).toBe( + isPPREnabled ? null : 'MISS' + ) + + retry(async () => { + const secondResponse = await next.fetch('/foo') + expect(secondResponse.status).toBe(200) + expect(secondResponse.headers.get('x-nextjs-cache')).toBe('HIT') + }) + }) +}) diff --git a/test/production/app-dir/empty-generate-static-params/next.config.js b/test/production/app-dir/empty-generate-static-params/next.config.js new file mode 100644 index 0000000000000..807126e4cf0bf --- /dev/null +++ b/test/production/app-dir/empty-generate-static-params/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig