Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <div id="page">Page {slug}</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ReactNode, Suspense } from 'react'

export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>
<Suspense>{children}</Suspense>
</body>
</html>
)
}
Loading