diff --git a/package.json b/package.json index 96c036da2cd..82e828a05d0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts b/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts index 6f1f280bd88..0446fa22936 100644 --- a/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformExpressions.spec.ts @@ -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` }] + }) + }) + }) }) diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index 2b14a65b9f4..e11b6e1b208 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -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" } diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts index a46fa80e911..408847a5bdc 100644 --- a/packages/compiler-core/src/errors.ts +++ b/packages/compiler-core/src/errors.ts @@ -16,9 +16,13 @@ export function defaultOnError(error: CompilerError) { export function createCompilerError( 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 @@ -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]: ` expects exactly one child component.`, // generic errors diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts index dc9d3631e16..68a8e3282ab 100644 --- a/packages/compiler-core/src/options.ts +++ b/packages/compiler-core/src/options.ts @@ -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 @@ -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 diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index af62ba86758..12c9023b626 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -117,6 +117,7 @@ export function createTransformContext( directiveTransforms = {}, transformHoist = null, isBuiltInComponent = NOOP, + expressionPlugins = [], scopeId = null, ssr = false, onError = defaultOnError @@ -131,6 +132,7 @@ export function createTransformContext( directiveTransforms, transformHoist, isBuiltInComponent, + expressionPlugins, scopeId, ssr, onError, diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts index b78daaf7226..b62bcefcd63 100644 --- a/packages/compiler-core/src/transforms/transformExpression.ts +++ b/packages/compiler-core/src/transforms/transformExpression.ts @@ -16,7 +16,6 @@ import { CompoundExpressionNode, createCompoundExpression } from '../ast' -import { Node, Function, Identifier, Property } from 'estree' import { advancePositionWithClone, isSimpleIdentifier, @@ -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') @@ -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)) { @@ -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 ( @@ -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 ) && diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index 7fe6a4985ff..2f57bcd9a01 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -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, @@ -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) @@ -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)) { @@ -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.` diff --git a/packages/template-explorer/index.html b/packages/template-explorer/index.html index f1a66e98a20..3718421823c 100644 --- a/packages/template-explorer/index.html +++ b/packages/template-explorer/index.html @@ -6,14 +6,11 @@
- - + diff --git a/packages/template-explorer/local.html b/packages/template-explorer/local.html index 349758adc1c..4ab1d97e803 100644 --- a/packages/template-explorer/local.html +++ b/packages/template-explorer/local.html @@ -6,14 +6,12 @@
- - + diff --git a/packages/template-explorer/src/index.ts b/packages/template-explorer/src/index.ts index af90ce6c4b1..f5579fbd6bf 100644 --- a/packages/template-explorer/src/index.ts +++ b/packages/template-explorer/src/index.ts @@ -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 { diff --git a/rollup.config.js b/rollup.config.js index b1248ee0d9b..b5150fdc630 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -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 @@ -136,6 +143,7 @@ function createConfig(format, output, plugins = []) { isGlobalBuild, isNodeBuild ), + ...nodePlugins, ...plugins ], output, diff --git a/yarn.lock b/yarn.lock index 9cc7c2989b0..44334f1fb0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -91,6 +91,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== +"@babel/parser@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.6.tgz#ba5c9910cddb77685a008e3c587af8d27b67962c" + integrity sha512-trGNYSfwq5s0SgM1BMEB8hX3NDmO7EP2wsDGDexiaKMB92BaRpS+qZfpkMqUBhcsOTBwNy9B/jieo4ad/t/z2g== + "@babel/plugin-syntax-object-rest-spread@^7.0.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e" @@ -131,6 +136,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.6.tgz#629ecc33c2557fcde7126e58053127afdb3e6d01" + integrity sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@cnakazawa/watch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef" @@ -371,6 +385,17 @@ "@nodelib/fs.scandir" "2.1.1" fastq "^1.6.0" +"@rollup/plugin-commonjs@^11.0.2": + version "11.0.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.2.tgz#837cc6950752327cb90177b608f0928a4e60b582" + integrity sha512-MPYGZr0qdbV5zZj8/2AuomVpnRVXRU5XKXb3HVniwRoRCreGlf5kOE081isNWeiLIi6IYkwTX9zE0/c7V8g81g== + dependencies: + "@rollup/pluginutils" "^3.0.0" + estree-walker "^1.0.1" + is-reference "^1.1.2" + magic-string "^0.25.2" + resolve "^1.11.0" + "@rollup/plugin-json@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.0.2.tgz#482185ee36ac7dd21c346e2dbcc22ffed0c6f2d6" @@ -378,6 +403,17 @@ dependencies: "@rollup/pluginutils" "^3.0.4" +"@rollup/plugin-node-resolve@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.1.tgz#8c6e59c4b28baf9d223028d0e450e06a485bb2b7" + integrity sha512-14ddhD7TnemeHE97a4rLOhobfYvUVcaYuqTnL8Ti7Jxi9V9Jr5LY7Gko4HZ5k4h4vqQM0gBQt6tsp9xXW94WPA== + dependencies: + "@rollup/pluginutils" "^3.0.6" + "@types/resolve" "0.0.8" + builtin-modules "^3.1.0" + is-module "^1.0.0" + resolve "^1.14.2" + "@rollup/plugin-replace@^2.2.1": version "2.3.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.3.1.tgz#16fb0563628f9e6c6ef9e05d48d3608916d466f5" @@ -386,7 +422,7 @@ "@rollup/pluginutils" "^3.0.4" magic-string "^0.25.5" -"@rollup/pluginutils@^3.0.4": +"@rollup/pluginutils@^3.0.0", "@rollup/pluginutils@^3.0.4", "@rollup/pluginutils@^3.0.6": version "3.0.8" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.0.8.tgz#4e94d128d94b90699e517ef045422960d18c8fde" integrity sha512-rYGeAc4sxcZ+kPG/Tw4/fwJODC3IXHYDH4qusdN/b6aLw5LPUbzpecYbEJh4sVQGPFJxd2dBU4kc1H3oy9/bnw== @@ -463,7 +499,7 @@ "@types/bluebird" "*" "@types/node" "*" -"@types/estree@*": +"@types/estree@*", "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== @@ -541,6 +577,13 @@ dependencies: "@types/node" "*" +"@types/resolve@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" + integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ== + dependencies: + "@types/node" "*" + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -1057,6 +1100,11 @@ builtin-modules@^1.0.0: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= +builtin-modules@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484" + integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw== + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -3003,6 +3051,11 @@ is-installed-globally@^0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= + is-npm@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" @@ -3080,6 +3133,13 @@ is-redirect@^1.0.0: resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= +is-reference@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.1.4.tgz#3f95849886ddb70256a3e6d062b1a68c13c51427" + integrity sha512-uJA/CDPO3Tao3GTrxYn6AwkM4nUPJiGGYu5+cB8qbC7WGFlrKZbiRo7SFKxUAEpFUfiHofWCXBUNhvYJMh+6zw== + dependencies: + "@types/estree" "0.0.39" + is-regex@^1.0.3, is-regex@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" @@ -3993,7 +4053,7 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -magic-string@^0.25.5: +magic-string@^0.25.2, magic-string@^0.25.5: version "0.25.6" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.6.tgz#5586387d1242f919c6d223579cc938bf1420795e" integrity sha512-3a5LOMSGoCTH5rbqobC2HuDNRtE2glHZ8J7pK+QZYppyWA36yuNpsX994rIY2nCuyP7CZYy7lQq/X2jygiZ89g== @@ -5381,6 +5441,13 @@ resolve@1.x, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.3.2: dependencies: path-parse "^1.0.6" +resolve@^1.11.0, resolve@^1.14.2: + version "1.15.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" + integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== + dependencies: + path-parse "^1.0.6" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"