Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: empty generateStaticParams should still create an ISR route #73358

Merged
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
5 changes: 1 addition & 4 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2278,10 +2278,7 @@ export default async function build(
})
}

if (
workerResult.prerenderedRoutes &&
workerResult.prerenderedRoutes.length > 0
) {
if (workerResult.prerenderedRoutes) {
staticPaths.set(
originalAppPath,
workerResult.prerenderedRoutes
Expand Down
30 changes: 22 additions & 8 deletions packages/next/src/build/segment-config/app/app-segments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Params[]>

Expand Down Expand Up @@ -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<void> {
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 = {
Expand All @@ -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
}

Expand Down
14 changes: 13 additions & 1 deletion packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -1437,7 +1447,9 @@ export async function buildAppStaticPaths({

let result: PartialStaticPathsResult = {
fallbackMode,
prerenderedRoutes: undefined,
prerenderedRoutes: lastDynamicSegmentHadGenerateStaticParams
? []
: undefined,
}

if (hadAllParamsGenerated && fallbackMode) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Suspense } from 'react'

export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
return (
<div>
Hello World
<div>
<Params params={params} />
</div>
</div>
)
}

async function Params({ params }: { params: Promise<{ slug: string }> }) {
return <Suspense>{(await params).slug}</Suspense>
}

export async function generateStaticParams() {
return []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ReactNode, Suspense } from 'react'
export default function Root({ children }: { children: ReactNode }) {
return (
<html>
<body>
<Suspense>{children}</Suspense>
</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>hello world</p>
}
Original file line number Diff line number Diff line change
@@ -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')
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig
Loading