Просмотр исходного кода

feat(compiler-vapor): implement basic usage of `v-slot` (#203)

Co-authored-by: Doctorwu <doctorwu@moego.pet>
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
Rizumu Ayaka 2 лет назад
Родитель
Сommit
0c33ace61c

+ 10 - 9
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap

@@ -29,15 +29,16 @@ const t0 = _template("<div></div>")
 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
 }"

+ 19 - 19
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
 }"
 `;

+ 6 - 6
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
 }"
 `;

+ 73 - 0
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("<div></div>")
+
+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("<span></span>")
+
+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("<div></div>")
+
+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
+}"
+`;

+ 3 - 1
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', () => {

+ 174 - 0
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(`<Comp><div/></Comp>`)
+    expect(code).toMatchSnapshot()
+
+    expect(ir.template).toEqual(['<div></div>'])
+    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(
+      `<Comp>
+        <template #one>foo</template>bar<span/>
+      </Comp>`,
+    )
+    expect(code).toMatchSnapshot()
+
+    expect(ir.template).toEqual(['foo', 'bar', '<span></span>'])
+    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(
+      `<Foo>
+        <template #one><Bar><div/></Bar></template>
+      </Foo>`,
+    )
+    expect(code).toMatchSnapshot()
+  })
+
+  test('dynamic slots name', () => {
+    const { ir, code } = compileWithSlots(
+      `<Comp>
+        <template #[name]>foo</template>
+      </Comp>`,
+    )
+    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 = `<Comp><template #default>foo</template>bar</Comp>`
+      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 = `<Comp><template #foo></template><template #foo></template></Comp>`
+      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,
+          },
+        },
+      })
+    })
+  })
+})

+ 2 - 0
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,
     ],

+ 38 - 4
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]
+}

+ 1 - 0
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'

+ 13 - 1
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<string, ComponentSlotBlockIRNode>
+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

+ 11 - 2
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<T extends AllNode = AllNode> {
 
   comment: CommentNode[] = []
   component: Set<string> = 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<T extends AllNode = AllNode> {
   }
 
   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<T extends AllNode = AllNode> {
       this.template = template
       this.dynamic = dynamic
       this.childrenTemplate = childrenTemplate
+      this.slots = slots
+      this.dynamicSlots = dynamicSlots
       isVFor && this.inVFor--
     }
   }

+ 3 - 1
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
 

+ 4 - 0
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) {

+ 120 - 0
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<ElementNode>,
+    )
+
+    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<ElementNode>,
+    )
+
+    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<ElementNode>,
+): [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)
+}

+ 8 - 0
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