Skip to content

Commit 8449a97

Browse files
committed
feat(compiler-core): switch to @babel/parser for expression parsing
This enables default support for parsing bigInt, optional chaining and nullish coalescing, and also adds the `expressionPlugins` compiler option for enabling additional parsing plugins listed at https://babeljs.io/docs/en/next/babel-parser#plugins.
1 parent 4809325 commit 8449a97

File tree

13 files changed

+207
-33
lines changed

13 files changed

+207
-33
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
},
3838
"devDependencies": {
3939
"@microsoft/api-extractor": "^7.3.9",
40+
"@rollup/plugin-commonjs": "^11.0.2",
4041
"@rollup/plugin-json": "^4.0.0",
42+
"@rollup/plugin-node-resolve": "^7.1.1",
4143
"@rollup/plugin-replace": "^2.2.1",
4244
"@types/jest": "^24.0.21",
4345
"@types/puppeteer": "^2.0.0",

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

+59-1
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,65 @@ describe('compiler: expression transform', () => {
380380
const onError = jest.fn()
381381
parseWithExpressionTransform(`{{ a( }}`, { onError })
382382
expect(onError.mock.calls[0][0].message).toMatch(
383-
`Invalid JavaScript expression.`
383+
`Error parsing JavaScript expression: Unexpected token`
384384
)
385385
})
386+
387+
describe('ES Proposals support', () => {
388+
test('bigInt', () => {
389+
const node = parseWithExpressionTransform(
390+
`{{ 13000n }}`
391+
) as InterpolationNode
392+
expect(node.content).toMatchObject({
393+
type: NodeTypes.SIMPLE_EXPRESSION,
394+
content: `13000n`,
395+
isStatic: false,
396+
isConstant: true
397+
})
398+
})
399+
400+
test('nullish colescing', () => {
401+
const node = parseWithExpressionTransform(
402+
`{{ a ?? b }}`
403+
) as InterpolationNode
404+
expect(node.content).toMatchObject({
405+
type: NodeTypes.COMPOUND_EXPRESSION,
406+
children: [{ content: `_ctx.a` }, ` ?? `, { content: `_ctx.b` }]
407+
})
408+
})
409+
410+
test('optional chaining', () => {
411+
const node = parseWithExpressionTransform(
412+
`{{ a?.b?.c }}`
413+
) as InterpolationNode
414+
expect(node.content).toMatchObject({
415+
type: NodeTypes.COMPOUND_EXPRESSION,
416+
children: [
417+
{ content: `_ctx.a` },
418+
`?.`,
419+
{ content: `b` },
420+
`?.`,
421+
{ content: `c` }
422+
]
423+
})
424+
})
425+
426+
test('Enabling additional plugins', () => {
427+
// enabling pipeline operator to replace filters:
428+
const node = parseWithExpressionTransform(`{{ a |> uppercase }}`, {
429+
expressionPlugins: [
430+
[
431+
'pipelineOperator',
432+
{
433+
proposal: 'minimal'
434+
}
435+
]
436+
]
437+
}) as InterpolationNode
438+
expect(node.content).toMatchObject({
439+
type: NodeTypes.COMPOUND_EXPRESSION,
440+
children: [{ content: `_ctx.a` }, ` |> `, { content: `_ctx.uppercase` }]
441+
})
442+
})
443+
})
386444
})

packages/compiler-core/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
},
3131
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-core#readme",
3232
"dependencies": {
33-
"acorn": "^7.1.0",
33+
"@babel/parser": "^7.8.6",
34+
"@babel/types": "^7.8.6",
3435
"estree-walker": "^0.8.1",
3536
"source-map": "^0.6.1"
3637
}

