import type {
ArrayExpression,
BooleanLiteral,
ExportDeclaration,
Identifier,
KeyValueProperty,
Module,
Node,
NullLiteral,
NumericLiteral,
ObjectExpression,
RegExpLiteral,
StringLiteral,
TemplateLiteral,
TsAsExpression,
TsConstAssertion,
TsTypeAssertion,
TsSatisfiesExpression,
VariableDeclaration,
} from '@swc/core'
function isExportDeclaration(node: Node): node is ExportDeclaration {
return node.type === 'ExportDeclaration'
}
function isVariableDeclaration(node: Node): node is VariableDeclaration {
return node.type === 'VariableDeclaration'
}
function isIdentifier(node: Node): node is Identifier {
return node.type === 'Identifier'
}
function isBooleanLiteral(node: Node): node is BooleanLiteral {
return node.type === 'BooleanLiteral'
}
function isNullLiteral(node: Node): node is NullLiteral {
return node.type === 'NullLiteral'
}
function isStringLiteral(node: Node): node is StringLiteral {
return node.type === 'StringLiteral'
}
function isNumericLiteral(node: Node): node is NumericLiteral {
return node.type === 'NumericLiteral'
}
function isArrayExpression(node: Node): node is ArrayExpression {
return node.type === 'ArrayExpression'
}
function isObjectExpression(node: Node): node is ObjectExpression {
return node.type === 'ObjectExpression'
}
function isKeyValueProperty(node: Node): node is KeyValueProperty {
return node.type === 'KeyValueProperty'
}
function isRegExpLiteral(node: Node): node is RegExpLiteral {
return node.type === 'RegExpLiteral'
}
function isTemplateLiteral(node: Node): node is TemplateLiteral {
return node.type === 'TemplateLiteral'
}
function isTsAsExpression(node: Node): node is TsAsExpression {
return node.type === 'TsAsExpression'
}
function isTsConstAssertion(node: Node): node is TsConstAssertion {
return node.type === 'TsConstAssertion'
}
function isTsTypeAssertion(node: Node): node is TsTypeAssertion {
return node.type === 'TsTypeAssertion'
}
function isTsSatisfiesExpression(node: Node): node is TsSatisfiesExpression {
return node.type === 'TsSatisfiesExpression'
}
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}`
}
}
return codePath
}
function extractValue(node: Node, path?: string[]): ExtractValueResult {
if (isNullLiteral(node)) {
return { value: null }
} else if (isBooleanLiteral(node)) {
// e.g. true / false
return { value: node.value }
} else if (isStringLiteral(node)) {
// e.g. "abc"
return { value: node.value }
} else if (isNumericLiteral(node)) {
// e.g. 123
return { value: node.value }
} else if (isRegExpLiteral(node)) {
// e.g. /abc/i
return { value: new RegExp(node.pattern, node.flags) }
} else if (isIdentifier(node)) {
switch (node.value) {
case 'undefined':
return { value: undefined }
default:
return {
unsupported: `Unknown identifier "${node.value}"`,
path: formatCodePath(path),
}
}
} else if (isArrayExpression(node)) {
// e.g. [1, 2, 3]
const arr = []
for (let i = 0, len = node.elements.length; i < len; i++) {
const elem = node.elements[i]
if (elem) {
if (elem.spread) {
// e.g. [ ...a ]
return {
unsupported: 'Unsupported spread operator in the Array Expression',
path: formatCodePath(path),
}
}
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 { 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 }
return {
unsupported: 'Unsupported spread operator in the Object Expression',
path: formatCodePath(path),
}
}
let key
if (isIdentifier(prop.key)) {
// e.g. { a: 1, b: 2 }
key = prop.key.value
} else if (isStringLiteral(prop.key)) {
// e.g. { "a": 1, "b": 2 }
key = prop.key.value
} else {
return {
unsupported: `Unsupported key type "${prop.key.type}" in the Object Expression`,
path: formatCodePath(path),
}
}
const result = extractValue(prop.value, path && [...path, key])
if ('unsupported' in result) return result
obj[key] = result.value
}
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'`?
return {
unsupported: 'Unsupported template literal with expressions',
path: formatCodePath(path),
}
}
// When TemplateLiteral has 0 expressions, the length of quasis is always 1.
// Because when parsing TemplateLiteral, the parser yields the first quasi,
// then the first expression, then the next quasi, then the next expression, etc.,
// until the last quasi.
// Thus if there is no expression, the parser ends at the frst and also last quasis
//
// A "cooked" interpretation where backslashes have special meaning, while a
// "raw" interpretation where backslashes do not have special meaning
// https://exploringjs.com/impatient-js/ch_template-literals.html#template-strings-cooked-vs-raw
const [{ cooked, raw }] = node.quasis
return { value: cooked ?? raw }
} else if (
isTsSatisfiesExpression(node) ||
isTsAsExpression(node) ||
isTsTypeAssertion(node) ||
isTsConstAssertion(node)
) {
return extractValue(node.expression)
} else {
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 returns unsupported):
* - string
* - boolean
* - number
* - null
* - undefined
* - array containing values listed in this list
* - object containing values listed in this list
*
* Returns null if the declaration is not found.
* Returns { unsupported, path? } if the value contains unsupported nodes.
*/
export function extractExportedConstValue(
module: Module | null,
exportedName: string
): ExtractValueResult | null {
if (!module) return null
for (const moduleItem of module.body) {
if (!isExportDeclaration(moduleItem)) {
continue
}
const declaration = moduleItem.declaration
if (!isVariableDeclaration(declaration)) {
continue
}
if (declaration.kind !== 'const') {
continue
}
for (const decl of declaration.declarations) {
if (
isIdentifier(decl.id) &&
decl.id.value === exportedName &&
decl.init
) {
return extractValue(decl.init, [exportedName])
}
}
}
return null
}