Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhance warning messages about unanalyzable config field #38907

Merged
merged 6 commits into from Jul 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
}
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'),
}