From 22c457fe24f737505356edfb8696c7e50fd9d971 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 10 Jun 2022 18:38:09 +0800 Subject: [PATCH] wip: strip with --- packages/compiler-sfc/src/compileTemplate.ts | 33 +- packages/compiler-sfc/src/stripWith.ts | 452 ++++++++++++++++++ .../compiler-sfc/test/compileStyle.spec.ts | 2 +- .../compiler-sfc/test/compileTemplate.spec.ts | 2 +- packages/compiler-sfc/test/stripWith.spec.ts | 55 +++ 5 files changed, 524 insertions(+), 20 deletions(-) create mode 100644 packages/compiler-sfc/src/stripWith.ts create mode 100644 packages/compiler-sfc/test/stripWith.spec.ts diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts index 8d768aa9..4b1933f9 100644 --- a/packages/compiler-sfc/src/compileTemplate.ts +++ b/packages/compiler-sfc/src/compileTemplate.ts @@ -10,7 +10,7 @@ import assetUrlsModule, { import srcsetModule from './templateCompilerModules/srcset' import consolidate from '@vue/consolidate' import * as _compiler from 'web/entry-compiler' -import transpile from 'vue-template-es2015-compiler' +import { stripWith } from './stripWith' export interface TemplateCompileOptions { source: string @@ -26,6 +26,7 @@ export interface TemplateCompileOptions { isFunctional?: boolean optimizeSSR?: boolean prettify?: boolean + isTS?: boolean } export interface TemplateCompileResult { @@ -108,7 +109,8 @@ function actuallyCompile( isProduction = process.env.NODE_ENV === 'production', isFunctional = false, optimizeSSR = false, - prettify = true + prettify = true, + isTS = false } = options const compile = @@ -142,25 +144,20 @@ function actuallyCompile( errors } } else { - // TODO better transpile - const finalTranspileOptions = Object.assign({}, transpileOptions, { - transforms: Object.assign({}, transpileOptions.transforms, { - stripWithFunctional: isFunctional - }) - }) - - const toFunction = (code: string): string => { - return `function (${isFunctional ? `_h,_vm` : ``}) {${code}}` - } - // transpile code with vue-template-es2015-compiler, which is a forked // version of Buble that applies ES2015 transforms + stripping `with` usage let code = - transpile( - `var __render__ = ${toFunction(render)}\n` + - `var __staticRenderFns__ = [${staticRenderFns.map(toFunction)}]`, - finalTranspileOptions - ) + `\n` + `var __render__ = ${stripWith( + render, + `render`, + isFunctional, + isTS, + transpileOptions + )}\n` + + `var __staticRenderFns__ = [${staticRenderFns.map(code => + stripWith(code, ``, isFunctional, isTS, transpileOptions) + )}]` + + `\n` // #23 we use __render__ to avoid `render` not being prefixed by the // transpiler when stripping with, but revert it back to `render` to diff --git a/packages/compiler-sfc/src/stripWith.ts b/packages/compiler-sfc/src/stripWith.ts new file mode 100644 index 00000000..4f22e051 --- /dev/null +++ b/packages/compiler-sfc/src/stripWith.ts @@ -0,0 +1,452 @@ +import MagicString from 'magic-string' +import { parseExpression, ParserOptions, ParserPlugin } from '@babel/parser' +import { walk } from 'estree-walker' +import { makeMap } from 'shared/util' + +import type { + Identifier, + Node, + Function, + ObjectProperty, + BlockStatement, + Program +} from '@babel/types' + +const doNotPrefix = makeMap( + 'Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + + 'require,' + // for webpack + 'arguments,' + // parsed as identifier but is a special keyword... + '_c' // cached to save property access +) + +/** + * The input is expected to be the render function code directly returned from + * `compile()` calls, e.g. `with(this){return ...}` + */ +export function stripWith( + source: string, + fnName = '', + isFunctional = false, + isTS = false, + babelOptions: ParserOptions = {} +) { + source = `function ${fnName}(${isFunctional ? `_c,_vm` : ``}){${source}\n}` + + const s = new MagicString(source) + + const plugins: ParserPlugin[] = [ + ...(isTS ? (['typescript'] as const) : []), + ...(babelOptions?.plugins || []) + ] + + const ast = parseExpression(source, { + ...babelOptions, + plugins + }) + + const parentStack: Node[] = [] + const knownIds: Record = Object.create(null) + + // based on https://github.com/vuejs/core/blob/main/packages/compiler-core/src/babelUtils.ts + ;(walk as any)(ast, { + enter(node: Node & { scopeIds?: Set }, parent: Node | undefined) { + parent && parentStack.push(parent) + if ( + parent && + parent.type.startsWith('TS') && + parent.type !== 'TSAsExpression' && + parent.type !== 'TSNonNullExpression' && + parent.type !== 'TSTypeAssertion' + ) { + return this.skip() + } + + if (node.type === 'WithStatement') { + s.remove(node.start!, node.body.start! + 1) + s.remove(node.end! - 1, node.end!) + if (!isFunctional) { + s.prependRight(node.start!, `var _vm=this;var _c=_vm._self._c;`) + } + } + + if (node.type === 'Identifier') { + const isLocal = !!knownIds[node.name] + const isRefed = isReferencedIdentifier(node, parent!, parentStack) + if (isRefed && !isLocal) { + if (doNotPrefix(node.name)) { + return + } + s.prependRight(node.start!, '_vm.') + } + } else if ( + node.type === 'ObjectProperty' && + parent!.type === 'ObjectPattern' + ) { + // mark property in destructure pattern + ;(node as any).inPattern = true + } else if (isFunctionType(node)) { + // walk function expressions and add its arguments to known identifiers + // so that we don't prefix them + walkFunctionParams(node, id => markScopeIdentifier(node, id, knownIds)) + } else if (node.type === 'BlockStatement') { + // #3445 record block-level local variables + walkBlockDeclarations(node, id => + markScopeIdentifier(node, id, knownIds) + ) + } + }, + leave(node: Node & { scopeIds?: Set }, parent: Node | undefined) { + parent && parentStack.pop() + if (node !== ast && node.scopeIds) { + for (const id of node.scopeIds) { + knownIds[id]-- + if (knownIds[id] === 0) { + delete knownIds[id] + } + } + } + } + }) + + return s.toString() +} + +export function isReferencedIdentifier( + id: Identifier, + parent: Node | null, + parentStack: Node[] +) { + if (!parent) { + return true + } + + // is a special keyword but parsed as identifier + if (id.name === 'arguments') { + return false + } + + if (isReferenced(id, parent)) { + return true + } + + // babel's isReferenced check returns false for ids being assigned to, so we + // need to cover those cases here + switch (parent.type) { + case 'AssignmentExpression': + case 'AssignmentPattern': + return true + case 'ObjectPattern': + case 'ArrayPattern': + return isInDestructureAssignment(parent, parentStack) + } + + return false +} + +export function isInDestructureAssignment( + parent: Node, + parentStack: Node[] +): boolean { + if ( + parent && + (parent.type === 'ObjectProperty' || parent.type === 'ArrayPattern') + ) { + let i = parentStack.length + while (i--) { + const p = parentStack[i] + if (p.type === 'AssignmentExpression') { + return true + } else if (p.type !== 'ObjectProperty' && !p.type.endsWith('Pattern')) { + break + } + } + } + return false +} + +export function walkFunctionParams( + node: Function, + onIdent: (id: Identifier) => void +) { + for (const p of node.params) { + for (const id of extractIdentifiers(p)) { + onIdent(id) + } + } +} + +export function walkBlockDeclarations( + block: BlockStatement | Program, + onIdent: (node: Identifier) => void +) { + for (const stmt of block.body) { + if (stmt.type === 'VariableDeclaration') { + if (stmt.declare) continue + for (const decl of stmt.declarations) { + for (const id of extractIdentifiers(decl.id)) { + onIdent(id) + } + } + } else if ( + stmt.type === 'FunctionDeclaration' || + stmt.type === 'ClassDeclaration' + ) { + if (stmt.declare || !stmt.id) continue + onIdent(stmt.id) + } + } +} + +export function extractIdentifiers( + param: Node, + nodes: Identifier[] = [] +): Identifier[] { + switch (param.type) { + case 'Identifier': + nodes.push(param) + break + + case 'MemberExpression': + let object: any = param + while (object.type === 'MemberExpression') { + object = object.object + } + nodes.push(object) + break + + case 'ObjectPattern': + for (const prop of param.properties) { + if (prop.type === 'RestElement') { + extractIdentifiers(prop.argument, nodes) + } else { + extractIdentifiers(prop.value, nodes) + } + } + break + + case 'ArrayPattern': + param.elements.forEach(element => { + if (element) extractIdentifiers(element, nodes) + }) + break + + case 'RestElement': + extractIdentifiers(param.argument, nodes) + break + + case 'AssignmentPattern': + extractIdentifiers(param.left, nodes) + break + } + + return nodes +} + +export function markScopeIdentifier( + node: Node & { scopeIds?: Set }, + child: Identifier, + knownIds: Record +) { + const { name } = child + if (node.scopeIds && node.scopeIds.has(name)) { + return + } + if (name in knownIds) { + knownIds[name]++ + } else { + knownIds[name] = 1 + } + ;(node.scopeIds || (node.scopeIds = new Set())).add(name) +} + +export const isFunctionType = (node: Node): node is Function => { + return /Function(?:Expression|Declaration)$|Method$/.test(node.type) +} + +export const isStaticProperty = (node: Node): node is ObjectProperty => + node && + (node.type === 'ObjectProperty' || node.type === 'ObjectMethod') && + !node.computed + +export const isStaticPropertyKey = (node: Node, parent: Node) => + isStaticProperty(parent) && parent.key === node + +/** + * Copied from https://github.com/babel/babel/blob/main/packages/babel-types/src/validators/isReferenced.ts + * To avoid runtime dependency on @babel/types (which includes process references) + * This file should not change very often in babel but we may need to keep it + * up-to-date from time to time. + * + * https://github.com/babel/babel/blob/main/LICENSE + * + */ +function isReferenced(node: Node, parent: Node, grandparent?: Node): boolean { + switch (parent.type) { + // yes: PARENT[NODE] + // yes: NODE.child + // no: parent.NODE + case 'MemberExpression': + case 'OptionalMemberExpression': + if (parent.property === node) { + return !!parent.computed + } + return parent.object === node + + case 'JSXMemberExpression': + return parent.object === node + // no: let NODE = init; + // yes: let id = NODE; + case 'VariableDeclarator': + return parent.init === node + + // yes: () => NODE + // no: (NODE) => {} + case 'ArrowFunctionExpression': + return parent.body === node + + // no: class { #NODE; } + // no: class { get #NODE() {} } + // no: class { #NODE() {} } + // no: class { fn() { return this.#NODE; } } + case 'PrivateName': + return false + + // no: class { NODE() {} } + // yes: class { [NODE]() {} } + // no: class { foo(NODE) {} } + case 'ClassMethod': + case 'ClassPrivateMethod': + case 'ObjectMethod': + if (parent.key === node) { + return !!parent.computed + } + return false + + // yes: { [NODE]: "" } + // no: { NODE: "" } + // depends: { NODE } + // depends: { key: NODE } + case 'ObjectProperty': + if (parent.key === node) { + return !!parent.computed + } + // parent.value === node + return !grandparent || grandparent.type !== 'ObjectPattern' + // no: class { NODE = value; } + // yes: class { [NODE] = value; } + // yes: class { key = NODE; } + case 'ClassProperty': + if (parent.key === node) { + return !!parent.computed + } + return true + case 'ClassPrivateProperty': + return parent.key !== node + + // no: class NODE {} + // yes: class Foo extends NODE {} + case 'ClassDeclaration': + case 'ClassExpression': + return parent.superClass === node + + // yes: left = NODE; + // no: NODE = right; + case 'AssignmentExpression': + return parent.right === node + + // no: [NODE = foo] = []; + // yes: [foo = NODE] = []; + case 'AssignmentPattern': + return parent.right === node + + // no: NODE: for (;;) {} + case 'LabeledStatement': + return false + + // no: try {} catch (NODE) {} + case 'CatchClause': + return false + + // no: function foo(...NODE) {} + case 'RestElement': + return false + + case 'BreakStatement': + case 'ContinueStatement': + return false + + // no: function NODE() {} + // no: function foo(NODE) {} + case 'FunctionDeclaration': + case 'FunctionExpression': + return false + + // no: export NODE from "foo"; + // no: export * as NODE from "foo"; + case 'ExportNamespaceSpecifier': + case 'ExportDefaultSpecifier': + return false + + // no: export { foo as NODE }; + // yes: export { NODE as foo }; + // no: export { NODE as foo } from "foo"; + case 'ExportSpecifier': + // @ts-expect-error + if (grandparent?.source) { + return false + } + return parent.local === node + + // no: import NODE from "foo"; + // no: import * as NODE from "foo"; + // no: import { NODE as foo } from "foo"; + // no: import { foo as NODE } from "foo"; + // no: import NODE from "bar"; + case 'ImportDefaultSpecifier': + case 'ImportNamespaceSpecifier': + case 'ImportSpecifier': + return false + + // no: import "foo" assert { NODE: "json" } + case 'ImportAttribute': + return false + + // no:
+ case 'JSXAttribute': + return false + + // no: [NODE] = []; + // no: ({ NODE }) = []; + case 'ObjectPattern': + case 'ArrayPattern': + return false + + // no: new.NODE + // no: NODE.target + case 'MetaProperty': + return false + + // yes: type X = { someProperty: NODE } + // no: type X = { NODE: OtherType } + case 'ObjectTypeProperty': + return parent.key !== node + + // yes: enum X { Foo = NODE } + // no: enum X { NODE } + case 'TSEnumMember': + return parent.id !== node + + // yes: { [NODE]: value } + // no: { NODE: value } + case 'TSPropertySignature': + if (parent.key === node) { + return !!parent.computed + } + + return true + } + + return true +} diff --git a/packages/compiler-sfc/test/compileStyle.spec.ts b/packages/compiler-sfc/test/compileStyle.spec.ts index 6f2a4f69..df13c6e6 100644 --- a/packages/compiler-sfc/test/compileStyle.spec.ts +++ b/packages/compiler-sfc/test/compileStyle.spec.ts @@ -1,7 +1,7 @@ import { parse } from '../src/parse' import { compileStyle, compileStyleAsync } from '../src/compileStyle' -test.only('preprocess less', () => { +test('preprocess less', () => { const style = parse({ source: '