Skip to content

Commit d7d913a

Browse files
authored
feat: added error when there's missing root params in generateStaticParams (vercel#73933)
When partial prerendering is enabled, this enables a build time error that prints when there are no root params provided using `generateStaticParams`. This ensures that we're able to test the code around invocations of `rootParams()` for other dynamic usage. <!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: ## For Contributors ### Improving Documentation - Run `pnpm prettier-fix` to fix formatting issues before opening the PR. - Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide ### Adding or Updating Examples - The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md - Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md ### Fixing a bug - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ### Adding a feature - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ## For Maintainers - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Closes NEXT- Fixes # -->
1 parent c422755 commit d7d913a

File tree

20 files changed

+241
-20
lines changed

20 files changed

+241
-20
lines changed

packages/next/errors.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,5 +618,7 @@
618618
"617": "A required parameter (%s) was not provided as a string received %s in generateStaticParams for %s",
619619
"618": "A required parameter (%s) was not provided as an array received %s in generateStaticParams for %s",
620620
"619": "Page not found",
621-
"620": "A required parameter (%s) was not provided as %s received %s in getStaticPaths for %s"
621+
"620": "A required parameter (%s) was not provided as %s received %s in getStaticPaths for %s",
622+
"621": "Required root params (%s) were not provided in generateStaticParams for %s, please provide at least one value for each.",
623+
"622": "A required root parameter (%s) was not provided in generateStaticParams for %s, please provide at least one value."
622624
}

