Skip to content

Commit

Permalink
feat(compiler-core): switch to @babel/parser for expression parsing
Browse files Browse the repository at this point in the history
    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.
  • Loading branch information
yyx990803 committed Feb 27, 2020
1 parent 4809325 commit 8449a97
Show file tree
Hide file tree
Showing 13 changed files with 207 additions and 33 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
},
"devDependencies": {
"@microsoft/api-extractor": "^7.3.9",
"@rollup/plugin-commonjs": "^11.0.2",
"@rollup/plugin-json": "^4.0.0",
"@rollup/plugin-node-resolve": "^7.1.1",
"@rollup/plugin-replace": "^2.2.1",
"@types/jest": "^24.0.21",
"@types/puppeteer": "^2.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,65 @@ describe('compiler: expression transform', () => {
const onError = jest.fn()
parseWithExpressionTransform(`{{ a( }}`, { onError })
expect(onError.mock.calls[0][0].message).toMatch(
`Invalid JavaScript expression.`
`Error parsing JavaScript expression: Unexpected token`
)
})

describe('ES Proposals support', () => {
test('bigInt', () => {
const node = parseWithExpressionTransform(
`{{ 13000n }}`
) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.SIMPLE_EXPRESSION,
content: `13000n`,
isStatic: false,
isConstant: true
})
})

test('nullish colescing', () => {
const node = parseWithExpressionTransform(
`{{ a ?? b }}`
) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [{ content: `_ctx.a` }, ` ?? `, { content: `_ctx.b` }]
})
})

test('optional chaining', () => {
const node = parseWithExpressionTransform(
`{{ a?.b?.c }}`
) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
{ content: `_ctx.a` },
`?.`,
{ content: `b` },
`?.`,
{ content: `c` }
]
})
})