packages/compiler-core/src/errors.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ export function defaultOnError(error: CompilerError) {
1616
export function createCompilerError<T extends number>(
1717
code: T,
1818
loc?: SourceLocation,
19-
messages?: { [code: number]: string }
19+
messages?: { [code: number]: string },
20+
additionalMessage?: string
2021
): T extends ErrorCodes ? CoreCompilerError : CompilerError {
21-
const msg = __DEV__ || !__BROWSER__ ? (messages || errorMessages)[code] : code
22+
const msg =
23+
__DEV__ || !__BROWSER__
24+
? (messages || errorMessages)[code] + (additionalMessage || ``)
25+
: code
2226
const error = new SyntaxError(String(msg)) as CompilerError
2327
error.code = code
2428
error.loc = loc
@@ -174,7 +178,7 @@ export const errorMessages: { [code: number]: string } = {
174178
[ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
175179
[ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,
176180
[ErrorCodes.X_V_MODEL_ON_SCOPE_VARIABLE]: `v-model cannot be used on v-for or v-slot scope variables because they are not writable.`,
177-
[ErrorCodes.X_INVALID_EXPRESSION]: `Invalid JavaScript expression.`,
181+
[ErrorCodes.X_INVALID_EXPRESSION]: `Error parsing JavaScript expression: `,
178182
[ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN]: `<KeepAlive> expects exactly one child component.`,
179183

180184
// generic errors

packages/compiler-core/src/options.ts

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
DirectiveTransform,
77
TransformContext
88
} from './transform'
9+
import { ParserPlugin } from '@babel/parser'
910

1011
export interface ParserOptions {
1112
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
@@ -61,6 +62,9 @@ export interface TransformOptions {
6162
// analysis to determine if a handler is safe to cache.
6263
// - Default: false
6364
cacheHandlers?: boolean
65+
// a list of parser plugins to enable for @babel/parser
66+
// https://babeljs.io/docs/en/next/babel-parser#plugins
67+
expressionPlugins?: ParserPlugin[]
6468
// SFC scoped styles ID
6569
scopeId?: string | null
6670
ssr?: boolean

packages/compiler-core/src/transform.ts

+2
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export function createTransformContext(
117117
directiveTransforms = {},
118118
transformHoist = null,
119119
isBuiltInComponent = NOOP,
120+
expressionPlugins = [],
120121
scopeId = null,
121122
ssr = false,
122123
onError = defaultOnError
@@ -131,6 +132,7 @@ export function createTransformContext(
131132
directiveTransforms,
132133
transformHoist,
133134
isBuiltInComponent,
135+
expressionPlugins,
134136
scopeId,
135137
ssr,
136138
onError,

packages/compiler-core/src/transforms/transformExpression.ts

+34-13
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
CompoundExpressionNode,
1717
createCompoundExpression
1818
} from '../ast'
19-
import { Node, Function, Identifier, Property } from 'estree'
2019
import {
2120
advancePositionWithClone,
2221
isSimpleIdentifier,
@@ -25,6 +24,7 @@ import {
2524
} from '../utils'
2625
import { isGloballyWhitelisted, makeMap } from '@vue/shared'
2726
import { createCompilerError, ErrorCodes } from '../errors'
27+
import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
2828

2929
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
3030

@@ -117,22 +117,39 @@ export function processExpression(
117117
? ` ${rawExp} `
118118
: `(${rawExp})${asParams ? `=>{}` : ``}`
119119
try {
120-
ast = parseJS(source, { ranges: true })
120+
ast = parseJS(source, {
121+
plugins: [
122+
...context.expressionPlugins,
123+
// by default we enable proposals slated for ES2020.
124+
// full list at https://babeljs.io/docs/en/next/babel-parser#plugins
125+
// this will need to be updated as the spec moves forward.
126+
'bigInt',
127+
'optionalChaining',
128+
'nullishCoalescingOperator'
129+
]
130+
}).program
121131
} catch (e) {
122132
context.onError(
123-
createCompilerError(ErrorCodes.X_INVALID_EXPRESSION, node.loc)
133+
createCompilerError(
134+
ErrorCodes.X_INVALID_EXPRESSION,
135+
node.loc,
136+
undefined,
137+
e.message
138+
)
124139
)
125140
return node
126141
}
127142

128143
const ids: (Identifier & PrefixMeta)[] = []
129144
const knownIds = Object.create(context.identifiers)
145+
const isDuplicate = (node: Node & PrefixMeta): boolean =>
146+
ids.some(id => id.start === node.start)
130147

131148
// walk the AST and look for identifiers that need to be prefixed with `_ctx.`.
132149
walkJS(ast, {
133150
enter(node: Node & PrefixMeta, parent) {
134151
if (node.type === 'Identifier') {
135-
if (!ids.includes(node)) {
152+
if (!isDuplicate(node)) {
136153
const needPrefix = shouldPrefix(node, parent)
137154
if (!knownIds[node.name] && needPrefix) {
138155
if (isPropertyShorthand(node, parent)) {
@@ -246,17 +263,20 @@ export function processExpression(
246263
const isFunction = (node: Node): node is Function =>
247264
/Function(Expression|Declaration)$/.test(node.type)
248265

249-
const isPropertyKey = (node: Node, parent: Node) =>
250-
parent &&
251-
parent.type === 'Property' &&
252-
parent.key === node &&
253-
!parent.computed
266+
const isStaticProperty = (node: Node): node is ObjectProperty =>
267+
node && node.type === 'ObjectProperty' && !node.computed
254268

255-
const isPropertyShorthand = (node: Node, parent: Node) =>
256-
isPropertyKey(node, parent) && (parent as Property).value === node
269+
const isPropertyShorthand = (node: Node, parent: Node) => {
270+
return (
271+
isStaticProperty(parent) &&
272+
parent.value === node &&
273+
parent.key.type === 'Identifier' &&
274+
parent.key.name === (node as Identifier).name
275+
)
276+
}
257277

258278
const isStaticPropertyKey = (node: Node, parent: Node) =>
259-
isPropertyKey(node, parent) && (parent as Property).value !== node
279+
isStaticProperty(parent) && parent.key === node
260280

261281
function shouldPrefix(identifier: Identifier, parent: Node) {
262282
if (
@@ -271,7 +291,8 @@ function shouldPrefix(identifier: Identifier, parent: Node) {
271291
!isStaticPropertyKey(identifier, parent) &&
272292
// not a property of a MemberExpression
273293
!(
274-
parent.type === 'MemberExpression' &&
294+
(parent.type === 'MemberExpression' ||
295+
parent.type === 'OptionalMemberExpression') &&
275296
parent.property === identifier &&
276297
!parent.computed
277298
) &&

packages/compiler-core/src/utils.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import {
2222
InterpolationNode,
2323
VNodeCall
2424
} from './ast'
25-
import { parse } from 'acorn'
26-
import { walk } from 'estree-walker'
2725
import { TransformContext } from './transform'
2826
import {
2927
MERGE_PROPS,
@@ -33,6 +31,8 @@ import {
3331
BASE_TRANSITION
3432
} from './runtimeHelpers'
3533
import { isString, isFunction, isObject, hyphenate } from '@vue/shared'
34+
import { parse } from '@babel/parser'
35+
import { Node } from '@babel/types'
3636

3737
export const isBuiltInType = (tag: string, expected: string): boolean =>
3838
tag === expected || tag === hyphenate(expected)
@@ -53,7 +53,7 @@ export function isCoreComponent(tag: string): symbol | void {
5353
// lazy require dependencies so that they don't end up in rollup's dep graph
5454
// and thus can be tree-shaken in browser builds.
5555
let _parse: typeof parse
56-
let _walk: typeof walk
56+
let _walk: any
5757

5858
export function loadDep(name: string) {
5959
if (!__BROWSER__ && typeof process !== 'undefined' && isFunction(require)) {
@@ -70,11 +70,18 @@ export const parseJS: typeof parse = (code, options) => {
7070
!__BROWSER__,
7171
`Expression AST analysis can only be performed in non-browser builds.`
7272
)
73-
const parse = _parse || (_parse = loadDep('acorn').parse)
74-
return parse(code, options)
73+
if (!_parse) {
74+
_parse = loadDep('@babel/parser').parse
75+
}
76+
return _parse(code, options)
77+
}
78+
79+
interface Walker {
80+
enter?(node: Node, parent: Node): void
81+
leave?(node: Node): void
7582
}
7683

77-
export const walkJS: typeof walk = (ast, walker) => {
84+
export const walkJS = (ast: Node, walker: Walker) => {
7885
assert(
7986
!__BROWSER__,
8087
`Expression AST analysis can only be performed in non-browser builds.`

packages/template-explorer/index.html

+1-3
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,11 @@
66
<div id="source" class="editor"></div>
77
<div id="output" class="editor"></div>
88

9-
<script src="https://unpkg.com/[email protected]/dist/acorn.js"></script>
109
<script src="https://unpkg.com/[email protected]/dist/estree-walker.umd.js"></script>
1110
<script src="https://unpkg.com/[email protected]/dist/source-map.js"></script>
1211
<script src="https://unpkg.com/[email protected]/min/vs/loader.js"></script>
13-
<script src="./dist/template-explorer.global.js"></script>
1412
<script>
1513
window._deps = {
16-
acorn,
1714
'estree-walker': estreeWalker,
1815
'source-map': sourceMap
1916
}
@@ -24,6 +21,7 @@
2421
}
2522
})
2623
</script>
24+
<script src="./dist/template-explorer.global.js"></script>
2725
<script>
2826
require(['vs/editor/editor.main'], init /* injected by build */)
2927
</script>

packages/template-explorer/local.html

+2-3
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@
66
<div id="source" class="editor"></div>
77
<div id="output" class="editor"></div>
88

9-
<script src="../../node_modules/acorn/dist/acorn.js"></script>
109
<script src="../../node_modules/estree-walker/dist/estree-walker.umd.js"></script>
1110
<script src="../../node_modules/source-map/dist/source-map.js"></script>
1211
<script src="../../node_modules/monaco-editor/min/vs/loader.js"></script>
13-
<script src="./dist/template-explorer.global.js"></script>
1412
<script>
1513
window._deps = {
16-
acorn,
14+
// @babel/parser is injected by the bundle
1715
'estree-walker': estreeWalker,
1816
'source-map': sourceMap
1917
}
@@ -24,6 +22,7 @@
2422
}
2523
})
2624
</script>
25+
<script src="./dist/template-explorer.global.js"></script>
2726
<script>
2827
require(['vs/editor/editor.main'], init /* injected by build */)
2928
</script>

packages/template-explorer/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { compile as ssrCompile } from '@vue/compiler-ssr'
44
import { compilerOptions, initOptions, ssrMode } from './options'
55
import { watchEffect } from '@vue/runtime-dom'
66
import { SourceMapConsumer } from 'source-map'
7+
import { parse } from '@babel/parser'
8+
9+
window._deps['@babel/parser'] = { parse }
710

811
declare global {
912
interface Window {

rollup.config.js

+8
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ function createConfig(format, output, plugins = []) {
116116
? []
117117
: knownExternals.concat(Object.keys(pkg.dependencies || []))
118118

119+
const nodePlugins = packageOptions.enableNonBrowserBranches
120+
? [
121+
require('@rollup/plugin-node-resolve')(),
122+
require('@rollup/plugin-commonjs')()
123+
]
124+
: []
125+
119126
return {
120127
input: resolve(entryFile),
121128
// Global and Browser ESM builds inlines everything so that they can be
@@ -136,6 +143,7 @@ function createConfig(format, output, plugins = []) {
136143
isGlobalBuild,
137144
isNodeBuild
138145
),
146+
...nodePlugins,
139147
...plugins
140148
],
141149
output,

0 commit comments

Comments
 (0)