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
55 changes: 55 additions & 0 deletions packages/next/src/build/analysis/extract-const-value.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
ArrayExpression,
BinaryExpression,
BooleanLiteral,
ExportDeclaration,
Identifier,
Expand All @@ -16,6 +17,7 @@ import type {
TsConstAssertion,
TsTypeAssertion,
TsSatisfiesExpression,
UnaryExpression,
VariableDeclaration,
} from '@swc/core'

Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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}"`,
Expand Down
93 changes: 93 additions & 0 deletions test/unit/extract-const-value.test.ts
Original file line number Diff line number Diff line change
@@ -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',
})
})
})
Loading