]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler): render <slot/> as block fragments
authorEvan You <yyx990803@gmail.com>
Thu, 3 Oct 2019 16:03:14 +0000 (12:03 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 3 Oct 2019 16:03:14 +0000 (12:03 -0400)
12 files changed:
packages/compiler-core/__tests__/transform.spec.ts
packages/compiler-core/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
packages/compiler-core/__tests__/transforms/__snapshots__/vIf.spec.ts.snap
packages/compiler-core/__tests__/transforms/vFor.spec.ts
packages/compiler-core/__tests__/transforms/vIf.spec.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/transfromSlotOutlet.ts
packages/compiler-core/src/transforms/vFor.ts
packages/compiler-core/src/transforms/vIf.ts
packages/compiler-core/src/utils.ts
packages/runtime-core/src/helpers/renderSlot.ts

index aa04c3f0dfed964f583c7a8b3dc8ef7c2ae794d7..44b88c9324f89b9d30cdd9b2ff609854e26df797 100644 (file)
@@ -274,16 +274,13 @@ describe('compiler: transform', () => {
 
     test('single <slot/>', () => {
       const ast = transformWithCodegen(`<slot/>`)
-      expect(ast.codegenNode).toMatchObject(
-        createBlockMatcher([
-          `_${FRAGMENT}`,
-          `null`,
-          {
-            type: NodeTypes.JS_CALL_EXPRESSION,
-            callee: `_${RENDER_SLOT}`
-          }
-        ])
-      )
+      expect(ast.codegenNode).toMatchObject({
+        codegenNode: {
+          type: NodeTypes.JS_CALL_EXPRESSION,
+          callee: `_${RENDER_SLOT}`,
+          arguments: ['$slots.default']
+        }
+      })
     })
 
     test('single element', () => {
index c2df6a1206a1b809289fca31533a6091f7ee9006..d40121beab6f73757e985fd064c4195afc2d37d4 100644 (file)
@@ -112,7 +112,21 @@ return function render() {
     const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, renderSlot: _renderSlot } = _Vue
     
     return (_openBlock(), _createBlock(_Fragment, null, _renderList(items, (item) => {
-      return (_openBlock(), _createBlock(_Fragment, null, _renderSlot($slots.default)))
+      return _renderSlot($slots.default)
+    }), 128 /* UNKEYED_FRAGMENT */))
+  }
+}"
+`;
+
+exports[`compiler: v-for codegen v-for on <slot/> 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { renderList: _renderList, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, renderSlot: _renderSlot } = _Vue
+    
+    return (_openBlock(), _createBlock(_Fragment, null, _renderList(items, (item) => {
+      return _renderSlot($slots.default)
     }), 128 /* UNKEYED_FRAGMENT */))
   }
 }"
index 3291e8dc2f588775f0d64f69fe83851b2763c9f1..8fd747609ca342fc4d33cf67f930f41ce61beb76 100644 (file)
@@ -37,10 +37,10 @@ exports[`compiler: v-if codegen template v-if w/ single <slot/> child 1`] = `
 
 return function render() {
   with (this) {
-    const { openBlock: _openBlock, renderSlot: _renderSlot, Fragment: _Fragment, createBlock: _createBlock, Empty: _Empty } = _Vue
+    const { openBlock: _openBlock, renderSlot: _renderSlot, createBlock: _createBlock, Empty: _Empty } = _Vue
     
     return (_openBlock(), ok
-      ? _createBlock(_Fragment, { key: 0 }, _renderSlot($slots.default))
+      ? _renderSlot($slots.default, { key: 0 })
       : _createBlock(_Empty))
   }
 }"
index 0357443c2df3f67d5d1276d42de3f1867fd65d1a..0bb391617cc24074827be7c95899f8392a2e0bd8 100644 (file)
@@ -567,7 +567,8 @@ describe('compiler: v-for', () => {
   describe('codegen', () => {
     function assertSharedCodegen(
       node: SequenceExpression,
-      keyed: boolean = false
+      keyed: boolean = false,
+      customReturn: boolean = false
     ) {
       expect(node).toMatchObject({
         type: NodeTypes.JS_SEQUENCE_EXPRESSION,
@@ -589,19 +590,21 @@ describe('compiler: v-for', () => {
                   {}, // to be asserted by each test
                   {
                     type: NodeTypes.JS_FUNCTION_EXPRESSION,
-                    returns: {
-                      type: NodeTypes.JS_SEQUENCE_EXPRESSION,
-                      expressions: [
-                        {
-                          type: NodeTypes.JS_CALL_EXPRESSION,
-                          callee: `_${OPEN_BLOCK}`
-                        },
-                        {
-                          type: NodeTypes.JS_CALL_EXPRESSION,
-                          callee: `_${CREATE_BLOCK}`
+                    returns: customReturn
+                      ? {}
+                      : {
+                          type: NodeTypes.JS_SEQUENCE_EXPRESSION,
+                          expressions: [
+                            {
+                              type: NodeTypes.JS_CALL_EXPRESSION,
+                              callee: `_${OPEN_BLOCK}`
+                            },
+                            {
+                              type: NodeTypes.JS_CALL_EXPRESSION,
+                              callee: `_${CREATE_BLOCK}`
+                            }
+                          ]
                         }
-                      ]
-                    }
                   }
                 ]
               },
@@ -621,7 +624,10 @@ describe('compiler: v-for', () => {
       return {
         source: renderListArgs[0] as SimpleExpressionNode,
         params: (renderListArgs[1] as any).params,
-        blockArgs: (renderListArgs[1] as any).returns.expressions[1].arguments
+        returns: (renderListArgs[1] as any).returns,
+        blockArgs: customReturn
+          ? null
+          : (renderListArgs[1] as any).returns.expressions[1].arguments
       }
     }
 
@@ -715,17 +721,33 @@ describe('compiler: v-for', () => {
       } = parseWithForTransform(
         '<template v-for="item in items"><slot/></template>'
       )
-      expect(assertSharedCodegen(codegenNode)).toMatchObject({
+      expect(
+        assertSharedCodegen(codegenNode, false, true /* custom return */)
+      ).toMatchObject({
         source: { content: `items` },
         params: [{ content: `item` }],
-        blockArgs: [
-          `_${FRAGMENT}`,
-          `null`,
-          {
-            type: NodeTypes.JS_CALL_EXPRESSION,
-            callee: `_${RENDER_SLOT}`
-          }
-        ]
+        returns: {
+          type: NodeTypes.JS_CALL_EXPRESSION,
+          callee: `_${RENDER_SLOT}`
+        }
+      })
+      expect(generate(root).code).toMatchSnapshot()
+    })
+
+    test('v-for on <slot/>', () => {
+      const {
+        root,
+        node: { codegenNode }
+      } = parseWithForTransform('<slot v-for="item in items"></slot>')
+      expect(
+        assertSharedCodegen(codegenNode, false, true /* custom return */)
+      ).toMatchObject({
+        source: { content: `items` },
+        params: [{ content: `item` }],
+        returns: {
+          type: NodeTypes.JS_CALL_EXPRESSION,
+          callee: `_${RENDER_SLOT}`
+        }
       })
       expect(generate(root).code).toMatchSnapshot()
     })
@@ -794,7 +816,7 @@ describe('compiler: v-for', () => {
               // should optimize v-if + v-for into a single Fragment block
               arguments: [
                 `_${FRAGMENT}`,
-                `{ key: 0 }`,
+                createObjectMatcher({ key: `[0]` }),
                 {
                   type: NodeTypes.JS_CALL_EXPRESSION,
                   callee: `_${RENDER_LIST}`,
index 1010e914139935aee38c09842f345142658ebeed..930313c304941ca357ba28e3892e4682a55a8c39 100644 (file)
@@ -316,7 +316,10 @@ describe('compiler: v-if', () => {
       assertSharedCodegen(codegenNode)
       const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
         .consequent as CallExpression
-      expect(branch1.arguments).toMatchObject([`"div"`, `{ key: 0 }`])
+      expect(branch1.arguments).toMatchObject([
+        `"div"`,
+        createObjectMatcher({ key: `[0]` })
+      ])
       const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
         .alternate as CallExpression
       expect(branch2.arguments).toMatchObject([`_${EMPTY}`])
@@ -333,7 +336,7 @@ describe('compiler: v-if', () => {
         .consequent as CallExpression
       expect(branch1.arguments).toMatchObject([
         `_${FRAGMENT}`,
-        `{ key: 0 }`,
+        createObjectMatcher({ key: `[0]` }),
         [
           { type: NodeTypes.ELEMENT, tag: 'div' },
           { type: NodeTypes.TEXT, content: `hello` },
@@ -351,17 +354,14 @@ describe('compiler: v-if', () => {
         root,
         node: { codegenNode }
       } = parseWithIfTransform(`<template v-if="ok"><slot/></template>`)
-      assertSharedCodegen(codegenNode)
+      // assertSharedCodegen(codegenNode)
       const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
         .consequent as CallExpression
-      expect(branch1.arguments).toMatchObject([
-        `_${FRAGMENT}`,
-        `{ key: 0 }`,
-        {
-          type: NodeTypes.JS_CALL_EXPRESSION,
-          callee: `_${RENDER_SLOT}`
-        }
-      ])
+      expect(branch1).toMatchObject({
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: `_${RENDER_SLOT}`,
+        arguments: ['$slots.default', createObjectMatcher({ key: `[0]` })]
+      })
       expect(generate(root).code).toMatchSnapshot()
     })
 
@@ -373,10 +373,16 @@ describe('compiler: v-if', () => {
       assertSharedCodegen(codegenNode)
       const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
         .consequent as CallExpression
-      expect(branch1.arguments).toMatchObject([`"div"`, `{ key: 0 }`])
+      expect(branch1.arguments).toMatchObject([
+        `"div"`,
+        createObjectMatcher({ key: `[0]` })
+      ])
       const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
         .alternate as CallExpression
-      expect(branch2.arguments).toMatchObject([`"p"`, `{ key: 1 }`])
+      expect(branch2.arguments).toMatchObject([
+        `"p"`,
+        createObjectMatcher({ key: `[1]` })
+      ])
       expect(generate(root).code).toMatchSnapshot()
     })
 
@@ -388,12 +394,15 @@ describe('compiler: v-if', () => {
       assertSharedCodegen(codegenNode, 1)
       const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
         .consequent as CallExpression
-      expect(branch1.arguments).toMatchObject([`"div"`, `{ key: 0 }`])
+      expect(branch1.arguments).toMatchObject([
+        `"div"`,
+        createObjectMatcher({ key: `[0]` })
+      ])
       const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
         .alternate as ConditionalExpression
       expect((branch2.consequent as CallExpression).arguments).toMatchObject([
         `"p"`,
-        `{ key: 1 }`
+        createObjectMatcher({ key: `[1]` })
       ])
       expect(generate(root).code).toMatchSnapshot()
     })
@@ -408,16 +417,19 @@ describe('compiler: v-if', () => {
       assertSharedCodegen(codegenNode, 1)
       const branch1 = (codegenNode.expressions[1] as ConditionalExpression)
         .consequent as CallExpression
-      expect(branch1.arguments).toMatchObject([`"div"`, `{ key: 0 }`])
+      expect(branch1.arguments).toMatchObject([
+        `"div"`,
+        createObjectMatcher({ key: `[0]` })
+      ])
       const branch2 = (codegenNode.expressions[1] as ConditionalExpression)
         .alternate as ConditionalExpression
       expect((branch2.consequent as CallExpression).arguments).toMatchObject([
         `"p"`,
-        `{ key: 1 }`
+        createObjectMatcher({ key: `[1]` })
       ])
       expect((branch2.alternate as CallExpression).arguments).toMatchObject([
         `_${FRAGMENT}`,
-        `{ key: 2 }`,
+        createObjectMatcher({ key: `[2]` }),
         [
           {
             type: NodeTypes.TEXT,
@@ -437,7 +449,7 @@ describe('compiler: v-if', () => {
       expect(branch1.arguments[1]).toMatchObject({
         type: NodeTypes.JS_CALL_EXPRESSION,
         callee: `_${MERGE_PROPS}`,
-        arguments: [`{ key: 0 }`, { content: `obj` }]
+        arguments: [createObjectMatcher({ key: `[0]` }), { content: `obj` }]
       })
     })
 
@@ -470,7 +482,7 @@ describe('compiler: v-if', () => {
         type: NodeTypes.JS_CALL_EXPRESSION,
         callee: `_${MERGE_PROPS}`,
         arguments: [
-          `{ key: 0 }`,
+          createObjectMatcher({ key: `[0]` }),
           { content: `obj` },
           createObjectMatcher({
             id: 'foo'
@@ -487,9 +499,11 @@ describe('compiler: v-if', () => {
         .consequent as CallExpression
       expect(branch1.callee).toBe(`_${APPLY_DIRECTIVES}`)
       const realBranch = branch1.arguments[0] as CallExpression
-      expect(realBranch.arguments[1]).toBe(`{ key: 0 }`)
+      expect(realBranch.arguments[1]).toMatchObject(
+        createObjectMatcher({ key: `[0]` })
+      )
     })
 
-    test('with comments', () => {})
+    test.todo('with comments')
   })
 })
index e1d236253a4760113212b94a78ea327f03b08580..6d1bb90ea903519a840633020b2dd0fe29abf3a1 100644 (file)
@@ -15,7 +15,7 @@ import {
 import { isString, isArray } from '@vue/shared'
 import { CompilerError, defaultOnError } from './errors'
 import { TO_STRING, COMMENT, CREATE_VNODE, FRAGMENT } from './runtimeConstants'
-import { isVSlot, createBlockExpression } from './utils'
+import { isVSlot, createBlockExpression, isSlotOutlet } from './utils'
 
 // There are two types of transforms:
 //
@@ -192,22 +192,20 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
   const { children } = root
   if (children.length === 1) {
     const child = children[0]
-    if (child.type === NodeTypes.ELEMENT && child.codegenNode) {
-      // only child is a <slot/> - it needs to be in a fragment block.
-      if (child.tagType === ElementTypes.SLOT) {
-        root.codegenNode = createBlockExpression(
-          [helper(FRAGMENT), `null`, child.codegenNode!],
-          context
-        )
-      } else {
-        // turn root element into a block
-        root.codegenNode = createBlockExpression(
-          child.codegenNode!.arguments,
-          context
-        )
-      }
+    if (
+      child.type === NodeTypes.ELEMENT &&
+      !isSlotOutlet(child) &&
+      child.codegenNode
+    ) {
+      // turn root element into a block
+      root.codegenNode = createBlockExpression(
+        child.codegenNode!.arguments,
+        context
+      )
     } else {
-      // IfNode, ForNode, TextNodes or transform calls without transformElement.
+      // - 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
     }
index e4f37a22d0598128fcf423f7be203aa2e9ea5a90..069c79345bb86f5aa2b294d5186a4f59ee32197b 100644 (file)
@@ -101,15 +101,9 @@ export const transformElement: NodeTransform = (node, context) => {
             if (hasDynamicTextChild) {
               patchFlag |= PatchFlags.TEXT
             }
-            // pass directly if the only child is one of:
-            // - text (plain / interpolation / expression)
-            // - <slot> outlet (already an array)
-            if (
-              type === NodeTypes.TEXT ||
-              hasDynamicTextChild ||
-              (type === NodeTypes.ELEMENT &&
-                (child as ElementNode).tagType === ElementTypes.SLOT)
-            ) {
+            // pass directly if the only child is a text node
+            // (plain / interpolation / expression)
+            if (hasDynamicTextChild || type === NodeTypes.TEXT) {
               args.push(child)
             } else {
               args.push(node.children)
@@ -171,7 +165,7 @@ export const transformElement: NodeTransform = (node, context) => {
   }
 }
 
-type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
+export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
 
 export function buildProps(
   props: ElementNode['props'],
index 24032550fc9715ce2a0e65343e450637c575a6d4..3ed95a4eeba04d3c6b87ddcb5adaec9124d75f2a 100644 (file)
@@ -1,19 +1,18 @@
 import { NodeTransform } from '../transform'
 import {
   NodeTypes,
-  ElementTypes,
   CompoundExpressionNode,
   createCompoundExpression,
   CallExpression,
   createCallExpression
 } from '../ast'
-import { isSimpleIdentifier } from '../utils'
+import { isSimpleIdentifier, isSlotOutlet } from '../utils'
 import { buildProps } from './transformElement'
 import { createCompilerError, ErrorCodes } from '../errors'
 import { RENDER_SLOT } from '../runtimeConstants'
 
 export const transformSlotOutlet: NodeTransform = (node, context) => {
-  if (node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT) {
+  if (isSlotOutlet(node)) {
     const { props, children, loc } = node
     const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots`
     let slot: string | CompoundExpressionNode = $slots + `.default`
index fbcbdfa57408181260725b7c295a4e8bb6c3274b..6ae47d3eb11b3f27545d84b8969faeb27c1ea1a6 100644 (file)
@@ -12,14 +12,18 @@ import {
   createCallExpression,
   createFunctionExpression,
   ElementTypes,
-  ObjectExpression,
   createObjectExpression,
-  createObjectProperty,
-  TemplateChildNode,
-  CallExpression
+  createObjectProperty
 } from '../ast'
 import { createCompilerError, ErrorCodes } from '../errors'
-import { getInnerRange, findProp, createBlockExpression } from '../utils'
+import {
+  getInnerRange,
+  findProp,
+  createBlockExpression,
+  isTemplateNode,
+  isSlotOutlet,
+  injectProp
+} from '../utils'
 import {
   RENDER_LIST,
   OPEN_BLOCK,
@@ -28,6 +32,7 @@ import {
 } from '../runtimeConstants'
 import { processExpression } from './transformExpression'
 import { PatchFlags, PatchFlagNames } from '@vue/shared'
+import { PropsExpression } from './transformElement'
 
 export const transformFor = createStructuralDirectiveTransform(
   'for',
@@ -91,40 +96,52 @@ export const transformFor = createStructuralDirectiveTransform(
 
           // finish the codegen now that all children have been traversed
           let childBlock
-          if (node.tagType === ElementTypes.TEMPLATE) {
+          const isTemplate = isTemplateNode(node)
+          const slotOutlet = isSlotOutlet(node)
+            ? node
+            : isTemplate &&
+              node.children.length === 1 &&
+              isSlotOutlet(node.children[0])
+              ? node.children[0]
+              : null
+          const keyProperty = keyProp
+            ? createObjectProperty(
+                `key`,
+                keyProp.type === NodeTypes.ATTRIBUTE
+                  ? createSimpleExpression(keyProp.value!.content, true)
+                  : keyProp.exp!
+              )
+            : null
+          if (slotOutlet) {
+            // <slot v-for="..."> or <template v-for="..."><slot/></template>
+            childBlock = slotOutlet.codegenNode!
+            if (isTemplate && keyProperty) {
+              // <template v-for="..." :key="..."><slot/></template>
+              // we need to inject the key to the renderSlot() call.
+              const existingProps = childBlock.arguments[1] as
+                | PropsExpression
+                | undefined
+                | 'null'
+              childBlock.arguments[1] = injectProp(
+                existingProps,
+                keyProperty,
+                context
+              )
+            }
+          } else if (isTemplate) {
             // <template v-for="...">
             // should genereate a fragment block for each loop
-            let childBlockProps: string | ObjectExpression = `null`
-            if (keyProp) {
-              childBlockProps = createObjectExpression([
-                createObjectProperty(
-                  `key`,
-                  keyProp.type === NodeTypes.ATTRIBUTE
-                    ? createSimpleExpression(keyProp.value!.content, true)
-                    : keyProp.exp!
-                )
-              ])
-            }
-            let childBlockChildren: TemplateChildNode[] | CallExpression =
-              node.children
-            // if the only child is a <slot/>, use it directly as fragment
-            // children since it already returns an array.
-            if (childBlockChildren.length === 1) {
-              const child = childBlockChildren[0]
-              if (
-                child.type === NodeTypes.ELEMENT &&
-                child.tagType === ElementTypes.SLOT
-              ) {
-                childBlockChildren = child.codegenNode!
-              }
-            }
             childBlock = createBlockExpression(
-              [helper(FRAGMENT), childBlockProps, childBlockChildren],
+              [
+                helper(FRAGMENT),
+                keyProperty ? createObjectExpression([keyProperty]) : `null`,
+                node.children
+              ],
               context
             )
           } else {
-            // Normal element v-for. Directly use the child's codegenNode arguments,
-            // but replace createVNode() with createBlock()
+            // Normal element v-for. Directly use the child's codegenNode
+            // arguments, but replace createVNode() with createBlock()
             childBlock = createBlockExpression(
               node.codegenNode!.arguments,
               context
index 167ca7d332b6af6a9817208d60fd880caf10748a..9dd27e1fad816590b25c9cb408712f908ef04488 100644 (file)
@@ -16,11 +16,8 @@ import {
   ConditionalExpression,
   CallExpression,
   createSimpleExpression,
-  JSChildNode,
-  ObjectExpression,
   createObjectProperty,
-  Property,
-  ExpressionNode
+  createObjectExpression
 } from '../ast'
 import { createCompilerError, ErrorCodes } from '../errors'
 import { processExpression } from './transformExpression'
@@ -30,9 +27,10 @@ import {
   EMPTY,
   FRAGMENT,
   APPLY_DIRECTIVES,
-  MERGE_PROPS
+  CREATE_VNODE
 } from '../runtimeConstants'
-import { isString } from '@vue/shared'
+import { injectProp } from '../utils'
+import { PropsExpression } from './transformElement'
 
 export const transformIf = createStructuralDirectiveTransform(
   /^(if|else|else-if)$/,
@@ -153,81 +151,52 @@ function createCodegenNodeForBranch(
 function createChildrenCodegenNode(
   branch: IfBranchNode,
   index: number,
-  { helper }: TransformContext
+  context: TransformContext
 ): CallExpression {
-  const keyExp = `{ key: ${index} }`
+  const { helper } = context
+  const keyProperty = createObjectProperty(
+    `key`,
+    createSimpleExpression(index + '', false)
+  )
   const { children } = branch
   const child = children[0]
   const needFragmentWrapper =
-    children.length !== 1 ||
-    child.type !== NodeTypes.ELEMENT ||
-    child.tagType === ElementTypes.SLOT
+    children.length !== 1 || child.type !== NodeTypes.ELEMENT
   if (needFragmentWrapper) {
     const blockArgs: CallExpression['arguments'] = [
       helper(FRAGMENT),
-      keyExp,
+      createObjectExpression([keyProperty]),
       children
     ]
-    if (children.length === 1) {
+    if (children.length === 1 && child.type === NodeTypes.FOR) {
       // optimize away nested fragments when child is a ForNode
-      if (child.type === NodeTypes.FOR) {
-        const forBlockArgs = (child.codegenNode
-          .expressions[1] as CallExpression).arguments
-        // directly use the for block's children and patchFlag
-        blockArgs[2] = forBlockArgs[2]
-        blockArgs[3] = forBlockArgs[3]
-      } else if (
-        child.type === NodeTypes.ELEMENT &&
-        child.tagType === ElementTypes.SLOT
-      ) {
-        // <template v-if="..."><slot/></template>
-        // since slot always returns array, use it directly as the fragment children.
-        blockArgs[2] = child.codegenNode!
-      }
+      const forBlockArgs = (child.codegenNode.expressions[1] as CallExpression)
+        .arguments
+      // directly use the for block's children and patchFlag
+      blockArgs[2] = forBlockArgs[2]
+      blockArgs[3] = forBlockArgs[3]
     }
     return createCallExpression(helper(CREATE_BLOCK), blockArgs)
   } else {
     const childCodegen = (child as ElementNode).codegenNode!
     let vnodeCall = childCodegen
+    // Element with custom directives. Locate the actual createVNode() call.
     if (vnodeCall.callee.includes(APPLY_DIRECTIVES)) {
       vnodeCall = vnodeCall.arguments[0] as CallExpression
     }
-    // change child to a block
-    vnodeCall.callee = helper(CREATE_BLOCK)
-    // branch key
-    const existingProps = vnodeCall.arguments[1]
-    if (!existingProps || existingProps === `null`) {
-      vnodeCall.arguments[1] = keyExp
-    } else {
-      // inject branch key if not already have a key
-      const props = existingProps as
-        | CallExpression
-        | ObjectExpression
-        | ExpressionNode
-      if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
-        // merged props... add ours
-        // only inject key to object literal if it's the first argument so that
-        // if doesn't override user provided keys
-        const first = props.arguments[0] as string | JSChildNode
-        if (!isString(first) && first.type === NodeTypes.JS_OBJECT_EXPRESSION) {
-          first.properties.unshift(createKeyProperty(index))
-        } else {
-          props.arguments.unshift(keyExp)
-        }
-      } else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
-        props.properties.unshift(createKeyProperty(index))
-      } else {
-        // single v-bind with expression
-        vnodeCall.arguments[1] = createCallExpression(helper(MERGE_PROPS), [
-          keyExp,
-          props
-        ])
-      }
+    // Change createVNode to createBlock.
+    // It's possible to have renderSlot() here as well - which already produces
+    // a block, so no need to change the callee. renderSlot() also accepts props
+    // as the 2nd argument, so the key injection logic below works for it too.
+    if (vnodeCall.callee.includes(CREATE_VNODE)) {
+      vnodeCall.callee = helper(CREATE_BLOCK)
     }
+    // inject branch key
+    const existingProps = vnodeCall.arguments[1] as
+      | PropsExpression
+      | undefined
+      | 'null'
+    vnodeCall.arguments[1] = injectProp(existingProps, keyProperty, context)
     return childCodegen
   }
 }
-
-function createKeyProperty(index: number): Property {
-  return createObjectProperty(`key`, createSimpleExpression(index + '', false))
-}
index 37cac6cc18c956be235e31c7c8c90094987b8c3a..ab19a7d0428a7f596c189aa40fe77d156146a576 100644 (file)
@@ -10,13 +10,18 @@ import {
   DirectiveNode,
   ElementTypes,
   TemplateChildNode,
-  RootNode
+  RootNode,
+  ObjectExpression,
+  Property,
+  JSChildNode,
+  createObjectExpression
 } from './ast'
 import { parse } from 'acorn'
 import { walk } from 'estree-walker'
 import { TransformContext } from './transform'
-import { OPEN_BLOCK, CREATE_BLOCK } from './runtimeConstants'
+import { OPEN_BLOCK, CREATE_BLOCK, MERGE_PROPS } from './runtimeConstants'
 import { isString } from '@vue/shared'
+import { PropsExpression } from './transforms/transformElement'
 
 // cache node requires
 // lazy require dependencies so that they don't end up in rollup's dep graph
@@ -165,5 +170,40 @@ export const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
 
 export const isTemplateNode = (
   node: RootNode | TemplateChildNode
-): node is ElementNode =>
+): node is ElementNode & { tagType: ElementTypes.TEMPLATE } =>
   node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE
+
+export const isSlotOutlet = (
+  node: RootNode | TemplateChildNode
+): node is ElementNode & { tagType: ElementTypes.SLOT } =>
+  node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT
+
+export function injectProp(
+  props: PropsExpression | undefined | 'null',
+  prop: Property,
+  context: TransformContext
+): ObjectExpression | CallExpression {
+  if (props == null || props === `null`) {
+    return createObjectExpression([prop])
+  } else if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
+    // merged props... add ours
+    // only inject key to object literal if it's the first argument so that
+    // if doesn't override user provided keys
+    const first = props.arguments[0] as string | JSChildNode
+    if (!isString(first) && first.type === NodeTypes.JS_OBJECT_EXPRESSION) {
+      first.properties.unshift(prop)
+    } else {
+      props.arguments.unshift(createObjectExpression([prop]))
+    }
+    return props
+  } else if (props.type === NodeTypes.JS_OBJECT_EXPRESSION) {
+    props.properties.unshift(prop)
+    return props
+  } else {
+    // single v-bind with expression, return a merged replacement
+    return createCallExpression(context.helper(MERGE_PROPS), [
+      createObjectExpression([prop]),
+      props
+    ])
+  }
+}
index 3cb2c25e9fafbf2ddcd4528df0a74aa003d10ff8..5e14df25ef55387150b753c45692c048509d3434 100644 (file)
@@ -1,12 +1,25 @@
 import { Slot } from '../componentSlots'
-import { VNodeChildren } from '../vnode'
+import {
+  VNodeChildren,
+  openBlock,
+  createBlock,
+  Fragment,
+  VNode
+} from '../vnode'
 
 export function renderSlot(
   slot: Slot | undefined,
   props: any = {},
   // this is not a user-facing function, so the fallback is always generated by
-  // the compiler.
-  fallback?: string | VNodeChildren
-): string | VNodeChildren | null {
-  return slot ? slot() : fallback || null
+  // the compiler and gurunteed to be an array
+  fallback?: VNodeChildren
+): VNode {
+  return (
+    openBlock(),
+    createBlock(
+      Fragment,
+      { key: props.key },
+      slot ? slot(props) : fallback || []
+    )
+  )
 }