diff --git a/packages/next/build/analysis/extract-const-value.ts b/packages/next/build/analysis/extract-const-value.ts index db9ce53187e7a14..e067b1c73eeb764 100644 --- a/packages/next/build/analysis/extract-const-value.ts +++ b/packages/next/build/analysis/extract-const-value.ts @@ -53,7 +53,7 @@ export function extractExportedConstValue( decl.id.value === exportedName && decl.init ) { - return extractValue(decl.init) + return extractValue(decl.init, [exportedName]) } } } @@ -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)) { @@ -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] // ^^ @@ -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 @@ -169,10 +207,13 @@ 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 @@ -180,7 +221,10 @@ function extractValue(node: Node): any { // 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. @@ -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 + ) } } diff --git a/packages/next/build/analysis/get-page-static-info.ts b/packages/next/build/analysis/get-page-static-info.ts index 77dda52ce689c07..8d7353e74e4db34 100644 --- a/packages/next/build/analysis/get-page-static-info.ts +++ b/packages/next/build/analysis/get-page-static-info.ts @@ -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 } @@ -235,15 +235,23 @@ let warnedAboutExperimentalEdgeApiFunctions = false const warnedUnsupportedValueMap = new Map() 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) } diff --git a/test/production/exported-runtimes-value-validation/index.test.ts b/test/production/exported-runtimes-value-validation/index.test.ts index 70157cab6498ba1..f99474d7b60704e 100644 --- a/test/production/exported-runtimes-value-validation/index.test.ts +++ b/test/production/exported-runtimes-value-validation/index.test.ts @@ -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"' + ) + ) }) }) diff --git a/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/array-spread-operator.js b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/array-spread-operator.js new file mode 100644 index 000000000000000..189f83143bbe36f --- /dev/null +++ b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/array-spread-operator.js @@ -0,0 +1,7 @@ +export default function Page() { + return

hello world

+} + +export const config = { + runtime: [...['nodejs']], +} diff --git a/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/binary-expression.js b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/binary-expression.js new file mode 100644 index 000000000000000..dbcbce7d89d7d7b --- /dev/null +++ b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/binary-expression.js @@ -0,0 +1,7 @@ +export default function Page() { + return

hello world

+} + +export const config = { + runtime: 1 + 1 > 2, +} diff --git a/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/invalid-identifier.js b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/invalid-identifier.js new file mode 100644 index 000000000000000..3256ab3de77aa2b --- /dev/null +++ b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/invalid-identifier.js @@ -0,0 +1,9 @@ +export default function Page() { + return

hello world

+} + +const runtime = Symbol('runtime') + +export const config = { + runtime: runtime, +} diff --git a/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/object-spread-operator.js b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/object-spread-operator.js new file mode 100644 index 000000000000000..e52bf75a1c1ad34 --- /dev/null +++ b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/object-spread-operator.js @@ -0,0 +1,7 @@ +export default function Page() { + return

hello world

+} + +export const config = { + runtime: { ...{ a: 'b' } }, +} diff --git a/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/index.js b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/template-literal-with-expressions.js similarity index 100% rename from test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/index.js rename to test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/template-literal-with-expressions.js diff --git a/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/unsupported-object-key.js b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/unsupported-object-key.js new file mode 100644 index 000000000000000..f2c9e5b9a6bde34 --- /dev/null +++ b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/unsupported-object-key.js @@ -0,0 +1,9 @@ +export default function Page() { + return

hello world

+} + +export const config = { + runtime: { + [Symbol('nodejs')]: true, + }, +} diff --git a/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/unsupported-value-type.js b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/unsupported-value-type.js new file mode 100644 index 000000000000000..19e279d5ba74715 --- /dev/null +++ b/test/production/exported-runtimes-value-validation/unsupported-syntax/app/pages/unsupported-value-type.js @@ -0,0 +1,7 @@ +export default function Page() { + return

hello world

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