diff --git a/packages/next/src/build/analysis/extract-const-value.ts b/packages/next/src/build/analysis/extract-const-value.ts index 25545efebd7f..bbd312e05556 100644 --- a/packages/next/src/build/analysis/extract-const-value.ts +++ b/packages/next/src/build/analysis/extract-const-value.ts @@ -1,5 +1,6 @@ import type { ArrayExpression, + BinaryExpression, BooleanLiteral, ExportDeclaration, Identifier, @@ -16,6 +17,7 @@ import type { TsConstAssertion, TsTypeAssertion, TsSatisfiesExpression, + UnaryExpression, VariableDeclaration, } from '@swc/core' @@ -83,6 +85,14 @@ function isTsSatisfiesExpression(node: Node): node is TsSatisfiesExpression { return node.type === 'TsSatisfiesExpression' } +function isBinaryExpression(node: Node): node is BinaryExpression { + return node.type === 'BinaryExpression' +} + +function isUnaryExpression(node: Node): node is UnaryExpression { + return node.type === 'UnaryExpression' +} + export type ExtractValueResult = | { value: any } | { unsupported: string; path?: string } @@ -218,6 +228,51 @@ function extractValue(node: Node, path?: string[]): ExtractValueResult { isTsConstAssertion(node) ) { return extractValue(node.expression) + } else if (isBinaryExpression(node)) { + // e.g. `60 * 5` — fold compile-time arithmetic / string-concat + // expressions. Restricted to a small set of pure operators so we + // don't accidentally evaluate side-effecting or short-circuiting + // semantics (logical, comparison, etc.) at extraction time. + const left = extractValue(node.left, path) + if ('unsupported' in left) return left + const right = extractValue(node.right, path) + if ('unsupported' in right) return right + switch (node.operator) { + case '+': + return { value: left.value + right.value } + case '-': + return { value: left.value - right.value } + case '*': + return { value: left.value * right.value } + case '/': + return { value: left.value / right.value } + case '%': + return { value: left.value % right.value } + case '**': + return { value: left.value ** right.value } + default: + return { + unsupported: `Unsupported binary operator "${node.operator}"`, + path: formatCodePath(path), + } + } + } else if (isUnaryExpression(node)) { + // e.g. `-5`, `+5`, `~0xff` — same restricted set as binary above. + const arg = extractValue(node.argument, path) + if ('unsupported' in arg) return arg + switch (node.operator) { + case '-': + return { value: -arg.value } + case '+': + return { value: +arg.value } + case '~': + return { value: ~arg.value } + default: + return { + unsupported: `Unsupported unary operator "${node.operator}"`, + path: formatCodePath(path), + } + } } else { return { unsupported: `Unsupported node type "${node.type}"`, diff --git a/test/unit/extract-const-value.test.ts b/test/unit/extract-const-value.test.ts new file mode 100644 index 000000000000..4c2b3cc337d4 --- /dev/null +++ b/test/unit/extract-const-value.test.ts @@ -0,0 +1,93 @@ +import { extractExportedConstValue } from 'next/dist/build/analysis/extract-const-value' +import { parseModule } from 'next/dist/build/analysis/parse-module' +import { installBindings } from 'next/dist/build/swc/install-bindings' + +async function extractFor(source: string, exportedName: string) { + const mod = await parseModule('virtual.ts', source) + return extractExportedConstValue(mod, exportedName) +} + +describe('extractExportedConstValue', () => { + beforeAll(async () => { + await installBindings() + }) + + it('extracts plain numeric literals', async () => { + const result = await extractFor( + `export const revalidate = 60`, + 'revalidate' + ) + expect(result).toEqual({ value: 60 }) + }) + + it('folds multiplicative BinaryExpression (regression for #72365)', async () => { + const result = await extractFor( + `export const revalidate = 60 * 5`, + 'revalidate' + ) + expect(result).toEqual({ value: 300 }) + }) + + it('folds additive BinaryExpression', async () => { + const result = await extractFor( + `export const revalidate = 60 + 30`, + 'revalidate' + ) + expect(result).toEqual({ value: 90 }) + }) + + it('folds nested BinaryExpression', async () => { + const result = await extractFor( + `export const revalidate = 60 * 60 * 24`, + 'revalidate' + ) + expect(result).toEqual({ value: 86400 }) + }) + + it('folds string concatenation', async () => { + const result = await extractFor( + `export const runtime = 'edge' + ''`, + 'runtime' + ) + expect(result).toEqual({ value: 'edge' }) + }) + + it('folds unary minus on a numeric literal', async () => { + const result = await extractFor(`export const x = -42`, 'x') + expect(result).toEqual({ value: -42 }) + }) + + it('folds unary minus inside a BinaryExpression', async () => { + const result = await extractFor(`export const x = 100 + -42`, 'x') + expect(result).toEqual({ value: 58 }) + }) + + it('folds the exponentiation operator', async () => { + const result = await extractFor(`export const x = 2 ** 10`, 'x') + expect(result).toEqual({ value: 1024 }) + }) + + it('reports unsupported binary operators', async () => { + const result = await extractFor(`export const x = 1 && 2`, 'x') + expect(result).toEqual({ + unsupported: 'Unsupported binary operator "&&"', + path: 'x', + }) + }) + + it('reports unsupported unary operators', async () => { + const result = await extractFor(`export const x = !true`, 'x') + expect(result).toEqual({ + unsupported: 'Unsupported unary operator "!"', + path: 'x', + }) + }) + + it('still rejects unsupported nested nodes', async () => { + const result = await extractFor(`export const x = 1 + foo`, 'x') + expect(result).toEqual({ + unsupported: 'Unknown identifier "foo"', + path: 'x', + }) + }) +})