]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler): expression prefixing + v-for scope analysis
authorEvan You <yyx990803@gmail.com>
Mon, 23 Sep 2019 17:25:18 +0000 (13:25 -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
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/errors.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/expression.ts
packages/compiler-core/src/transforms/vFor.ts
packages/compiler-core/src/transforms/vIf.ts

index 2153fd08eac5836a0ac4ffbc416b2185c7c396ec..33b077440befa4f6c70be010b552a09c00461307 100644 (file)
@@ -3,13 +3,16 @@ import { compile } from '../../src'
 
 test(`should work`, async () => {
   const { code, map } = compile(
-    `<div v-if="hello">{{ ({ a }, b) => a + b + c }}</div>`,
+    `<div v-for="i in foo">
+      {{ ({ a }, b) => a + b + i + c }} {{ i + 'fe' }} {{ i }}
+    </div>
+    <p>{{ i }}</p>
+    `,
     {
       useWith: false
     }
   )
   console.log(code)
-  console.log(map)
   const consumer = await new SourceMapConsumer(map!)
   const pos = consumer.originalPositionFor({
     line: 4,
index f66245adfa4ec87d53eec559ea746c0acdbe2ab4..1a32b0e215476d319e944e1fa6ffa5033933f1ef 100644 (file)
@@ -348,17 +348,14 @@ function genFor(node: ForNode, context: CodegenContext) {
   genExpression(source, context)
   push(`, (`)
   if (valueAlias) {
-    // not using genExpression here because these aliases can only be code
-    // that is valid in the function argument position, so the parse rule can
-    // be off and they don't need identifier prefixing anyway.
-    push(valueAlias.content, valueAlias)
+    genExpression(valueAlias, context)
   }
   if (keyAlias) {
     if (!valueAlias) {
       push(`_`)
     }
     push(`, `)
-    push(keyAlias.content, keyAlias)
+    genExpression(keyAlias, context)
   }
   if (objectIndexAlias) {
     if (!keyAlias) {
@@ -369,7 +366,7 @@ function genFor(node: ForNode, context: CodegenContext) {
       }
     }
     push(`, `)
-    push(objectIndexAlias.content, objectIndexAlias)
+    genExpression(objectIndexAlias, context)
   }
   push(`) => `)
   genChildren(children, context)
index 431feab4500e7759593ed8779b42914e5195c99a..14e89429aff903339bd2b56b80c694c584935232 100644 (file)
@@ -2,7 +2,7 @@ import { SourceLocation } from './ast'
 
 export interface CompilerError extends SyntaxError {
   code: ErrorCodes
-  loc: SourceLocation
+  loc?: SourceLocation
 }
 
 export function defaultOnError(error: CompilerError) {
@@ -11,13 +11,11 @@ export function defaultOnError(error: CompilerError) {
 
 export function createCompilerError(
   code: ErrorCodes,
-  loc: SourceLocation
+  loc?: SourceLocation
 ): CompilerError {
-  const error = new SyntaxError(
-    `${__DEV__ || !__BROWSER__ ? errorMessages[code] : code} (${
-      loc.start.line
-    }:${loc.start.column})`
-  ) as CompilerError
+  const msg = __DEV__ || !__BROWSER__ ? errorMessages[code] : code
+  const locInfo = loc ? ` (${loc.start.line}:${loc.start.column})` : ``
+  const error = new SyntaxError(msg + locInfo) as CompilerError
   error.code = code
   error.loc = loc
   return error
@@ -56,6 +54,8 @@ export const enum ErrorCodes {
   UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
   UNEXPECTED_SOLIDUS_IN_TAG,
   UNKNOWN_NAMED_CHARACTER_REFERENCE,
+
+  // Vue-specific parse errors
   X_INVALID_END_TAG,
   X_MISSING_END_TAG,
   X_MISSING_INTERPOLATION_END,
@@ -66,7 +66,10 @@ export const enum ErrorCodes {
   X_ELSE_NO_ADJACENT_IF,
   X_FOR_NO_EXPRESSION,
   X_FOR_MALFORMED_EXPRESSION,
-  X_V_BIND_NO_EXPRESSION
+  X_V_BIND_NO_EXPRESSION,
+
+  // generic errors
+  X_STRIP_WITH_NOT_SUPPORTED
 }
 
 export const errorMessages: { [code: number]: string } = {
@@ -116,14 +119,21 @@ export const errorMessages: { [code: number]: string } = {
     "'<?' is allowed only in XML context.",
   [ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.",
   [ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE]: 'Unknown entity name.',
+
+  // Vue-specific parse errors
   [ErrorCodes.X_INVALID_END_TAG]: 'Invalid end tag.',
   [ErrorCodes.X_MISSING_END_TAG]: 'End tag was not found.',
   [ErrorCodes.X_MISSING_INTERPOLATION_END]:
     'Interpolation end sign was not found.',
+  [ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END]:
+    'End bracket for dynamic directive argument was not found.',
 
   // transform errors
   [ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF]: `v-else-if has no adjacent v-if`,
   [ErrorCodes.X_ELSE_NO_ADJACENT_IF]: `v-else has no adjacent v-if`,
   [ErrorCodes.X_FOR_NO_EXPRESSION]: `v-for has no expression`,
-  [ErrorCodes.X_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression`
+  [ErrorCodes.X_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression`,
+
+  // generic errors
+  [ErrorCodes.X_STRIP_WITH_NOT_SUPPORTED]: `useWith: false is not supported in this build of compiler because it is optimized for payload size.`
 }
index a3a3e7117e9b7b41202de89443049aab04e74b5b..0307f23ae973204310a4b64068811c57820db6e2 100644 (file)
@@ -8,7 +8,8 @@ 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'
+import { expressionTransform } from './transforms/expression'
+import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
 
 export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
 
@@ -17,13 +18,21 @@ export function compile(
   options: CompilerOptions = {}
 ): CodegenResult {
   const ast = isString(template) ? parse(template, options) : template
+  const useWith = __BROWSER__ || options.useWith !== false
+
+  if (__BROWSER__ && options.useWith === false) {
+    ;(options.onError || defaultOnError)(
+      createCompilerError(ErrorCodes.X_STRIP_WITH_NOT_SUPPORTED)
+    )
+  }
 
   transform(ast, {
     ...options,
+    useWith,
     nodeTransforms: [
-      ...(!__BROWSER__ && options.useWith === false ? [rewriteExpression] : []),
       transformIf,
       transformFor,
+      ...(useWith ? [] : [expressionTransform]),
       prepareElementForCodegen,
       ...(options.nodeTransforms || []) // user transforms
     ],
index d3b442b387a53cc0358d81afeccc69dc36d5edbb..862fce6558f3129d04b9edb9285eb8c2e869972e 100644 (file)
@@ -5,9 +5,10 @@ import {
   ChildNode,
   ElementNode,
   DirectiveNode,
-  Property
+  Property,
+  ExpressionNode
 } from './ast'
-import { isString } from '@vue/shared'
+import { isString, isArray } from '@vue/shared'
 import { CompilerError, defaultOnError } from './errors'
 
 // There are two types of transforms:
@@ -15,7 +16,10 @@ import { CompilerError, defaultOnError } from './errors'
 // - NodeTransform:
 //   Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
 //   replace or remove the node being processed.
-export type NodeTransform = (node: ChildNode, context: TransformContext) => void
+export type NodeTransform = (
+  node: ChildNode,
+  context: TransformContext
+) => void | (() => void) | (() => void)[]
 
 // - DirectiveTransform:
 //   Transforms that handles a single directive attribute on an element.
@@ -34,11 +38,12 @@ export type StructuralDirectiveTransform = (
   node: ElementNode,
   dir: DirectiveNode,
   context: TransformContext
-) => void
+) => void | (() => void)
 
 export interface TransformOptions {
   nodeTransforms?: NodeTransform[]
   directiveTransforms?: { [name: string]: DirectiveTransform }
+  useWith?: boolean
   onError?: (error: CompilerError) => void
 }
 
@@ -53,19 +58,27 @@ export interface TransformContext extends Required<TransformOptions> {
   replaceNode(node: ChildNode): void
   removeNode(node?: ChildNode): void
   onNodeRemoved: () => void
+  addIdentifier(exp: ExpressionNode): void
+  removeIdentifier(exp: ExpressionNode): void
 }
 
 function createTransformContext(
   root: RootNode,
-  options: TransformOptions
+  {
+    useWith = true,
+    nodeTransforms = [],
+    directiveTransforms = {},
+    onError = defaultOnError
+  }: TransformOptions
 ): TransformContext {
   const context: TransformContext = {
     imports: new Set(),
     statements: [],
     identifiers: {},
-    nodeTransforms: options.nodeTransforms || [],
-    directiveTransforms: options.directiveTransforms || {},
-    onError: options.onError || defaultOnError,
+    useWith,
+    nodeTransforms,
+    directiveTransforms,
+    onError,
     parent: root,
     ancestors: [],
     childIndex: 0,
@@ -99,7 +112,13 @@ function createTransformContext(
       }
       context.parent.children.splice(removalIndex, 1)
     },
-    onNodeRemoved: () => {}
+    onNodeRemoved: () => {},
+    addIdentifier(exp) {
+      context.identifiers[exp.content] = true
+    },
+    removeIdentifier(exp) {
+      delete context.identifiers[exp.content]
+    }
   }
   return context
 }
@@ -115,10 +134,7 @@ export function traverseChildren(
   parent: ParentNode,
   context: TransformContext
 ) {
-  // ancestors and identifiers need to be cached here since they may get
-  // replaced during a child's traversal
   const ancestors = context.ancestors.concat(parent)
-  const identifiers = context.identifiers
   let i = 0
   const nodeRemoved = () => {
     i--
@@ -131,7 +147,6 @@ export function traverseChildren(
     context.ancestors = ancestors
     context.childIndex = i
     context.onNodeRemoved = nodeRemoved
-    context.identifiers = identifiers
     traverseNode(child, context)
   }
 }
@@ -139,9 +154,17 @@ export function traverseChildren(
 export function traverseNode(node: ChildNode, context: TransformContext) {
   // apply transform plugins
   const { nodeTransforms } = context
+  const exitFns = []
   for (let i = 0; i < nodeTransforms.length; i++) {
     const plugin = nodeTransforms[i]
-    plugin(node, context)
+    const onExit = plugin(node, context)
+    if (onExit) {
+      if (isArray(onExit)) {
+        exitFns.push(...onExit)
+      } else {
+        exitFns.push(onExit)
+      }
+    }
     if (!context.currentNode) {
       // node was removed
       return
@@ -163,6 +186,11 @@ export function traverseNode(node: ChildNode, context: TransformContext) {
       traverseChildren(node, context)
       break
   }
+
+  // exit transforms
+  for (let i = 0; i < exitFns.length; i++) {
+    exitFns[i]()
+  }
 }
 
 export function createStructuralDirectiveTransform(
@@ -176,6 +204,7 @@ export function createStructuralDirectiveTransform(
   return (node, context) => {
     if (node.type === NodeTypes.ELEMENT) {
       const { props } = node
+      const exitFns = []
       for (let i = 0; i < props.length; i++) {
         const prop = props[i]
         if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
@@ -184,9 +213,11 @@ export function createStructuralDirectiveTransform(
           // traverse itself in case it moves the node around
           props.splice(i, 1)
           i--
-          fn(node, prop, context)
+          const onExit = fn(node, prop, context)
+          if (onExit) exitFns.push(onExit)
         }
       }
+      return exitFns
     }
   }
 }
index d365091ab871c109a2515d4a00384ebfdb28f798..3859b13a7bf3978a556a75cffd8e593be2a9c1a7 100644 (file)
@@ -15,30 +15,56 @@ import { NodeTypes, createExpression, ExpressionNode } from '../ast'
 import { Node, Function, Identifier } from 'estree'
 import { advancePositionWithClone } from '../utils'
 
-export const rewriteExpression: NodeTransform = (node, context) => {
+export const expressionTransform: NodeTransform = (node, context) => {
   if (node.type === NodeTypes.EXPRESSION && !node.isStatic) {
-    context.replaceNode(convertExpression(node, context))
+    processExpression(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)
+          processExpression(prop.exp, context)
         }
         if (prop.arg && !prop.arg.isStatic) {
-          prop.arg = convertExpression(prop.arg, context)
+          processExpression(prop.arg, context)
         }
       }
     }
   }
 }
 
-function convertExpression(
+const simpleIdRE = /^[a-zA-Z$_][\w$]*$/
+
+// cache node requires
+let _parseScript: typeof parseScript
+let _walk: typeof walk
+
+export function processExpression(
   node: ExpressionNode,
   context: TransformContext
-): ExpressionNode {
-  const ast = parseScript(`(${node.content})`, { ranges: true }) as any
+) {
+  // lazy require dependencies so that they don't end up in rollup's dep graph
+  // and thus can be tree-shaken in browser builds.
+  const parseScript =
+    _parseScript || (_parseScript = require('meriyah').parseScript)
+  const walk = _walk || (_walk = require('estree-walker').walk)
+
+  // fast path if expression is a simple identifier.
+  if (simpleIdRE.test(node.content)) {
+    if (!context.identifiers[node.content]) {
+      node.content = `_ctx.${node.content}`
+    }
+    return
+  }
+
+  let ast
+  try {
+    ast = parseScript(`(${node.content})`, { ranges: true }) as any
+  } catch (e) {
+    context.onError(e)
+    return
+  }
   const ids: Node[] = []
   const knownIds = Object.create(context.identifiers)
 
@@ -98,10 +124,7 @@ function convertExpression(
     }
   })
 
-  return {
-    ...node,
-    children
-  }
+  node.children = children
 }
 
 const globals = new Set(
index ed42fa698152ec20733df9bc55d75ca64527ed1b..9771e85903f31a3206d18a7ad2f358f90aded920 100644 (file)
@@ -1,8 +1,17 @@
-import { createStructuralDirectiveTransform } from '../transform'
-import { NodeTypes, ExpressionNode, createExpression } from '../ast'
+import {
+  createStructuralDirectiveTransform,
+  TransformContext
+} from '../transform'
+import {
+  NodeTypes,
+  ExpressionNode,
+  createExpression,
+  SourceLocation
+} from '../ast'
 import { createCompilerError, ErrorCodes } from '../errors'
 import { getInnerRange } from '../utils'
 import { RENDER_LIST } from '../runtimeConstants'
+import { processExpression } from './expression'
 
 const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/
 const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
@@ -13,23 +22,35 @@ export const transformFor = createStructuralDirectiveTransform(
   (node, dir, context) => {
     if (dir.exp) {
       context.imports.add(RENDER_LIST)
-      const aliases = parseAliasExpressions(dir.exp.content)
+      const parseResult = parseForExpression(dir.exp, context)
+
+      if (parseResult) {
+        const { source, value, key, index } = parseResult
 
-      if (aliases) {
-        // TODO inject identifiers to context
-        // and remove on exit
         context.replaceNode({
           type: NodeTypes.FOR,
           loc: node.loc,
-          source: maybeCreateExpression(
-            aliases.source,
-            dir.exp
-          ) as ExpressionNode,
-          valueAlias: maybeCreateExpression(aliases.value, dir.exp),
-          keyAlias: maybeCreateExpression(aliases.key, dir.exp),
-          objectIndexAlias: maybeCreateExpression(aliases.index, dir.exp),
+          source,
+          valueAlias: value,
+          keyAlias: key,
+          objectIndexAlias: index,
           children: [node]
         })
+
+        // scope management
+        const { addIdentifier, removeIdentifier } = context
+
+        // inject identifiers to context
+        value && addIdentifier(value)
+        key && addIdentifier(key)
+        index && addIdentifier(index)
+
+        return () => {
+          // remove injected identifiers on exit
+          value && removeIdentifier(value)
+          key && removeIdentifier(key)
+          index && removeIdentifier(index)
+        }
       } else {
         context.onError(
           createCompilerError(ErrorCodes.X_FOR_MALFORMED_EXPRESSION, dir.loc)
@@ -43,28 +64,31 @@ export const transformFor = createStructuralDirectiveTransform(
   }
 )
 
-interface AliasExpression {
-  offset: number
-  content: string
-}
-
-interface AliasExpressions {
-  source: AliasExpression
-  value: AliasExpression | undefined
-  key: AliasExpression | undefined
-  index: AliasExpression | undefined
+interface ForParseResult {
+  source: ExpressionNode
+  value: ExpressionNode | undefined
+  key: ExpressionNode | undefined
+  index: ExpressionNode | undefined
 }
 
-function parseAliasExpressions(source: string): AliasExpressions | null {
+function parseForExpression(
+  input: ExpressionNode,
+  context: TransformContext
+): ForParseResult | null {
+  const loc = input.loc
+  const source = input.content
   const inMatch = source.match(forAliasRE)
   if (!inMatch) return null
 
   const [, LHS, RHS] = inMatch
-  const result: AliasExpressions = {
-    source: {
-      offset: source.indexOf(RHS, LHS.length),
-      content: RHS.trim()
-    },
+  const result: ForParseResult = {
+    source: createAliasExpression(
+      loc,
+      RHS.trim(),
+      source.indexOf(RHS, LHS.length),
+      context,
+      !context.useWith
+    ),
     value: undefined,
     key: undefined,
     index: undefined
@@ -80,49 +104,60 @@ function parseAliasExpressions(source: string): AliasExpressions | null {
     valueContent = valueContent.replace(forIteratorRE, '').trim()
 
     const keyContent = iteratorMatch[1].trim()
+    let keyOffset: number | undefined
     if (keyContent) {
-      result.key = {
-        offset: source.indexOf(keyContent, trimmedOffset + valueContent.length),
-        content: keyContent
-      }
+      keyOffset = source.indexOf(
+        keyContent,
+        trimmedOffset + valueContent.length
+      )
+      result.key = createAliasExpression(loc, keyContent, keyOffset, context)
     }
 
     if (iteratorMatch[2]) {
       const indexContent = iteratorMatch[2].trim()
 
       if (indexContent) {
-        result.index = {
-          offset: source.indexOf(
+        result.index = createAliasExpression(
+          loc,
+          indexContent,
+          source.indexOf(
             indexContent,
             result.key
-              ? result.key.offset + result.key.content.length
+              ? keyOffset! + keyContent.length
               : trimmedOffset + valueContent.length
           ),
-          content: indexContent
-        }
+          context
+        )
       }
     }
   }
 
   if (valueContent) {
-    result.value = {
-      offset: trimmedOffset,
-      content: valueContent
-    }
+    result.value = createAliasExpression(
+      loc,
+      valueContent,
+      trimmedOffset,
+      context
+    )
   }
 
   return result
 }
 
-function maybeCreateExpression(
-  alias: AliasExpression | undefined,
-  node: ExpressionNode
-): ExpressionNode | undefined {
-  if (alias) {
-    return createExpression(
-      alias.content,
-      false,
-      getInnerRange(node.loc, alias.offset, alias.content.length)
-    )
+function createAliasExpression(
+  range: SourceLocation,
+  content: string,
+  offset: number,
+  context: TransformContext,
+  process: boolean = false
+): ExpressionNode {
+  const exp = createExpression(
+    content,
+    false,
+    getInnerRange(range, offset, content.length)
+  )
+  if (!__BROWSER__ && process) {
+    processExpression(exp, context)
   }
+  return exp
 }
index bcb0203d27f8605290b5401d80c2b55adc1d978d..3bfa2341f04bb7b799676a4114683deaf05f1471 100644 (file)
@@ -10,10 +10,14 @@ import {
   IfBranchNode
 } from '../ast'
 import { createCompilerError, ErrorCodes } from '../errors'
+import { processExpression } from './expression'
 
 export const transformIf = createStructuralDirectiveTransform(
   /^(if|else|else-if)$/,
   (node, dir, context) => {
+    if (!__BROWSER__ && !context.useWith && dir.exp) {
+      processExpression(dir.exp, context)
+    }
     if (dir.name === 'if') {
       context.replaceNode({
         type: NodeTypes.IF,