diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index 1e5e4cf4f..9e401d0ba 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -29,15 +29,16 @@ const t0 = _template("
") export function render(_ctx) { const _component_Bar = _resolveComponent("Bar") const _component_Comp = _resolveComponent("Comp") - const n0 = _createIf(() => (true), () => { - const n3 = t0() - const n2 = _createComponent(_component_Bar) - _withDirectives(n2, [[_resolveDirective("vHello"), void 0, void 0, { world: true }]]) - _insert(n2, n3) - return n3 - }) - _insert(n0, n4) - const n4 = _createComponent(_component_Comp, null, true) + const n4 = _createComponent(_component_Comp, null, { default: () => { + const n0 = _createIf(() => (true), () => { + const n3 = t0() + const n2 = _createComponent(_component_Bar) + _withDirectives(n2, [[_resolveDirective("vHello"), void 0, void 0, { world: true }]]) + _insert(n2, n3) + return n3 + }) + return n0 + } }, null, true) _withDirectives(n4, [[_resolveDirective("vTest")]]) return n4 }" diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap index 75195b027..2d3bc2f23 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap @@ -5,7 +5,7 @@ exports[`compiler: element transform > component > do not resolve component from export function render(_ctx) { const _component_Example = _resolveComponent("Example") - const n0 = _createComponent(_component_Example, null, true) + const n0 = _createComponent(_component_Example, null, null, null, true) return n0 }" `; @@ -25,7 +25,7 @@ exports[`compiler: element transform > component > generate single root componen "import { createComponent as _createComponent } from 'vue/vapor'; export function render(_ctx) { - const n0 = _createComponent(_ctx.Comp, null, true) + const n0 = _createComponent(_ctx.Comp, null, null, null, true) return n0 }" `; @@ -35,21 +35,21 @@ exports[`compiler: element transform > component > import + resolve component 1` export function render(_ctx) { const _component_Foo = _resolveComponent("Foo") - const n0 = _createComponent(_component_Foo, null, true) + const n0 = _createComponent(_component_Foo, null, null, null, true) return n0 }" `; exports[`compiler: element transform > component > resolve component from setup bindings (inline const) 1`] = ` "(() => { - const n0 = _createComponent(Example, null, true) + const n0 = _createComponent(Example, null, null, null, true) return n0 })()" `; exports[`compiler: element transform > component > resolve component from setup bindings (inline) 1`] = ` "(() => { - const n0 = _createComponent(_unref(Example), null, true) + const n0 = _createComponent(_unref(Example), null, null, null, true) return n0 })()" `; @@ -58,14 +58,14 @@ exports[`compiler: element transform > component > resolve component from setup "import { createComponent as _createComponent } from 'vue/vapor'; export function render(_ctx) { - const n0 = _createComponent(_ctx.Example, null, true) + const n0 = _createComponent(_ctx.Example, null, null, null, true) return n0 }" `; exports[`compiler: element transform > component > resolve namespaced component from props bindings (inline) 1`] = ` "(() => { - const n0 = _createComponent(Foo.Example, null, true) + const n0 = _createComponent(Foo.Example, null, null, null, true) return n0 })()" `; @@ -74,14 +74,14 @@ exports[`compiler: element transform > component > resolve namespaced component "import { createComponent as _createComponent } from 'vue/vapor'; export function render(_ctx) { - const n0 = _createComponent(_ctx.Foo.Example, null, true) + const n0 = _createComponent(_ctx.Foo.Example, null, null, null, true) return n0 }" `; exports[`compiler: element transform > component > resolve namespaced component from setup bindings (inline const) 1`] = ` "(() => { - const n0 = _createComponent(Foo.Example, null, true) + const n0 = _createComponent(Foo.Example, null, null, null, true) return n0 })()" `; @@ -90,7 +90,7 @@ exports[`compiler: element transform > component > resolve namespaced component "import { createComponent as _createComponent } from 'vue/vapor'; export function render(_ctx) { - const n0 = _createComponent(_ctx.Foo.Example, null, true) + const n0 = _createComponent(_ctx.Foo.Example, null, null, null, true) return n0 }" `; @@ -102,7 +102,7 @@ export function render(_ctx) { const _component_Foo = _resolveComponent("Foo") const n0 = _createComponent(_component_Foo, [ { onBar: () => $event => (_ctx.handleBar($event)) } - ], true) + ], null, null, true) return n0 }" `; @@ -117,7 +117,7 @@ export function render(_ctx) { id: () => ("foo"), class: () => ("bar") } - ], true) + ], null, null, true) return n0 }" `; @@ -129,7 +129,7 @@ export function render(_ctx) { const _component_Foo = _resolveComponent("Foo") const n0 = _createComponent(_component_Foo, [ () => (_ctx.obj) - ], true) + ], null, null, true) return n0 }" `; @@ -142,7 +142,7 @@ export function render(_ctx) { const n0 = _createComponent(_component_Foo, [ { id: () => ("foo") }, () => (_ctx.obj) - ], true) + ], null, null, true) return n0 }" `; @@ -155,7 +155,7 @@ export function render(_ctx) { const n0 = _createComponent(_component_Foo, [ () => (_ctx.obj), { id: () => ("foo") } - ], true) + ], null, null, true) return n0 }" `; @@ -169,7 +169,7 @@ export function render(_ctx) { { id: () => ("foo") }, () => (_ctx.obj), { class: () => ("bar") } - ], true) + ], null, null, true) return n0 }" `; @@ -181,7 +181,7 @@ export function render(_ctx) { const _component_Foo = _resolveComponent("Foo") const n0 = _createComponent(_component_Foo, [ () => (_toHandlers(_ctx.obj)) - ], true) + ], null, null, true) return n0 }" `; @@ -195,7 +195,7 @@ export function render(_ctx) { const n0 = _createComponent(_component_Foo, [ () => ({ [_toHandlerKey(_ctx.foo-_ctx.bar)]: () => _ctx.bar }), () => ({ [_toHandlerKey(_ctx.baz)]: () => _ctx.qux }) - ], true) + ], null, null, true) return n0 }" `; @@ -208,7 +208,7 @@ export function render(_ctx) { const n0 = _createComponent(_component_Foo, [ () => ({ [_ctx.foo-_ctx.bar]: _ctx.bar }), () => ({ [_ctx.baz]: _ctx.qux }) - ], true) + ], null, null, true) return n0 }" `; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap index 5f44f54cf..62e0ece59 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap @@ -9,7 +9,7 @@ export function render(_ctx) { { modelValue: () => (_ctx.foo), "onUpdate:modelValue": () => $event => (_ctx.foo = $event), modelModifiers: () => ({ trim: true, "bar-baz": true }) } - ], true) + ], null, null, true) return n0 }" `; @@ -22,7 +22,7 @@ export function render(_ctx) { const n0 = _createComponent(_component_Comp, [ { modelValue: () => (_ctx.foo), "onUpdate:modelValue": () => $event => (_ctx.foo = $event) } - ], true) + ], null, null, true) return n0 }" `; @@ -41,7 +41,7 @@ export function render(_ctx) { "onUpdate:bar": () => $event => (_ctx.bar = $event), barModifiers: () => ({ number: true }) } - ], true) + ], null, null, true) return n0 }" `; @@ -54,7 +54,7 @@ export function render(_ctx) { const n0 = _createComponent(_component_Comp, [ { bar: () => (_ctx.foo), "onUpdate:bar": () => $event => (_ctx.foo = $event) } - ], true) + ], null, null, true) return n0 }" `; @@ -71,7 +71,7 @@ export function render(_ctx) { () => ({ [_ctx.bar]: _ctx.bar, ["onUpdate:" + _ctx.bar]: () => $event => (_ctx.bar = $event), [_ctx.bar + "Modifiers"]: () => ({ number: true }) }) - ], true) + ], null, null, true) return n0 }" `; @@ -84,7 +84,7 @@ export function render(_ctx) { const n0 = _createComponent(_component_Comp, [ () => ({ [_ctx.arg]: _ctx.foo, ["onUpdate:" + _ctx.arg]: () => $event => (_ctx.foo = $event) }) - ], true) + ], null, null, true) return n0 }" `; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap new file mode 100644 index 000000000..ff692bbf6 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap @@ -0,0 +1,73 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`compiler: transform slot > dynamic slots name 1`] = ` +"import { resolveComponent as _resolveComponent, createComponent as _createComponent, template as _template } from 'vue/vapor'; +const t0 = _template("foo") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n2 = _createComponent(_component_Comp, null, null, () => [{ + name: _ctx.name, + fn: () => { + const n0 = t0() + return n0 + } + }], true) + return n2 +}" +`; + +exports[`compiler: transform slot > implicit default slot 1`] = ` +"import { resolveComponent as _resolveComponent, createComponent as _createComponent, template as _template } from 'vue/vapor'; +const t0 = _template("
") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n1 = _createComponent(_component_Comp, null, { default: () => { + const n0 = t0() + return n0 + } }, null, true) + return n1 +}" +`; + +exports[`compiler: transform slot > named slots w/ implicit default slot 1`] = ` +"import { resolveComponent as _resolveComponent, createComponent as _createComponent, template as _template } from 'vue/vapor'; +const t0 = _template("foo") +const t1 = _template("bar") +const t2 = _template("") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n4 = _createComponent(_component_Comp, null, { + one: () => { + const n0 = t0() + return n0 + }, + default: () => { + const n2 = t1() + const n3 = t2() + return [n2, n3] + } + }, null, true) + return n4 +}" +`; + +exports[`compiler: transform slot > nested slots 1`] = ` +"import { resolveComponent as _resolveComponent, createComponent as _createComponent, template as _template } from 'vue/vapor'; +const t0 = _template("
") + +export function render(_ctx) { + const _component_Bar = _resolveComponent("Bar") + const _component_Foo = _resolveComponent("Foo") + const n3 = _createComponent(_component_Foo, null, { one: () => { + const n1 = _createComponent(_component_Bar, null, { default: () => { + const n0 = t0() + return n0 + } }) + return n1 + } }, null, true) + return n3 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts index 2f5be196e..248241246 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts @@ -182,7 +182,9 @@ describe('compiler: element transform', () => { bindingMetadata: { Comp: BindingTypes.SETUP_CONST }, }) expect(code).toMatchSnapshot() - expect(code).contains('_createComponent(_ctx.Comp, null, true)') + expect(code).contains( + '_createComponent(_ctx.Comp, null, null, null, true)', + ) }) test('generate multi root component', () => { diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts new file mode 100644 index 000000000..c98b75538 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -0,0 +1,174 @@ +import { ErrorCodes, NodeTypes } from '@vue/compiler-core' +import { + IRNodeTypes, + transformChildren, + transformElement, + transformSlotOutlet, + transformText, + transformVBind, + transformVFor, + transformVIf, + transformVOn, + transformVSlot, +} from '../../src' +import { makeCompile } from './_utils' + +const compileWithSlots = makeCompile({ + nodeTransforms: [ + transformText, + transformVIf, + transformVFor, + transformSlotOutlet, + transformElement, + transformVSlot, + transformChildren, + ], + directiveTransforms: { + bind: transformVBind, + on: transformVOn, + }, +}) + +describe('compiler: transform slot', () => { + test('implicit default slot', () => { + const { ir, code } = compileWithSlots(`
`) + expect(code).toMatchSnapshot() + + expect(ir.template).toEqual(['
']) + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.CREATE_COMPONENT_NODE, + id: 1, + tag: 'Comp', + props: [[]], + slots: { + default: { + type: IRNodeTypes.BLOCK, + dynamic: { + children: [{ template: 0 }], + }, + }, + }, + }, + ]) + expect(ir.block.returns).toEqual([1]) + expect(ir.block.dynamic).toMatchObject({ + children: [{ id: 1 }], + }) + }) + + test('named slots w/ implicit default slot', () => { + const { ir, code } = compileWithSlots( + ` + bar + `, + ) + expect(code).toMatchSnapshot() + + expect(ir.template).toEqual(['foo', 'bar', '']) + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.CREATE_COMPONENT_NODE, + id: 4, + tag: 'Comp', + props: [[]], + slots: { + one: { + type: IRNodeTypes.BLOCK, + dynamic: { + children: [{ template: 0 }], + }, + }, + default: { + type: IRNodeTypes.BLOCK, + dynamic: { + children: [{}, { template: 1 }, { template: 2 }], + }, + }, + }, + }, + ]) + }) + + test('nested slots', () => { + const { code } = compileWithSlots( + ` + + `, + ) + expect(code).toMatchSnapshot() + }) + + test('dynamic slots name', () => { + const { ir, code } = compileWithSlots( + ` + + `, + ) + expect(code).toMatchSnapshot() + expect(ir.block.operation[0].type).toBe(IRNodeTypes.CREATE_COMPONENT_NODE) + expect(ir.block.operation).toMatchObject([ + { + type: IRNodeTypes.CREATE_COMPONENT_NODE, + tag: 'Comp', + slots: undefined, + dynamicSlots: [ + { + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'name', + isStatic: false, + }, + fn: { type: IRNodeTypes.BLOCK }, + }, + ], + }, + ]) + }) + + describe('errors', () => { + test('error on extraneous children w/ named default slot', () => { + const onError = vi.fn() + const source = `bar` + compileWithSlots(source, { onError }) + const index = source.indexOf('bar') + expect(onError.mock.calls[0][0]).toMatchObject({ + code: ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN, + loc: { + start: { + offset: index, + line: 1, + column: index + 1, + }, + end: { + offset: index + 3, + line: 1, + column: index + 4, + }, + }, + }) + }) + + test('error on duplicated slot names', () => { + const onError = vi.fn() + const source = `` + compileWithSlots(source, { onError }) + const index = source.lastIndexOf('#foo') + expect(onError.mock.calls[0][0]).toMatchObject({ + code: ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES, + loc: { + start: { + offset: index, + line: 1, + column: index + 1, + }, + end: { + offset: index + 4, + line: 1, + column: index + 5, + }, + }, + }) + }) + }) +}) diff --git a/packages/compiler-vapor/src/compile.ts b/packages/compiler-vapor/src/compile.ts index 25790d65c..25a27f23d 100644 --- a/packages/compiler-vapor/src/compile.ts +++ b/packages/compiler-vapor/src/compile.ts @@ -28,6 +28,7 @@ import { transformVIf } from './transforms/vIf' import { transformVFor } from './transforms/vFor' import { transformComment } from './transforms/transformComment' import { transformSlotOutlet } from './transforms/transformSlotOutlet' +import { transformVSlot } from './transforms/vSlot' import type { HackOptions } from './ir' export { wrapTemplate } from './transforms/utils' @@ -108,6 +109,7 @@ export function getBaseTransformPreset( transformTemplateRef, transformText, transformElement, + transformVSlot, transformComment, transformChildren, ], diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 3256e514c..5b91a8446 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -1,6 +1,8 @@ import { camelize, extend, isArray } from '@vue/shared' import type { CodegenContext } from '../generate' import { + type ComponentDynamicSlot, + type ComponentSlots, type CreateComponentIRNode, IRDynamicPropsKind, type IRProp, @@ -10,6 +12,7 @@ import { import { type CodeFragment, NEWLINE, + SEGMENTS_ARRAY, SEGMENTS_ARRAY_NEWLINE, SEGMENTS_OBJECT, SEGMENTS_OBJECT_NEWLINE, @@ -22,8 +25,8 @@ import { createSimpleExpression } from '@vue/compiler-dom' import { genEventHandler } from './event' import { genDirectiveModifiers, genDirectivesForElement } from './directive' import { genModelHandler } from './modelValue' +import { genBlock } from './block' -// TODO: generate component slots export function genCreateComponent( oper: CreateComponentIRNode, context: CodegenContext, @@ -31,7 +34,7 @@ export function genCreateComponent( const { vaporHelper } = context const tag = genTag() - const isRoot = oper.root + const { root, slots, dynamicSlots } = oper const rawProps = genRawProps(oper.props, context) return [ @@ -40,8 +43,14 @@ export function genCreateComponent( ...genCall( vaporHelper('createComponent'), tag, - rawProps || (isRoot ? 'null' : false), - isRoot && 'true', + rawProps || (slots || dynamicSlots || root ? 'null' : false), + slots ? genSlots(slots, context) : dynamicSlots || root ? 'null' : false, + dynamicSlots + ? genDynamicSlots(dynamicSlots, context) + : root + ? 'null' + : false, + root && 'true', ), ...genDirectivesForElement(oper.id, context), ] @@ -134,3 +143,28 @@ function genModelModifiers( const modifiersVal = genDirectiveModifiers(modelModifiers) return [',', NEWLINE, ...modifiersKey, `: () => ({ ${modifiersVal} })`] } + +function genSlots(slots: ComponentSlots, context: CodegenContext) { + const slotList = Object.entries(slots) + return genMulti( + slotList.length > 1 ? SEGMENTS_OBJECT_NEWLINE : SEGMENTS_OBJECT, + ...slotList.map(([name, slot]) => [name, ': ', ...genBlock(slot, context)]), + ) +} + +function genDynamicSlots( + dynamicSlots: ComponentDynamicSlot[], + context: CodegenContext, +) { + const slotsExpr = genMulti( + dynamicSlots.length > 1 ? SEGMENTS_ARRAY_NEWLINE : SEGMENTS_ARRAY, + ...dynamicSlots.map(({ name, fn }) => + genMulti( + SEGMENTS_OBJECT_NEWLINE, + ['name: ', ...genExpression(name, context)], + ['fn: ', ...genBlock(fn, context)], + ), + ), + ) + return ['() => ', ...slotsExpr] +} diff --git a/packages/compiler-vapor/src/index.ts b/packages/compiler-vapor/src/index.ts index e222fadee..6ee58bc54 100644 --- a/packages/compiler-vapor/src/index.ts +++ b/packages/compiler-vapor/src/index.ts @@ -49,3 +49,4 @@ export { transformVFor } from './transforms/vFor' export { transformVModel } from './transforms/vModel' export { transformComment } from './transforms/transformComment' export { transformSlotOutlet } from './transforms/transformSlotOutlet' +export { transformVSlot } from './transforms/vSlot' diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index bba2f3cfc..23e3114ec 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -199,12 +199,24 @@ export interface WithDirectiveIRNode extends BaseIRNode { builtin?: VaporHelper } +export interface ComponentSlotBlockIRNode extends BlockIRNode { + // TODO slot props +} +export type ComponentSlots = Record +export interface ComponentDynamicSlot { + name: SimpleExpressionNode + fn: ComponentSlotBlockIRNode + key?: string +} + export interface CreateComponentIRNode extends BaseIRNode { type: IRNodeTypes.CREATE_COMPONENT_NODE id: number tag: string props: IRProps[] - // TODO slots + + slots?: ComponentSlots + dynamicSlots?: ComponentDynamicSlot[] resolve: boolean root: boolean diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 4399fbf90..893efce1f 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -16,6 +16,8 @@ import { import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared' import { type BlockIRNode, + type ComponentDynamicSlot, + type ComponentSlots, DynamicFlag, type HackOptions, type IRDynamicInfo, @@ -77,11 +79,13 @@ export class TransformContext { comment: CommentNode[] = [] component: Set = this.ir.component + slots?: ComponentSlots + dynamicSlots?: ComponentDynamicSlot[] private globalId = 0 constructor( - private ir: RootIRNode, + public ir: RootIRNode, public node: T, options: TransformOptions = {}, ) { @@ -90,11 +94,14 @@ export class TransformContext { } enterBlock(ir: BlockIRNode, isVFor: boolean = false): () => void { - const { block, template, dynamic, childrenTemplate } = this + const { block, template, dynamic, childrenTemplate, slots, dynamicSlots } = + this this.block = ir this.dynamic = ir.dynamic this.template = '' this.childrenTemplate = [] + this.slots = undefined + this.dynamicSlots = undefined isVFor && this.inVFor++ return () => { // exit @@ -103,6 +110,8 @@ export class TransformContext { this.template = template this.dynamic = dynamic this.childrenTemplate = childrenTemplate + this.slots = slots + this.dynamicSlots = dynamicSlots isVFor && this.inVFor-- } } diff --git a/packages/compiler-vapor/src/transforms/transformChildren.ts b/packages/compiler-vapor/src/transforms/transformChildren.ts index da007fc1b..68e63efea 100644 --- a/packages/compiler-vapor/src/transforms/transformChildren.ts +++ b/packages/compiler-vapor/src/transforms/transformChildren.ts @@ -15,7 +15,9 @@ import { DynamicFlag, type IRDynamicInfo, IRNodeTypes } from '../ir' export const transformChildren: NodeTransform = (node, context) => { const isFragment = node.type === NodeTypes.ROOT || - (node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE) + (node.type === NodeTypes.ELEMENT && + (node.tagType === ElementTypes.TEMPLATE || + node.tagType === ElementTypes.COMPONENT)) if (!isFragment && node.type !== NodeTypes.ELEMENT) return diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index e45428af4..c16ddd2e7 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -104,7 +104,11 @@ function transformComponentElement( props: propsResult[0] ? propsResult[1] : [propsResult[1]], resolve, root, + slots: context.slots, + dynamicSlots: context.dynamicSlots, }) + context.slots = undefined + context.dynamicSlots = undefined } function resolveSetupReference(name: string, context: TransformContext) { diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts new file mode 100644 index 000000000..8d4554d9e --- /dev/null +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -0,0 +1,120 @@ +import { + type ElementNode, + ElementTypes, + ErrorCodes, + NodeTypes, + type TemplateChildNode, + createCompilerError, + isTemplateNode, + isVSlot, +} from '@vue/compiler-core' +import type { NodeTransform, TransformContext } from '../transform' +import { newBlock } from './utils' +import { type BlockIRNode, DynamicFlag, type VaporDirectiveNode } from '../ir' +import { findDir, resolveExpression } from '../utils' + +// TODO dynamic slots +export const transformVSlot: NodeTransform = (node, context) => { + if (node.type !== NodeTypes.ELEMENT) return + + let dir: VaporDirectiveNode | undefined + const { tagType, children } = node + const { parent } = context + + const isDefaultSlot = tagType === ElementTypes.COMPONENT && children.length + const isSlotTemplate = + isTemplateNode(node) && + parent && + parent.node.type === NodeTypes.ELEMENT && + parent.node.tagType === ElementTypes.COMPONENT + + if (isDefaultSlot) { + const defaultChildren = children.filter( + n => + isNonWhitespaceContent(node) && + !(n.type === NodeTypes.ELEMENT && n.props.some(isVSlot)), + ) + + const [block, onExit] = createSlotBlock( + node, + context as TransformContext, + ) + + const slots = (context.slots ||= {}) + const dynamicSlots = (context.dynamicSlots ||= []) + + return () => { + onExit() + + if (defaultChildren.length) { + if (slots.default) { + context.options.onError( + createCompilerError( + ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN, + defaultChildren[0].loc, + ), + ) + } else { + slots.default = block + } + context.slots = slots + } else if (Object.keys(slots).length) { + context.slots = slots + } + + if (dynamicSlots.length) context.dynamicSlots = dynamicSlots + } + } else if (isSlotTemplate && (dir = findDir(node, 'slot', true))) { + let { arg } = dir + + context.dynamic.flags |= DynamicFlag.NON_TEMPLATE + + const slots = context.slots! + const dynamicSlots = context.dynamicSlots! + + const [block, onExit] = createSlotBlock( + node, + context as TransformContext, + ) + + arg &&= resolveExpression(arg) + + if (!arg || arg.isStatic) { + const slotName = arg ? arg.content : 'default' + + if (slots[slotName]) { + context.options.onError( + createCompilerError( + ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES, + dir.loc, + ), + ) + } else { + slots[slotName] = block + } + } else { + dynamicSlots.push({ + name: arg, + fn: block, + }) + } + return () => onExit() + } +} + +function createSlotBlock( + slotNode: ElementNode, + context: TransformContext, +): [BlockIRNode, () => void] { + const branch: BlockIRNode = newBlock(slotNode) + const exitBlock = context.enterBlock(branch) + return [branch, exitBlock] +} + +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) +} diff --git a/packages/compiler-vapor/src/utils.ts b/packages/compiler-vapor/src/utils.ts index 627978538..fdbd101a0 100644 --- a/packages/compiler-vapor/src/utils.ts +++ b/packages/compiler-vapor/src/utils.ts @@ -5,6 +5,7 @@ import { type ElementNode, NodeTypes, type SimpleExpressionNode, + findDir as _findDir, findProp as _findProp, createSimpleExpression, isLiteralWhitelisted, @@ -19,6 +20,13 @@ export const findProp = _findProp as ( allowEmpty?: boolean, ) => AttributeNode | VaporDirectiveNode | undefined +/** find directive */ +export const findDir = _findDir as ( + node: ElementNode, + name: string | RegExp, + allowEmpty?: boolean, +) => VaporDirectiveNode | undefined + export function propToExpression(prop: AttributeNode | VaporDirectiveNode) { return prop.type === NodeTypes.ATTRIBUTE ? prop.value