]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test: tests for hoistStatic
authorEvan You <yyx990803@gmail.com>
Mon, 7 Oct 2019 21:12:22 +0000 (17:12 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 7 Oct 2019 21:12:22 +0000 (17:12 -0400)
packages/compiler-core/__tests__/testUtils.ts
packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap [new file with mode: 0644]
packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts [new file with mode: 0644]
packages/compiler-core/__tests__/transforms/transformElement.spec.ts
packages/compiler-core/__tests__/transforms/vFor.spec.ts
packages/compiler-core/__tests__/transforms/vSlot.spec.ts
packages/compiler-core/src/ast.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/hoistStatic.ts

index 16bd554e2b7ba020fdf06e9bb617d511182f6c1f..74855d5183a49cb91b99dfd6e5bcf9d4f81c34e9 100644 (file)
@@ -7,7 +7,7 @@ import {
   ElementCodegenNode
 } from '../src'
 import { CREATE_VNODE } from '../src/runtimeHelpers'
-import { isString } from '@vue/shared'
+import { isString, PatchFlags, PatchFlagNames, isArray } from '@vue/shared'
 
 const leadingBracketRE = /^\[/
 const bracketsRE = /^\[|\]$/g
@@ -58,3 +58,15 @@ export function createElementWithCodegen(
     }
   }
 }
