]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test: tests for codegen
authorEvan You <yyx990803@gmail.com>
Tue, 24 Sep 2019 19:49:02 +0000 (15:49 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 24 Sep 2019 19:49:02 +0000 (15:49 -0400)
12 files changed:
packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap [new file with mode: 0644]
packages/compiler-core/__tests__/codegen.spec.ts
packages/compiler-core/__tests__/compile.spec.ts [new file with mode: 0644]
packages/compiler-core/__tests__/transform.spec.ts
packages/compiler-core/__tests__/transforms/vBind.spec.ts [new file with mode: 0644]
packages/compiler-core/__tests__/transforms/vIf.spec.ts
packages/compiler-core/__tests__/transforms/vOn.spec.ts [new file with mode: 0644]
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/runtimeConstants.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/vIf.ts

diff --git a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap
new file mode 100644 (file)
index 0000000..1bafff9
--- /dev/null
@@ -0,0 +1,109 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`compiler: codegen callExpression + objectExpression + arrayExpression 1`] = `
+"return function render() {
+  with (this) {
+    return createVNode(\\"div\\", {
+      id: \\"foo\\",
+      [prop]: bar
+    }, [
+      foo,
+      createVNode(\\"p\\")
+    ])
+  }
+}"
+`;
+
+exports[`compiler: codegen comment 1`] = `
+"return function render() {
+  with (this) {
+    return createVNode(Comment, 0, \\"foo\\")
+  }
+}"
+`;
+
+exports[`compiler: codegen forNode 1`] = `
+"return function render() {
+  with (this) {
+    return renderList(list, (v, k, i) => toString(v))
+  }
+}"
+`;
+
+exports[`compiler: codegen function mode preamble 1`] = `
+"const { helperOne, helperTwo } = Vue
+
+return function render() {
+  with (this) {
+    return null
+  }
+}"
+`;
+
+exports[`compiler: codegen ifNode 1`] = `
+"return function render() {
+  with (this) {
+    return (foo)
+      ? \\"foo\\"
+      : (bar)
+        ? toString(bye)
+        : createVNode(Comment, 0, \\"foo\\")
+  }
+}"
+`;
+
+exports[`compiler: codegen interpolation 1`] = `
+"return function render() {
+  with (this) {
+    return toString(hello)
+  }
+}"
+`;
+
+exports[`compiler: codegen module mode preamble 1`] = `
+"import { helperOne, helperTwo } from 'vue'
+
+export default function render() {
+  with (this) {
+    return null
+  }
+}"
+`;
+
+exports[`compiler: codegen prefixIdentifiers: true should inject _ctx statement 1`] = `
+"return function render() {
+  const _ctx = this
+  return null
+}"
+`;
+
+exports[`compiler: codegen statement preambles 1`] = `
+"return function render() {
+  const a = 1
+  const b = 2
+  
+  with (this) {
+    return null
+  }
+}"
+`;
+
+exports[`compiler: codegen static text 1`] = `
+"return function render() {
+  with (this) {
+    return \\"hello\\"
+  }
+}"
+`;
+
+exports[`compiler: codegen text + comment + interpolation 1`] = `
+"return function render() {
+  with (this) {
+    return [
+      \\"foo\\",
+      toString(hello),
+      createVNode(Comment, 0, \\"foo\\")
+    ]
+  }
+}"
+`;
index 16c846fc01981da59c12aedb325ff4d598a4af11..daf84568ad8f8452199dff066054b2a64851ec0a 100644 (file)
-import { parse, generate } from '../src'
+import {
+  parse,
+  generate,
+  NodeTypes,
+  RootNode,
+  SourceLocation,
+  createExpression,
+  Namespaces,
+  ElementTypes,
+  createObjectExpression,
+  createObjectProperty,
+  createArrayExpression
+} from '../src'
 import { SourceMapConsumer, RawSourceMap } from 'source-map'
