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>
This commit is contained in:
Rizumu Ayaka 2024-05-12 17:57:00 +08:00 committed by GitHub
parent 1c54cae29a
commit 0c33ace61c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 485 additions and 43 deletions

View File

@ -29,15 +29,16 @@ const t0 = _template("<div></div>")
export function render(_ctx) { export function render(_ctx) {
const _component_Bar = _resolveComponent("Bar") const _component_Bar = _resolveComponent("Bar")
const _component_Comp = _resolveComponent("Comp") const _component_Comp = _resolveComponent("Comp")
const n0 = _createIf(() => (true), () => { const n4 = _createComponent(_component_Comp, null, { default: () => {
const n3 = t0() const n0 = _createIf(() => (true), () => {
const n2 = _createComponent(_component_Bar) const n3 = t0()
_withDirectives(n2, [[_resolveDirective("vHello"), void 0, void 0, { world: true }]]) const n2 = _createComponent(_component_Bar)
_insert(n2, n3) _withDirectives(n2, [[_resolveDirective("vHello"), void 0, void 0, { world: true }]])
return n3 _insert(n2, n3)
}) return n3
_insert(n0, n4) })
const n4 = _createComponent(_component_Comp, null, true) return n0
} }, null, true)
_withDirectives(n4, [[_resolveDirective("vTest")]]) _withDirectives(n4, [[_resolveDirective("vTest")]])
return n4 return n4
}" }"

View File

@ -5,7 +5,7 @@ exports[`compiler: element transform > component > do not resolve component from
export function render(_ctx) { export function render(_ctx) {
const _component_Example = _resolveComponent("Example") const _component_Example = _resolveComponent("Example")
const n0 = _createComponent(_component_Example, null, true) const n0 = _createComponent(_component_Example, null, null, null, true)
return n0 return n0
}" }"
`; `;
@ -25,7 +25,7 @@ exports[`compiler: element transform > component > generate single root componen
"import { createComponent as _createComponent } from 'vue/vapor'; "import { createComponent as _createComponent } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const n0 = _createComponent(_ctx.Comp, null, true) const n0 = _createComponent(_ctx.Comp, null, null, null, true)
return n0 return n0
}" }"
`; `;
@ -35,21 +35,21 @@ exports[`compiler: element transform > component > import + resolve component 1`
export function render(_ctx) { export function render(_ctx) {
const _component_Foo = _resolveComponent("Foo") const _component_Foo = _resolveComponent("Foo")
const n0 = _createComponent(_component_Foo, null, true) const n0 = _createComponent(_component_Foo, null, null, null, true)
return n0 return n0
}" }"
`; `;
exports[`compiler: element transform > component > resolve component from setup bindings (inline const) 1`] = ` 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 return n0
})()" })()"
`; `;
exports[`compiler: element transform > component > resolve component from setup bindings (inline) 1`] = ` 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 return n0
})()" })()"
`; `;
@ -58,14 +58,14 @@ exports[`compiler: element transform > component > resolve component from setup
"import { createComponent as _createComponent } from 'vue/vapor'; "import { createComponent as _createComponent } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const n0 = _createComponent(_ctx.Example, null, true) const n0 = _createComponent(_ctx.Example, null, null, null, true)
return n0 return n0
}" }"
`; `;
exports[`compiler: element transform > component > resolve namespaced component from props bindings (inline) 1`] = ` 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 return n0
})()" })()"
`; `;
@ -74,14 +74,14 @@ exports[`compiler: element transform > component > resolve namespaced component
"import { createComponent as _createComponent } from 'vue/vapor'; "import { createComponent as _createComponent } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const n0 = _createComponent(_ctx.Foo.Example, null, true) const n0 = _createComponent(_ctx.Foo.Example, null, null, null, true)
return n0 return n0
}" }"
`; `;
exports[`compiler: element transform > component > resolve namespaced component from setup bindings (inline const) 1`] = ` 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 return n0
})()" })()"
`; `;
@ -90,7 +90,7 @@ exports[`compiler: element transform > component > resolve namespaced component
"import { createComponent as _createComponent } from 'vue/vapor'; "import { createComponent as _createComponent } from 'vue/vapor';
export function render(_ctx) { export function render(_ctx) {
const n0 = _createComponent(_ctx.Foo.Example, null, true) const n0 = _createComponent(_ctx.Foo.Example, null, null, null, true)
return n0 return n0
}" }"
`; `;
@ -102,7 +102,7 @@ export function render(_ctx) {
const _component_Foo = _resolveComponent("Foo") const _component_Foo = _resolveComponent("Foo")
const n0 = _createComponent(_component_Foo, [ const n0 = _createComponent(_component_Foo, [
{ onBar: () => $event => (_ctx.handleBar($event)) } { onBar: () => $event => (_ctx.handleBar($event)) }
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;
@ -117,7 +117,7 @@ export function render(_ctx) {
id: () => ("foo"), id: () => ("foo"),
class: () => ("bar") class: () => ("bar")
} }
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;
@ -129,7 +129,7 @@ export function render(_ctx) {
const _component_Foo = _resolveComponent("Foo") const _component_Foo = _resolveComponent("Foo")
const n0 = _createComponent(_component_Foo, [ const n0 = _createComponent(_component_Foo, [
() => (_ctx.obj) () => (_ctx.obj)
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;
@ -142,7 +142,7 @@ export function render(_ctx) {
const n0 = _createComponent(_component_Foo, [ const n0 = _createComponent(_component_Foo, [
{ id: () => ("foo") }, { id: () => ("foo") },
() => (_ctx.obj) () => (_ctx.obj)
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;
@ -155,7 +155,7 @@ export function render(_ctx) {
const n0 = _createComponent(_component_Foo, [ const n0 = _createComponent(_component_Foo, [
() => (_ctx.obj), () => (_ctx.obj),
{ id: () => ("foo") } { id: () => ("foo") }
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;
@ -169,7 +169,7 @@ export function render(_ctx) {
{ id: () => ("foo") }, { id: () => ("foo") },
() => (_ctx.obj), () => (_ctx.obj),
{ class: () => ("bar") } { class: () => ("bar") }
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;
@ -181,7 +181,7 @@ export function render(_ctx) {
const _component_Foo = _resolveComponent("Foo") const _component_Foo = _resolveComponent("Foo")
const n0 = _createComponent(_component_Foo, [ const n0 = _createComponent(_component_Foo, [
() => (_toHandlers(_ctx.obj)) () => (_toHandlers(_ctx.obj))
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;
@ -195,7 +195,7 @@ export function render(_ctx) {
const n0 = _createComponent(_component_Foo, [ const n0 = _createComponent(_component_Foo, [
() => ({ [_toHandlerKey(_ctx.foo-_ctx.bar)]: () => _ctx.bar }), () => ({ [_toHandlerKey(_ctx.foo-_ctx.bar)]: () => _ctx.bar }),
() => ({ [_toHandlerKey(_ctx.baz)]: () => _ctx.qux }) () => ({ [_toHandlerKey(_ctx.baz)]: () => _ctx.qux })
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;
@ -208,7 +208,7 @@ export function render(_ctx) {
const n0 = _createComponent(_component_Foo, [ const n0 = _createComponent(_component_Foo, [
() => ({ [_ctx.foo-_ctx.bar]: _ctx.bar }), () => ({ [_ctx.foo-_ctx.bar]: _ctx.bar }),
() => ({ [_ctx.baz]: _ctx.qux }) () => ({ [_ctx.baz]: _ctx.qux })
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;

View File

@ -9,7 +9,7 @@ export function render(_ctx) {
{ modelValue: () => (_ctx.foo), { modelValue: () => (_ctx.foo),
"onUpdate:modelValue": () => $event => (_ctx.foo = $event), "onUpdate:modelValue": () => $event => (_ctx.foo = $event),
modelModifiers: () => ({ trim: true, "bar-baz": true }) } modelModifiers: () => ({ trim: true, "bar-baz": true }) }
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;
@ -22,7 +22,7 @@ export function render(_ctx) {
const n0 = _createComponent(_component_Comp, [ const n0 = _createComponent(_component_Comp, [
{ modelValue: () => (_ctx.foo), { modelValue: () => (_ctx.foo),
"onUpdate:modelValue": () => $event => (_ctx.foo = $event) } "onUpdate:modelValue": () => $event => (_ctx.foo = $event) }
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;
@ -41,7 +41,7 @@ export function render(_ctx) {
"onUpdate:bar": () => $event => (_ctx.bar = $event), "onUpdate:bar": () => $event => (_ctx.bar = $event),
barModifiers: () => ({ number: true }) barModifiers: () => ({ number: true })
} }
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;
@ -54,7 +54,7 @@ export function render(_ctx) {
const n0 = _createComponent(_component_Comp, [ const n0 = _createComponent(_component_Comp, [
{ bar: () => (_ctx.foo), { bar: () => (_ctx.foo),
"onUpdate:bar": () => $event => (_ctx.foo = $event) } "onUpdate:bar": () => $event => (_ctx.foo = $event) }
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;
@ -71,7 +71,7 @@ export function render(_ctx) {
() => ({ [_ctx.bar]: _ctx.bar, () => ({ [_ctx.bar]: _ctx.bar,
["onUpdate:" + _ctx.bar]: () => $event => (_ctx.bar = $event), ["onUpdate:" + _ctx.bar]: () => $event => (_ctx.bar = $event),
[_ctx.bar + "Modifiers"]: () => ({ number: true }) }) [_ctx.bar + "Modifiers"]: () => ({ number: true }) })
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;
@ -84,7 +84,7 @@ export function render(_ctx) {
const n0 = _createComponent(_component_Comp, [ const n0 = _createComponent(_component_Comp, [
() => ({ [_ctx.arg]: _ctx.foo, () => ({ [_ctx.arg]: _ctx.foo,
["onUpdate:" + _ctx.arg]: () => $event => (_ctx.foo = $event) }) ["onUpdate:" + _ctx.arg]: () => $event => (_ctx.foo = $event) })
], true) ], null, null, true)
return n0 return n0
}" }"
`; `;

View File

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

View File

@ -182,7 +182,9 @@ describe('compiler: element transform', () => {
bindingMetadata: { Comp: BindingTypes.SETUP_CONST }, bindingMetadata: { Comp: BindingTypes.SETUP_CONST },
}) })
expect(code).toMatchSnapshot() 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', () => { test('generate multi root component', () => {

View File

@ -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,
},
},
})
})
})
})

View File

@ -28,6 +28,7 @@ import { transformVIf } from './transforms/vIf'
import { transformVFor } from './transforms/vFor' import { transformVFor } from './transforms/vFor'
import { transformComment } from './transforms/transformComment' import { transformComment } from './transforms/transformComment'
import { transformSlotOutlet } from './transforms/transformSlotOutlet' import { transformSlotOutlet } from './transforms/transformSlotOutlet'
import { transformVSlot } from './transforms/vSlot'
import type { HackOptions } from './ir' import type { HackOptions } from './ir'
export { wrapTemplate } from './transforms/utils' export { wrapTemplate } from './transforms/utils'
@ -108,6 +109,7 @@ export function getBaseTransformPreset(
transformTemplateRef, transformTemplateRef,
transformText, transformText,
transformElement, transformElement,
transformVSlot,
transformComment, transformComment,
transformChildren, transformChildren,
], ],

View File

@ -1,6 +1,8 @@
import { camelize, extend, isArray } from '@vue/shared' import { camelize, extend, isArray } from '@vue/shared'
import type { CodegenContext } from '../generate' import type { CodegenContext } from '../generate'
import { import {
type ComponentDynamicSlot,
type ComponentSlots,
type CreateComponentIRNode, type CreateComponentIRNode,
IRDynamicPropsKind, IRDynamicPropsKind,
type IRProp, type IRProp,
@ -10,6 +12,7 @@ import {
import { import {
type CodeFragment, type CodeFragment,
NEWLINE, NEWLINE,
SEGMENTS_ARRAY,
SEGMENTS_ARRAY_NEWLINE, SEGMENTS_ARRAY_NEWLINE,
SEGMENTS_OBJECT, SEGMENTS_OBJECT,
SEGMENTS_OBJECT_NEWLINE, SEGMENTS_OBJECT_NEWLINE,
@ -22,8 +25,8 @@ import { createSimpleExpression } from '@vue/compiler-dom'
import { genEventHandler } from './event' import { genEventHandler } from './event'
import { genDirectiveModifiers, genDirectivesForElement } from './directive' import { genDirectiveModifiers, genDirectivesForElement } from './directive'
import { genModelHandler } from './modelValue' import { genModelHandler } from './modelValue'
import { genBlock } from './block'
// TODO: generate component slots
export function genCreateComponent( export function genCreateComponent(
oper: CreateComponentIRNode, oper: CreateComponentIRNode,
context: CodegenContext, context: CodegenContext,
@ -31,7 +34,7 @@ export function genCreateComponent(
const { vaporHelper } = context const { vaporHelper } = context
const tag = genTag() const tag = genTag()
const isRoot = oper.root const { root, slots, dynamicSlots } = oper
const rawProps = genRawProps(oper.props, context) const rawProps = genRawProps(oper.props, context)
return [ return [
@ -40,8 +43,14 @@ export function genCreateComponent(
...genCall( ...genCall(
vaporHelper('createComponent'), vaporHelper('createComponent'),
tag, tag,
rawProps || (isRoot ? 'null' : false), rawProps || (slots || dynamicSlots || root ? 'null' : false),
isRoot && 'true', slots ? genSlots(slots, context) : dynamicSlots || root ? 'null' : false,
dynamicSlots
? genDynamicSlots(dynamicSlots, context)
: root
? 'null'
: false,
root && 'true',
), ),
...genDirectivesForElement(oper.id, context), ...genDirectivesForElement(oper.id, context),
] ]
@ -134,3 +143,28 @@ function genModelModifiers(
const modifiersVal = genDirectiveModifiers(modelModifiers) const modifiersVal = genDirectiveModifiers(modelModifiers)
return [',', NEWLINE, ...modifiersKey, `: () => ({ ${modifiersVal} })`] 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]
}

View File

@ -49,3 +49,4 @@ export { transformVFor } from './transforms/vFor'
export { transformVModel } from './transforms/vModel' export { transformVModel } from './transforms/vModel'
export { transformComment } from './transforms/transformComment' export { transformComment } from './transforms/transformComment'
export { transformSlotOutlet } from './transforms/transformSlotOutlet' export { transformSlotOutlet } from './transforms/transformSlotOutlet'
export { transformVSlot } from './transforms/vSlot'

View File

@ -199,12 +199,24 @@ export interface WithDirectiveIRNode extends BaseIRNode {
builtin?: VaporHelper 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 { export interface CreateComponentIRNode extends BaseIRNode {
type: IRNodeTypes.CREATE_COMPONENT_NODE type: IRNodeTypes.CREATE_COMPONENT_NODE
id: number id: number
tag: string tag: string
props: IRProps[] props: IRProps[]
// TODO slots
slots?: ComponentSlots
dynamicSlots?: ComponentDynamicSlot[]
resolve: boolean resolve: boolean
root: boolean root: boolean

View File

@ -16,6 +16,8 @@ import {
import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared' import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared'
import { import {
type BlockIRNode, type BlockIRNode,
type ComponentDynamicSlot,
type ComponentSlots,
DynamicFlag, DynamicFlag,
type HackOptions, type HackOptions,
type IRDynamicInfo, type IRDynamicInfo,
@ -77,11 +79,13 @@ export class TransformContext<T extends AllNode = AllNode> {
comment: CommentNode[] = [] comment: CommentNode[] = []
component: Set<string> = this.ir.component component: Set<string> = this.ir.component
slots?: ComponentSlots
dynamicSlots?: ComponentDynamicSlot[]
private globalId = 0 private globalId = 0
constructor( constructor(
private ir: RootIRNode, public ir: RootIRNode,
public node: T, public node: T,
options: TransformOptions = {}, options: TransformOptions = {},
) { ) {
@ -90,11 +94,14 @@ export class TransformContext<T extends AllNode = AllNode> {
} }
enterBlock(ir: BlockIRNode, isVFor: boolean = false): () => void { 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.block = ir
this.dynamic = ir.dynamic this.dynamic = ir.dynamic
this.template = '' this.template = ''
this.childrenTemplate = [] this.childrenTemplate = []
this.slots = undefined
this.dynamicSlots = undefined
isVFor && this.inVFor++ isVFor && this.inVFor++
return () => { return () => {
// exit // exit
@ -103,6 +110,8 @@ export class TransformContext<T extends AllNode = AllNode> {
this.template = template this.template = template
this.dynamic = dynamic this.dynamic = dynamic
this.childrenTemplate = childrenTemplate this.childrenTemplate = childrenTemplate
this.slots = slots
this.dynamicSlots = dynamicSlots
isVFor && this.inVFor-- isVFor && this.inVFor--
} }
} }

View File

@ -15,7 +15,9 @@ import { DynamicFlag, type IRDynamicInfo, IRNodeTypes } from '../ir'
export const transformChildren: NodeTransform = (node, context) => { export const transformChildren: NodeTransform = (node, context) => {
const isFragment = const isFragment =
node.type === NodeTypes.ROOT || 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 if (!isFragment && node.type !== NodeTypes.ELEMENT) return

View File

@ -104,7 +104,11 @@ function transformComponentElement(
props: propsResult[0] ? propsResult[1] : [propsResult[1]], props: propsResult[0] ? propsResult[1] : [propsResult[1]],
resolve, resolve,
root, root,
slots: context.slots,
dynamicSlots: context.dynamicSlots,
}) })
context.slots = undefined
context.dynamicSlots = undefined
} }
function resolveSetupReference(name: string, context: TransformContext) { function resolveSetupReference(name: string, context: TransformContext) {

View File

@ -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)
}

View File

@ -5,6 +5,7 @@ import {
type ElementNode, type ElementNode,
NodeTypes, NodeTypes,
type SimpleExpressionNode, type SimpleExpressionNode,
findDir as _findDir,
findProp as _findProp, findProp as _findProp,
createSimpleExpression, createSimpleExpression,
isLiteralWhitelisted, isLiteralWhitelisted,
@ -19,6 +20,13 @@ export const findProp = _findProp as (
allowEmpty?: boolean, allowEmpty?: boolean,
) => AttributeNode | VaporDirectiveNode | undefined ) => 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) { export function propToExpression(prop: AttributeNode | VaporDirectiveNode) {
return prop.type === NodeTypes.ATTRIBUTE return prop.type === NodeTypes.ATTRIBUTE
? prop.value ? prop.value