diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts
index 05a2afcdc6e..9d84e80c2a6 100644
--- a/packages/compiler-core/__tests__/parse.spec.ts
+++ b/packages/compiler-core/__tests__/parse.spec.ts
@@ -14,6 +14,7 @@ import {
} from '../src/ast'
import { baseParse } from '../src/parser'
+import { Program } from '@babel/types'
/* eslint jest/no-disabled-tests: "off" */
@@ -2170,6 +2171,63 @@ describe('compiler: parse', () => {
})
})
+ describe('expression parsing', () => {
+ test('interpolation', () => {
+ const ast = baseParse(`{{ a + b }}`, { prefixIdentifiers: true })
+ // @ts-ignore
+ expect((ast.children[0] as InterpolationNode).content.ast?.type).toBe(
+ 'BinaryExpression'
+ )
+ })
+
+ test('v-bind', () => {
+ const ast = baseParse(`
`, {
+ prefixIdentifiers: true
+ })
+ const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
+ // @ts-ignore
+ expect(dir.arg?.ast?.type).toBe('BinaryExpression')
+ // @ts-ignore
+ expect(dir.exp?.ast?.type).toBe('CallExpression')
+ })
+
+ test('v-on multi statements', () => {
+ const ast = baseParse(``, {
+ prefixIdentifiers: true
+ })
+ const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
+ // @ts-ignore
+ expect(dir.exp?.ast?.type).toBe('Program')
+ expect((dir.exp?.ast as Program).body).toMatchObject([
+ { type: 'ExpressionStatement' },
+ { type: 'ExpressionStatement' }
+ ])
+ })
+
+ test('v-slot', () => {
+ const ast = baseParse(``, {
+ prefixIdentifiers: true
+ })
+ const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
+ // @ts-ignore
+ expect(dir.exp?.ast?.type).toBe('ArrowFunctionExpression')
+ })
+
+ test('v-for', () => {
+ const ast = baseParse(``, {
+ prefixIdentifiers: true
+ })
+ const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
+ const { source, value, key, index } = dir.forParseResult!
+ // @ts-ignore
+ expect(source.ast?.type).toBe('MemberExpression')
+ // @ts-ignore
+ expect(value?.ast?.type).toBe('ArrowFunctionExpression')
+ expect(key?.ast).toBeNull() // simple ident
+ expect(index?.ast).toBeNull() // simple ident
+ })
+ })
+
describe('Errors', () => {
// HTML parsing errors as specified at
// https://html.spec.whatwg.org/multipage/parsing.html#parse-errors
diff --git a/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts b/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts
index a9697930c95..b33cbbd80f6 100644
--- a/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts
@@ -18,7 +18,7 @@ function parseWithExpressionTransform(
template: string,
options: CompilerOptions = {}
) {
- const ast = parse(template)
+ const ast = parse(template, options)
transform(ast, {
prefixIdentifiers: true,
nodeTransforms: [transformIf, transformExpression],
diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts
index 2bc85bf53d8..203fa8b2c6b 100644
--- a/packages/compiler-core/src/ast.ts
+++ b/packages/compiler-core/src/ast.ts
@@ -14,6 +14,7 @@ import {
} from './runtimeHelpers'
import { PropsExpression } from './transforms/transformElement'
import { ImportItem, TransformContext } from './transform'
+import { Node as BabelNode } from '@babel/types'
// Vue template is a platform-agnostic superset of HTML (syntax only).
// More namespaces can be declared by platform specific compilers.
@@ -226,6 +227,12 @@ export interface SimpleExpressionNode extends Node {
content: string
isStatic: boolean
constType: ConstantTypes
+ /**
+ * - `null` means the expression is a simple identifier that doesn't need
+ * parsing
+ * - `false` means there was a parsing error
+ */
+ ast?: BabelNode | null | false
/**
* Indicates this is an identifier for a hoist vnode call and points to the
* hoisted node.
@@ -246,6 +253,12 @@ export interface InterpolationNode extends Node {
export interface CompoundExpressionNode extends Node {
type: NodeTypes.COMPOUND_EXPRESSION
+ /**
+ * - `null` means the expression is a simple identifier that doesn't need
+ * parsing
+ * - `false` means there was a parsing error
+ */
+ ast?: BabelNode | null | false
children: (
| SimpleExpressionNode
| CompoundExpressionNode
diff --git a/packages/compiler-core/src/babelUtils.ts b/packages/compiler-core/src/babelUtils.ts
index 1f1e3896a1e..f3ef5df29db 100644
--- a/packages/compiler-core/src/babelUtils.ts
+++ b/packages/compiler-core/src/babelUtils.ts
@@ -28,9 +28,9 @@ export function walkIdentifiers(
}
const rootExp =
- root.type === 'Program' &&
- root.body[0].type === 'ExpressionStatement' &&
- root.body[0].expression
+ root.type === 'Program'
+ ? root.body[0].type === 'ExpressionStatement' && root.body[0].expression
+ : root
walk(root, {
enter(node: Node & { scopeIds?: Set }, parent: Node | undefined) {
diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts
index e0c4099e40e..5710039ca10 100644
--- a/packages/compiler-core/src/options.ts
+++ b/packages/compiler-core/src/options.ts
@@ -86,6 +86,17 @@ export interface ParserOptions
* This defaults to `true` in development and `false` in production builds.
*/
comments?: boolean
+ /**
+ * Parse JavaScript expressions with Babel.
+ * @default false
+ */
+ prefixIdentifiers?: boolean
+ /**
+ * A list of parser plugins to enable for `@babel/parser`, which is used to
+ * parse expressions in bindings and interpolations.
+ * https://babeljs.io/docs/en/next/babel-parser#plugins
+ */
+ expressionPlugins?: ParserPlugin[]
}
export type HoistTransform = (
diff --git a/packages/compiler-core/src/parser.ts b/packages/compiler-core/src/parser.ts
index f4399d7c67f..f1d712b3643 100644
--- a/packages/compiler-core/src/parser.ts
+++ b/packages/compiler-core/src/parser.ts
@@ -38,14 +38,25 @@ import {
defaultOnError,
defaultOnWarn
} from './errors'
-import { forAliasRE, isCoreComponent, isStaticArgOf } from './utils'
+import {
+ forAliasRE,
+ isCoreComponent,
+ isSimpleIdentifier,
+ isStaticArgOf
+} from './utils'
import { decodeHTML } from 'entities/lib/decode.js'
+import {
+ parse,
+ parseExpression,
+ type ParserOptions as BabelOptions
+} from '@babel/parser'
type OptionalOptions =
| 'decodeEntities'
| 'whitespace'
| 'isNativeTag'
| 'isBuiltInComponent'
+ | 'expressionPlugins'
| keyof CompilerCompatOptions
export type MergedParserOptions = Omit<
@@ -64,7 +75,8 @@ export const defaultParserOptions: MergedParserOptions = {
isCustomElement: NO,
onError: defaultOnError,
onWarn: defaultOnWarn,
- comments: __DEV__
+ comments: __DEV__,
+ prefixIdentifiers: false
}
let currentOptions: MergedParserOptions = defaultParserOptions
@@ -116,7 +128,7 @@ const tokenizer = new Tokenizer(stack, {
}
addNode({
type: NodeTypes.INTERPOLATION,
- content: createSimpleExpression(exp, false, getLoc(innerStart, innerEnd)),
+ content: createExp(exp, false, getLoc(innerStart, innerEnd)),
loc: getLoc(start, end)
})
},
@@ -245,7 +257,7 @@ const tokenizer = new Tokenizer(stack, {
setLocEnd((currentProp as AttributeNode).nameLoc, end)
} else {
const isStatic = arg[0] !== `[`
- ;(currentProp as DirectiveNode).arg = createSimpleExpression(
+ ;(currentProp as DirectiveNode).arg = createExp(
isStatic ? arg : arg.slice(1, -1),
isStatic,
getLoc(start, end),
@@ -346,10 +358,25 @@ const tokenizer = new Tokenizer(stack, {
}
} else {
// directive
- currentProp.exp = createSimpleExpression(
+ let expParseMode = ExpParseMode.Normal
+ if (!__BROWSER__) {
+ if (currentProp.name === 'for') {
+ expParseMode = ExpParseMode.Skip
+ } else if (currentProp.name === 'slot') {
+ expParseMode = ExpParseMode.Params
+ } else if (
+ currentProp.name === 'on' &&
+ currentAttrValue.includes(';')
+ ) {
+ expParseMode = ExpParseMode.Statements
+ }
+ }
+ currentProp.exp = createExp(
currentAttrValue,
false,
- getLoc(currentAttrStartIndex, currentAttrEndIndex)
+ getLoc(currentAttrStartIndex, currentAttrEndIndex),
+ ConstantTypes.NOT_CONSTANT,
+ expParseMode
)
if (currentProp.name === 'for') {
currentProp.forParseResult = parseForExpression(currentProp.exp)
@@ -477,10 +504,20 @@ function parseForExpression(
const [, LHS, RHS] = inMatch
- const createAliasExpression = (content: string, offset: number) => {
+ const createAliasExpression = (
+ content: string,
+ offset: number,
+ asParam = false
+ ) => {
const start = loc.start.offset + offset
const end = start + content.length
- return createSimpleExpression(content, false, getLoc(start, end))
+ return createExp(
+ content,
+ false,
+ getLoc(start, end),
+ ConstantTypes.NOT_CONSTANT,
+ asParam ? ExpParseMode.Params : ExpParseMode.Normal
+ )
}
const result: ForParseResult = {
@@ -502,7 +539,7 @@ function parseForExpression(
let keyOffset: number | undefined
if (keyContent) {
keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
- result.key = createAliasExpression(keyContent, keyOffset)
+ result.key = createAliasExpression(keyContent, keyOffset, true)
}
if (iteratorMatch[2]) {
@@ -516,14 +553,15 @@ function parseForExpression(
result.key
? keyOffset! + keyContent.length
: trimmedOffset + valueContent.length
- )
+ ),
+ true
)
}
}
}
if (valueContent) {
- result.value = createAliasExpression(valueContent, trimmedOffset)
+ result.value = createAliasExpression(valueContent, trimmedOffset, true)
}
return result
@@ -929,8 +967,58 @@ function dirToAttr(dir: DirectiveNode): AttributeNode {
return attr
}
-function emitError(code: ErrorCodes, index: number) {
- currentOptions.onError(createCompilerError(code, getLoc(index, index)))
+enum ExpParseMode {
+ Normal,
+ Params,
+ Statements,
+ Skip
+}
+
+function createExp(
+ content: SimpleExpressionNode['content'],
+ isStatic: SimpleExpressionNode['isStatic'] = false,
+ loc: SourceLocation,
+ constType: ConstantTypes = ConstantTypes.NOT_CONSTANT,
+ parseMode = ExpParseMode.Normal
+) {
+ const exp = createSimpleExpression(content, isStatic, loc, constType)
+ if (
+ !__BROWSER__ &&
+ !isStatic &&
+ currentOptions.prefixIdentifiers &&
+ parseMode !== ExpParseMode.Skip &&
+ content.trim()
+ ) {
+ if (isSimpleIdentifier(content)) {
+ exp.ast = null // fast path
+ return exp
+ }
+ try {
+ const plugins = currentOptions.expressionPlugins
+ const options: BabelOptions = {
+ plugins: plugins ? [...plugins, 'typescript'] : ['typescript']
+ }
+ if (parseMode === ExpParseMode.Statements) {
+ // v-on with multi-inline-statements, pad 1 char
+ exp.ast = parse(` ${content} `, options).program
+ } else if (parseMode === ExpParseMode.Params) {
+ exp.ast = parseExpression(`(${content})=>{}`, options)
+ } else {
+ // normal exp, wrap with parens
+ exp.ast = parseExpression(`(${content})`, options)
+ }
+ } catch (e: any) {
+ exp.ast = false // indicate an error
+ emitError(ErrorCodes.X_INVALID_EXPRESSION, loc.start.offset, e.message)
+ }
+ }
+ return exp
+}
+
+function emitError(code: ErrorCodes, index: number, message?: string) {
+ currentOptions.onError(
+ createCompilerError(code, getLoc(index, index), undefined, message)
+ )
}
function reset() {
diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts
index 4d9a2497886..263ada4f137 100644
--- a/packages/compiler-core/src/transforms/transformExpression.ts
+++ b/packages/compiler-core/src/transforms/transformExpression.ts
@@ -223,7 +223,14 @@ export function processExpression(
// bail constant on parens (function invocation) and dot (member access)
const bailConstant = constantBailRE.test(rawExp)
- if (isSimpleIdentifier(rawExp)) {
+ let ast = node.ast
+
+ if (ast === false) {
+ // ast being false means it has caused an error already during parse phase
+ return node
+ }
+
+ if (ast === null || (!ast && isSimpleIdentifier(rawExp))) {
const isScopeVarReference = context.identifiers[rawExp]
const isAllowedGlobal = isGloballyAllowed(rawExp)
const isLiteral = isLiteralWhitelisted(rawExp)
@@ -249,29 +256,30 @@ export function processExpression(
return node
}
- let ast: any
- // exp needs to be parsed differently:
- // 1. Multiple inline statements (v-on, with presence of `;`): parse as raw
- // exp, but make sure to pad with spaces for consistent ranges
- // 2. Expressions: wrap with parens (for e.g. object expressions)
- // 3. Function arguments (v-for, v-slot): place in a function argument position
- const source = asRawStatements
- ? ` ${rawExp} `
- : `(${rawExp})${asParams ? `=>{}` : ``}`
- try {
- ast = parse(source, {
- plugins: context.expressionPlugins
- }).program
- } catch (e: any) {
- context.onError(
- createCompilerError(
- ErrorCodes.X_INVALID_EXPRESSION,
- node.loc,
- undefined,
- e.message
+ if (!ast) {
+ // exp needs to be parsed differently:
+ // 1. Multiple inline statements (v-on, with presence of `;`): parse as raw
+ // exp, but make sure to pad with spaces for consistent ranges
+ // 2. Expressions: wrap with parens (for e.g. object expressions)
+ // 3. Function arguments (v-for, v-slot): place in a function argument position
+ const source = asRawStatements
+ ? ` ${rawExp} `
+ : `(${rawExp})${asParams ? `=>{}` : ``}`
+ try {
+ ast = parse(source, {
+ plugins: context.expressionPlugins
+ }).program
+ } catch (e: any) {
+ context.onError(
+ createCompilerError(
+ ErrorCodes.X_INVALID_EXPRESSION,
+ node.loc,
+ undefined,
+ e.message
+ )
)
- )
- return node
+ return node
+ }
}
type QualifiedId = Identifier & PrefixMeta
@@ -351,6 +359,7 @@ export function processExpression(
let ret
if (children.length) {
ret = createCompoundExpression(children, node.loc)
+ ret.ast = ast
} else {
ret = node
ret.constType = bailConstant
diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
index 4cc3cf611d8..e26dfef53d9 100644
--- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
+++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
@@ -748,6 +748,51 @@ return { get FooBaz() { return FooBaz }, get Last() { return Last } }
})"
`;
+exports[`SFC compile
-
+