Explorar el Código

wip: strip with

Evan You hace 3 años
padre
commit
22c457fe24

+ 15 - 18
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

+ 452 - 0
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<string, number> = 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<string> }, 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<string> }, 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<string> },
+  child: Identifier,
+  knownIds: Record<string, number>
+) {
+  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: <div NODE="foo" />
+    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
+}

+ 1 - 1
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:
       '<style lang="less">\n' +

+ 1 - 1
packages/compiler-sfc/test/compileTemplate.spec.ts

@@ -115,7 +115,7 @@ test('warn missing preprocessor', () => {
   expect(result.errors.length).toBe(1)
 })
 
-test.only('transform assetUrls', () => {
+test('transform assetUrls', () => {
   const source = `
 <div>
   <img src="./logo.png">

+ 55 - 0
packages/compiler-sfc/test/stripWith.spec.ts

@@ -0,0 +1,55 @@
+import { stripWith } from '../src/stripWith'
+import { compile } from 'web/entry-compiler'
+import { format } from 'prettier'
+
+it('should work', () => {
+  const { render } = compile(`<div id="app">
+  <div>{{ foo }}</div>
+  <p v-for="i in list">{{ i }}</p>
+  <foo inline-template>
+    <div>{{ bar }}</div>
+  </foo>
+</div>`)
+
+  const result = format(stripWith(render, `render`), {
+    semi: false,
+    parser: 'babel'
+  })
+
+  expect(result).not.toMatch(`_vm._c`)
+  expect(result).toMatch(`_vm.foo`)
+  expect(result).toMatch(`_vm.list`)
+  expect(result).not.toMatch(`_vm.i`)
+  expect(result).not.toMatch(`with (this)`)
+
+  expect(result).toMatchInlineSnapshot(`
+    "function render() {
+      var _vm = this
+      var _c = _vm._self._c
+      return _c(
+        \\"div\\",
+        { attrs: { id: \\"app\\" } },
+        [
+          _c(\\"div\\", [_vm._v(_vm._s(_vm.foo))]),
+          _vm._v(\\" \\"),
+          _vm._l(_vm.list, function (i) {
+            return _c(\\"p\\", [_vm._v(_vm._s(i))])
+          }),
+          _vm._v(\\" \\"),
+          _c(\\"foo\\", {
+            inlineTemplate: {
+              render: function () {
+                var _vm = this
+                var _c = _vm._self._c
+                return _c(\\"div\\", [_vm._v(_vm._s(_vm.bar))])
+              },
+              staticRenderFns: [],
+            },
+          }),
+        ],
+        2
+      )
+    }
+    "
+  `)
+})