]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: expression rewrite
authorEvan You <yyx990803@gmail.com>
Mon, 23 Sep 2019 06:52:54 +0000 (02:52 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 23 Sep 2019 17:29:52 +0000 (13:29 -0400)
packages/compiler-core/__tests__/transforms/expression.spec.ts [new file with mode: 0644]
packages/compiler-core/package.json
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/element.ts
packages/compiler-core/src/transforms/expression.ts
packages/compiler-core/src/transforms/vIf.ts
yarn.lock

diff --git a/packages/compiler-core/__tests__/transforms/expression.spec.ts b/packages/compiler-core/__tests__/transforms/expression.spec.ts
new file mode 100644 (file)
index 0000000..2153fd0
--- /dev/null
@@ -0,0 +1,19 @@
+import { SourceMapConsumer } from 'source-map'
+import { compile } from '../../src'
+
+test(`should work`, async () => {
+  const { code, map } = compile(
+    `<div v-if="hello">{{ ({ a }, b) => a + b + c }}</div>`,
+    {
+      useWith: false
+    }
+  )
+  console.log(code)
+  console.log(map)
+  const consumer = await new SourceMapConsumer(map!)
+  const pos = consumer.originalPositionFor({
+    line: 4,
+    column: 31
+  })
+  console.log(pos)
+})
index db71ca548a878a61787bb15f5fd08d3efc1ad990..bfefbbae342ddce374bd3f7b4785c782b4cbd9c2 100644 (file)
@@ -10,7 +10,9 @@
   ],
   "types": "dist/compiler-core.d.ts",
   "buildOptions": {
-    "formats": ["cjs"]
+    "formats": [
+      "cjs"
+    ]
   },
   "repository": {
     "type": "git",
@@ -26,6 +28,8 @@
   },
   "homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-core#readme",
   "dependencies": {
+    "estree-walker": "^0.8.1",
+    "meriyah": "^1.7.2",
     "source-map": "^0.7.3"
   }
 }
