Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 66 additions & 68 deletions packages/next/src/build/analysis/extract-const-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ import type {
VariableDeclaration,
} from '@swc/core'

export class NoSuchDeclarationError extends Error {}

function isExportDeclaration(node: Node): node is ExportDeclaration {
return node.type === 'ExportDeclaration'
}
Expand Down Expand Up @@ -70,60 +68,52 @@ function isTsSatisfiesExpression(node: Node): node is TsSatisfiesExpression {
return node.type === 'TsSatisfiesExpression'
}

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}`
}
}
}
export type ExtractValueResult =
| { value: any }
| { unsupported: string; path?: string }

/** Formats a path array like `["config", "runtime", "[0]", "value"]` → `"config.runtime[0].value"` */
function formatCodePath(paths?: string[]): string | undefined {
if (!paths) return undefined
let 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
}
return codePath
}

function extractValue(node: Node, path?: string[]): any {
function extractValue(node: Node, path?: string[]): ExtractValueResult {
if (isNullLiteral(node)) {
return null
return { value: null }
} else if (isBooleanLiteral(node)) {
// e.g. true / false
return node.value
return { value: node.value }
} else if (isStringLiteral(node)) {
// e.g. "abc"
return node.value
return { value: node.value }
} else if (isNumericLiteral(node)) {
// e.g. 123
return node.value
return { value: node.value }
} else if (isRegExpLiteral(node)) {
// e.g. /abc/i
return new RegExp(node.pattern, node.flags)
return { value: new RegExp(node.pattern, node.flags) }
} else if (isIdentifier(node)) {
switch (node.value) {
case 'undefined':
return undefined
return { value: undefined }
default:
throw new UnsupportedValueError(
`Unknown identifier "${node.value}"`,
path
)
return {
unsupported: `Unknown identifier "${node.value}"`,
path: formatCodePath(path),
}
}
} else if (isArrayExpression(node)) {
// e.g. [1, 2, 3]
Expand All @@ -133,30 +123,35 @@ function extractValue(node: Node, path?: string[]): any {
if (elem) {
if (elem.spread) {
// e.g. [ ...a ]
throw new UnsupportedValueError(
'Unsupported spread operator in the Array Expression',
path
)
return {
unsupported: 'Unsupported spread operator in the Array Expression',
path: formatCodePath(path),
}
}

arr.push(extractValue(elem.expression, path && [...path, `[${i}]`]))
const result = extractValue(
elem.expression,
path && [...path, `[${i}]`]
)
if ('unsupported' in result) return result
arr.push(result.value)
} else {
// e.g. [1, , 2]
// ^^
arr.push(undefined)
}
}
return arr
return { value: arr }
} else if (isObjectExpression(node)) {
// e.g. { a: 1, b: 2 }
const obj: any = {}
for (const prop of node.properties) {
if (!isKeyValueProperty(prop)) {
// e.g. { ...a }
throw new UnsupportedValueError(
'Unsupported spread operator in the Object Expression',
path
)
return {
unsupported: 'Unsupported spread operator in the Object Expression',
path: formatCodePath(path),
}
}

let key
Expand All @@ -167,24 +162,26 @@ function extractValue(node: Node, path?: string[]): any {
// e.g. { "a": 1, "b": 2 }
key = prop.key.value
} else {
throw new UnsupportedValueError(
`Unsupported key type "${prop.key.type}" in the Object Expression`,
path
)
return {
unsupported: `Unsupported key type "${prop.key.type}" in the Object Expression`,
path: formatCodePath(path),
}
}

obj[key] = extractValue(prop.value, path && [...path, key])
const result = extractValue(prop.value, path && [...path, key])
if ('unsupported' in result) return result
obj[key] = result.value
}

return obj
return { value: 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(
'Unsupported template literal with expressions',
path
)
return {
unsupported: 'Unsupported template literal with expressions',
path: formatCodePath(path),
}
}

// When TemplateLiteral has 0 expressions, the length of quasis is always 1.
Expand All @@ -198,21 +195,21 @@ function extractValue(node: Node, path?: string[]): any {
// https://exploringjs.com/impatient-js/ch_template-literals.html#template-strings-cooked-vs-raw
const [{ cooked, raw }] = node.quasis

return cooked ?? raw
return { value: cooked ?? raw }
} else if (isTsSatisfiesExpression(node)) {
return extractValue(node.expression)
} else {
throw new UnsupportedValueError(
`Unsupported node type "${node.type}"`,
path
)
return {
unsupported: `Unsupported node type "${node.type}"`,
path: formatCodePath(path),
}
}
}

/**
* Extracts the value of an exported const variable named `exportedName`
* (e.g. "export const config = { runtime: 'edge' }") from swc's AST.
* The value must be one of (or throws UnsupportedValueError):
* The value must be one of (or returns unsupported):
* - string
* - boolean
* - number
Expand All @@ -221,12 +218,13 @@ function extractValue(node: Node, path?: string[]): any {
* - array containing values listed in this list
* - object containing values listed in this list
*
* Throws NoSuchDeclarationError if the declaration is not found.
* Returns null if the declaration is not found.
* Returns { unsupported, path? } if the value contains unsupported nodes.
*/
export function extractExportedConstValue(
module: Module,
exportedName: string
): any {
): ExtractValueResult | null {
for (const moduleItem of module.body) {
if (!isExportDeclaration(moduleItem)) {
continue
Expand All @@ -252,5 +250,5 @@ export function extractExportedConstValue(
}
}

throw new NoSuchDeclarationError()
return null
}
57 changes: 24 additions & 33 deletions packages/next/src/build/analysis/get-page-static-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import type { RouteHas } from '../../lib/load-custom-routes'
import { promises as fs } from 'fs'
import { relative } from 'path'
import { LRUCache } from '../../server/lib/lru-cache'
import {
extractExportedConstValue,
UnsupportedValueError,
} from './extract-const-value'
import { extractExportedConstValue } from './extract-const-value'
import { parseModule } from './parse-module'
import * as Log from '../output/log'
import {
Expand Down Expand Up @@ -568,7 +565,7 @@ const warnedUnsupportedValueMap = new LRUCache<boolean>(250, () => 1)
function warnAboutUnsupportedValue(
pageFilePath: string,
page: string | undefined,
error: UnsupportedValueError
result: { unsupported: string; path?: string }
) {
hadUnsupportedValue = true
const isProductionBuild = process.env.NODE_ENV === 'production'
Expand All @@ -587,8 +584,8 @@ function warnAboutUnsupportedValue(
`Next.js can't recognize the exported \`config\` field in ` +
(page ? `route "${page}"` : `"${pageFilePath}"`) +
':\n' +
error.message +
(error.path ? ` at "${error.path}"` : '') +
result.unsupported +
(result.path ? ` at "${result.path}"` : '') +
'.\n' +
'Read More - https://nextjs.org/docs/messages/invalid-page-config'