+
+export function genFlagText(flag: PatchFlags | PatchFlags[]) {
+  if (isArray(flag)) {
+    let f = 0
+    flag.forEach(ff => {
+      f |= ff
+    })
+    return `${f} /* ${flag.map(f => PatchFlagNames[f]).join(', ')} */`
+  } else {
+    return `${flag} /* ${PatchFlagNames[flag]} */`
+  }
+}
diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap
new file mode 100644 (file)
index 0000000..5ca448b
--- /dev/null
@@ -0,0 +1,205 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`compiler: hositStatic transform hoist nested static tree 1`] = `
+"const _Vue = Vue
+const _createVNode = Vue.createVNode
+
+const _hoisted_1 = _createVNode(\\"p\\", null, [
+  _createVNode(\\"span\\"),
+  _createVNode(\\"span\\")
+])
+
+return function render() {
+  with (this) {
+    const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      _hoisted_1
+    ]))
+  }
+}"
+`;
+
+exports[`compiler: hositStatic transform hoist siblings with common non-hoistable parent 1`] = `
+"const _Vue = Vue
+const _createVNode = Vue.createVNode
+
+const _hoisted_1 = _createVNode(\\"span\\")
+const _hoisted_2 = _createVNode(\\"div\\")
+
+return function render() {
+  with (this) {
+    const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      _hoisted_1,
+      _hoisted_2
+    ]))
+  }
+}"
+`;
+
+exports[`compiler: hositStatic transform hoist simple element 1`] = `
+"const _Vue = Vue
+const _createVNode = Vue.createVNode
+
+const _hoisted_1 = _createVNode(\\"span\\", { class: \\"inline\\" }, \\"hello\\")
+
+return function render() {
+  with (this) {
+    const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      _hoisted_1
+    ]))
+  }
+}"
+`;
+
+exports[`compiler: hositStatic transform hoist static props for elements with directives 1`] = `
+"const _Vue = Vue
+const _createVNode = Vue.createVNode
+
+const _hoisted_1 = { id: \\"foo\\" }
+
+return function render() {
+  with (this) {
+    const { createVNode: _createVNode, applyDirectives: _applyDirectives, resolveDirective: _resolveDirective, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+    
+    const _directive_foo = _resolveDirective(\\"foo\\")
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      _applyDirectives(_createVNode(\\"div\\", _hoisted_1, null, 32 /* NEED_PATCH */), [
+        [_directive_foo]
+      ])
+    ]))
+  }
+}"
+`;
+
+exports[`compiler: hositStatic transform hoist static props for elements with dynamic text children 1`] = `
+"const _Vue = Vue
+const _createVNode = Vue.createVNode
+
+const _hoisted_1 = { id: \\"foo\\" }
+
+return function render() {
+  with (this) {
+    const { toString: _toString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      _createVNode(\\"div\\", _hoisted_1, _toString(hello), 1 /* TEXT */)
+    ]))
+  }
+}"
+`;
+
+exports[`compiler: hositStatic transform hoist static props for elements with unhoistable children 1`] = `
+"const _Vue = Vue
+const _createVNode = Vue.createVNode
+
+const _hoisted_1 = { id: \\"foo\\" }
+
+return function render() {
+  with (this) {
+    const { resolveComponent: _resolveComponent, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+    
+    const _component_Comp = _resolveComponent(\\"Comp\\")
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      _createVNode(\\"div\\", _hoisted_1, [
+        _createVNode(_component_Comp)
+      ])
+    ]))
+  }
+}"
+`;
+
+exports[`compiler: hositStatic transform should NOT hoist components 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { resolveComponent: _resolveComponent, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+    
+    const _component_Comp = _resolveComponent(\\"Comp\\")
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      _createVNode(_component_Comp)
+    ]))
+  }
+}"
+`;
+
+exports[`compiler: hositStatic transform should NOT hoist element with dynamic props 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      _createVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"])
+    ]))
+  }
+}"
+`;
+
+exports[`compiler: hositStatic transform should NOT hoist root node 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+    
+    return (_openBlock(), _createBlock(\\"div\\"))
+  }
+}"
+`;
+
+exports[`compiler: hositStatic transform should hoist v-for children if static 1`] = `
+"const _Vue = Vue
+const _createVNode = Vue.createVNode
+
+const _hoisted_1 = { id: \\"foo\\" }
+const _hoisted_2 = _createVNode(\\"span\\")
+
+return function render() {
+  with (this) {
+    const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createVNode: _createVNode } = _Vue
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      (_openBlock(), _createBlock(_Fragment, null, _renderList(list, (i) => {
+        return (_openBlock(), _createBlock(\\"div\\", _hoisted_1, [
+          _hoisted_2
+        ]))
+      }), 128 /* UNKEYED_FRAGMENT */))
+    ]))
+  }
+}"
+`;
+
+exports[`compiler: hositStatic transform should hoist v-if props/children if static 1`] = `
+"const _Vue = Vue
+const _createVNode = Vue.createVNode
+
+const _hoisted_1 = {
+  key: 0,
+  id: \\"foo\\"
+}
+const _hoisted_2 = _createVNode(\\"span\\")
+
+return function render() {
+  with (this) {
+    const { openBlock: _openBlock, createVNode: _createVNode, createBlock: _createBlock, Empty: _Empty } = _Vue
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      (_openBlock(), ok
+        ? _createBlock(\\"div\\", _hoisted_1, [
+            _hoisted_2
+          ])
+        : _createBlock(_Empty))
+    ]))
+  }
+}"
+`;
diff --git a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts
new file mode 100644 (file)
index 0000000..7e5e201
--- /dev/null
@@ -0,0 +1,359 @@
+import { parse, transform, NodeTypes, generate } from '../../src'
+import {
+  OPEN_BLOCK,
+  CREATE_BLOCK,
+  CREATE_VNODE,
+  APPLY_DIRECTIVES,
+  FRAGMENT,
+  RENDER_LIST
+} from '../../src/runtimeHelpers'
+import { transformElement } from '../../src/transforms/transformElement'
+import { transformIf } from '../../src/transforms/vIf'
+import { transformFor } from '../../src/transforms/vFor'
+import { transformBind } from '../../src/transforms/vBind'
+import { createObjectMatcher, genFlagText } from '../testUtils'
+import { PatchFlags } from '@vue/shared'
+
+function transformWithHoist(template: string) {
+  const ast = parse(template)
+  transform(ast, {
+    hoistStatic: true,
+    nodeTransforms: [transformIf, transformFor, transformElement],
+    directiveTransforms: {
+      bind: transformBind
+    }
+  })
+  expect(ast.codegenNode).toMatchObject({
+    type: NodeTypes.JS_SEQUENCE_EXPRESSION,
+    expressions: [
+      {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: OPEN_BLOCK
+      },
+      {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_BLOCK
+      }
+    ]
+  })
+  return {
+    root: ast,
+    args: (ast.codegenNode as any).expressions[1].arguments
+  }
+}
+
+describe('compiler: hositStatic transform', () => {
+  test('should NOT hoist root node', () => {
+    // if the whole tree is static, the root still needs to be a block
+    // so that it's patched in optimized mode to skip children
+    const { root, args } = transformWithHoist(`<div/>`)
+    expect(root.hoists.length).toBe(0)
+    expect(args).toEqual([`"div"`])
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('hoist simple element', () => {
+    const { root, args } = transformWithHoist(
+      `<div><span class="inline">hello</span></div>`
+    )
+    expect(root.hoists).toMatchObject([
+      {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_VNODE,
+        arguments: [
+          `"span"`,
+          createObjectMatcher({ class: 'inline' }),
+          {
+            type: NodeTypes.TEXT,
+            content: `hello`
+          }
+        ]
+      }
+    ])
+    expect(args).toMatchObject([
+      `"div"`,
+      `null`,
+      [
+        {
+          type: NodeTypes.ELEMENT,
+          codegenNode: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: `_hoisted_1`
+          }
+        }
+      ]
+    ])
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('hoist nested static tree', () => {
+    const { root, args } = transformWithHoist(
+      `<div><p><span/><span/></p></div>`
+    )
+    expect(root.hoists).toMatchObject([
+      {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_VNODE,
+        arguments: [
+          `"p"`,
+          `null`,
+          [
+            { type: NodeTypes.ELEMENT, tag: `span` },
+            { type: NodeTypes.ELEMENT, tag: `span` }
+          ]
+        ]
+      }
+    ])
+    expect(args[2]).toMatchObject([
+      {
+        type: NodeTypes.ELEMENT,
+        codegenNode: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: `_hoisted_1`
+        }
+      }
+    ])
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('hoist siblings with common non-hoistable parent', () => {
+    const { root, args } = transformWithHoist(`<div><span/><div/></div>`)
+    expect(root.hoists).toMatchObject([
+      {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_VNODE,
+        arguments: [`"span"`]
+      },
+      {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_VNODE,
+        arguments: [`"div"`]
+      }
+    ])
+    expect(args[2]).toMatchObject([
+      {
+        type: NodeTypes.ELEMENT,
+        codegenNode: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: `_hoisted_1`
+        }
+      },
+      {
+        type: NodeTypes.ELEMENT,
+        codegenNode: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: `_hoisted_2`
+        }
+      }
+    ])
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('should NOT hoist components', () => {
+    const { root, args } = transformWithHoist(`<div><Comp/></div>`)
+    expect(root.hoists.length).toBe(0)
+    expect(args[2]).toMatchObject([
+      {
+        type: NodeTypes.ELEMENT,
+        codegenNode: {
+          callee: CREATE_VNODE,
+          arguments: [`_component_Comp`]
+        }
+      }
+    ])
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('should NOT hoist element with dynamic props', () => {
+    const { root, args } = transformWithHoist(`<div><div :id="foo"/></div>`)
+    expect(root.hoists.length).toBe(0)
+    expect(args[2]).toMatchObject([
+      {
+        type: NodeTypes.ELEMENT,
+        codegenNode: {
+          callee: CREATE_VNODE,
+          arguments: [
+            `"div"`,
+            createObjectMatcher({
+              id: `[foo]`
+            }),
+            `null`,
+            genFlagText(PatchFlags.PROPS),
+            `["id"]`
+          ]
+        }
+      }
+    ])
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('hoist static props for elements with directives', () => {
+    const { root, args } = transformWithHoist(
+      `<div><div id="foo" v-foo/></div>`
+    )
+    expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
+    expect(args[2]).toMatchObject([
+      {
+        type: NodeTypes.ELEMENT,
+        codegenNode: {
+          callee: APPLY_DIRECTIVES,
+          arguments: [
+            {
+              callee: CREATE_VNODE,
+              arguments: [
+                `"div"`,
+                {
+                  type: NodeTypes.SIMPLE_EXPRESSION,
+                  content: `_hoisted_1`
+                },
+                `null`,
+                genFlagText(PatchFlags.NEED_PATCH)
+              ]
+            },
+            {
+              type: NodeTypes.JS_ARRAY_EXPRESSION
+            }
+          ]
+        }
+      }
+    ])
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('hoist static props for elements with dynamic text children', () => {
+    const { root, args } = transformWithHoist(
+      `<div><div id="foo">{{ hello }}</div></div>`
+    )
+    expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
+    expect(args[2]).toMatchObject([
+      {
+        type: NodeTypes.ELEMENT,
+        codegenNode: {
+          callee: CREATE_VNODE,
+          arguments: [
+            `"div"`,
+            { content: `_hoisted_1` },
+            { type: NodeTypes.INTERPOLATION },
+            genFlagText(PatchFlags.TEXT)
+          ]
+        }
+      }
+    ])
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('hoist static props for elements with unhoistable children', () => {
+    const { root, args } = transformWithHoist(
+      `<div><div id="foo"><Comp/></div></div>`
+    )
+    expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
+    expect(args[2]).toMatchObject([
+      {
+        type: NodeTypes.ELEMENT,
+        codegenNode: {
+          callee: CREATE_VNODE,
+          arguments: [
+            `"div"`,
+            { content: `_hoisted_1` },
+            [{ type: NodeTypes.ELEMENT, tag: `Comp` }]
+          ]
+        }
+      }
+    ])
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('should hoist v-if props/children if static', () => {
+    const { root, args } = transformWithHoist(
+      `<div><div v-if="ok" id="foo"><span/></div></div>`
+    )
+    expect(root.hoists).toMatchObject([
+      createObjectMatcher({
+        key: `[0]`, // key injected by v-if branch
+        id: 'foo'
+      }),
+      {
+        callee: CREATE_VNODE,
+        arguments: [`"span"`]
+      }
+    ])
+    expect(args[2][0].codegenNode).toMatchObject({
+      type: NodeTypes.JS_SEQUENCE_EXPRESSION,
+      expressions: [
+        { callee: OPEN_BLOCK },
+        {
+          type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
+          consequent: {
+            // blocks should NOT be hoisted
+            callee: CREATE_BLOCK,
+            arguments: [
+              `"div"`,
+              { content: `_hoisted_1` },
+              [
+                {
+                  codegenNode: { content: `_hoisted_2` }
+                }
+              ]
+            ]
+          }
+        }
+      ]
+    })
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('should hoist v-for children if static', () => {
+    const { root, args } = transformWithHoist(
+      `<div><div v-for="i in list" id="foo"><span/></div></div>`
+    )
+    expect(root.hoists).toMatchObject([
+      createObjectMatcher({
+        id: 'foo'
+      }),
+      {
+        callee: CREATE_VNODE,
+        arguments: [`"span"`]
+      }
+    ])
+    const forBlockCodegen = args[2][0].codegenNode
+    expect(forBlockCodegen).toMatchObject({
+      type: NodeTypes.JS_SEQUENCE_EXPRESSION,
+      expressions: [
+        { callee: OPEN_BLOCK },
+        {
+          callee: CREATE_BLOCK,
+          arguments: [
+            FRAGMENT,
+            `null`,
+            {
+              type: NodeTypes.JS_CALL_EXPRESSION,
+              callee: RENDER_LIST
+            },
+            genFlagText(PatchFlags.UNKEYED_FRAGMENT)
+          ]
+        }
+      ]
+    })
+    const innerBlockCodegen =
+      forBlockCodegen.expressions[1].arguments[2].arguments[1].returns
+    expect(innerBlockCodegen).toMatchObject({
+      type: NodeTypes.JS_SEQUENCE_EXPRESSION,
+      expressions: [
+        { callee: OPEN_BLOCK },
+        {
+          callee: CREATE_BLOCK,
+          arguments: [
+            `"div"`,
+            { content: `_hoisted_1` },
+            [
+              {
+                codegenNode: { content: `_hoisted_2` }
+              }
+            ]
+          ]
+        }
+      ]
+    })
+    expect(generate(root).code).toMatchSnapshot()
+  })
+})
index d82f87ab242394776e988457d5f12398b532e72f..2bd6e186c44ada7a7ac0dea17341524d67d6d676 100644 (file)
@@ -25,7 +25,7 @@ import { transformStyle } from '../../../compiler-dom/src/transforms/transformSt
 import { transformOn } from '../../src/transforms/vOn'
 import { transformBind } from '../../src/transforms/vBind'
 import { PatchFlags } from '@vue/shared'
-import { createObjectMatcher } from '../testUtils'
+import { createObjectMatcher, genFlagText } from '../testUtils'
 import { optimizeText } from '../../src/transforms/optimizeText'
 
 function parseWithElementTransform(
@@ -324,7 +324,7 @@ describe('compiler: element transform', () => {
           `"div"`,
           `null`,
           `null`,
-          `${PatchFlags.NEED_PATCH} /* NEED_PATCH */` // should generate appropriate flag
+          genFlagText(PatchFlags.NEED_PATCH) // should generate appropriate flag
         ]
       },
       {
@@ -573,30 +573,30 @@ describe('compiler: element transform', () => {
 
       const { node: node2 } = parseWithBind(`<div>{{ foo }}</div>`)
       expect(node2.arguments.length).toBe(4)
-      expect(node2.arguments[3]).toBe(`${PatchFlags.TEXT} /* TEXT */`)
+      expect(node2.arguments[3]).toBe(genFlagText(PatchFlags.TEXT))
 
       // multiple nodes, merged with optimize text
       const { node: node3 } = parseWithBind(`<div>foo {{ bar }} baz</div>`)
       expect(node3.arguments.length).toBe(4)
-      expect(node3.arguments[3]).toBe(`${PatchFlags.TEXT} /* TEXT */`)
+      expect(node3.arguments[3]).toBe(genFlagText(PatchFlags.TEXT))
     })
 
     test('CLASS', () => {
       const { node } = parseWithBind(`<div :class="foo" />`)
       expect(node.arguments.length).toBe(4)
-      expect(node.arguments[3]).toBe(`${PatchFlags.CLASS} /* CLASS */`)
+      expect(node.arguments[3]).toBe(genFlagText(PatchFlags.CLASS))
     })
 
     test('STYLE', () => {
       const { node } = parseWithBind(`<div :style="foo" />`)
       expect(node.arguments.length).toBe(4)
-      expect(node.arguments[3]).toBe(`${PatchFlags.STYLE} /* STYLE */`)
+      expect(node.arguments[3]).toBe(genFlagText(PatchFlags.STYLE))
     })
 
     test('PROPS', () => {
       const { node } = parseWithBind(`<div id="foo" :foo="bar" :baz="qux" />`)
       expect(node.arguments.length).toBe(5)
-      expect(node.arguments[3]).toBe(`${PatchFlags.PROPS} /* PROPS */`)
+      expect(node.arguments[3]).toBe(genFlagText(PatchFlags.PROPS))
       expect(node.arguments[4]).toBe(`["foo", "baz"]`)
     })
 
@@ -606,9 +606,7 @@ describe('compiler: element transform', () => {
       )
       expect(node.arguments.length).toBe(5)
       expect(node.arguments[3]).toBe(
-        `${PatchFlags.PROPS |
-          PatchFlags.CLASS |
-          PatchFlags.STYLE} /* CLASS, STYLE, PROPS */`
+        genFlagText([PatchFlags.CLASS, PatchFlags.STYLE, PatchFlags.PROPS])
       )
       expect(node.arguments[4]).toBe(`["foo", "baz"]`)
     })
@@ -616,17 +614,13 @@ describe('compiler: element transform', () => {
     test('FULL_PROPS (v-bind)', () => {
       const { node } = parseWithBind(`<div v-bind="foo" />`)
       expect(node.arguments.length).toBe(4)
-      expect(node.arguments[3]).toBe(
-        `${PatchFlags.FULL_PROPS} /* FULL_PROPS */`
-      )
+      expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS))
     })
 
     test('FULL_PROPS (dynamic key)', () => {
       const { node } = parseWithBind(`<div :[foo]="bar" />`)
       expect(node.arguments.length).toBe(4)
-      expect(node.arguments[3]).toBe(
-        `${PatchFlags.FULL_PROPS} /* FULL_PROPS */`
-      )
+      expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS))
     })
 
     test('FULL_PROPS (w/ others)', () => {
@@ -634,34 +628,26 @@ describe('compiler: element transform', () => {
         `<div id="foo" v-bind="bar" :class="cls" />`
       )
       expect(node.arguments.length).toBe(4)
-      expect(node.arguments[3]).toBe(
-        `${PatchFlags.FULL_PROPS} /* FULL_PROPS */`
-      )
+      expect(node.arguments[3]).toBe(genFlagText(PatchFlags.FULL_PROPS))
     })
 
     test('NEED_PATCH (static ref)', () => {
       const { node } = parseWithBind(`<div ref="foo" />`)
       expect(node.arguments.length).toBe(4)
-      expect(node.arguments[3]).toBe(
-        `${PatchFlags.NEED_PATCH} /* NEED_PATCH */`
-      )
+      expect(node.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
     })
 
     test('NEED_PATCH (dynamic ref)', () => {
       const { node } = parseWithBind(`<div :ref="foo" />`)
       expect(node.arguments.length).toBe(4)
-      expect(node.arguments[3]).toBe(
-        `${PatchFlags.NEED_PATCH} /* NEED_PATCH */`
-      )
+      expect(node.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
     })
 
     test('NEED_PATCH (custom directives)', () => {
       const { node } = parseWithBind(`<div v-foo />`)
       const vnodeCall = node.arguments[0] as CallExpression
       expect(vnodeCall.arguments.length).toBe(4)
-      expect(vnodeCall.arguments[3]).toBe(
-        `${PatchFlags.NEED_PATCH} /* NEED_PATCH */`
-      )
+      expect(vnodeCall.arguments[3]).toBe(genFlagText(PatchFlags.NEED_PATCH))
     })
   })
 })
index 2697358c0566e0ca8e85d053c5083e200c8cfe3c..ff6a4d4586fc323cddccf0d562ff462d52aa0266 100644 (file)
@@ -25,8 +25,7 @@ import {
   RENDER_SLOT
 } from '../../src/runtimeHelpers'
 import { PatchFlags } from '@vue/runtime-dom'
-import { PatchFlagNames } from '@vue/shared'
-import { createObjectMatcher } from '../testUtils'
+import { createObjectMatcher, genFlagText } from '../testUtils'
 
 function parseWithForTransform(
   template: string,
@@ -609,12 +608,8 @@ describe('compiler: v-for', () => {
                 ]
               },
               keyed
-                ? `${PatchFlags.KEYED_FRAGMENT} /* ${
-                    PatchFlagNames[PatchFlags.KEYED_FRAGMENT]
-                  } */`
-                : `${PatchFlags.UNKEYED_FRAGMENT} /* ${
-                    PatchFlagNames[PatchFlags.UNKEYED_FRAGMENT]
-                  } */`
+                ? genFlagText(PatchFlags.KEYED_FRAGMENT)
+                : genFlagText(PatchFlags.UNKEYED_FRAGMENT)
             ]
           }
         ]
@@ -842,9 +837,7 @@ describe('compiler: v-for', () => {
                     }
                   ]
                 },
-                `${PatchFlags.UNKEYED_FRAGMENT} /* ${
-                  PatchFlagNames[PatchFlags.UNKEYED_FRAGMENT]
-                } */`
+                genFlagText(PatchFlags.UNKEYED_FRAGMENT)
               ]
             }
           }
index 1641ef8114c748a5d2de275efe707a82af4f0b45..bd1057f36dfca6720363c6b89180df18cb0b587a 100644 (file)
@@ -18,8 +18,8 @@ import {
   trackVForSlotScopes
 } from '../../src/transforms/vSlot'
 import { CREATE_SLOTS, RENDER_LIST } from '../../src/runtimeHelpers'
-import { createObjectMatcher } from '../testUtils'
-import { PatchFlags, PatchFlagNames } from '@vue/shared'
+import { createObjectMatcher, genFlagText } from '../testUtils'
+import { PatchFlags } from '@vue/shared'
 import { transformFor } from '../../src/transforms/vFor'
 import { transformIf } from '../../src/transforms/vIf'
 
@@ -308,9 +308,7 @@ describe('compiler: transform component slots', () => {
                   }),
                   // nested slot should be forced dynamic, since scope variables
                   // are not tracked as dependencies of the slot.
-                  `${PatchFlags.DYNAMIC_SLOTS} /* ${
-                    PatchFlagNames[PatchFlags.DYNAMIC_SLOTS]
-                  } */`
+                  genFlagText(PatchFlags.DYNAMIC_SLOTS)
                 ]
               }
             },
index 5fe82eae9866d9007b60ad522fe855f465c41871..dcfc3732bbef3512593916ea633f9264354f9928 100644 (file)
@@ -136,6 +136,10 @@ export interface SlotOutletNode extends BaseElementNode {
 
 export interface TemplateNode extends BaseElementNode {
   tagType: ElementTypes.TEMPLATE
+  codegenNode:
+    | ElementCodegenNode
+    | CodegenNodeWithDirective<ElementCodegenNode>
+    | undefined
 }
 
 export interface TextNode extends Node {
@@ -278,8 +282,7 @@ export interface ConditionalExpression extends Node {
 // createVNode(...)
 export interface ElementCodegenNode extends CallExpression {
   callee: typeof CREATE_VNODE
-  arguments: // tag, props, children, patchFlag, dynamicProps
-
+  arguments:  // tag, props, children, patchFlag, dynamicProps
     | [string | RuntimeHelper]
     | [string | RuntimeHelper, PropsExpression]
     | [string | RuntimeHelper, 'null' | PropsExpression, TemplateChildNode[]]
@@ -305,8 +308,7 @@ export type ElementCodegenNodeWithDirective = CodegenNodeWithDirective<
 // createVNode(...)
 export interface ComponentCodegenNode extends CallExpression {
   callee: typeof CREATE_VNODE
-  arguments: // Comp, props, slots, patchFlag, dynamicProps
-
+  arguments:  // Comp, props, slots, patchFlag, dynamicProps
     | [string | RuntimeHelper]
     | [string | RuntimeHelper, PropsExpression]
     | [string | RuntimeHelper, 'null' | PropsExpression, SlotsExpression]
@@ -394,8 +396,7 @@ export interface DirectiveArguments extends ArrayExpression {
 }
 
 export interface DirectiveArgumentNode extends ArrayExpression {
-  elements: // dir, exp, arg, modifiers
-
+  elements:  // dir, exp, arg, modifiers
     | [string]
     | [string, ExpressionNode]
     | [string, ExpressionNode, ExpressionNode]
@@ -405,8 +406,7 @@ export interface DirectiveArgumentNode extends ArrayExpression {
 // renderSlot(...)
 export interface SlotOutletCodegenNode extends CallExpression {
   callee: typeof RENDER_SLOT
-  arguments: // $slots, name, props, fallback
-
+  arguments:  // $slots, name, props, fallback
     | [string, string | ExpressionNode]
     | [string, string | ExpressionNode, PropsExpression]
     | [
@@ -557,7 +557,7 @@ type InferCodegenNodeType<T> = T extends typeof CREATE_VNODE
   : T extends typeof CREATE_BLOCK
     ? BlockElementCodegenNode | BlockComponentCodegenNode
     : T extends typeof APPLY_DIRECTIVES
-      ? CodegenNodeWithDirective<ElementCodegenNode | ComponentCodegenNode>
+      ? ElementCodegenNodeWithDirective | CompoenntCodegenNodeWithDirective
       : T extends typeof RENDER_SLOT ? SlotOutletCodegenNode : CallExpression
 
 export function createCallExpression<T extends CallExpression['callee']>(
index 363b7918e234d2bfeb618820eceefb747109bbc2..54977c35c7bcf1f116c6d6ae4ec7c74c25fff3e0 100644 (file)
@@ -22,8 +22,8 @@ import {
   RuntimeHelper,
   helperNameMap
 } from './runtimeHelpers'
-import { isVSlot, createBlockExpression, isSlotOutlet } from './utils'
-import { hoistStatic } from './transforms/hoistStatic'
+import { isVSlot, createBlockExpression } from './utils'
+import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
 
 // There are two types of transforms:
 //
@@ -229,26 +229,19 @@ export function transform(root: RootNode, options: TransformOptions) {
 function finalizeRoot(root: RootNode, context: TransformContext) {
   const { helper } = context
   const { children } = root
-  if (children.length === 1) {
-    const child = children[0]
-    if (
-      child.type === NodeTypes.ELEMENT &&
-      !isSlotOutlet(child) &&
-      child.codegenNode &&
-      child.codegenNode.type === NodeTypes.JS_CALL_EXPRESSION
-    ) {
-      // turn root element into a block
-      root.codegenNode = createBlockExpression(
-        child.codegenNode!.arguments,
-        context
-      )
-    } else {
-      // - single <slot/>, IfNode, ForNode: already blocks.
-      // - single text node: always patched.
-      // - transform calls without transformElement (only during tests)
-      // Just generate the node as-is
-      root.codegenNode = child
-    }
+  const child = children[0]
+  if (isSingleElementRoot(root, child) && child.codegenNode) {
+    // turn root element into a block
+    root.codegenNode = createBlockExpression(
+      child.codegenNode.arguments,
+      context
+    )
+  } else if (children.length === 1) {
+    // - single <slot/>, IfNode, ForNode: already blocks.
+    // - single text node: always patched.
+    // - transform calls without transformElement (only during tests)
+    // Just generate the node as-is
+    root.codegenNode = child
   } else if (children.length > 1) {
     // root has multiple nodes - return a fragment block.
     root.codegenNode = createBlockExpression(
@@ -256,7 +249,6 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
       context
     )
   }
-
   // finalize meta information
   root.helpers = [...context.helpers]
   root.components = [...context.components]
index 155aa7725962f108d4e07d886168f5ac1eea8ecb..b949db22ac43d7ded8bfc76329c523d2b154ba76 100644 (file)
@@ -5,20 +5,42 @@ import {
   ElementNode,
   ElementTypes,
   ElementCodegenNode,
-  ElementCodegenNodeWithDirective
+  ElementCodegenNodeWithDirective,
+  PlainElementNode,
+  ComponentNode,
+  TemplateNode
 } from '../ast'
 import { TransformContext } from '../transform'
 import { APPLY_DIRECTIVES } from '../runtimeHelpers'
 import { PatchFlags } from '@vue/shared'
+import { isSlotOutlet } from '../utils'
 
 export function hoistStatic(root: RootNode, context: TransformContext) {
-  walk(root.children, context, new Map<TemplateChildNode, boolean>())
+  walk(
+    root.children,
+    context,
+    new Map(),
+    isSingleElementRoot(root, root.children[0])
+  )
+}
+
+export function isSingleElementRoot(
+  root: RootNode,
+  child: TemplateChildNode
+): child is PlainElementNode | ComponentNode | TemplateNode {
+  const { children } = root
+  return (
+    children.length === 1 &&
+    child.type === NodeTypes.ELEMENT &&
+    !isSlotOutlet(child)
+  )
 }
 
 function walk(
   children: TemplateChildNode[],
   context: TransformContext,
-  resultCache: Map<TemplateChildNode, boolean>
+  resultCache: Map<TemplateChildNode, boolean>,
+  doNotHoistNode: boolean = false
 ) {
   for (let i = 0; i < children.length; i++) {
     const child = children[i]
@@ -27,7 +49,7 @@ function walk(
       child.type === NodeTypes.ELEMENT &&
       child.tagType === ElementTypes.ELEMENT
     ) {
-      if (isStaticNode(child, resultCache)) {
+      if (!doNotHoistNode && isStaticNode(child, resultCache)) {
         // whole tree is static
         ;(child as any).codegenNode = context.hoist(child.codegenNode!)
         continue
@@ -51,11 +73,16 @@ function walk(
         }
       }
     }
-    if (child.type === NodeTypes.ELEMENT || child.type === NodeTypes.FOR) {
+    if (child.type === NodeTypes.ELEMENT) {
       walk(child.children, context, resultCache)
+    } else if (child.type === NodeTypes.FOR) {
+      // Do not hoist v-for single child because it has to be a block
+      walk(child.children, context, resultCache, child.children.length === 1)
     } else if (child.type === NodeTypes.IF) {
       for (let i = 0; i < child.branches.length; i++) {
-        walk(child.branches[i].children, context, resultCache)
+        const branchChildren = child.branches[i].children
+        // Do not hoist v-if single child because it has to be a block
+        walk(branchChildren, context, resultCache, branchChildren.length === 1)
       }
     }
   }