Skip to content

Commit 2e244ed

Browse files
committed
feat: added error when there's missing root params
1 parent c422755 commit 2e244ed

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)