diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 0436a63ad1863..a8abe04a1c835 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -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 '../telemetry/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((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 ): [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, ] @@ -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}`) diff --git a/packages/next/src/shared/lib/zod.ts b/packages/next/src/shared/lib/zod.ts new file mode 100644 index 0000000000000..5070394672516 --- /dev/null +++ b/packages/next/src/shared/lib/zod.ts @@ -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((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 + }) +}