Expand Down Expand Up @@ -648,23 +645,20 @@ export async function getAppPageStaticInfo({
const exportedConfig: Record<string, unknown> = {}
if (exports) {
for (const property of exports) {
try {
exportedConfig[property] = extractExportedConstValue(ast, property)
} catch (e) {
if (e instanceof UnsupportedValueError) {
warnAboutUnsupportedValue(pageFilePath, page, e)
}
const result = extractExportedConstValue(ast, property)
if (result !== null && 'unsupported' in result) {
warnAboutUnsupportedValue(pageFilePath, page, result)
} else if (result !== null) {
exportedConfig[property] = result.value
}
}
}

try {
exportedConfig.config = extractExportedConstValue(ast, 'config')
} catch (e) {
if (e instanceof UnsupportedValueError) {
warnAboutUnsupportedValue(pageFilePath, page, e)
}
// `export config` doesn't exist, or other unknown error thrown by swc, silence them
const configResult = extractExportedConstValue(ast, 'config')
if (configResult !== null && 'unsupported' in configResult) {
warnAboutUnsupportedValue(pageFilePath, page, configResult)
} else if (configResult !== null) {
exportedConfig.config = configResult.value
}

const route = normalizeAppPath(page)
Expand Down Expand Up @@ -749,23 +743,20 @@ export async function getPagesPageStaticInfo({
const exportedConfig: Record<string, unknown> = {}
if (exports) {
for (const property of exports) {
try {
exportedConfig[property] = extractExportedConstValue(ast, property)
} catch (e) {
if (e instanceof UnsupportedValueError) {
warnAboutUnsupportedValue(pageFilePath, page, e)
}
const result = extractExportedConstValue(ast, property)
if (result !== null && 'unsupported' in result) {
warnAboutUnsupportedValue(pageFilePath, page, result)
} else if (result !== null) {
exportedConfig[property] = result.value
}
}
}

try {
exportedConfig.config = extractExportedConstValue(ast, 'config')
} catch (e) {
if (e instanceof UnsupportedValueError) {
warnAboutUnsupportedValue(pageFilePath, page, e)
}
// `export config` doesn't exist, or other unknown error thrown by swc, silence them
const configResult = extractExportedConstValue(ast, 'config')
if (configResult !== null && 'unsupported' in configResult) {
warnAboutUnsupportedValue(pageFilePath, page, configResult)
} else if (configResult !== null) {
exportedConfig.config = configResult.value
}

// Validate the config.
Expand Down
Loading