diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap index 5bb9ea6e..008fc7ba 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap @@ -209,3 +209,48 @@ return function render(_ctx, _cache) { })) }" `; + +exports[`compiler: transform component slots with whitespace: 'preserve' implicit default slot 1`] = ` +"const { createVNode: _createVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent(\\"Comp\\") + + return (_openBlock(), _createBlock(_component_Comp, null, { + header: _withCtx(() => [\\" Header \\"]), + default: _withCtx(() => [ + \\" \\", + _createVNode(\\"p\\") + ]), + _: 1 /* STABLE */ + })) +}" +`; + +exports[`compiler: transform component slots with whitespace: 'preserve' named default slot + implicit whitespace content 1`] = ` +"const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent(\\"Comp\\") + + return (_openBlock(), _createBlock(_component_Comp, null, { + header: _withCtx(() => [\\" Header \\"]), + default: _withCtx(() => [\\" Default \\"]), + _: 1 /* STABLE */ + })) +}" +`; + +exports[`compiler: transform component slots with whitespace: 'preserve' should not generate whitespace only default slot 1`] = ` +"const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue + +return function render(_ctx, _cache) { + const _component_Comp = _resolveComponent(\\"Comp\\") + + return (_openBlock(), _createBlock(_component_Comp, null, { + header: _withCtx(() => [\\" Header \\"]), + footer: _withCtx(() => [\\" Footer \\"]), + _: 1 /* STABLE */ + })) +}" +`; diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts index 99fc009b..beddd212 100644 --- a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts @@ -9,7 +9,9 @@ import { ForNode, ComponentNode, VNodeCall, - SlotsExpression + SlotsExpression, + ObjectExpression, + SimpleExpressionNode } from '../../src' import { transformElement } from '../../src/transforms/transformElement' import { transformOn } from '../../src/transforms/vOn' @@ -27,7 +29,9 @@ import { transformFor } from '../../src/transforms/vFor' import { transformIf } from '../../src/transforms/vIf' function parseWithSlots(template: string, options: CompilerOptions = {}) { - const ast = parse(template) + const ast = parse(template, { + whitespace: options.whitespace + }) transform(ast, { nodeTransforms: [ transformIf, @@ -862,4 +866,64 @@ describe('compiler: transform component slots', () => { }) }) }) + + describe(`with whitespace: 'preserve'`, () => { + test('named default slot + implicit whitespace content', () => { + const source = ` + + + + + ` + const { root } = parseWithSlots(source, { + whitespace: 'preserve' + }) + + expect( + `Extraneous children found when component already has explicitly named default slot.` + ).not.toHaveBeenWarned() + expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() + }) + + test('implicit default slot', () => { + const source = ` + + +

+ + ` + const { root } = parseWithSlots(source, { + whitespace: 'preserve' + }) + + expect( + `Extraneous children found when component already has explicitly named default slot.` + ).not.toHaveBeenWarned() + expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() + }) + + test('should not generate whitespace only default slot', () => { + const source = ` + + + + + ` + const { root } = parseWithSlots(source, { + whitespace: 'preserve' + }) + + // slots is vnodeCall's children as an ObjectExpression + const slots = (root as any).children[0].codegenNode.children + .properties as ObjectExpression['properties'] + + // should be: header, footer, _ (no default) + expect(slots.length).toBe(3) + expect( + slots.some(p => (p.key as SimpleExpressionNode).content === 'default') + ).toBe(false) + + expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot() + }) + }) }) diff --git a/packages/compiler-core/src/transforms/vSlot.ts b/packages/compiler-core/src/transforms/vSlot.ts index 4c249d23..327bac44 100644 --- a/packages/compiler-core/src/transforms/vSlot.ts +++ b/packages/compiler-core/src/transforms/vSlot.ts @@ -311,7 +311,13 @@ export function buildSlots( if (!hasTemplateSlots) { // implicit default slot (on component) slotsProperties.push(buildDefaultSlotProperty(undefined, children)) - } else if (implicitDefaultChildren.length) { + } else if ( + implicitDefaultChildren.length && + // #3766 + // with whitespace: 'preserve', whitespaces between slots will end up in + // implicitDefaultChildren. Ignore if all implicit children are whitespaces. + implicitDefaultChildren.some(node => isNonWhitespaceContent(node)) + ) { // implicit default slot (mixed with named slots) if (hasNamedDefaultSlot) { context.onError( @@ -397,3 +403,11 @@ function hasForwardedSlots(children: TemplateChildNode[]): boolean { } return false } + +function isNonWhitespaceContent(node: TemplateChildNode): boolean { + if (node.type !== NodeTypes.TEXT && node.type !== NodeTypes.TEXT_CALL) + return true + return node.type === NodeTypes.TEXT + ? !!node.content.trim() + : isNonWhitespaceContent(node.content) +}