index 897eaafa09d13f2b9b566969a67cf6942c4565df..1c5e87a59fe513febe75c4edba1fa9fb206aaadc 100644 (file)
@@ -16,6 +16,7 @@ export const enum NodeTypes {
   ATTRIBUTE,
   DIRECTIVE,
   // containers
+  COMPOUND_EXPRESSION,
   IF,
   IF_BRANCH,
   FOR,
@@ -109,6 +110,7 @@ export interface ExpressionNode extends Node {
   type: NodeTypes.EXPRESSION
   content: string
   isStatic: boolean
+  children?: (ExpressionNode | string)[]
 }
 
 export interface IfNode extends Node {
index 3b2d1f3292fe5483c32c2677a4236075b95086c8..820b1d97162a7cbf4fe52c5b6e9591b31d388347 100644 (file)
@@ -119,7 +119,7 @@ export function generate(
   options: CodegenOptions = {}
 ): CodegenResult {
   const context = createCodegenContext(ast, options)
-  const { mode, push, useWith, indent, deindent } = context
+  const { mode, push, useWith, indent, deindent, newline } = context
   const imports = ast.imports.join(', ')
   if (mode === 'function') {
     // generate const declarations for helpers
@@ -135,13 +135,19 @@ export function generate(
     push(`export default `)
   }
   push(`function render() {`)
+  indent()
   // generate asset resolution statements
-  ast.statements.forEach(s => push(s + `\n`))
+  if (ast.statements.length) {
+    ast.statements.forEach(s => {
+      push(s)
+      newline()
+    })
+    newline()
+  }
   if (useWith) {
-    indent()
     push(`with (this) {`)
+    indent()
   }
-  indent()
   push(`return `)
   genChildren(ast.children, context)
   if (useWith) {
@@ -236,6 +242,10 @@ function genNode(node: CodegenNode, context: CodegenContext) {
       break
     case NodeTypes.JS_ARRAY_EXPRESSION:
       genArrayExpression(node, context)
+      break
+    default:
+      __DEV__ &&
+        assert(false, `unhandled codegen node type: ${(node as any).type}`)
   }
 }
 
@@ -254,9 +264,9 @@ function genText(node: TextNode | ExpressionNode, context: CodegenContext) {
 }
 
 function genExpression(node: ExpressionNode, context: CodegenContext) {
-  // if (node.codegenNode) {
-  //   TODO handle transformed expression
-  // }
+  if (node.children) {
+    return genCompoundExpression(node, context)
+  }
   const text = node.isStatic ? JSON.stringify(node.content) : node.content
   context.push(text, node)
 }
@@ -265,9 +275,9 @@ function genExpressionAsPropertyKey(
   node: ExpressionNode,
   context: CodegenContext
 ) {
-  // if (node.codegenNode) {
-  //   TODO handle transformed expression
-  // }
+  if (node.children) {
+    return genCompoundExpression(node, context)
+  }
   if (node.isStatic) {
     // only quote keys if necessary
     const text = /^\d|[^\w]/.test(node.content)
@@ -279,6 +289,17 @@ function genExpressionAsPropertyKey(
   }
 }
 
+function genCompoundExpression(node: ExpressionNode, context: CodegenContext) {
+  for (let i = 0; i < node.children!.length; i++) {
+    const child = node.children![i]
+    if (isString(child)) {
+      context.push(child)
+    } else {
+      genExpression(child, context)
+    }
+  }
+}
+
 function genComment(node: CommentNode, context: CodegenContext) {
   context.push(`<!--${node.content}-->`, node)
 }
index 2409c9200abdc6c0c5735aca8f4b91182b653e3c..5af9df1944b1e115c5ac37e0959f1fbc891b5ffb 100644 (file)
@@ -8,6 +8,7 @@ import { transformFor } from './transforms/vFor'
 import { prepareElementForCodegen } from './transforms/element'
 import { transformOn } from './transforms/vOn'
 import { transformBind } from './transforms/vBind'
+import { rewriteExpression } from './transforms/expression'
 
 export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
 
@@ -22,6 +23,7 @@ export function compile(
     nodeTransforms: [
       transformIf,
       transformFor,
+      ...(!__BROWSER__ && options.useWith === false ? [rewriteExpression] : []),
       prepareElementForCodegen,
       ...(options.nodeTransforms || []) // user transforms
     ],
@@ -31,7 +33,6 @@ export function compile(
       ...(options.directiveTransforms || {}) // user transforms
     }
   })
-
   return generate(ast, options)
 }
 
index 3cf26ecd837f068b0e160931ddb6761ed6cb7ae4..d3b442b387a53cc0358d81afeccc69dc36d5edbb 100644 (file)
@@ -79,7 +79,7 @@ function createTransformContext(
     removeNode(node) {
       const list = context.parent.children
       const removalIndex = node
-        ? list.indexOf(node)
+        ? list.indexOf(node as any)
         : context.currentNode
           ? context.childIndex
           : -1
@@ -124,12 +124,15 @@ export function traverseChildren(
     i--
   }
   for (; i < parent.children.length; i++) {
+    const child = parent.children[i]
+    if (isString(child)) continue
+    context.currentNode = child
     context.parent = parent
     context.ancestors = ancestors
     context.childIndex = i
     context.onNodeRemoved = nodeRemoved
     context.identifiers = identifiers
-    traverseNode((context.currentNode = parent.children[i]), context)
+    traverseNode(child, context)
   }
 }
 
index 3844f4e06c4a1b28ecd0b5f80b081736e283819d..115d9a0281e5152e7cf4cdeb8472f20909d9e95c 100644 (file)
@@ -192,9 +192,7 @@ function createDirectiveArgs(
   // inject statement for resolving directive
   const dirIdentifier = `_directive_${toValidId(dir.name)}`
   context.statements.push(
-    `const ${dirIdentifier} = _${RESOLVE_DIRECTIVE}(${JSON.stringify(
-      dir.name
-    )})`
+    `const ${dirIdentifier} = ${RESOLVE_DIRECTIVE}(${JSON.stringify(dir.name)})`
   )
   const dirArgs: ArrayExpression['elements'] = [dirIdentifier]
   const { loc } = dir
index ba13f11f032c583e922319cd139b85ca44feda0a..943cb5ace28a9658da146e8a3785553460291f0b 100644 (file)
@@ -1,5 +1,5 @@
-// - Parse expressions in templates into more detailed JavaScript ASTs so that
-//   source-maps are more accurate
+// - Parse expressions in templates into compound expressions so that each
+//   identifier gets more accurate source-map locations.
 //
 // - Prefix identifiers with `_ctx.` so that they are accessed from the render
 //   context
@@ -7,3 +7,146 @@
 // - This transform is only applied in non-browser builds because it relies on
 //   an additional JavaScript parser. In the browser, there is no source-map
 //   support and the code is wrapped in `with (this) { ... }`.
+
+import { parseScript } from 'meriyah'
+import { walk } from 'estree-walker'
+import { NodeTransform, TransformContext } from '../transform'
+import { NodeTypes, createExpression, ExpressionNode } from '../ast'
+import { Node, Function, Identifier } from 'estree'
+import { advancePositionWithClone } from '../utils'
+
+export const rewriteExpression: NodeTransform = (node, context) => {
+  if (node.type === NodeTypes.EXPRESSION && !node.isStatic) {
+    context.replaceNode(convertExpression(node, context))
+  } else if (node.type === NodeTypes.ELEMENT) {
+    // handle directives on element
+    for (let i = 0; i < node.props.length; i++) {
+      const prop = node.props[i]
+      if (prop.type === NodeTypes.DIRECTIVE) {
+        if (prop.exp) {
+          prop.exp = convertExpression(prop.exp, context)
+        }
+        if (prop.arg && !prop.arg.isStatic) {
+          prop.arg = convertExpression(prop.arg, context)
+        }
+      }
+    }
+  } else if (node.type === NodeTypes.IF) {
+    for (let i = 0; i < node.branches.length; i++) {}
+  } else if (node.type === NodeTypes.FOR) {
+  }
+}
+
+function convertExpression(
+  node: ExpressionNode,
+  context: TransformContext
+): ExpressionNode {
+  const ast = parseScript(`(${node.content})`, { ranges: true }) as any
+  const ids: Node[] = []
+  const knownIds = Object.create(context.identifiers)
+
+  walk(ast, {
+    enter(node, parent) {
+      if (node.type === 'Identifier') {
+        if (ids.indexOf(node) === -1) {
+          ids.push(node)
+          if (!knownIds[node.name] && shouldPrependContext(node, parent)) {
+            node.name = `_ctx.${node.name}`
+          }
+        }
+      } else if (isFunction(node)) {
+        node.params.forEach(p =>
+          walk(p, {
+            enter(child) {
+              if (child.type === 'Identifier') {
+                knownIds[child.name] = true
+                ;(
+                  (node as any)._scopeIds ||
+                  ((node as any)._scopeIds = new Set())
+                ).add(child.name)
+              }
+            }
+          })
+        )
+      }
+    },
+    leave(node: any) {
+      if (node._scopeIds) {
+        node._scopeIds.forEach((id: string) => {
+          delete knownIds[id]
+        })
+      }
+    }
+  })
+
+  const full = node.content
+  const children: ExpressionNode['children'] = []
+  ids.sort((a: any, b: any) => a.start - b.start)
+  ids.forEach((id: any, i) => {
+    const last = ids[i - 1] as any
+    const text = full.slice(last ? last.end - 1 : 0, id.start - 1)
+    if (text.length) {
+      children.push(text)
+    }
+    const source = full.slice(id.start, id.end)
+    children.push(
+      createExpression(id.name, false, {
+        source,
+        start: advancePositionWithClone(node.loc.start, source, id.start),
+        end: advancePositionWithClone(node.loc.start, source, id.end)
+      })
+    )
+    if (i === ids.length - 1 && id.end < full.length - 1) {
+      children.push(full.slice(id.end))
+    }
+  })
+
+  return {
+    type: NodeTypes.EXPRESSION,
+    content: '',
+    isStatic: false,
+    loc: node.loc,
+    children
+  }
+}
+
+const globals = new Set(
+  (
+    '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...
+    .split(',')
+)
+
+const isFunction = (node: Node): node is Function =>
+  /Function(Expression|Declaration)$/.test(node.type)
+
+function shouldPrependContext(identifier: Identifier, parent: Node) {
+  if (
+    // not id of a FunctionDeclaration
+    !(parent.type === 'FunctionDeclaration' && parent.id === identifier) &&
+    // not a params of a function
+    !(isFunction(parent) && parent.params.indexOf(identifier) > -1) &&
+    // not a key of Property
+    !(
+      parent.type === 'Property' &&
+      parent.key === identifier &&
+      !parent.computed
+    ) &&
+    // not a property of a MemberExpression
+    !(
+      parent.type === 'MemberExpression' &&
+      parent.property === identifier &&
+      !parent.computed
+    ) &&
+    // not in an Array destructure pattern
+    !(parent.type === 'ArrayPattern') &&
+    // skip globals + commonly used shorthands
+    !globals.has(identifier.name)
+  ) {
+    return true
+  }
+}
index 69b5db1e863964ad1697433cf3fb705c1c660369..bcb0203d27f8605290b5401d80c2b55adc1d978d 100644 (file)
@@ -24,7 +24,7 @@ export const transformIf = createStructuralDirectiveTransform(
       // locate the adjacent v-if
       const siblings = context.parent.children
       const comments = []
-      let i = siblings.indexOf(node)
+      let i = siblings.indexOf(node as any)
       while (i-- >= -1) {
         const sibling = siblings[i]
         if (__DEV__ && sibling && sibling.type === NodeTypes.COMMENT) {
index 0cfeed06e116b863eaa27b8b179b14277030668d..6e3e1b3b708399c3bd39401864371d02f8b855db 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -2627,6 +2627,11 @@ estree-walker@^0.6.1:
   resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362"
   integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==
 
+estree-walker@^0.8.1:
+  version "0.8.1"
+  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.8.1.tgz#6230ce2ec9a5cb03888afcaf295f97d90aa52b79"
+  integrity sha512-H6cJORkqvrNziu0KX2hqOMAlA2CiuAxHeGJXSIoKA/KLv229Dw806J3II6mKTm5xiDX1At1EXCfsOQPB+tMB+g==
+
 esutils@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
@@ -4785,6 +4790,11 @@ merge2@^1.2.3:
   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.4.tgz#c9269589e6885a60cf80605d9522d4b67ca646e3"
   integrity sha512-FYE8xI+6pjFOhokZu0We3S5NKCirLbCzSh2Usf3qEyr4X8U+0jNg9P8RZ4qz+V2UoECLVwSyzU3LxXBaLGtD3A==
 
+meriyah@^1.7.2:
+  version "1.7.2"
+  resolved "https://registry.yarnpkg.com/meriyah/-/meriyah-1.7.2.tgz#c47d07d8f1284577658827cd134b180e80ae4bef"
+  integrity sha512-bBXN6hJ9RHA0mEae5O2Ocr6giK0S87nsz/W7tnBRm4kpW04LEEpXSOfwaID9GZgPRVcn3rAHzHHDDD68DLQgWw==
+
 micromatch@^3.1.10, micromatch@^3.1.4:
   version "3.1.10"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"