diff --git a/packages/compiler-core/__tests__/transform.spec.ts b/packages/compiler-core/__tests__/transform.spec.ts index aa04c3f0..44b88c93 100644 --- a/packages/compiler-core/__tests__/transform.spec.ts +++ b/packages/compiler-core/__tests__/transform.spec.ts @@ -274,16 +274,13 @@ describe('compiler: transform', () => { test('single ', () => { const ast = transformWithCodegen(``) - 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', () => { diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vFor.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vFor.spec.ts.snap index c2df6a12..d40121be 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vFor.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vFor.spec.ts.snap @@ -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 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 */)) } }" diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vIf.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vIf.spec.ts.snap index 3291e8dc..8fd74760 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vIf.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vIf.spec.ts.snap @@ -37,10 +37,10 @@ exports[`compiler: v-if codegen template v-if w/ single 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)) } }" diff --git a/packages/compiler-core/__tests__/transforms/vFor.spec.ts b/packages/compiler-core/__tests__/transforms/vFor.spec.ts index 0357443c..0bb39161 100644 --- a/packages/compiler-core/__tests__/transforms/vFor.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vFor.spec.ts @@ -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( '' ) - 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 ', () => { + const { + root, + node: { codegenNode } + } = parseWithForTransform('') + 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}`, diff --git a/packages/compiler-core/__tests__/transforms/vIf.spec.ts b/packages/compiler-core/__tests__/transforms/vIf.spec.ts index 1010e914..930313c3 100644 --- a/packages/compiler-core/__tests__/transforms/vIf.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vIf.spec.ts @@ -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(``) - 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') }) }) diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index e1d23625..6d1bb90e 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -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 - 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 , 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 } diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index e4f37a22..069c7934 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -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) - // - 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'], diff --git a/packages/compiler-core/src/transforms/transfromSlotOutlet.ts b/packages/compiler-core/src/transforms/transfromSlotOutlet.ts index 24032550..3ed95a4e 100644 --- a/packages/compiler-core/src/transforms/transfromSlotOutlet.ts +++ b/packages/compiler-core/src/transforms/transfromSlotOutlet.ts @@ -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` diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts index fbcbdfa5..6ae47d3e 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -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) { + // or + childBlock = slotOutlet.codegenNode! + if (isTemplate && keyProperty) { + // + // 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) { //