Skip to content

Commit

Permalink
feat: enhance warning messages about unanalyzable config field (#38907
Browse files Browse the repository at this point in the history
)

x-ref: #38750
x-ref: #38750 (comment)

cc @ijjk 

The PR implements the details about un-extractable fields in the `UnsupportedValueError`.

The PR also enhances the warning message about the unrecognizable `config` field. Now the warning message will look like this:

```
warn  - Next.js can't recognize the exported `config` field in route "/unsupported-value-type":
Unsupported node type "CallExpression" at "config.runtime".
The default config will be used instead.
Read More - https://nextjs.org/docs/messages/invalid-page-config
```

The corresponding production test case has also been updated.
  • Loading branch information
SukkaW committed Jul 22, 2022
1 parent e8a0492 commit 25d3405
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 23 deletions.
71 changes: 59 additions & 12 deletions packages/next/build/analysis/extract-const-value.ts
Expand Up @@ -53,7 +53,7 @@ export function extractExportedConstValue(
decl.id.value === exportedName &&
decl.init
) {
return extractValue(decl.init)
return extractValue(decl.init, [exportedName])
}
}
}
Expand Down Expand Up @@ -109,10 +109,38 @@ function isTemplateLiteral(node: Node): node is TemplateLiteral {
return node.type === 'TemplateLiteral'
}

export class UnsupportedValueError extends Error {}
export class UnsupportedValueError extends Error {
/** @example `config.runtime[0].value` */
path?: string

constructor(message: string, paths?: string[]) {
super(message)

// Generating "path" that looks like "config.runtime[0].value"
let codePath: string | undefined
if (paths) {
codePath = ''
for (const path of paths) {
if (path[0] === '[') {
// "array" + "[0]"
codePath += path
} else {
if (codePath === '') {
codePath = path
} else {
// "object" + ".key"
codePath += `.${path}`
}
}
}
}

this.path = codePath
}
}
export class NoSuchDeclarationError extends Error {}

