diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index cd81837f82675..17c77aca36d3a 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -3286,6 +3286,17 @@ export default async function build( } } + // After processing all static prerendered routes, check if any had revalidate: 0 + // and update the parent page accordingly. This handles the case where + // generateStaticParams was used but the routes turned out to be dynamic. + if (hasRevalidateZero && pageInfos.get(page)?.isSSG) { + pageInfos.set(page, { + ...(pageInfos.get(page) as PageInfo), + isStatic: false, + isSSG: false, + }) + } + if (!hasRevalidateZero && isDynamicRoute(page)) { // When PPR fallbacks aren't used, we need to include it here. If // they are enabled, then it'll already be included in the diff --git a/test/production/app-dir/build-output-tree-view/build-output-tree-view.test.ts b/test/production/app-dir/build-output-tree-view/build-output-tree-view.test.ts index 5524a14c072ee..ca813cba137e1 100644 --- a/test/production/app-dir/build-output-tree-view/build-output-tree-view.test.ts +++ b/test/production/app-dir/build-output-tree-view/build-output-tree-view.test.ts @@ -67,13 +67,83 @@ describe('build-output-tree-view', () => { `) }) }) + + describe('with dynamic access and generateStaticParams', () => { + describe.each([true, false])('cache components: %s', (cacheComponents) => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'fixtures/dynamic-generate-static-params'), + env: { + __NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT: '1', + }, + // We don't skip start in this test because we want to actually hit the + // dynamic pages, and starting again would cause the current API to + // re-build the app again. + nextConfig: { + experimental: { + cacheComponents, + }, + }, + }) + + it('should mark routes with connection() as dynamic, not SSG', async () => { + if (cacheComponents) { + // When cache components are enabled, routes with connection() should be + // marked as partial prerendered. + expect(getTreeView(next.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ /_not-found + └ ◐ /dynamic/[slug] + ├ /dynamic/[slug] + ├ /dynamic/slug-01 + ├ /dynamic/slug-02 + └ /dynamic/slug-03 + + + ○ (Static) prerendered as static content + ◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content" + `) + } else { + // Routes with generateStaticParams that use connection() should be marked as dynamic + // because connection() makes the page dynamic. This is a regression test for a bug + // where pages with generateStaticParams were incorrectly marked as SSG even when + // the individual routes used dynamic APIs. + expect(getTreeView(next.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ /_not-found + └ ƒ /dynamic/[slug] + ├ /dynamic/slug-01 + ├ /dynamic/slug-02 + └ /dynamic/slug-03 + + + ○ (Static) prerendered as static content + ƒ (Dynamic) server-rendered on demand" + `) + } + }) + + it('should render the dynamic pages correctly', async () => { + // Test one of the generated routes. We expect it to render and not + // error. + const $ = await next.render$('/dynamic/slug-01') + expect($('#page').text()).toBe('Page slug-01') + }) + }) + }) }) function getTreeView(cliOutput: string): string { let foundStart = false + let cliHeader = 0 const lines: string[] = [] for (const line of cliOutput.split('\n')) { + // Once we've seen the CLI header twice, we can stop reading the output, + // as we've already collected the first command (the `next build` command) + // and we can ignore the rest of the output. + if (line.includes('▲ Next.js')) cliHeader++ + if (cliHeader === 2) break + foundStart ||= line.startsWith('Route ') if (foundStart) { diff --git a/test/production/app-dir/build-output-tree-view/fixtures/dynamic-generate-static-params/app/dynamic/[slug]/page.tsx b/test/production/app-dir/build-output-tree-view/fixtures/dynamic-generate-static-params/app/dynamic/[slug]/page.tsx new file mode 100644 index 0000000000000..6c84de297f4db --- /dev/null +++ b/test/production/app-dir/build-output-tree-view/fixtures/dynamic-generate-static-params/app/dynamic/[slug]/page.tsx @@ -0,0 +1,16 @@ +import { connection } from 'next/server' + +export async function generateStaticParams() { + return [{ slug: 'slug-01' }, { slug: 'slug-02' }, { slug: 'slug-03' }] +} + +export default async function Page({ + params, +}: { + params: Promise<{ slug: string }> +}) { + await connection() + const { slug } = await params + + return