test('Enabling additional plugins', () => {
// enabling pipeline operator to replace filters:
const node = parseWithExpressionTransform(`{{ a |> uppercase }}`, {
expressionPlugins: [
[
'pipelineOperator',
{
proposal: 'minimal'
}
]
]
}) as InterpolationNode
expect(node.content).toMatchObject({
type: NodeTypes.COMPOUND_EXPRESSION,
children: [{ content: `_ctx.a` }, ` |> `, { content: `_ctx.uppercase` }]
})
})
})
})
3 changes: 2 additions & 1 deletion packages/compiler-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
},
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-core#readme",
"dependencies": {
"acorn": "^7.1.0",
"@babel/parser": "^7.8.6",
"@babel/types": "^7.8.6",
"estree-walker": "^0.8.1",
"source-map": "^0.6.1"
}
Expand Down
10 changes: 7 additions & 3 deletions packages/compiler-core/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ export function defaultOnError(error: CompilerError) {
export function createCompilerError<T extends number>(
code: T,
loc?: SourceLocation,
messages?: { [code: number]: string }
messages?: { [code: number]: string },
additionalMessage?: string
): T extends ErrorCodes ? CoreCompilerError : CompilerError {
const msg = __DEV__ || !__BROWSER__ ? (messages || errorMessages)[code] : code
const msg =
__DEV__ || !__BROWSER__
? (messages || errorMessages)[code] + (additionalMessage || ``)
: code
const error = new SyntaxError(String(msg)) as CompilerError
error.code = code
error.loc = loc
Expand Down Expand Up @@ -174,7 +178,7 @@ export const errorMessages: { [code: number]: string } = {
[ErrorCodes.X_V_MODEL_NO_EXPRESSION]: `v-model is missing expression.`,
[ErrorCodes.X_V_MODEL_MALFORMED_EXPRESSION]: `v-model value must be a valid JavaScript member expression.`,
[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.`,
[ErrorCodes.X_INVALID_EXPRESSION]: `Invalid JavaScript expression.`,
[ErrorCodes.X_INVALID_EXPRESSION]: `Error parsing JavaScript expression: `,
[ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN]: `<KeepAlive> expects exactly one child component.`,

// generic errors
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler-core/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DirectiveTransform,
TransformContext
} from './transform'
import { ParserPlugin } from '@babel/parser'

export interface ParserOptions {
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
Expand Down Expand Up @@ -61,6 +62,9 @@ export interface TransformOptions {
// analysis to determine if a handler is safe to cache.
// - Default: false
cacheHandlers?: boolean
// a list of parser plugins to enable for @babel/parser
// https://babeljs.io/docs/en/next/babel-parser#plugins
expressionPlugins?: ParserPlugin[]
// SFC scoped styles ID
scopeId?: string | null
ssr?: boolean
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler-core/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export function createTransformContext(
directiveTransforms = {},
transformHoist = null,
isBuiltInComponent = NOOP,
expressionPlugins = [],
scopeId = null,
ssr = false,
onError = defaultOnError
Expand All @@ -131,6 +132,7 @@ export function createTransformContext(
directiveTransforms,
transformHoist,
isBuiltInComponent,
expressionPlugins,
scopeId,
ssr,
onError,
Expand Down
47 changes: 34 additions & 13 deletions packages/compiler-core/src/transforms/transformExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
CompoundExpressionNode,
createCompoundExpression
} from '../ast'
import { Node, Function, Identifier, Property } from 'estree'
import {
advancePositionWithClone,
isSimpleIdentifier,
Expand All @@ -25,6 +24,7 @@ import {
} from '../utils'
import { isGloballyWhitelisted, makeMap } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors'
import { Node, Function, Identifier, ObjectProperty } from '@babel/types'

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

Expand Down Expand Up @@ -117,22 +117,39 @@ export function processExpression(
? ` ${rawExp} `
: `(${rawExp})${asParams ? `=>{}` : ``}`
try {
ast = parseJS(source, { ranges: true })
ast = parseJS(source, {
plugins: [
...context.expressionPlugins,
// by default we enable proposals slated for ES2020.
// full list at https://babeljs.io/docs/en/next/babel-parser#plugins
// this will need to be updated as the spec moves forward.
'bigInt',
'optionalChaining',
'nullishCoalescingOperator'
]
}).program
} catch (e) {
context.onError(
createCompilerError(ErrorCodes.X_INVALID_EXPRESSION, node.loc)
createCompilerError(
ErrorCodes.X_INVALID_EXPRESSION,
node.loc,
undefined,
e.message
)
)
return node
}

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

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

const isPropertyKey = (node: Node, parent: Node) =>
parent &&
parent.type === 'Property' &&
parent.key === node &&
!parent.computed
const isStaticProperty = (node: Node): node is ObjectProperty =>
node && node.type === 'ObjectProperty' && !node.computed

const isPropertyShorthand = (node: Node, parent: Node) =>
isPropertyKey(node, parent) && (parent as Property).value === node
const isPropertyShorthand = (node: Node, parent: Node) => {
return (
isStaticProperty(parent) &&
parent.value === node &&
parent.key.type === 'Identifier' &&
parent.key.name === (node as Identifier).name
)
}

const isStaticPropertyKey = (node: Node, parent: Node) =>
isPropertyKey(node, parent) && (parent as Property).value !== node
isStaticProperty(parent) && parent.key === node

function shouldPrefix(identifier: Identifier, parent: Node) {
if (
Expand All @@ -271,7 +291,8 @@ function shouldPrefix(identifier: Identifier, parent: Node) {
!isStaticPropertyKey(identifier, parent) &&
// not a property of a MemberExpression
!(
parent.type === 'MemberExpression' &&
(parent.type === 'MemberExpression' ||
parent.type === 'OptionalMemberExpression') &&
parent.property === identifier &&
!parent.computed
) &&
Expand Down
19 changes: 13 additions & 6 deletions packages/compiler-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import {
InterpolationNode,
VNodeCall
} from './ast'
import { parse } from 'acorn'
import { walk } from 'estree-walker'
import { TransformContext } from './transform'
import {
MERGE_PROPS,
Expand All @@ -33,6 +31,8 @@ import {
BASE_TRANSITION
} from './runtimeHelpers'
import { isString, isFunction, isObject, hyphenate } from '@vue/shared'
import { parse } from '@babel/parser'
import { Node } from '@babel/types'

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

export function loadDep(name: string) {
if (!__BROWSER__ && typeof process !== 'undefined' && isFunction(require)) {
Expand All @@ -70,11 +70,18 @@ export const parseJS: typeof parse = (code, options) => {
!__BROWSER__,
`Expression AST analysis can only be performed in non-browser builds.`
)
const parse = _parse || (_parse = loadDep('acorn').parse)
return parse(code, options)
if (!_parse) {
_parse = loadDep('@babel/parser').parse
}
return _parse(code, options)
}

interface Walker {
enter?(node: Node, parent: Node): void
leave?(node: Node): void
}

export const walkJS: typeof walk = (ast, walker) => {
export const walkJS = (ast: Node, walker: Walker) => {
assert(
!__BROWSER__,
`Expression AST analysis can only be performed in non-browser builds.`
Expand Down
4 changes: 1 addition & 3 deletions packages/template-explorer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@
<div id="source" class="editor"></div>
<div id="output" class="editor"></div>

<script src="https://unpkg.com/acorn@7.1.0/dist/acorn.js"></script>
<script src="https://unpkg.com/estree-walker@0.8.1/dist/estree-walker.umd.js"></script>
<script src="https://unpkg.com/source-map@0.6.1/dist/source-map.js"></script>
<script src="https://unpkg.com/monaco-editor@0.18.1/min/vs/loader.js"></script>
<script src="./dist/template-explorer.global.js"></script>
<script>
window._deps = {
acorn,
'estree-walker': estreeWalker,
'source-map': sourceMap
}
Expand All @@ -24,6 +21,7 @@
}
})
</script>
<script src="./dist/template-explorer.global.js"></script>
<script>
require(['vs/editor/editor.main'], init /* injected by build */)
</script>
5 changes: 2 additions & 3 deletions packages/template-explorer/local.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@
<div id="source" class="editor"></div>
<div id="output" class="editor"></div>

<script src="../../node_modules/acorn/dist/acorn.js"></script>
<script src="../../node_modules/estree-walker/dist/estree-walker.umd.js"></script>
<script src="../../node_modules/source-map/dist/source-map.js"></script>
<script src="../../node_modules/monaco-editor/min/vs/loader.js"></script>
<script src="./dist/template-explorer.global.js"></script>
<script>
window._deps = {
acorn,
// @babel/parser is injected by the bundle
'estree-walker': estreeWalker,
'source-map': sourceMap
}
Expand All @@ -24,6 +22,7 @@
}
})
</script>
<script src="./dist/template-explorer.global.js"></script>
<script>
require(['vs/editor/editor.main'], init /* injected by build */)
</script>
3 changes: 3 additions & 0 deletions packages/template-explorer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { compile as ssrCompile } from '@vue/compiler-ssr'
import { compilerOptions, initOptions, ssrMode } from './options'
import { watchEffect } from '@vue/runtime-dom'
import { SourceMapConsumer } from 'source-map'
import { parse } from '@babel/parser'

window._deps['@babel/parser'] = { parse }

declare global {
interface Window {
Expand Down
8 changes: 8 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ function createConfig(format, output, plugins = []) {
? []
: knownExternals.concat(Object.keys(pkg.dependencies || []))

const nodePlugins = packageOptions.enableNonBrowserBranches
? [
require('@rollup/plugin-node-resolve')(),
require('@rollup/plugin-commonjs')()
]
: []

return {
input: resolve(entryFile),
// Global and Browser ESM builds inlines everything so that they can be
Expand All @@ -136,6 +143,7 @@ function createConfig(format, output, plugins = []) {
isGlobalBuild,
isNodeBuild
),
...nodePlugins,
...plugins
],
output,
Expand Down
Loading

0 comments on commit 8449a97

Please sign in to comment.