Skip to content

Commit

Permalink
refactor: extracted zod configuration (vercel#70478)
Browse files Browse the repository at this point in the history
<!-- 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 #

-->
This just moves the zod helper utilities into it's own file so we can
re-use it in future PR's.
  • Loading branch information
wyattjoh authored and abhi12299 committed Sep 29, 2024
1 parent 4d1e753 commit a4aed41
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 66 deletions.
75 changes: 9 additions & 66 deletions packages/next/src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,87 +22,28 @@ import { setHttpClientAndAgentOptions } from './setup-http-agent-env'
import { pathHasPrefix } from '../shared/lib/router/utils/path-has-prefix'
import { matchRemotePattern } from '../shared/lib/match-remote-pattern'

import { ZodParsedType, util as ZodUtil } from 'next/dist/compiled/zod'
import type { ZodError, ZodIssue } from 'next/dist/compiled/zod'
import type { ZodError } from 'next/dist/compiled/zod'
import { hasNextSupport } from '../server/ci-info'
import { transpileConfig } from '../build/next-config-ts/transpile-config'
import { dset } from '../shared/lib/dset'
import { normalizeZodErrors } from '../shared/lib/zod'

export { normalizeConfig } from './config-shared'
export type { DomainLocale, NextConfig } from './config-shared'

function processZodErrorMessage(issue: ZodIssue) {
let message = issue.message

let path = ''

if (issue.path.length > 0) {
if (issue.path.length === 1) {
const identifier = issue.path[0]
if (typeof identifier === 'number') {
// The first identifier inside path is a number
path = `index ${identifier}`
} else {
path = `"${identifier}"`
}
} else {
// joined path to be shown in the error message
path = `"${issue.path.reduce<string>((acc, cur) => {
if (typeof cur === 'number') {
// array index
return `${acc}[${cur}]`
}
if (cur.includes('"')) {
// escape quotes
return `${acc}["${cur.replaceAll('"', '\\"')}"]`
}
// dot notation
const separator = acc.length === 0 ? '' : '.'
return acc + separator + cur
}, '')}"`
}
}

if (
issue.code === 'invalid_type' &&
issue.received === ZodParsedType.undefined
) {
// missing key in object
return `${path} is missing, expected ${issue.expected}`
}
if (issue.code === 'invalid_enum_value') {
// Remove "Invalid enum value" prefix from zod default error message
return `Expected ${ZodUtil.joinValues(issue.options)}, received '${
issue.received
}' at ${path}`
}

return message + (path ? ` at ${path}` : '')
}

function normalizeZodErrors(
function normalizeNextConfigZodErrors(
error: ZodError<NextConfig>
): [errorMessages: string[], shouldExit: boolean] {
let shouldExit = false
const issues = normalizeZodErrors(error)
return [
error.issues.flatMap((issue) => {
const messages = [processZodErrorMessage(issue)]
issues.flatMap(({ issue, message }) => {
if (issue.path[0] === 'images') {
// We exit the build when encountering an error in the images config
shouldExit = true
}

if ('unionErrors' in issue) {
issue.unionErrors
.map(normalizeZodErrors)
.forEach(([unionMessages, unionShouldExit]) => {
messages.push(...unionMessages)
// If any of the union results shows exit the build, we exit the build
shouldExit = shouldExit || unionShouldExit
})
}

return messages
return message
}),
shouldExit,
]
Expand Down Expand Up @@ -1085,7 +1026,9 @@ export default async function loadConfig(
// error message header
const messages = [`Invalid ${configFileName} options detected: `]

const [errorMessages, shouldExit] = normalizeZodErrors(state.error)
const [errorMessages, shouldExit] = normalizeNextConfigZodErrors(
state.error
)
// ident list item
for (const error of errorMessages) {
messages.push(` ${error}`)
Expand Down
67 changes: 67 additions & 0 deletions packages/next/src/shared/lib/zod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { ZodError } from 'next/dist/compiled/zod'
import { ZodParsedType, util, type ZodIssue } from 'next/dist/compiled/zod'

function processZodErrorMessage(issue: ZodIssue) {
let message = issue.message

let path: string

if (issue.path.length > 0) {
if (issue.path.length === 1) {
const identifier = issue.path[0]
if (typeof identifier === 'number') {
// The first identifier inside path is a number
path = `index ${identifier}`
} else {
path = `"${identifier}"`
}
} else {
// joined path to be shown in the error message
path = `"${issue.path.reduce<string>((acc, cur) => {
if (typeof cur === 'number') {
// array index
return `${acc}[${cur}]`
}
if (cur.includes('"')) {
// escape quotes
return `${acc}["${cur.replaceAll('"', '\\"')}"]`
}
// dot notation
const separator = acc.length === 0 ? '' : '.'
return acc + separator + cur
}, '')}"`
}
} else {
path = ''
}

if (
issue.code === 'invalid_type' &&
issue.received === ZodParsedType.undefined
) {
// Missing key in object.
return `${path} is missing, expected ${issue.expected}`
}

if (issue.code === 'invalid_enum_value') {
// Remove "Invalid enum value" prefix from zod default error message
return `Expected ${util.joinValues(issue.options)}, received '${
issue.received
}' at ${path}`
}

return message + (path ? ` at ${path}` : '')
}

export function normalizeZodErrors(error: ZodError) {
return error.issues.flatMap((issue) => {
const issues = [{ issue, message: processZodErrorMessage(issue) }]
if ('unionErrors' in issue) {
for (const unionError of issue.unionErrors) {
issues.push(...normalizeZodErrors(unionError))
}
}

return issues
})
}

0 comments on commit a4aed41

Please sign in to comment.