From 3a95a2f1482a94479466423d4c36a745d2edf03e Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 1 Oct 2019 23:53:52 -0400 Subject: [PATCH] fix(compiler): generate correct fragment children when it contains single text node or slot outlet --- .../__snapshots__/compile.spec.ts.snap | 12 ++++-- .../__snapshots__/vFor.spec.ts.snap | 14 +++++++ .../transforms/__snapshots__/vIf.spec.ts.snap | 18 ++++++++- .../__tests__/transforms/vFor.spec.ts | 27 ++++++++++++- .../__tests__/transforms/vIf.spec.ts | 25 +++++++++++- packages/compiler-core/src/ast.ts | 2 +- packages/compiler-core/src/codegen.ts | 39 +++++-------------- .../src/transforms/transformElement.ts | 22 ++++++++++- packages/compiler-core/src/transforms/vFor.ts | 17 +++++++- packages/compiler-core/src/transforms/vIf.ts | 25 ++++++++---- 10 files changed, 153 insertions(+), 48 deletions(-) diff --git a/packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap index 4748ed19..99b7dd6c 100644 --- a/packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/compile.spec.ts.snap @@ -14,7 +14,9 @@ return function render() { _toString(world.burn()), (_openBlock(), ok ? _createBlock(\\"div\\", { key: 0 }, \\"yes\\") - : _createBlock(_Fragment, { key: 1 }, \\"no\\")), + : _createBlock(_Fragment, { key: 1 }, [ + \\"no\\" + ])), _createVNode(_Fragment, null, _renderList(list, (value, index) => { return (_openBlock(), _createBlock(\\"div\\", null, [ _createVNode(\\"span\\", null, _toString(value + index)) @@ -37,7 +39,9 @@ return function render() { toString(_ctx.world.burn()), (openBlock(), (_ctx.ok) ? createBlock(\\"div\\", { key: 0 }, \\"yes\\") - : createBlock(Fragment, { key: 1 }, \\"no\\")), + : createBlock(Fragment, { key: 1 }, [ + \\"no\\" + ])), createVNode(Fragment, null, renderList(_ctx.list, (value, index) => { return (openBlock(), createBlock(\\"div\\", null, [ createVNode(\\"span\\", null, toString(value + index)) @@ -59,7 +63,9 @@ export default function render() { _toString(_ctx.world.burn()), (openBlock(), (_ctx.ok) ? createBlock(\\"div\\", { key: 0 }, \\"yes\\") - : createBlock(Fragment, { key: 1 }, \\"no\\")), + : createBlock(Fragment, { key: 1 }, [ + \\"no\\" + ])), createVNode(Fragment, null, renderList(_ctx.list, (value, index) => { return (openBlock(), createBlock(\\"div\\", null, [ createVNode(\\"span\\", null, _toString(value + index)) 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 352f0f62..29a75151 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vFor.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vFor.spec.ts.snap @@ -104,6 +104,20 @@ return function render() { }" `; +exports[`compiler: v-for codegen template v-for w/ 1`] = ` +"const _Vue = Vue + +return function render() { + with (this) { + const { renderList: _renderList, createVNode: _createVNode, Fragment: _Fragment, renderSlot: _renderSlot, openBlock: _openBlock, createBlock: _createBlock } = _Vue + + return _createVNode(_Fragment, null, _renderList(items, (item) => { + return (_openBlock(), _createBlock(_Fragment, null, _renderSlot($slots.default))) + }), 128 /* UNKEYED_FRAGMENT */) + } +}" +`; + exports[`compiler: v-for codegen v-if + v-for 1`] = ` "const _Vue = Vue 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 bbd8a8d3..b44c2eae 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vIf.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vIf.spec.ts.snap @@ -32,6 +32,20 @@ return function render() { }" `; +exports[`compiler: v-if codegen template v-if w/ single child 1`] = ` +"const _Vue = Vue + +return function render() { + with (this) { + const { openBlock: _openBlock, renderSlot: _renderSlot, Fragment: _Fragment, createBlock: _createBlock, Empty: _Empty } = _Vue + + return (_openBlock(), ok + ? _createBlock(_Fragment, { key: 0 }, _renderSlot($slots.default)) + : _createBlock(_Empty)) + } +}" +`; + exports[`compiler: v-if codegen v-if + v-else 1`] = ` "const _Vue = Vue @@ -57,7 +71,9 @@ return function render() { ? _createBlock(\\"div\\", { key: 0 }) : orNot ? _createBlock(\\"p\\", { key: 1 }) - : _createBlock(_Fragment, { key: 2 }, \\"fine\\")) + : _createBlock(_Fragment, { key: 2 }, [ + \\"fine\\" + ])) } }" `; diff --git a/packages/compiler-core/__tests__/transforms/vFor.spec.ts b/packages/compiler-core/__tests__/transforms/vFor.spec.ts index 38956095..161a5e0b 100644 --- a/packages/compiler-core/__tests__/transforms/vFor.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vFor.spec.ts @@ -4,6 +4,7 @@ import { transformIf } from '../../src/transforms/vIf' import { transformFor } from '../../src/transforms/vFor' import { transformBind } from '../../src/transforms/vBind' import { transformElement } from '../../src/transforms/transformElement' +import { transformSlotOutlet } from '../../src/transforms/transfromSlotOutlet' import { transformExpression } from '../../src/transforms/transformExpression' import { ForNode, @@ -20,7 +21,8 @@ import { CREATE_BLOCK, FRAGMENT, RENDER_LIST, - CREATE_VNODE + CREATE_VNODE, + RENDER_SLOT } from '../../src/runtimeConstants' import { PatchFlags } from '@vue/runtime-dom' import { PatchFlagNames } from '@vue/shared' @@ -36,6 +38,7 @@ function parseWithForTransform( transformIf, transformFor, ...(options.prefixIdentifiers ? [transformExpression] : []), + transformSlotOutlet, transformElement ], directiveTransforms: { @@ -692,6 +695,28 @@ describe('compiler: v-for', () => { expect(generate(root).code).toMatchSnapshot() }) + test('template v-for w/ ', () => { + const { + root, + node: { codegenNode } + } = parseWithForTransform( + '' + ) + expect(assertSharedCodegen(codegenNode)).toMatchObject({ + source: { content: `items` }, + params: [{ content: `item` }], + blockArgs: [ + `_${FRAGMENT}`, + `null`, + { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: `_${RENDER_SLOT}` + } + ] + }) + expect(generate(root).code).toMatchSnapshot() + }) + test('keyed v-for', () => { const { root, diff --git a/packages/compiler-core/__tests__/transforms/vIf.spec.ts b/packages/compiler-core/__tests__/transforms/vIf.spec.ts index f99d7ddb..07545152 100644 --- a/packages/compiler-core/__tests__/transforms/vIf.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vIf.spec.ts @@ -2,6 +2,7 @@ import { parse } from '../../src/parse' import { transform } from '../../src/transform' import { transformIf } from '../../src/transforms/vIf' import { transformElement } from '../../src/transforms/transformElement' +import { transformSlotOutlet } from '../../src/transforms/transfromSlotOutlet' import { IfNode, NodeTypes, @@ -21,7 +22,8 @@ import { EMPTY, FRAGMENT, MERGE_PROPS, - APPLY_DIRECTIVES + APPLY_DIRECTIVES, + RENDER_SLOT } from '../../src/runtimeConstants' import { createObjectMatcher } from '../testUtils' @@ -32,7 +34,7 @@ function parseWithIfTransform( ) { const ast = parse(template, options) transform(ast, { - nodeTransforms: [transformIf, transformElement], + nodeTransforms: [transformIf, transformSlotOutlet, transformElement], ...options }) if (!options.onError) { @@ -344,6 +346,25 @@ describe('compiler: v-if', () => { expect(generate(root).code).toMatchSnapshot() }) + test('template v-if w/ single child', () => { + const { + root, + node: { codegenNode } + } = parseWithIfTransform(``) + 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(generate(root).code).toMatchSnapshot() + }) + test('v-if + v-else', () => { const { root, diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 999c875a..c0941afc 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -176,7 +176,7 @@ export type JSChildNode = export interface CallExpression extends Node { type: NodeTypes.JS_CALL_EXPRESSION callee: string - arguments: (string | JSChildNode | TemplateChildNode[])[] + arguments: (string | JSChildNode | TemplateChildNode | TemplateChildNode[])[] } export interface ObjectExpression extends Node { diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 81b7cb3f..ac395092 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -1,7 +1,6 @@ import { RootNode, TemplateChildNode, - ElementNode, TextNode, CommentNode, ExpressionNode, @@ -15,7 +14,6 @@ import { InterpolationNode, CompoundExpressionNode, SimpleExpressionNode, - ElementTypes, FunctionExpression, SequenceExpression, ConditionalExpression @@ -232,7 +230,8 @@ export function generate( // generate the VNode tree expression push(`return `) - genChildren(ast.children, context, true) + + genRoot(ast, context) if (useWithBlock) { deindent() @@ -257,31 +256,13 @@ function genHoists(hoists: JSChildNode[], context: CodegenContext) { context.newline() } -// This will generate a single vnode call if: -// - The target position explicitly allows a single node (root, if, for) -// - The list has length === 1, AND The only child is a: -// - text -// - outlet, which always produces an array -function genChildren( - children: TemplateChildNode[], - context: CodegenContext, - allowSingle: boolean = false -) { - if (!children.length) { - return context.push(`null`) - } - const child = children[0] - const type = child.type - if ( - children.length === 1 && - (allowSingle || - type === NodeTypes.TEXT || - type === NodeTypes.INTERPOLATION || - type === NodeTypes.COMPOUND_EXPRESSION || - (type === NodeTypes.ELEMENT && - (child as ElementNode).tagType === ElementTypes.SLOT)) - ) { - genNode(child, context) +function genRoot(root: RootNode, context: CodegenContext) { + // TODO handle blocks + const { children } = root + if (children.length === 0) { + context.push(`null`) + } else if (children.length === 1) { + genNode(children[0], context) } else { genNodeListAsArray(children, context) } @@ -316,7 +297,7 @@ function genNodeList( if (isString(node)) { push(node) } else if (isArray(node)) { - genChildren(node, context) + genNodeListAsArray(node, context) } else { genNode(node, context) } diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index de6d1ffb..26d10f77 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -87,8 +87,26 @@ export const transformElement: NodeTransform = (node, context) => { patchFlag |= PatchFlags.DYNAMIC_SLOTS } } else { - // only v-for fragments will have keyed/unkeyed flags - args.push(node.children) + if (node.children.length === 1) { + const child = node.children[0] + const type = child.type + // pass directly if the only child is one of: + // - text (plain / interpolation / expression) + // - outlet (already an array) + if ( + type === NodeTypes.TEXT || + type === NodeTypes.INTERPOLATION || + type === NodeTypes.COMPOUND_EXPRESSION || + (type === NodeTypes.ELEMENT && + (child as ElementNode).tagType === ElementTypes.SLOT) + ) { + args.push(child) + } else { + args.push(node.children) + } + } else { + args.push(node.children) + } } } // patchFlag & dynamicPropNames diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts index 3af7f861..e73e280e 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -14,7 +14,9 @@ import { ElementTypes, ObjectExpression, createObjectExpression, - createObjectProperty + createObjectProperty, + TemplateChildNode, + CallExpression } from '../ast' import { createCompilerError, ErrorCodes } from '../errors' import { getInnerRange, findProp } from '../utils' @@ -120,12 +122,23 @@ export const transformFor = createStructuralDirectiveTransform( ) ]) } + let childBlockChildren: TemplateChildNode[] | CallExpression = + node.children + if (childBlockChildren.length === 1) { + const child = childBlockChildren[0] + if ( + child.type === NodeTypes.ELEMENT && + child.tagType === ElementTypes.SLOT + ) { + childBlockChildren = child.codegenNode! + } + } childBlock = createSequenceExpression([ createCallExpression(helper(OPEN_BLOCK)), createCallExpression(helper(CREATE_BLOCK), [ helper(FRAGMENT), childBlockProps, - node.children + childBlockChildren ]) ]) } else { diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts index b3c6371b..40466270 100644 --- a/packages/compiler-core/src/transforms/vIf.ts +++ b/packages/compiler-core/src/transforms/vIf.ts @@ -164,19 +164,30 @@ function createChildrenCodegenNode( const { children } = branch const child = children[0] const needFragmentWrapper = - children.length > 1 || child.type !== NodeTypes.ELEMENT + children.length !== 1 || + child.type !== NodeTypes.ELEMENT || + child.tagType === ElementTypes.SLOT if (needFragmentWrapper) { const blockArgs: CallExpression['arguments'] = [ helper(FRAGMENT), keyExp, children ] - // optimize away nested fragments when child is a ForNode - if (children.length === 1 && child.type === NodeTypes.FOR) { - const forBlockExp = child.codegenNode - // directly use the for block's children and patchFlag - blockArgs[2] = forBlockExp.arguments[2] - blockArgs[3] = forBlockExp.arguments[3] + if (children.length === 1) { + // optimize away nested fragments when child is a ForNode + if (child.type === NodeTypes.FOR) { + const forBlockArgs = child.codegenNode.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 + ) { + // + // since slot always returns array, use it directly as the fragment children. + blockArgs[2] = child.codegenNode! + } } return createCallExpression(helper(CREATE_BLOCK), blockArgs) } else {