+import { CREATE_VNODE, COMMENT, TO_STRING } from '../src/runtimeConstants'
+
+const mockLoc: SourceLocation = {
+  source: ``,
+  start: {
+    offset: 0,
+    line: 1,
+    column: 1
+  },
+  end: {
+    offset: 3,
+    line: 1,
+    column: 4
+  }
+}
+
+function createRoot(options: Partial<RootNode> = {}): RootNode {
+  return {
+    type: NodeTypes.ROOT,
+    children: [],
+    imports: [],
+    statements: [],
+    loc: mockLoc,
+    ...options
+  }
+}
 
 describe('compiler: codegen', () => {
+  test('module mode preamble', () => {
+    const root = createRoot({
+      imports: [`helperOne`, `helperTwo`]
+    })
+    const { code } = generate(root, { mode: 'module' })
+    expect(code).toMatch(`import { helperOne, helperTwo } from 'vue'`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('function mode preamble', () => {
+    const root = createRoot({
+      imports: [`helperOne`, `helperTwo`]
+    })
+    const { code } = generate(root, { mode: 'function' })
+    expect(code).toMatch(`const { helperOne, helperTwo } = Vue`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('statement preambles', () => {
+    const root = createRoot({
+      statements: [`const a = 1`, `const b = 2`]
+    })
+    const { code } = generate(root, { mode: 'function' })
+    expect(code).toMatch(`const a = 1\n`)
+    expect(code).toMatch(`const b = 2\n`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('prefixIdentifiers: true should inject _ctx statement', () => {
+    const { code } = generate(createRoot(), { prefixIdentifiers: true })
+    expect(code).toMatch(`const _ctx = this\n`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('static text', () => {
+    const { code } = generate(
+      createRoot({
+        children: [
+          {
+            type: NodeTypes.TEXT,
+            content: 'hello',
+            isEmpty: false,
+            loc: mockLoc
+          }
+        ]
+      })
+    )
+    expect(code).toMatch(`return "hello"`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('interpolation', () => {
+    const { code } = generate(
+      createRoot({
+        children: [createExpression(`hello`, false, mockLoc, true)]
+      })
+    )
+    expect(code).toMatch(`return toString(hello)`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('comment', () => {
+    const { code } = generate(
+      createRoot({
+        children: [
+          {
+            type: NodeTypes.COMMENT,
+            content: 'foo',
+            loc: mockLoc
+          }
+        ]
+      })
+    )
+    expect(code).toMatch(`return ${CREATE_VNODE}(${COMMENT}, 0, "foo")`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('text + comment + interpolation', () => {
+    const { code } = generate(
+      createRoot({
+        children: [
+          {
+            type: NodeTypes.TEXT,
+            content: 'foo',
+            isEmpty: false,
+            loc: mockLoc
+          },
+          createExpression(`hello`, false, mockLoc, true),
+          {
+            type: NodeTypes.COMMENT,
+            content: 'foo',
+            loc: mockLoc
+          }
+        ]
+      })
+    )
+    expect(code).toMatch(`
+    return [
+      "foo",
+      toString(hello),
+      ${CREATE_VNODE}(${COMMENT}, 0, "foo")
+    ]`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('ifNode', () => {
+    const { code } = generate(
+      createRoot({
+        children: [
+          {
+            type: NodeTypes.IF,
+            loc: mockLoc,
+            isRoot: true,
+            branches: [
+              {
+                type: NodeTypes.IF_BRANCH,
+                condition: createExpression('foo', false, mockLoc),
+                loc: mockLoc,
+                isRoot: true,
+                children: [
+                  {
+                    type: NodeTypes.TEXT,
+                    content: 'foo',
+                    isEmpty: false,
+                    loc: mockLoc
+                  }
+                ]
+              },
+              {
+                type: NodeTypes.IF_BRANCH,
+                condition: createExpression('bar', false, mockLoc),
+                loc: mockLoc,
+                isRoot: true,
+                children: [createExpression(`bye`, false, mockLoc, true)]
+              },
+              {
+                type: NodeTypes.IF_BRANCH,
+                condition: undefined,
+                loc: mockLoc,
+                isRoot: true,
+                children: [
+                  {
+                    type: NodeTypes.COMMENT,
+                    content: 'foo',
+                    loc: mockLoc
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      })
+    )
+    expect(code).toMatch(`
+    return (foo)
+      ? "foo"
+      : (bar)
+        ? ${TO_STRING}(bye)
+        : ${CREATE_VNODE}(${COMMENT}, 0, "foo")`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('forNode', () => {
+    const { code } = generate(
+      createRoot({
+        children: [
+          {
+            type: NodeTypes.FOR,
+            loc: mockLoc,
+            source: createExpression(`list`, false, mockLoc),
+            valueAlias: createExpression(`v`, false, mockLoc),
+            keyAlias: createExpression(`k`, false, mockLoc),
+            objectIndexAlias: createExpression(`i`, false, mockLoc),
+            children: [createExpression(`v`, false, mockLoc, true)]
+          }
+        ]
+      })
+    )
+    expect(code).toMatch(`renderList(list, (v, k, i) => toString(v))`)
+    expect(code).toMatchSnapshot()
+  })
+
+  test('callExpression + objectExpression + arrayExpression', () => {
+    const { code } = generate(
+      createRoot({
+        children: [
+          {
+            type: NodeTypes.ELEMENT,
+            loc: mockLoc,
+            ns: Namespaces.HTML,
+            tag: 'div',
+            tagType: ElementTypes.ELEMENT,
+            isSelfClosing: false,
+            props: [],
+            children: [],
+            codegenNode: {
+              type: NodeTypes.JS_CALL_EXPRESSION,
+              loc: mockLoc,
+              callee: CREATE_VNODE,
+              arguments: [
+                `"div"`,
+                createObjectExpression(
+                  [
+                    createObjectProperty(
+                      createExpression(`id`, true, mockLoc),
+                      createExpression(`foo`, true, mockLoc),
+                      mockLoc
+                    ),
+                    createObjectProperty(
+                      createExpression(`prop`, false, mockLoc),
+                      createExpression(`bar`, false, mockLoc),
+                      mockLoc
+                    )
+                  ],
+                  mockLoc
+                ),
+                createArrayExpression(
+                  [
+                    'foo',
+                    {
+                      type: NodeTypes.JS_CALL_EXPRESSION,
+                      loc: mockLoc,
+                      callee: CREATE_VNODE,
+                      arguments: [`"p"`]
+                    }
+                  ],
+                  mockLoc
+                )
+              ]
+            }
+          }
+        ]
+      })
+    )
+    expect(code).toMatch(`
+    return ${CREATE_VNODE}("div", {
+      id: "foo",
+      [prop]: bar
+    }, [
+      foo,
+      ${CREATE_VNODE}("p")
+    ])`)
+    expect(code).toMatchSnapshot()
+  })
+
   test('basic source map support', async () => {
     const source = `hello {{ world }}`
     const ast = parse(source)
     const { code, map } = generate(ast, {
+      sourceMap: true,
       filename: `foo.vue`
     })
-    expect(code).toBe(
+    expect(code).toMatch(
       `return function render() {
   with (this) {
     return [
diff --git a/packages/compiler-core/__tests__/compile.spec.ts b/packages/compiler-core/__tests__/compile.spec.ts
new file mode 100644 (file)
index 0000000..6f3f60b
--- /dev/null
@@ -0,0 +1 @@
+// Integration tests for parser + transform + codegen
index 3d262f2e937eb0d774ba2f211505291fabb3584e..e752698b4fe23c145ed01a224c4748f8ca128bf5 100644 (file)
@@ -2,6 +2,7 @@ import { parse } from '../src/parse'
 import { transform, NodeTransform } from '../src/transform'
 import { ElementNode, NodeTypes } from '../src/ast'
 import { ErrorCodes, createCompilerError } from '../src/errors'
+import { TO_STRING, CREATE_VNODE, COMMENT } from '../src/runtimeConstants'
 
 describe('compiler: transform', () => {
   test('context state', () => {
@@ -180,4 +181,17 @@ describe('compiler: transform', () => {
       }
     ])
   })
+
+  test('should inject toString helper for interpolations', () => {
+    const ast = parse(`{{ foo }}`)
+    transform(ast, {})
+    expect(ast.imports).toContain(TO_STRING)
+  })
+
+  test('should inject createVNode and Comment for comments', () => {
+    const ast = parse(`<!--foo-->`)
+    transform(ast, {})
+    expect(ast.imports).toContain(CREATE_VNODE)
+    expect(ast.imports).toContain(COMMENT)
+  })
 })
diff --git a/packages/compiler-core/__tests__/transforms/vBind.spec.ts b/packages/compiler-core/__tests__/transforms/vBind.spec.ts
new file mode 100644 (file)
index 0000000..70b786d
--- /dev/null
@@ -0,0 +1 @@
+// TODO
index a9da468896da29d07766f401b57fcb0739db49fe..2c6ddc46324aa9c2b95be229013f5e1f13061000 100644 (file)
@@ -29,7 +29,9 @@ describe('compiler: transform v-if', () => {
   test('basic v-if', () => {
     const node = parseWithIfTransform(`<div v-if="ok"/>`)
     expect(node.type).toBe(NodeTypes.IF)
+    expect(node.isRoot).toBe(true)
     expect(node.branches.length).toBe(1)
+    expect(node.branches[0].isRoot).toBe(true)
     expect(node.branches[0].condition!.content).toBe(`ok`)
     expect(node.branches[0].children.length).toBe(1)
     expect(node.branches[0].children[0].type).toBe(NodeTypes.ELEMENT)
diff --git a/packages/compiler-core/__tests__/transforms/vOn.spec.ts b/packages/compiler-core/__tests__/transforms/vOn.spec.ts
new file mode 100644 (file)
index 0000000..70b786d
--- /dev/null
@@ -0,0 +1 @@
+// TODO
index 68d2a288d6c3577da4ece7004cb77b69e61f2d1e..24b8401100bd1a1d609163eb7fd3f6a32f6142ac 100644 (file)
@@ -117,12 +117,14 @@ export interface ExpressionNode extends Node {
 export interface IfNode extends Node {
   type: NodeTypes.IF
   branches: IfBranchNode[]
+  isRoot: boolean
 }
 
 export interface IfBranchNode extends Node {
   type: NodeTypes.IF_BRANCH
   condition: ExpressionNode | undefined // else
   children: ChildNode[]
+  isRoot: boolean
 }
 
 export interface ForNode extends Node {
@@ -203,14 +205,15 @@ export function createObjectProperty(
 export function createExpression(
   content: string,
   isStatic: boolean,
-  loc: SourceLocation
+  loc: SourceLocation,
+  isInterpolation = false
 ): ExpressionNode {
   return {
     type: NodeTypes.EXPRESSION,
     loc,
     content,
     isStatic,
-    isInterpolation: false
+    isInterpolation
   }
 }
 
index e07a20f773e5e791c7082c4d5c89ad6b22e0b0f2..0e0c085e2f972c2c165c947cb9eb6f2df9e0b9c4 100644 (file)
@@ -17,17 +17,33 @@ import {
 import { SourceMapGenerator, RawSourceMap } from 'source-map'
 import { advancePositionWithMutation, assert } from './utils'
 import { isString, isArray } from '@vue/shared'
-import { RENDER_LIST, TO_STRING } from './runtimeConstants'
+import {
+  RENDER_LIST,
+  TO_STRING,
+  CREATE_VNODE,
+  COMMENT
+} from './runtimeConstants'
 
 type CodegenNode = ChildNode | JSChildNode
 
 export interface CodegenOptions {
-  //  will generate import statements for
-  // runtime helpers; otherwise will grab the helpers from global `Vue`.
-  // default: false
+  // - Module mode will generate ES module import statements for helpers
+  //   and export the render function as the default export.
+  // - Function mode will generate a single `const { helpers... } = Vue`
+  //   statement and return the render function. It is meant to be used with
+  //   `new Function(code)()` to generate a render function at runtime.
+  // Default: 'function'
   mode?: 'module' | 'function'
+  // Prefix suitable identifiers with _ctx.
+  // If this option is false, the generated code will be wrapped in a
+  // `with (this) { ... }` block.
+  // Default: false
   prefixIdentifiers?: boolean
+  // Generate source map?
+  // Default: false
+  sourceMap?: boolean
   // Filename for source map generation.
+  // Default: `template.vue.html`
   filename?: string
 }
 
@@ -55,12 +71,14 @@ function createCodegenContext(
   {
     mode = 'function',
     prefixIdentifiers = false,
+    sourceMap = false,
     filename = `template.vue.html`
   }: CodegenOptions
 ): CodegenContext {
   const context: CodegenContext = {
     mode,
     prefixIdentifiers,
+    sourceMap,
     filename,
     source: ast.loc.source,
     code: ``,
@@ -70,9 +88,10 @@ function createCodegenContext(
     indentLevel: 0,
 
     // lazy require source-map implementation, only in non-browser builds!
-    map: __BROWSER__
-      ? undefined
-      : new (require('source-map')).SourceMapGenerator(),
+    map:
+      __BROWSER__ || !sourceMap
+        ? undefined
+        : new (require('source-map')).SourceMapGenerator(),
 
     push(code, node?: CodegenNode) {
       context.code += code
@@ -108,8 +127,8 @@ function createCodegenContext(
     }
   }
   const newline = (n: number) => context.push('\n' + `  `.repeat(n))
-  if (!__BROWSER__) {
-    context.map!.setSourceContent(filename, context.source)
+  if (!__BROWSER__ && context.map) {
+    context.map.setSourceContent(filename, context.source)
   }
   return context
 }
@@ -174,6 +193,9 @@ function genChildren(
   context: CodegenContext,
   asRoot: boolean = false
 ) {
+  if (!children.length) {
+    return context.push(`null`)
+  }
   const child = children[0]
   if (
     children.length === 1 &&
@@ -321,7 +343,12 @@ function genCompoundExpression(node: ExpressionNode, context: CodegenContext) {
 }
 
 function genComment(node: CommentNode, context: CodegenContext) {
-  context.push(`<!--${node.content}-->`, node)
+  if (__DEV__) {
+    context.push(
+      `${CREATE_VNODE}(${COMMENT}, 0, ${JSON.stringify(node.content)})`,
+      node
+    )
+  }
 }
 
 // control flow
@@ -330,7 +357,7 @@ function genIf(node: IfNode, context: CodegenContext) {
 }
 
 function genIfBranch(
-  { condition, children }: IfBranchNode,
+  { condition, children, isRoot }: IfBranchNode,
   branches: IfBranchNode[],
   nextIndex: number,
   context: CodegenContext
@@ -344,7 +371,7 @@ function genIfBranch(
     indent()
     context.indentLevel++
     push(`? `)
-    genChildren(children, context)
+    genChildren(children, context, isRoot)
     context.indentLevel--
     newline()
     push(`: `)
@@ -357,7 +384,7 @@ function genIfBranch(
   } else {
     // v-else
     __DEV__ && assert(nextIndex === branches.length)
-    genChildren(children, context)
+    genChildren(children, context, isRoot)
   }
 }
 
index bc5859956800785be908d7bc01fbff2a23ff7da2..74178b95680400214402b18428ef980385d6f531 100644 (file)
@@ -1,5 +1,10 @@
 // Name mapping constants for runtime helpers that need to be imported in
 // generated code. Make sure these are correctly exported in the runtime!
+export const FRAGMENT = `Fragment`
+export const PORTAL = `Portal`
+export const COMMENT = `Comment`
+export const TEXT = `Text`
+export const SUSPENSE = `Suspense`
 export const CREATE_VNODE = `createVNode`
 export const RESOLVE_COMPONENT = `resolveComponent`
 export const RESOLVE_DIRECTIVE = `resolveDirective`
index d5840f9126d33f832cd51d8e7068b18793d1638e..b0a1278c6722b46710969fec186f1fca6b92c79a 100644 (file)
@@ -10,7 +10,7 @@ import {
 } from './ast'
 import { isString, isArray } from '@vue/shared'
 import { CompilerError, defaultOnError } from './errors'
-import { TO_STRING } from './runtimeConstants'
+import { TO_STRING, COMMENT, CREATE_VNODE } from './runtimeConstants'
 
 // There are two types of transforms:
 //
@@ -49,11 +49,11 @@ export interface TransformOptions {
 }
 
 export interface TransformContext extends Required<TransformOptions> {
+  root: RootNode
   imports: Set<string>
   statements: string[]
   identifiers: { [name: string]: number | undefined }
   parent: ParentNode
-  ancestors: ParentNode[]
   childIndex: number
   currentNode: ChildNode | null
   replaceNode(node: ChildNode): void
@@ -73,6 +73,7 @@ function createTransformContext(
   }: TransformOptions
 ): TransformContext {
   const context: TransformContext = {
+    root,
     imports: new Set(),
     statements: [],
     identifiers: {},
@@ -81,7 +82,6 @@ function createTransformContext(
     directiveTransforms,
     onError,
     parent: root,
-    ancestors: [],
     childIndex: 0,
     currentNode: null,
     replaceNode(node) {
@@ -139,7 +139,6 @@ export function traverseChildren(
   parent: ParentNode,
   context: TransformContext
 ) {
-  const ancestors = context.ancestors.concat(parent)
   let i = 0
   const nodeRemoved = () => {
     i--
@@ -149,7 +148,6 @@ export function traverseChildren(
     if (isString(child)) continue
     context.currentNode = child
     context.parent = parent
-    context.ancestors = ancestors
     context.childIndex = i
     context.onNodeRemoved = nodeRemoved
     traverseNode(child, context)
@@ -180,6 +178,12 @@ export function traverseNode(node: ChildNode, context: TransformContext) {
   }
 
   switch (node.type) {
+    case NodeTypes.COMMENT:
+      context.imports.add(CREATE_VNODE)
+      // inject import for the Comment symbol, which is needed for creating
+      // comment nodes with `createVNode`
+      context.imports.add(COMMENT)
+      break
     case NodeTypes.EXPRESSION:
       // no need to traverse, but we need to inject toString helper
       if (node.isInterpolation) {
index 1d8fe08aaf6db6aa08c9a42f9f27b45a754aab9a..70120d5f1dab9f99fa7dee401090e49d234dfe34 100644 (file)
@@ -19,10 +19,14 @@ export const transformIf = createStructuralDirectiveTransform(
       processExpression(dir.exp, context)
     }
     if (dir.name === 'if') {
+      // check if this v-if is root - so that in codegen we can avoid generating
+      // arrays for each branch
+      const isRoot = context.parent === context.root
       context.replaceNode({
         type: NodeTypes.IF,
         loc: node.loc,
-        branches: [createIfBranch(node, dir)]
+        branches: [createIfBranch(node, dir, isRoot)],
+        isRoot
       })
     } else {
       // locate the adjacent v-if
@@ -39,7 +43,7 @@ export const transformIf = createStructuralDirectiveTransform(
         if (sibling && sibling.type === NodeTypes.IF) {
           // move the node to the if node's branches
           context.removeNode()
-          const branch = createIfBranch(node, dir)
+          const branch = createIfBranch(node, dir, sibling.isRoot)
           if (__DEV__ && comments.length) {
             branch.children = [...comments, ...branch.children]
           }
@@ -63,11 +67,16 @@ export const transformIf = createStructuralDirectiveTransform(
   }
 )
 
-function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode {
+function createIfBranch(
+  node: ElementNode,
+  dir: DirectiveNode,
+  isRoot: boolean
+): IfBranchNode {
   return {
     type: NodeTypes.IF_BRANCH,
     loc: node.loc,
     condition: dir.name === 'else' ? undefined : dir.exp,
-    children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node]
+    children: node.tagType === ElementTypes.TEMPLATE ? node.children : [node],
+    isRoot
   }
 }