packages/next/src/build/static-paths/app.ts

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ function areParamValuesEqual(a: ParamValue, b: ParamValue) {
5151
/**
5252
* Filters out duplicate parameters from a list of parameters.
5353
*
54-
* @param paramKeys - The keys of the parameters.
54+
* @param routeParamKeys - The keys of the parameters.
5555
* @param routeParams - The list of parameters to filter.
5656
* @returns The list of unique parameters.
5757
*/
5858
function filterUniqueParams(
59-
paramKeys: readonly string[],
59+
routeParamKeys: readonly string[],
6060
routeParams: readonly Params[]
6161
): Params[] {
6262
const unique: Params[] = []
@@ -66,8 +66,8 @@ function filterUniqueParams(
6666
for (; i < unique.length; i++) {
6767
const item = unique[i]
6868
let j = 0
69-
for (; j < paramKeys.length; j++) {
70-
const key = paramKeys[j]
69+
for (; j < routeParamKeys.length; j++) {
70+
const key = routeParamKeys[j]
7171

7272
// If the param is not the same, then we need to break out of the loop.
7373
if (!areParamValuesEqual(item[key], params[key])) {
@@ -77,7 +77,7 @@ function filterUniqueParams(
7777

7878
// If we got to the end of the paramKeys array, then it means that we
7979
// found a duplicate. Skip it.
80-
if (j === paramKeys.length) {
80+
if (j === routeParamKeys.length) {
8181
break
8282
}
8383
}
@@ -159,23 +159,46 @@ function filterRootParamsCombinations(
159159
* @param page - The page to validate.
160160
* @param regex - The route regex.
161161
* @param isRoutePPREnabled - Whether the route has partial prerendering enabled.
162-
* @param paramKeys - The keys of the parameters.
162+
* @param routeParamKeys - The keys of the parameters.
163+
* @param rootParamKeys - The keys of the root params.
163164
* @param routeParams - The list of parameters to validate.
164165
* @returns The list of validated parameters.
165166
*/
166167
function validateParams(
167168
page: string,
168169
regex: RouteRegex,
169170
isRoutePPREnabled: boolean,
170-
paramKeys: readonly string[],
171+
routeParamKeys: readonly string[],
172+
rootParamKeys: readonly string[],
171173
routeParams: readonly Params[]
172174
): Params[] {
173175
const valid: Params[] = []
174176

177+
// Validate that if there are any root params, that the user has provided at
178+
// least one value for them only if we're using partial prerendering.
179+
if (isRoutePPREnabled && rootParamKeys.length > 0) {
180+
if (
181+
routeParams.length === 0 ||
182+
rootParamKeys.some((key) =>
183+
routeParams.some((params) => !(key in params))
184+
)
185+
) {
186+
if (rootParamKeys.length === 1) {
187+
throw new Error(
188+
`A required root parameter (${rootParamKeys[0]}) was not provided in generateStaticParams for ${page}, please provide at least one value.`
189+
)
190+
}
191+
192+
throw new Error(
193+
`Required root params (${rootParamKeys.join(', ')}) were not provided in generateStaticParams for ${page}, please provide at least one value for each.`
194+
)
195+
}
196+
}
197+
175198
for (const params of routeParams) {
176199
const item: Params = {}
177200

178-
for (const key of paramKeys) {
201+
for (const key of routeParamKeys) {
179202
const { repeat, optional } = regex.groups[key]
180203

181204
let paramValue = params[key]
@@ -309,7 +332,7 @@ export async function buildAppStaticPaths({
309332
})
310333

311334
const regex = getRouteRegex(page)
312-
const paramKeys = Object.keys(getRouteMatcher(regex)(page) || {})
335+
const routeParamKeys = Object.keys(getRouteMatcher(regex)(page) || {})
313336

314337
const afterRunner = new AfterRunner()
315338

@@ -425,10 +448,10 @@ export async function buildAppStaticPaths({
425448

426449
// Determine if all the segments have had their parameters provided.
427450
const hadAllParamsGenerated =
428-
paramKeys.length === 0 ||
451+
routeParamKeys.length === 0 ||
429452
(routeParams.length > 0 &&
430453
routeParams.every((params) => {
431-
for (const key of paramKeys) {
454+
for (const key of routeParamKeys) {
432455
if (key in params) continue
433456
return false
434457
}
@@ -463,25 +486,44 @@ export async function buildAppStaticPaths({
463486
if (hadAllParamsGenerated || isRoutePPREnabled) {
464487
if (isRoutePPREnabled) {
465488
// Discover all unique combinations of the rootParams so we can generate
466-
// shells for each of them.
489+
// shells for each of them if they're available.
467490
routeParams.unshift(
468-
// We're inserting an empty object at the beginning of the array so that
469-
// we can generate a shell for when all params are unknown.
470-
{},
471491
...filterRootParamsCombinations(rootParamKeys, routeParams)
472492
)
493+
494+
result.prerenderedRoutes ??= []
495+
result.prerenderedRoutes.push({
496+
pathname: page,
497+
encodedPathname: page,
498+
fallbackRouteParams: routeParamKeys,
499+
fallbackMode: dynamicParams
500+
? // If the fallback params includes any root params, then we need to
501+
// perform a blocking static render.
502+
rootParamKeys.length > 0
503+
? FallbackMode.BLOCKING_STATIC_RENDER
504+
: fallbackMode
505+
: FallbackMode.NOT_FOUND,
506+
fallbackRootParams: rootParamKeys,
507+
})
473508
}
474509

475510
filterUniqueParams(
476-
paramKeys,
477-
validateParams(page, regex, isRoutePPREnabled, paramKeys, routeParams)
511+
routeParamKeys,
512+
validateParams(
513+
page,
514+
regex,
515+
isRoutePPREnabled,
516+
routeParamKeys,
517+
rootParamKeys,
518+
routeParams
519+
)
478520
).forEach((params) => {
479521
let pathname: string = page
480522
let encodedPathname: string = page
481523

482524
const fallbackRouteParams: string[] = []
483525

484-
for (const key of paramKeys) {
526+
for (const key of routeParamKeys) {
485527
if (fallbackRouteParams.length > 0) {
486528
// This is a partial route, so we should add the value to the
487529
// fallbackRouteParams.

packages/next/src/build/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1116,7 +1116,10 @@ export async function isPageStatic({
11161116
appConfig.revalidate = 0
11171117
}
11181118

1119-
if (isDynamicRoute(page)) {
1119+
// If the page is dynamic and we're not in edge runtime, then we need to
1120+
// build the static paths. The edge runtime doesn't support static
1121+
// paths.
1122+
if (isDynamicRoute(page) && !pathIsEdgeRuntime) {
11201123
;({ prerenderedRoutes, fallbackMode: prerenderFallbackMode } =
11211124
await buildAppStaticPaths({
11221125
dir,

test/e2e/app-dir/app-root-params/fixtures/multiple-roots/app/(dashboard)/[id]/layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@ export default function Root({ children }: { children: ReactNode }) {
77
</html>
88
)
99
}
10+
11+
export const revalidate = 0
12+
export async function generateStaticParams() {
13+
return [{ id: '1' }]
14+
}

test/e2e/app-dir/app-root-params/fixtures/simple/app/[lang]/[locale]/layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@ export default function Root({ children }: { children: ReactNode }) {
77
</html>
88
)
99
}
10+
11+
export const revalidate = 0
12+
export async function generateStaticParams() {
13+
return [{ lang: 'en', locale: 'us' }]
14+
}

test/e2e/app-dir/dynamic-interception-route-revalidate/app/[locale]/layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,8 @@ export default function Root({
1616
</html>
1717
)
1818
}
19+
20+
export const revalidate = 0
21+
export async function generateStaticParams() {
22+
return [{ locale: 'en' }]
23+
}

test/e2e/app-dir/global-error/catch-all/app/[lang]/layout.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ export default async function RootLayout({ children }) {
77
}
88

99
export const dynamic = 'force-dynamic'
10+
export async function generateStaticParams() {
11+
return [{ lang: 'en' }]
12+
}

test/e2e/app-dir/parallel-route-not-found-params/app/[locale]/layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ export default async function Layout(props: {
1313
</html>
1414
)
1515
}
16+
17+
export const revalidate = 0
18+
export async function generateStaticParams() {
19+
return [{ locale: 'en' }]
20+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Root({ children }) {
2+
return (
3+
<html>
4+
<body>{children}</body>
5+
</html>
6+
)
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}

0 commit comments

Comments
 (0)