Skip to content

Commit

Permalink
feat: added error when there's missing root params in generateStaticP…
Browse files Browse the repository at this point in the history
…arams (#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 #

-->
  • Loading branch information
wyattjoh authored Dec 17, 2024
1 parent c422755 commit d7d913a
Show file tree
Hide file tree
Showing 20 changed files with 241 additions and 20 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
5 changes: 4 additions & 1 deletion packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1116,7 +1116,10 @@ export async function isPageStatic({
appConfig.revalidate = 0
}

if (isDynamicRoute(page)) {
// If the page is dynamic and we're not in edge runtime, then we need to
// build the static paths. The edge runtime doesn't support static
// paths.
if (isDynamicRoute(page) && !pathIsEdgeRuntime) {
;({ prerenderedRoutes, fallbackMode: prerenderFallbackMode } =
await buildAppStaticPaths({
dir,
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
@@ -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 d7d913a

Please sign in to comment.