function extractValue(node: Node): any {
function extractValue(node: Node, path?: string[]): any {
if (isNullLiteral(node)) {
return null
} else if (isBooleanLiteral(node)) {
Expand All @@ -132,19 +160,26 @@ function extractValue(node: Node): any {
case 'undefined':
return undefined
default:
throw new UnsupportedValueError()
throw new UnsupportedValueError(
`Unknown identifier "${node.value}"`,
path
)
}
} else if (isArrayExpression(node)) {
// e.g. [1, 2, 3]
const arr = []
for (const elem of node.elements) {
for (let i = 0, len = node.elements.length; i < len; i++) {
const elem = node.elements[i]
if (elem) {
if (elem.spread) {
// e.g. [ ...a ]
throw new UnsupportedValueError()
throw new UnsupportedValueError(
'Unsupported spread operator in the Array Expression',
path
)
}

arr.push(extractValue(elem.expression))
arr.push(extractValue(elem.expression, path && [...path, `[${i}]`]))
} else {
// e.g. [1, , 2]
// ^^
Expand All @@ -158,7 +193,10 @@ function extractValue(node: Node): any {
for (const prop of node.properties) {
if (!isKeyValueProperty(prop)) {
// e.g. { ...a }
throw new UnsupportedValueError()
throw new UnsupportedValueError(
'Unsupported spread operator in the Object Expression',
path
)
}

let key
Expand All @@ -169,18 +207,24 @@ function extractValue(node: Node): any {
// e.g. { "a": 1, "b": 2 }
key = prop.key.value
} else {
throw new UnsupportedValueError()
throw new UnsupportedValueError(
`Unsupported key type "${prop.key.type}" in the Object Expression`,
path
)
}

obj[key] = extractValue(prop.value)
obj[key] = extractValue(prop.value, path && [...path, key])
}

return obj
} else if (isTemplateLiteral(node)) {
// e.g. `abc`
if (node.expressions.length !== 0) {
// TODO: should we add support for `${'e'}d${'g'}'e'`?
throw new UnsupportedValueError()
throw new UnsupportedValueError(
'Unsupported template literal with expressions',
path
)
}

// When TemplateLiteral has 0 expressions, the length of quasis is always 1.
Expand All @@ -196,6 +240,9 @@ function extractValue(node: Node): any {

return cooked ?? raw
} else {
throw new UnsupportedValueError()
throw new UnsupportedValueError(
`Unsupported node type "${node.type}"`,
path
)
}
}
18 changes: 13 additions & 5 deletions packages/next/build/analysis/get-page-static-info.ts
Expand Up @@ -48,7 +48,7 @@ export async function getPageStaticInfo(params: {
config = extractExportedConstValue(swcAST, 'config')
} catch (e) {
if (e instanceof UnsupportedValueError) {
warnAboutUnsupportedValue(pageFilePath, page)
warnAboutUnsupportedValue(pageFilePath, page, e)
}
// `export config` doesn't exist, or other unknown error throw by swc, silence them
}
Expand Down Expand Up @@ -235,15 +235,23 @@ let warnedAboutExperimentalEdgeApiFunctions = false
const warnedUnsupportedValueMap = new Map<string, boolean>()
function warnAboutUnsupportedValue(
pageFilePath: string,
page: string | undefined
page: string | undefined,
error: UnsupportedValueError
) {
if (warnedUnsupportedValueMap.has(pageFilePath)) {
return
}

Log.warn(
`You have exported a \`config\` field in ${
page ? `route "${page}"` : `"${pageFilePath}"`
} that Next.js can't recognize, so it will be ignored. See: https://nextjs.org/docs/messages/invalid-page-config`
`Next.js can't recognize the exported \`config\` field in ` +
(page ? `route "${page}"` : `"${pageFilePath}"`) +
':\n' +
error.message +
(error.path ? ` at "${error.path}"` : '') +
'.\n' +
'The default config will be used instead.\n' +
'Read More - https://nextjs.org/docs/messages/invalid-page-config'
)

warnedUnsupportedValueMap.set(pageFilePath, true)
}
88 changes: 82 additions & 6 deletions test/production/exported-runtimes-value-validation/index.test.ts
Expand Up @@ -23,11 +23,87 @@ describe('Exported runtimes value validation', () => {
{ stdout: true, stderr: true }
)

expect(result).toMatchObject({
code: 0,
stderr: expect.stringContaining(
`You have exported a \`config\` field in route "/" that Next.js can't recognize, so it will be ignored`
),
})
console.log(result.stderr, result.stdout)

// The build should still succeed with invalid config being ignored
expect(result.code).toBe(0)

// Template Literal with Expressions
expect(result.stderr).toEqual(
expect.stringContaining(
'Next.js can\'t recognize the exported `config` field in route "/template-literal-with-expressions"'
)
)
expect(result.stderr).toEqual(
expect.stringContaining(
'Unsupported template literal with expressions at "config.runtime".'
)
)
// Binary Expression
expect(result.stderr).toEqual(
expect.stringContaining(
'Next.js can\'t recognize the exported `config` field in route "/binary-expression"'
)
)
expect(result.stderr).toEqual(
expect.stringContaining(
'Unsupported node type "BinaryExpression" at "config.runtime"'
)
)
// Spread Operator within Object Expression
expect(result.stderr).toEqual(
expect.stringContaining(
'Next.js can\'t recognize the exported `config` field in route "/object-spread-operator"'
)
)
expect(result.stderr).toEqual(
expect.stringContaining(
'Unsupported spread operator in the Object Expression at "config.runtime"'
)
)
// Spread Operator within Array Expression
expect(result.stderr).toEqual(
expect.stringContaining(
'Next.js can\'t recognize the exported `config` field in route "/array-spread-operator"'
)
)
expect(result.stderr).toEqual(
expect.stringContaining(
'Unsupported spread operator in the Array Expression at "config.runtime"'
)
)
// Unknown Identifier
expect(result.stderr).toEqual(
expect.stringContaining(
'Next.js can\'t recognize the exported `config` field in route "/invalid-identifier"'
)
)
expect(result.stderr).toEqual(
expect.stringContaining(
'Unknown identifier "runtime" at "config.runtime".'
)
)
// Unknown Expression Type
expect(result.stderr).toEqual(
expect.stringContaining(
'Next.js can\'t recognize the exported `config` field in route "/unsupported-value-type"'
)
)
expect(result.stderr).toEqual(
expect.stringContaining(
'Unsupported node type "CallExpression" at "config.runtime"'
)
)
// Unknown Object Key
expect(result.stderr).toEqual(
expect.stringContaining(
'Next.js can\'t recognize the exported `config` field in route "/unsupported-object-key"'
)
)
expect(result.stderr).toEqual(
expect.stringContaining(
'Unsupported key type "Computed" in the Object Expression at "config.runtime"'
)
)
})
})
@@ -0,0 +1,7 @@
export default function Page() {
return <p>hello world</p>
}

export const config = {
runtime: [...['nodejs']],
}
@@ -0,0 +1,7 @@
export default function Page() {
return <p>hello world</p>
}

export const config = {
runtime: 1 + 1 > 2,
}
@@ -0,0 +1,9 @@
export default function Page() {
return <p>hello world</p>
}

const runtime = Symbol('runtime')

export const config = {
runtime: runtime,
}
@@ -0,0 +1,7 @@
export default function Page() {
return <p>hello world</p>
}

export const config = {
runtime: { ...{ a: 'b' } },
}
@@ -0,0 +1,9 @@
export default function Page() {
return <p>hello world</p>
}

export const config = {
runtime: {
[Symbol('nodejs')]: true,
},
}
@@ -0,0 +1,7 @@
export default function Page() {
return <p>hello world</p>
}

export const config = {
runtime: Symbol('nodejs'),
}

0 comments on commit 25d3405

Please sign in to comment.