Skip to content

Commit

Permalink
feat(config): implement a human readable ajv errors (#39291)
Browse files Browse the repository at this point in the history
  • Loading branch information
SukkaW committed Aug 3, 2022
1 parent 2f49a4f commit 45ae757
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 18 deletions.
1 change: 1 addition & 0 deletions packages/next/compiled/@segment/ajv-human-errors/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -0,0 +1 @@
{"name":"@segment/ajv-human-errors","main":"index.js","license":"MIT"}
1 change: 1 addition & 0 deletions packages/next/package.json
Expand Up @@ -126,6 +126,7 @@
"@next/react-dev-overlay": "12.2.4-canary.10",
"@next/react-refresh-utils": "12.2.4-canary.10",
"@next/swc": "12.2.4-canary.10",
"@segment/ajv-human-errors": "2.1.2",
"@taskr/clear": "1.1.0",
"@taskr/esnext": "1.1.0",
"@taskr/watch": "1.1.0",
Expand Down
7 changes: 7 additions & 0 deletions packages/next/server/config-schema.ts
Expand Up @@ -393,6 +393,7 @@ const configSchema = {
},
exportPathMap: {
isFunction: true,
errorMessage: 'must be a function that returns a Promise',
} as any,
future: {
additionalProperties: false,
Expand All @@ -401,12 +402,14 @@ const configSchema = {
},
generateBuildId: {
isFunction: true,
errorMessage: 'must be a function that returns a Promise',
} as any,
generateEtags: {
type: 'boolean',
},
headers: {
isFunction: true,
errorMessage: 'must be a function that returns a Promise',
} as any,
httpAgentOptions: {
additionalProperties: false,
Expand Down Expand Up @@ -560,9 +563,11 @@ const configSchema = {
},
redirects: {
isFunction: true,
errorMessage: 'must be a function that returns a Promise',
} as any,
rewrites: {
isFunction: true,
errorMessage: 'must be a function that returns a Promise',
} as any,
sassOptions: {
type: 'object',
Expand Down Expand Up @@ -597,6 +602,8 @@ const configSchema = {
},
webpack: {
isFunction: true,
errorMessage:
'must be a function that returns a webpack configuration object',
} as any,
},
} as JSONSchemaType<NextConfig>
Expand Down
12 changes: 11 additions & 1 deletion packages/next/server/config.ts
Expand Up @@ -772,8 +772,18 @@ export default async function loadConfig(

if (validateResult.errors) {
Log.warn(`Invalid next.config.js options detected: `)

// Only load @segment/ajv-human-errors when invalid config is detected
const { AggregateAjvError } =
require('next/dist/compiled/@segment/ajv-human-errors') as typeof import('next/dist/compiled/@segment/ajv-human-errors')
const aggregatedAjvErrors = new AggregateAjvError(validateResult.errors, {
fieldLabels: 'js',
})
for (const error of aggregatedAjvErrors) {
console.error(` - ${error.message}`)
}

console.error(
JSON.stringify(validateResult.errors, null, 2),
'\nSee more info here: https://nextjs.org/docs/messages/invalid-next-config'
)
}
Expand Down
24 changes: 23 additions & 1 deletion packages/next/taskfile.js
Expand Up @@ -188,7 +188,15 @@ export async function compile_config_schema(task, opts) {
// eslint-disable-next-line
const standaloneCode = require('ajv/dist/standalone').default
// eslint-disable-next-line
const ajv = new Ajv({ code: { source: true }, allErrors: true })
const ajv = new Ajv({
code: { source: true },
allErrors: true,
verbose: true,
})

// errorMessage keyword will be consumed by @segment/ajv-human-errors to provide a custom error message
ajv.addKeyword('errorMessage')

ajv.addKeyword({
keyword: 'isFunction',
schemaType: 'boolean',
Expand Down Expand Up @@ -951,6 +959,19 @@ export async function ncc_async_sema(task, opts) {
.ncc({ packageName: 'async-sema', externals })
.target('compiled/async-sema')
}
// eslint-disable-next-line camelcase
export async function ncc_segment_ajv_human_errors(task, opts) {
await task
.source(
opts.src ||
relative(__dirname, require.resolve('@segment/ajv-human-errors/'))
)
.ncc({
packageName: '@segment/ajv-human-errors/',
externals,
})
.target('compiled/@segment/ajv-human-errors')
}

const babelCorePackages = {
'code-frame': 'next/dist/compiled/babel/code-frame',
Expand Down Expand Up @@ -1803,6 +1824,7 @@ export async function ncc(task, opts) {
'ncc_arg',
'ncc_async_retry',
'ncc_async_sema',
'ncc_segment_ajv_human_errors',
'ncc_assert',
'ncc_browser_zlib',
'ncc_buffer',
Expand Down
5 changes: 5 additions & 0 deletions packages/next/types/misc.d.ts
Expand Up @@ -329,6 +329,11 @@ declare module 'next/dist/compiled/@edge-runtime/primitives' {
export = m
}

declare module 'next/dist/compiled/@segment/ajv-human-errors' {
import * as m from '@segment/ajv-human-errors'
export = m
}

declare module 'pnp-webpack-plugin' {
import webpack from 'webpack4'

Expand Down
31 changes: 26 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 5 additions & 11 deletions test/integration/config-validation/test/index.test.ts
Expand Up @@ -18,13 +18,9 @@ describe('next.config.js validation', () => {
}
`,
outputs: [
'/images/loader',
'must be equal to one of the allowed values',
'imgix',
'/rewrites',
'must pass \\"isFunction\\" keyword validation',
'/swcMinify',
'must be boolean',
'The value at .images.loader must be one of',
'The value at .rewrites must be a function that returns a Promise',
'The value at .swcMinify must be a boolean but it was a string',
],
},
{
Expand All @@ -38,10 +34,8 @@ describe('next.config.js validation', () => {
}
`,
outputs: [
'nonExistent',
'must NOT have additional properties',
'anotherNonExistent',
'must NOT have additional properties',
'The root value has an unexpected property, nonExistent,',
'The value at .experimental has an unexpected property, anotherNonExistent',
],
},
])(
Expand Down

0 comments on commit 45ae757

Please sign in to comment.