Skip to content

Commit

Permalink
feat: added error when there's missing root params
Browse files Browse the repository at this point in the history
  • Loading branch information
wyattjoh committed Dec 16, 2024
1 parent 9cb16ca commit 040105c
Show file tree
Hide file tree
Showing 20 changed files with 242 additions and 19 deletions.
4 changes: 3 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -618,5 +618,7 @@
"617": "A required parameter (%s) was not provided as a string received %s in generateStaticParams for %s",
"618": "A required parameter (%s) was not provided as an array received %s in generateStaticParams for %s",
"619": "Page not found",
"620": "A required parameter (%s) was not provided as %s received %s in getStaticPaths for %s"
"620": "A required parameter (%s) was not provided as %s received %s in getStaticPaths for %s",
"621": "Required root params (%s) were not provided in generateStaticParams for %s, please provide at least one value for each.",
"622": "A required root parameter (%s) was not provided in generateStaticParams for %s, please provide at least one value."
}
78 changes: 60 additions & 18 deletions packages/next/src/build/static-paths/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ function areParamValuesEqual(a: ParamValue, b: ParamValue) {
/**
* Filters out duplicate parameters from a list of parameters.
*
* @param paramKeys - The keys of the parameters.
* @param routeParamKeys - The keys of the parameters.
* @param routeParams - The list of parameters to filter.
* @returns The list of unique parameters.
*/
function filterUniqueParams(
paramKeys: readonly string[],
routeParamKeys: readonly string[],
routeParams: readonly Params[]
): Params[] {
const unique: Params[] = []
Expand All @@ -66,8 +66,8 @@ function filterUniqueParams(
for (; i < unique.length; i++) {
const item = unique[i]
let j = 0
for (; j < paramKeys.length; j++) {
const key = paramKeys[j]
for (; j < routeParamKeys.length; j++) {
const key = routeParamKeys[j]

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

// If we got to the end of the paramKeys array, then it means that we
// found a duplicate. Skip it.
if (j === paramKeys.length) {
if (j === routeParamKeys.length) {
break
}
}
Expand Down Expand Up @@ -159,23 +159,46 @@ function filterRootParamsCombinations(
* @param page - The page to validate.
* @param regex - The route regex.
* @param isRoutePPREnabled - Whether the route has partial prerendering enabled.
* @param paramKeys - The keys of the parameters.
* @param routeParamKeys - The keys of the parameters.
* @param rootParamKeys - The keys of the root params.
* @param routeParams - The list of parameters to validate.
* @returns The list of validated parameters.
*/
function validateParams(
page: string,
regex: RouteRegex,
isRoutePPREnabled: boolean,
paramKeys: readonly string[],
routeParamKeys: readonly string[],
rootParamKeys: readonly string[],
routeParams: readonly Params[]
): Params[] {
const valid: Params[] = []

// Validate that if there are any root params, that the user has provided at
// least one value for them only if we're using partial prerendering.
if (isRoutePPREnabled && rootParamKeys.length > 0) {
if (
routeParams.length === 0 ||
rootParamKeys.some((key) =>
routeParams.some((params) => !(key in params))
)
) {
if (rootParamKeys.length === 1) {
throw new Error(
`A required root parameter (${rootParamKeys[0]}) was not provided in generateStaticParams for ${page}, please provide at least one value.`
)
}

throw new Error(
`Required root params (${rootParamKeys.join(', ')}) were not provided in generateStaticParams for ${page}, please provide at least one value for each.`
)
}
}

for (const params of routeParams) {
const item: Params = {}

for (const key of paramKeys) {
for (const key of routeParamKeys) {
const { repeat, optional } = regex.groups[key]

let paramValue = params[key]
Expand Down Expand Up @@ -309,7 +332,7 @@ export async function buildAppStaticPaths({
})

const regex = getRouteRegex(page)
const paramKeys = Object.keys(getRouteMatcher(regex)(page) || {})
const routeParamKeys = Object.keys(getRouteMatcher(regex)(page) || {})

const afterRunner = new AfterRunner()

Expand Down Expand Up @@ -425,10 +448,10 @@ export async function buildAppStaticPaths({

// Determine if all the segments have had their parameters provided.
const hadAllParamsGenerated =
paramKeys.length === 0 ||
routeParamKeys.length === 0 ||
(routeParams.length > 0 &&
routeParams.every((params) => {
for (const key of paramKeys) {
for (const key of routeParamKeys) {
if (key in params) continue
return false
}
Expand Down Expand Up @@ -463,25 +486,44 @@ export async function buildAppStaticPaths({
if (hadAllParamsGenerated || isRoutePPREnabled) {
if (isRoutePPREnabled) {
// Discover all unique combinations of the rootParams so we can generate
// shells for each of them.
// shells for each of them if they're available.
routeParams.unshift(
// We're inserting an empty object at the beginning of the array so that
// we can generate a shell for when all params are unknown.
{},
...filterRootParamsCombinations(rootParamKeys, routeParams)
)

result.prerenderedRoutes ??= []
result.prerenderedRoutes.push({
pathname: page,
encodedPathname: page,
fallbackRouteParams: routeParamKeys,
fallbackMode: dynamicParams
? // If the fallback params includes any root params, then we need to
// perform a blocking static render.
rootParamKeys.length > 0
? FallbackMode.BLOCKING_STATIC_RENDER
: fallbackMode
: FallbackMode.NOT_FOUND,
fallbackRootParams: rootParamKeys,
})
}

filterUniqueParams(
paramKeys,
validateParams(page, regex, isRoutePPREnabled, paramKeys, routeParams)
routeParamKeys,
validateParams(
page,
regex,
isRoutePPREnabled,
routeParamKeys,
rootParamKeys,
routeParams
)
).forEach((params) => {
let pathname: string = page
let encodedPathname: string = page

const fallbackRouteParams: string[] = []

for (const key of paramKeys) {
for (const key of routeParamKeys) {
if (fallbackRouteParams.length > 0) {
// This is a partial route, so we should add the value to the
// fallbackRouteParams.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ export default function Root({ children }: { children: ReactNode }) {
</html>
)
}

export const revalidate = 0
export async function generateStaticParams() {
return [{ id: '1' }]
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ export default function Root({ children }: { children: ReactNode }) {
</html>
)
}

export const revalidate = 0
export async function generateStaticParams() {
return [{ lang: 'en', locale: 'us' }]
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ export default function Root({
</html>
)
}

export const revalidate = 0
export async function generateStaticParams() {
return [{ locale: 'en' }]
}
3 changes: 3 additions & 0 deletions test/e2e/app-dir/global-error/catch-all/app/[lang]/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ export default async function RootLayout({ children }) {
}

export const dynamic = 'force-dynamic'
export async function generateStaticParams() {
return [{ lang: 'en' }]
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ export default async function Layout(props: {
</html>
)
}

export const revalidate = 0
export async function generateStaticParams() {
return [{ locale: 'en' }]
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ export default async function RootLayout({
</html>
)
}

export const revalidate = 0
export const generateStaticParams = async () => {
return [{ locale: 'en' }]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Root({ children }) {
return (
<html>
<body>{children}</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,11 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
experimental: {
ppr: true,
dynamicIO: true,
},
}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Page() {
return <p>hello world</p>
}

export async function generateStaticParams() {
return [{ slug: 'hello-world' }]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Root({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
experimental: {
ppr: true,
dynamicIO: true,
},
}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Root({ children }) {
return (
<html>
<body>{children}</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,11 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
experimental: {
ppr: true,
dynamicIO: true,
},
}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { nextTestSetup } from 'e2e-utils'
import path from 'path'

describe('ppr-missing-root-params (single)', () => {
const { next, isNextDev } = nextTestSetup({
files: path.join(__dirname, 'fixtures/single'),
skipStart: true,
skipDeployment: true,
})

beforeAll(async () => {
try {
await next.start()
} catch {}
})

it('should result in a build error', async () => {
if (isNextDev) {
await next.fetch('/en')
}

expect(next.cliOutput).toContain(
`Error: A required root parameter (lang) was not provided in generateStaticParams for /[lang], please provide at least one value.`
)
})
})

describe('ppr-missing-root-params (multiple)', () => {
const { next, isNextDev } = nextTestSetup({
files: path.join(__dirname, 'fixtures/multiple'),
skipStart: true,
skipDeployment: true,
})

beforeAll(async () => {
try {
await next.start()
} catch {}
})

it('should result in a build error', async () => {
if (isNextDev) {
await next.fetch('/en/us')
}

expect(next.cliOutput).toContain(
`Error: Required root params (lang, region) were not provided in generateStaticParams for /[lang]/[region], please provide at least one value for each.`
)
})
})

describe('ppr-missing-root-params (nested)', () => {
const { next, isNextDev } = nextTestSetup({
files: path.join(__dirname, 'fixtures/nested'),
skipStart: true,
skipDeployment: true,
})

beforeAll(async () => {
try {
await next.start()
} catch {}
})

it('should result in a build error', async () => {
if (isNextDev) {
await next.fetch('/en/blog/hello')
}

expect(next.cliOutput).toContain(
`Error: A required root parameter (lang) was not provided in generateStaticParams for /[lang]/blog/[slug], please provide at least one value.`
)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ export default function Root({ children }) {
</html>
)
}

export const revalidate = 0
export async function generateStaticParams() {
return [{ slug: ['slug'] }]
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ export default async function Layout({
export async function generateMetadata() {
return {}
}

export const revalidate = 0
export async function generateStaticParams() {
return [{ param: 'test' }]
}

0 comments on commit 040105c

Please sign in to comment.