Skip to content

Commit 0ba131a

Browse files
yyx990803sxzz
authored andcommitted
feat(compiler-sfc): analyze import usage in template via AST (#9729)
close #8897 close nuxt/nuxt#22416
1 parent 25f90b2 commit 0ba131a

File tree

12 files changed

+335
-105
lines changed

12 files changed

+335
-105
lines changed

packages/compiler-core/__tests__/parse.spec.ts

+58
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '../src/ast'
1515

1616
import { baseParse } from '../src/parser'
17+
import { Program } from '@babel/types'
1718

1819
/* eslint jest/no-disabled-tests: "off" */
1920

@@ -2170,6 +2171,63 @@ describe('compiler: parse', () => {
21702171
})
21712172
})
21722173

2174+
describe('expression parsing', () => {
2175+
test('interpolation', () => {
2176+
const ast = baseParse(`{{ a + b }}`, { prefixIdentifiers: true })
2177+
// @ts-ignore
2178+
expect((ast.children[0] as InterpolationNode).content.ast?.type).toBe(
2179+
'BinaryExpression'
2180+
)
2181+
})
2182+
2183+
test('v-bind', () => {
2184+
const ast = baseParse(`<div :[key+1]="foo()" />`, {
2185+
prefixIdentifiers: true
2186+
})
2187+
const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
2188+
// @ts-ignore
2189+
expect(dir.arg?.ast?.type).toBe('BinaryExpression')
2190+
// @ts-ignore
2191+
expect(dir.exp?.ast?.type).toBe('CallExpression')
2192+
})
2193+
2194+
test('v-on multi statements', () => {
2195+
const ast = baseParse(`<div @click="a++;b++" />`, {
2196+
prefixIdentifiers: true
2197+
})
2198+
const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
2199+
// @ts-ignore
2200+
expect(dir.exp?.ast?.type).toBe('Program')
2201+
expect((dir.exp?.ast as Program).body).toMatchObject([
2202+
{ type: 'ExpressionStatement' },
2203+
{ type: 'ExpressionStatement' }
2204+
])
2205+
})
2206+
2207+
test('v-slot', () => {
2208+
const ast = baseParse(`<Comp #foo="{ a, b }" />`, {
2209+
prefixIdentifiers: true
2210+
})
2211+
const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
2212+
// @ts-ignore
2213+
expect(dir.exp?.ast?.type).toBe('ArrowFunctionExpression')
2214+
})
2215+
2216+
test('v-for', () => {
2217+
const ast = baseParse(`<div v-for="({ a, b }, key, index) of a.b" />`, {
2218+
prefixIdentifiers: true
2219+
})
2220+
const dir = (ast.children[0] as ElementNode).props[0] as DirectiveNode
2221+
const { source, value, key, index } = dir.forParseResult!
2222+
// @ts-ignore
2223+
expect(source.ast?.type).toBe('MemberExpression')
2224+
// @ts-ignore
2225+
expect(value?.ast?.type).toBe('ArrowFunctionExpression')
2226+
expect(key?.ast).toBeNull() // simple ident
2227+
expect(index?.ast).toBeNull() // simple ident
2228+
})
2229+
})
2230+
21732231
describe('Errors', () => {
21742232
// HTML parsing errors as specified at
21752233
// https://html.spec.whatwg.org/multipage/parsing.html#parse-errors

packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function parseWithExpressionTransform(
1818
template: string,
1919
options: CompilerOptions = {}
2020
) {
21-
const ast = parse(template)
21+
const ast = parse(template, options)
2222
transform(ast, {
2323
prefixIdentifiers: true,
2424
nodeTransforms: [transformIf, transformExpression],

packages/compiler-core/src/ast.ts

+13
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from './runtimeHelpers'
1515
import { PropsExpression } from './transforms/transformElement'
1616
import { ImportItem, TransformContext } from './transform'
17+
import { Node as BabelNode } from '@babel/types'
1718

1819
// Vue template is a platform-agnostic superset of HTML (syntax only).
1920
// More namespaces can be declared by platform specific compilers.
@@ -233,6 +234,12 @@ export interface SimpleExpressionNode extends Node {
233234
content: string
234235
isStatic: boolean
235236
constType: ConstantTypes
237+
/**
238+
* - `null` means the expression is a simple identifier that doesn't need
239+
* parsing
240+
* - `false` means there was a parsing error
241+
*/
242+
ast?: BabelNode | null | false
236243
/**
237244
* Indicates this is an identifier for a hoist vnode call and points to the
238245
* hoisted node.
@@ -253,6 +260,12 @@ export interface InterpolationNode extends Node {
253260

254261
export interface CompoundExpressionNode extends Node {
255262
type: NodeTypes.COMPOUND_EXPRESSION
263+
/**
264+
* - `null` means the expression is a simple identifier that doesn't need
265+
* parsing
266+
* - `false` means there was a parsing error
267+
*/
268+
ast?: BabelNode | null | false
256269
children: (
257270
| SimpleExpressionNode
258271
| CompoundExpressionNode

packages/compiler-core/src/babelUtils.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ export function walkIdentifiers(
2828
}
2929

3030
const rootExp =
31-
root.type === 'Program' &&
32-
root.body[0].type === 'ExpressionStatement' &&
33-
root.body[0].expression
31+
root.type === 'Program'
32+
? root.body[0].type === 'ExpressionStatement' && root.body[0].expression
33+
: root
3434

3535
walk(root, {
3636
enter(node: Node & { scopeIds?: Set<string> }, parent: Node | undefined) {

packages/compiler-core/src/options.ts

+11
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,17 @@ export interface ParserOptions
8686
* This defaults to `true` in development and `false` in production builds.
8787
*/
8888
comments?: boolean
89+
/**
90+
* Parse JavaScript expressions with Babel.
91+
* @default false
92+
*/
93+
prefixIdentifiers?: boolean
94+
/**
95+
* A list of parser plugins to enable for `@babel/parser`, which is used to
96+
* parse expressions in bindings and interpolations.
97+
* https://babeljs.io/docs/en/next/babel-parser#plugins
98+
*/
99+
expressionPlugins?: ParserPlugin[]
89100
}
90101

91102
export type HoistTransform = (

packages/compiler-core/src/parser.ts

+101-13
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,25 @@ import {
3838
defaultOnError,
3939
defaultOnWarn
4040
} from './errors'
41-
import { forAliasRE, isCoreComponent, isStaticArgOf } from './utils'
41+
import {
42+
forAliasRE,
43+
isCoreComponent,
44+
isSimpleIdentifier,
45+
isStaticArgOf
46+
} from './utils'
4247
import { decodeHTML } from 'entities/lib/decode.js'
48+
import {
49+
parse,
50+
parseExpression,
51+
type ParserOptions as BabelOptions
52+
} from '@babel/parser'
4353

4454
type OptionalOptions =
4555
| 'decodeEntities'
4656
| 'whitespace'
4757
| 'isNativeTag'
4858
| 'isBuiltInComponent'
59+
| 'expressionPlugins'
4960
| keyof CompilerCompatOptions
5061

5162
export type MergedParserOptions = Omit<
@@ -64,7 +75,8 @@ export const defaultParserOptions: MergedParserOptions = {
6475
isCustomElement: NO,
6576
onError: defaultOnError,
6677
onWarn: defaultOnWarn,
67-
comments: __DEV__
78+
comments: __DEV__,
79+
prefixIdentifiers: false
6880
}
6981

7082
let currentOptions: MergedParserOptions = defaultParserOptions
@@ -116,7 +128,7 @@ const tokenizer = new Tokenizer(stack, {
116128
}
117129
addNode({
118130
type: NodeTypes.INTERPOLATION,
119-
content: createSimpleExpression(exp, false, getLoc(innerStart, innerEnd)),
131+
content: createExp(exp, false, getLoc(innerStart, innerEnd)),
120132
loc: getLoc(start, end)
121133
})
122134
},
@@ -245,7 +257,7 @@ const tokenizer = new Tokenizer(stack, {
245257
setLocEnd((currentProp as AttributeNode).nameLoc, end)
246258
} else {
247259
const isStatic = arg[0] !== `[`
248-
;(currentProp as DirectiveNode).arg = createSimpleExpression(
260+
;(currentProp as DirectiveNode).arg = createExp(
249261
isStatic ? arg : arg.slice(1, -1),
250262
isStatic,
251263
getLoc(start, end),
@@ -346,10 +358,25 @@ const tokenizer = new Tokenizer(stack, {
346358
}
347359
} else {
348360
// directive
349-
currentProp.exp = createSimpleExpression(
361+
let expParseMode = ExpParseMode.Normal
362+
if (!__BROWSER__) {
363+
if (currentProp.name === 'for') {
364+
expParseMode = ExpParseMode.Skip
365+
} else if (currentProp.name === 'slot') {
366+
expParseMode = ExpParseMode.Params
367+
} else if (
368+
currentProp.name === 'on' &&
369+
currentAttrValue.includes(';')
370+
) {
371+
expParseMode = ExpParseMode.Statements
372+
}
373+
}
374+
currentProp.exp = createExp(
350375
currentAttrValue,
351376
false,
352-
getLoc(currentAttrStartIndex, currentAttrEndIndex)
377+
getLoc(currentAttrStartIndex, currentAttrEndIndex),
378+
ConstantTypes.NOT_CONSTANT,
379+
expParseMode
353380
)
354381
if (currentProp.name === 'for') {
355382
currentProp.forParseResult = parseForExpression(currentProp.exp)
@@ -477,10 +504,20 @@ function parseForExpression(
477504

478505
const [, LHS, RHS] = inMatch
479506

480-
const createAliasExpression = (content: string, offset: number) => {
507+
const createAliasExpression = (
508+
content: string,
509+
offset: number,
510+
asParam = false
511+
) => {
481512
const start = loc.start.offset + offset
482513
const end = start + content.length
483-
return createSimpleExpression(content, false, getLoc(start, end))
514+
return createExp(
515+
content,
516+
false,
517+
getLoc(start, end),
518+
ConstantTypes.NOT_CONSTANT,
519+
asParam ? ExpParseMode.Params : ExpParseMode.Normal
520+
)
484521
}
485522

486523
const result: ForParseResult = {
@@ -502,7 +539,7 @@ function parseForExpression(
502539
let keyOffset: number | undefined
503540
if (keyContent) {
504541
keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
505-
result.key = createAliasExpression(keyContent, keyOffset)
542+
result.key = createAliasExpression(keyContent, keyOffset, true)
506543
}
507544

508545
if (iteratorMatch[2]) {
@@ -516,14 +553,15 @@ function parseForExpression(
516553
result.key
517554
? keyOffset! + keyContent.length
518555
: trimmedOffset + valueContent.length
519-
)
556+
),
557+
true
520558
)
521559
}
522560
}
523561
}
524562

525563
if (valueContent) {
526-
result.value = createAliasExpression(valueContent, trimmedOffset)
564+
result.value = createAliasExpression(valueContent, trimmedOffset, true)
527565
}
528566

529567
return result
@@ -929,8 +967,58 @@ function dirToAttr(dir: DirectiveNode): AttributeNode {
929967
return attr
930968
}
931969

932-
function emitError(code: ErrorCodes, index: number) {
933-
currentOptions.onError(createCompilerError(code, getLoc(index, index)))
970+
enum ExpParseMode {
971+
Normal,
972+
Params,
973+
Statements,
974+
Skip
975+
}
976+
977+
function createExp(
978+
content: SimpleExpressionNode['content'],
979+
isStatic: SimpleExpressionNode['isStatic'] = false,
980+
loc: SourceLocation,
981+
constType: ConstantTypes = ConstantTypes.NOT_CONSTANT,
982+
parseMode = ExpParseMode.Normal
983+
) {
984+
const exp = createSimpleExpression(content, isStatic, loc, constType)
985+
if (
986+
!__BROWSER__ &&
987+
!isStatic &&
988+
currentOptions.prefixIdentifiers &&
989+
parseMode !== ExpParseMode.Skip &&
990+
content.trim()
991+
) {
992+
if (isSimpleIdentifier(content)) {
993+
exp.ast = null // fast path
994+
return exp
995+
}
996+
try {
997+
const plugins = currentOptions.expressionPlugins
998+
const options: BabelOptions = {
999+
plugins: plugins ? [...plugins, 'typescript'] : ['typescript']
1000+
}
1001+
if (parseMode === ExpParseMode.Statements) {
1002+
// v-on with multi-inline-statements, pad 1 char
1003+
exp.ast = parse(` ${content} `, options).program
1004+
} else if (parseMode === ExpParseMode.Params) {
1005+
exp.ast = parseExpression(`(${content})=>{}`, options)
1006+
} else {
1007+
// normal exp, wrap with parens
1008+
exp.ast = parseExpression(`(${content})`, options)
1009+
}
1010+
} catch (e: any) {
1011+
exp.ast = false // indicate an error
1012+
emitError(ErrorCodes.X_INVALID_EXPRESSION, loc.start.offset, e.message)
1013+
}
1014+
}
1015+
return exp
1016+
}
1017+
1018+
function emitError(code: ErrorCodes, index: number, message?: string) {
1019+
currentOptions.onError(
1020+
createCompilerError(code, getLoc(index, index), undefined, message)
1021+
)
9341022
}
9351023

9361024
function reset() {

0 commit comments

Comments
 (0)