From b51c735b45cc73d70ad472b3de9a74dce451a255 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Wed, 8 May 2024 01:52:12 +0800 Subject: [PATCH 01/16] feat(compiler-vapor): static v-slot --- .../__snapshots__/compile.spec.ts.snap | 19 +++--- .../transformElement.spec.ts.snap | 38 +++++------ .../__snapshots__/vModel.spec.ts.snap | 12 ++-- .../__snapshots__/vSlot.spec.ts.snap | 56 ++++++++++++++++ .../transforms/transformElement.spec.ts | 4 +- .../__tests__/transforms/vSlot.spec.ts | 53 +++++++++++++++ packages/compiler-vapor/src/compile.ts | 2 + .../src/generators/component.ts | 16 ++++- packages/compiler-vapor/src/index.ts | 1 + packages/compiler-vapor/src/ir.ts | 11 ++- packages/compiler-vapor/src/transform.ts | 6 +- .../src/transforms/transformChildren.ts | 7 +- .../src/transforms/transformElement.ts | 2 + .../compiler-vapor/src/transforms/vSlot.ts | 67 +++++++++++++++++++ packages/compiler-vapor/src/utils.ts | 8 +++ 15 files changed, 261 insertions(+), 41 deletions(-) create mode 100644 packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap create mode 100644 packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts create mode 100644 packages/compiler-vapor/src/transforms/vSlot.ts 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..1bc370f03 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap @@ -0,0 +1,56 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +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..d9eb9d7bf --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -0,0 +1,53 @@ +import { + 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 { code } = compileWithSlots(`
`) + expect(code).toMatchSnapshot() + }) + + test('named slots w/ implicit default slot', () => { + const { code } = compileWithSlots( + ` + bar + `, + ) + expect(code).toMatchSnapshot() + }) + + test('nested slots', () => { + const { code } = compileWithSlots( + ` + + `, + ) + expect(code).toMatchSnapshot() + }) +}) diff --git a/packages/compiler-vapor/src/compile.ts b/packages/compiler-vapor/src/compile.ts index 25790d65c..7aa9659b1 100644 --- a/packages/compiler-vapor/src/compile.ts +++ b/packages/compiler-vapor/src/compile.ts @@ -29,6 +29,7 @@ import { transformVFor } from './transforms/vFor' import { transformComment } from './transforms/transformComment' import { transformSlotOutlet } from './transforms/transformSlotOutlet' import type { HackOptions } from './ir' +import { transformVSlot } from './transforms/vSlot' 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..08e707363 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -22,8 +22,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, @@ -40,7 +40,9 @@ export function genCreateComponent( ...genCall( vaporHelper('createComponent'), tag, - rawProps || (isRoot ? 'null' : false), + rawProps || (oper.slots || isRoot ? 'null' : false), + oper.slots ? genSlots(oper, context) : isRoot ? 'null' : false, + isRoot && 'null', isRoot && 'true', ), ...genDirectivesForElement(oper.id, context), @@ -134,3 +136,13 @@ function genModelModifiers( const modifiersVal = genDirectiveModifiers(modelModifiers) return [',', NEWLINE, ...modifiersKey, `: () => ({ ${modifiersVal} })`] } + +function genSlots(oper: CreateComponentIRNode, context: CodegenContext) { + const slotList = Object.entries(oper.slots!) + return genMulti( + slotList.length > 1 ? SEGMENTS_OBJECT_NEWLINE : SEGMENTS_OBJECT, + ...slotList.map(([name, slot]) => { + return [name, ': ', ...genBlock(slot, context)] + }), + ) +} 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..3d8cd3601 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -199,12 +199,21 @@ export interface WithDirectiveIRNode extends BaseIRNode { builtin?: VaporHelper } +export interface ComponentSlotBlockIRNode extends BlockIRNode {} +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?: Record + dynamicSlots?: ComponentDynamicSlot[] resolve: boolean root: boolean diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 4399fbf90..48e8214fc 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -16,6 +16,7 @@ import { import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared' import { type BlockIRNode, + type ComponentSlotBlockIRNode, DynamicFlag, type HackOptions, type IRDynamicInfo, @@ -77,6 +78,7 @@ export class TransformContext { comment: CommentNode[] = [] component: Set = this.ir.component + slots: Record | null = null private globalId = 0 @@ -90,11 +92,12 @@ export class TransformContext { } enterBlock(ir: BlockIRNode, isVFor: boolean = false): () => void { - const { block, template, dynamic, childrenTemplate } = this + const { block, template, dynamic, childrenTemplate, slots } = this this.block = ir this.dynamic = ir.dynamic this.template = '' this.childrenTemplate = [] + this.slots = null isVFor && this.inVFor++ return () => { // exit @@ -103,6 +106,7 @@ export class TransformContext { this.template = template this.dynamic = dynamic this.childrenTemplate = childrenTemplate + this.slots = slots isVFor && this.inVFor-- } } diff --git a/packages/compiler-vapor/src/transforms/transformChildren.ts b/packages/compiler-vapor/src/transforms/transformChildren.ts index da007fc1b..18a1835df 100644 --- a/packages/compiler-vapor/src/transforms/transformChildren.ts +++ b/packages/compiler-vapor/src/transforms/transformChildren.ts @@ -13,6 +13,9 @@ import { import { DynamicFlag, type IRDynamicInfo, IRNodeTypes } from '../ir' export const transformChildren: NodeTransform = (node, context) => { + const isComponent = + node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.COMPONENT + const isFragment = node.type === NodeTypes.ROOT || (node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE) @@ -27,7 +30,7 @@ export const transformChildren: NodeTransform = (node, context) => { ) transformNode(childContext) - if (isFragment) { + if (isFragment || isComponent) { childContext.reference() childContext.registerTemplate() @@ -44,7 +47,7 @@ export const transformChildren: NodeTransform = (node, context) => { context.dynamic.children[i] = childContext.dynamic } - if (!isFragment) { + if (!(isFragment || isComponent)) { processDynamicChildren(context as TransformContext) } } diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index e45428af4..1dbc45982 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -104,7 +104,9 @@ function transformComponentElement( props: propsResult[0] ? propsResult[1] : [propsResult[1]], resolve, root, + slots: context.slots || undefined, }) + context.slots = null } 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..006d3d37a --- /dev/null +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -0,0 +1,67 @@ +import { + type ElementNode, + ElementTypes, + NodeTypes, + 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 } from '../utils' + +// TODO dynamic slots +export const transformVSlot: NodeTransform = (node, context) => { + if (node.type !== NodeTypes.ELEMENT) return + + const { tagType, children } = node + const { parent } = context + let dir: VaporDirectiveNode | undefined + + 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 hasDefalutSlot = children.some( + n => !(n.type === NodeTypes.ELEMENT && n.props.some(isVSlot)), + ) + + const [block, onExit] = createSlotBlock( + node, + context as TransformContext, + ) + + const slots = (context.slots ||= {}) + + return () => { + onExit() + if (hasDefalutSlot) slots.default = block + if (Object.keys(slots).length) context.slots = slots + } + } else if (isSlotTemplate && (dir = findDir(node, 'slot', true))) { + context.dynamic.flags |= DynamicFlag.NON_TEMPLATE + + const slots = context.slots! + + const [block, onExit] = createSlotBlock( + node, + context as TransformContext, + ) + + slots[dir.arg!.content] = block + return () => onExit() + } +} + +function createSlotBlock( + slotNode: ElementNode, + context: TransformContext, +): [BlockIRNode, () => void] { + const branch: BlockIRNode = newBlock(slotNode) + const exitBlock = context.enterBlock(branch) + return [branch, exitBlock] +} 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 From 0c0a131bddec71b154ac88d5981c7bb328027acf Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Wed, 8 May 2024 02:04:47 +0800 Subject: [PATCH 02/16] test: improve the test --- .../__tests__/transforms/vSlot.spec.ts | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts index d9eb9d7bf..330872b74 100644 --- a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -1,4 +1,5 @@ import { + IRNodeTypes, transformChildren, transformElement, transformSlotOutlet, @@ -29,17 +30,63 @@ const compileWithSlots = makeCompile({ describe('compiler: transform slot', () => { test('implicit default slot', () => { - const { code } = compileWithSlots(`
`) + 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 { code } = compileWithSlots( + 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', () => { From 8ed97a39b33f65b58356bbede297d059c3832410 Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Wed, 8 May 2024 16:47:20 +0800 Subject: [PATCH 03/16] feat(compiler-vapor): impl dynamic slot name --- .../__snapshots__/vSlot.spec.ts.snap | 14 +++++ .../__tests__/transforms/vSlot.spec.ts | 60 +++++++------------ .../src/generators/component.ts | 12 +++- packages/compiler-vapor/src/ir.ts | 7 ++- packages/compiler-vapor/src/transform.ts | 4 +- .../compiler-vapor/src/transforms/vSlot.ts | 16 +++-- 6 files changed, 65 insertions(+), 48 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap index 1bc370f03..d96e9eb97 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap @@ -1,5 +1,19 @@ // 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, { [_ctx.dynamicName]: () => { + const n0 = t0() + return n0 + } }, null, 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("
") diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts index 330872b74..2db3e28ec 100644 --- a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -1,4 +1,6 @@ +import { createSimpleExpression } from '@vue/compiler-dom' import { + type CreateComponentIRNode, IRNodeTypes, transformChildren, transformElement, @@ -34,22 +36,10 @@ describe('compiler: transform slot', () => { 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.operation[0].type).toBe(IRNodeTypes.CREATE_COMPONENT_NODE) + const slots = (ir.block.operation[0] as CreateComponentIRNode).slots! + expect(slots.length).toBe(1) + expect(slots[0].name.content).toBe('default') expect(ir.block.returns).toEqual([1]) expect(ir.block.dynamic).toMatchObject({ children: [{ id: 1 }], @@ -65,28 +55,11 @@ describe('compiler: transform slot', () => { 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 }], - }, - }, - }, - }, - ]) + expect(ir.block.operation[0].type).toBe(IRNodeTypes.CREATE_COMPONENT_NODE) + const slots = (ir.block.operation[0] as CreateComponentIRNode).slots! + expect(slots.length).toBe(2) + expect(slots[0].name.content).toBe('one') + expect(slots[1].name.content).toBe('default') }) test('nested slots', () => { @@ -97,4 +70,15 @@ describe('compiler: transform slot', () => { ) expect(code).toMatchSnapshot() }) + + test('dynamic slots name', () => { + const { ir, code } = compileWithSlots(` + + `) + expect(ir.block.operation[0].type).toBe(IRNodeTypes.CREATE_COMPONENT_NODE) + const slots = (ir.block.operation[0] as CreateComponentIRNode).slots! + expect(slots.length).toBe(1) + expect(slots[0].name.isStatic).toBe(false) + expect(code).toMatchSnapshot() + }) }) diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 08e707363..05afb5438 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -138,11 +138,17 @@ function genModelModifiers( } function genSlots(oper: CreateComponentIRNode, context: CodegenContext) { - const slotList = Object.entries(oper.slots!) + const slotList = oper.slots! return genMulti( slotList.length > 1 ? SEGMENTS_OBJECT_NEWLINE : SEGMENTS_OBJECT, - ...slotList.map(([name, slot]) => { - return [name, ': ', ...genBlock(slot, context)] + ...slotList.map(({ name, block }) => { + return [ + ...(name.isStatic + ? [name.content] + : ['[', ...genExpression(name, context), ']']), + ': ', + ...genBlock(block, context), + ] }), ) } diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 3d8cd3601..8d985c0e3 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -199,6 +199,11 @@ export interface WithDirectiveIRNode extends BaseIRNode { builtin?: VaporHelper } +export interface ComponentStaticSlot { + name: SimpleExpressionNode + block: ComponentSlotBlockIRNode +} + export interface ComponentSlotBlockIRNode extends BlockIRNode {} export interface ComponentDynamicSlot { name: SimpleExpressionNode @@ -212,7 +217,7 @@ export interface CreateComponentIRNode extends BaseIRNode { tag: string props: IRProps[] - slots?: Record + slots?: ComponentStaticSlot[] dynamicSlots?: ComponentDynamicSlot[] resolve: boolean diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 48e8214fc..1eceb8e4d 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -16,7 +16,7 @@ import { import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared' import { type BlockIRNode, - type ComponentSlotBlockIRNode, + type ComponentStaticSlot, DynamicFlag, type HackOptions, type IRDynamicInfo, @@ -78,7 +78,7 @@ export class TransformContext { comment: CommentNode[] = [] component: Set = this.ir.component - slots: Record | null = null + slots: ComponentStaticSlot[] | null = null private globalId = 0 diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index 006d3d37a..9ea33a8d3 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -2,6 +2,7 @@ import { type ElementNode, ElementTypes, NodeTypes, + createSimpleExpression, isTemplateNode, isVSlot, } from '@vue/compiler-core' @@ -35,12 +36,16 @@ export const transformVSlot: NodeTransform = (node, context) => { context as TransformContext, ) - const slots = (context.slots ||= {}) + const slots = (context.slots ||= []) return () => { onExit() - if (hasDefalutSlot) slots.default = block - if (Object.keys(slots).length) context.slots = slots + if (hasDefalutSlot) + slots.push({ + name: createSimpleExpression('default', true), + block, + }) + if (slots.length) context.slots = slots } } else if (isSlotTemplate && (dir = findDir(node, 'slot', true))) { context.dynamic.flags |= DynamicFlag.NON_TEMPLATE @@ -52,7 +57,10 @@ export const transformVSlot: NodeTransform = (node, context) => { context as TransformContext, ) - slots[dir.arg!.content] = block + slots.push({ + name: dir.arg!, + block, + }) return () => onExit() } } From 4683a64b0c9d4292fdb934ee09e5ba80b4af097e Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Wed, 8 May 2024 22:36:18 +0800 Subject: [PATCH 04/16] refactor(compiler-vapor): impl dynamic slot name + add error detection --- .../__snapshots__/vSlot.spec.ts.snap | 11 +- .../__tests__/transforms/vSlot.spec.ts | 126 +++++++++++++++--- .../src/generators/component.ts | 50 +++++-- packages/compiler-vapor/src/ir.ts | 10 +- packages/compiler-vapor/src/transform.ts | 11 +- .../src/transforms/transformElement.ts | 2 + .../compiler-vapor/src/transforms/vSlot.ts | 77 ++++++++--- 7 files changed, 226 insertions(+), 61 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap index d96e9eb97..ff692bbf6 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap @@ -6,10 +6,13 @@ const t0 = _template("foo") export function render(_ctx) { const _component_Comp = _resolveComponent("Comp") - const n2 = _createComponent(_component_Comp, null, { [_ctx.dynamicName]: () => { - const n0 = t0() - return n0 - } }, null, true) + const n2 = _createComponent(_component_Comp, null, null, () => [{ + name: _ctx.name, + fn: () => { + const n0 = t0() + return n0 + } + }], true) return n2 }" `; diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts index 2db3e28ec..c98b75538 100644 --- a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -1,6 +1,5 @@ -import { createSimpleExpression } from '@vue/compiler-dom' +import { ErrorCodes, NodeTypes } from '@vue/compiler-core' import { - type CreateComponentIRNode, IRNodeTypes, transformChildren, transformElement, @@ -36,10 +35,22 @@ describe('compiler: transform slot', () => { expect(code).toMatchSnapshot() expect(ir.template).toEqual(['
']) - expect(ir.block.operation[0].type).toBe(IRNodeTypes.CREATE_COMPONENT_NODE) - const slots = (ir.block.operation[0] as CreateComponentIRNode).slots! - expect(slots.length).toBe(1) - expect(slots[0].name.content).toBe('default') + 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 }], @@ -55,11 +66,28 @@ describe('compiler: transform slot', () => { expect(code).toMatchSnapshot() expect(ir.template).toEqual(['foo', 'bar', '']) - expect(ir.block.operation[0].type).toBe(IRNodeTypes.CREATE_COMPONENT_NODE) - const slots = (ir.block.operation[0] as CreateComponentIRNode).slots! - expect(slots.length).toBe(2) - expect(slots[0].name.content).toBe('one') - expect(slots[1].name.content).toBe('default') + 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', () => { @@ -72,13 +100,75 @@ describe('compiler: transform slot', () => { }) test('dynamic slots name', () => { - const { ir, code } = compileWithSlots(` - - `) - expect(ir.block.operation[0].type).toBe(IRNodeTypes.CREATE_COMPONENT_NODE) - const slots = (ir.block.operation[0] as CreateComponentIRNode).slots! - expect(slots.length).toBe(1) - expect(slots[0].name.isStatic).toBe(false) + 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/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 05afb5438..8be5f67b3 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, @@ -31,7 +34,7 @@ export function genCreateComponent( const { vaporHelper } = context const tag = genTag() - const isRoot = oper.root + const { root: isRoot, slots, dynamicSlots } = oper const rawProps = genRawProps(oper.props, context) return [ @@ -40,9 +43,17 @@ export function genCreateComponent( ...genCall( vaporHelper('createComponent'), tag, - rawProps || (oper.slots || isRoot ? 'null' : false), - oper.slots ? genSlots(oper, context) : isRoot ? 'null' : false, - isRoot && 'null', + rawProps || (slots || dynamicSlots || isRoot ? 'null' : false), + slots + ? genSlots(slots, context) + : dynamicSlots || isRoot + ? 'null' + : false, + dynamicSlots + ? genDynamicSlots(dynamicSlots, context) + : isRoot + ? 'null' + : false, isRoot && 'true', ), ...genDirectivesForElement(oper.id, context), @@ -137,18 +148,29 @@ function genModelModifiers( return [',', NEWLINE, ...modifiersKey, `: () => ({ ${modifiersVal} })`] } -function genSlots(oper: CreateComponentIRNode, context: CodegenContext) { - const slotList = oper.slots! +function genSlots(slots: ComponentSlots, context: CodegenContext) { + const slotList = Object.entries(slots!) return genMulti( slotList.length > 1 ? SEGMENTS_OBJECT_NEWLINE : SEGMENTS_OBJECT, - ...slotList.map(({ name, block }) => { - return [ - ...(name.isStatic - ? [name.content] - : ['[', ...genExpression(name, context), ']']), - ': ', - ...genBlock(block, context), - ] + ...slotList.map(([name, slot]) => { + return [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 }) => { + return genMulti( + SEGMENTS_OBJECT_NEWLINE, + ['name: ', ...genExpression(name, context)], + ['fn: ', ...genBlock(fn, context)], + ) + }), + ) + return ['() => ', ...slotsExpr] +} diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 8d985c0e3..23e3114ec 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -199,12 +199,10 @@ export interface WithDirectiveIRNode extends BaseIRNode { builtin?: VaporHelper } -export interface ComponentStaticSlot { - name: SimpleExpressionNode - block: ComponentSlotBlockIRNode +export interface ComponentSlotBlockIRNode extends BlockIRNode { + // TODO slot props } - -export interface ComponentSlotBlockIRNode extends BlockIRNode {} +export type ComponentSlots = Record export interface ComponentDynamicSlot { name: SimpleExpressionNode fn: ComponentSlotBlockIRNode @@ -217,7 +215,7 @@ export interface CreateComponentIRNode extends BaseIRNode { tag: string props: IRProps[] - slots?: ComponentStaticSlot[] + slots?: ComponentSlots dynamicSlots?: ComponentDynamicSlot[] resolve: boolean diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 1eceb8e4d..363db3774 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -16,7 +16,8 @@ import { import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared' import { type BlockIRNode, - type ComponentStaticSlot, + type ComponentDynamicSlot, + type ComponentSlots, DynamicFlag, type HackOptions, type IRDynamicInfo, @@ -78,7 +79,8 @@ export class TransformContext { comment: CommentNode[] = [] component: Set = this.ir.component - slots: ComponentStaticSlot[] | null = null + slots: ComponentSlots | null = null + dynamicSlots: ComponentDynamicSlot[] | null = null private globalId = 0 @@ -92,12 +94,14 @@ export class TransformContext { } enterBlock(ir: BlockIRNode, isVFor: boolean = false): () => void { - const { block, template, dynamic, childrenTemplate, slots } = this + const { block, template, dynamic, childrenTemplate, slots, dynamicSlots } = + this this.block = ir this.dynamic = ir.dynamic this.template = '' this.childrenTemplate = [] this.slots = null + this.dynamicSlots = null isVFor && this.inVFor++ return () => { // exit @@ -107,6 +111,7 @@ export class TransformContext { this.dynamic = dynamic this.childrenTemplate = childrenTemplate this.slots = slots + this.dynamicSlots = dynamicSlots isVFor && this.inVFor-- } } diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 1dbc45982..d2d3f07d9 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -105,8 +105,10 @@ function transformComponentElement( resolve, root, slots: context.slots || undefined, + dynamicSlots: context.dynamicSlots || undefined, }) context.slots = null + context.dynamicSlots = null } function resolveSetupReference(name: string, context: TransformContext) { diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index 9ea33a8d3..8d4554d9e 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -1,23 +1,25 @@ import { type ElementNode, ElementTypes, + ErrorCodes, NodeTypes, - createSimpleExpression, + 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 } from '../utils' +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 - let dir: VaporDirectiveNode | undefined const isDefaultSlot = tagType === ElementTypes.COMPONENT && children.length const isSlotTemplate = @@ -27,8 +29,10 @@ export const transformVSlot: NodeTransform = (node, context) => { parent.node.tagType === ElementTypes.COMPONENT if (isDefaultSlot) { - const hasDefalutSlot = children.some( - n => !(n.type === NodeTypes.ELEMENT && n.props.some(isVSlot)), + const defaultChildren = children.filter( + n => + isNonWhitespaceContent(node) && + !(n.type === NodeTypes.ELEMENT && n.props.some(isVSlot)), ) const [block, onExit] = createSlotBlock( @@ -36,31 +40,64 @@ export const transformVSlot: NodeTransform = (node, context) => { context as TransformContext, ) - const slots = (context.slots ||= []) + const slots = (context.slots ||= {}) + const dynamicSlots = (context.dynamicSlots ||= []) return () => { onExit() - if (hasDefalutSlot) - slots.push({ - name: createSimpleExpression('default', true), - block, - }) - if (slots.length) context.slots = slots + + 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, ) - slots.push({ - name: dir.arg!, - block, - }) + 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() } } @@ -73,3 +110,11 @@ function createSlotBlock( 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) +} From 0852d627811bc2b6d8b1dae07b53727d5d8e8693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Fri, 10 May 2024 21:30:52 +0800 Subject: [PATCH 05/16] refactor --- packages/compiler-vapor/src/compile.ts | 2 +- .../src/generators/component.ts | 28 ++++++++----------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/compiler-vapor/src/compile.ts b/packages/compiler-vapor/src/compile.ts index 7aa9659b1..25a27f23d 100644 --- a/packages/compiler-vapor/src/compile.ts +++ b/packages/compiler-vapor/src/compile.ts @@ -28,8 +28,8 @@ import { transformVIf } from './transforms/vIf' import { transformVFor } from './transforms/vFor' import { transformComment } from './transforms/transformComment' import { transformSlotOutlet } from './transforms/transformSlotOutlet' -import type { HackOptions } from './ir' import { transformVSlot } from './transforms/vSlot' +import type { HackOptions } from './ir' export { wrapTemplate } from './transforms/utils' diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 8be5f67b3..5b91a8446 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -34,7 +34,7 @@ export function genCreateComponent( const { vaporHelper } = context const tag = genTag() - const { root: isRoot, slots, dynamicSlots } = oper + const { root, slots, dynamicSlots } = oper const rawProps = genRawProps(oper.props, context) return [ @@ -43,18 +43,14 @@ export function genCreateComponent( ...genCall( vaporHelper('createComponent'), tag, - rawProps || (slots || dynamicSlots || isRoot ? 'null' : false), - slots - ? genSlots(slots, context) - : dynamicSlots || isRoot - ? 'null' - : false, + rawProps || (slots || dynamicSlots || root ? 'null' : false), + slots ? genSlots(slots, context) : dynamicSlots || root ? 'null' : false, dynamicSlots ? genDynamicSlots(dynamicSlots, context) - : isRoot + : root ? 'null' : false, - isRoot && 'true', + root && 'true', ), ...genDirectivesForElement(oper.id, context), ] @@ -149,12 +145,10 @@ function genModelModifiers( } function genSlots(slots: ComponentSlots, context: CodegenContext) { - const slotList = Object.entries(slots!) + const slotList = Object.entries(slots) return genMulti( slotList.length > 1 ? SEGMENTS_OBJECT_NEWLINE : SEGMENTS_OBJECT, - ...slotList.map(([name, slot]) => { - return [name, ': ', ...genBlock(slot, context)] - }), + ...slotList.map(([name, slot]) => [name, ': ', ...genBlock(slot, context)]), ) } @@ -164,13 +158,13 @@ function genDynamicSlots( ) { const slotsExpr = genMulti( dynamicSlots.length > 1 ? SEGMENTS_ARRAY_NEWLINE : SEGMENTS_ARRAY, - ...dynamicSlots.map(({ name, fn }) => { - return genMulti( + ...dynamicSlots.map(({ name, fn }) => + genMulti( SEGMENTS_OBJECT_NEWLINE, ['name: ', ...genExpression(name, context)], ['fn: ', ...genBlock(fn, context)], - ) - }), + ), + ), ) return ['() => ', ...slotsExpr] } From 2e99a89eadafb203aa12d3aadb560261a853b186 Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Sat, 11 May 2024 15:40:25 +0800 Subject: [PATCH 06/16] feat(compiler-vapor, runtime-vapor): implement slots + v-for related #154 --- .../src/generators/component.ts | 57 ++++++++++++-- packages/compiler-vapor/src/ir.ts | 9 +++ .../compiler-vapor/src/transforms/vSlot.ts | 9 ++- packages/runtime-vapor/src/apiCreateFor.ts | 77 +++++++++++-------- packages/runtime-vapor/src/index.ts | 2 +- 5 files changed, 113 insertions(+), 41 deletions(-) diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 5b91a8446..89e583d46 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -21,7 +21,11 @@ import { } from './utils' import { genExpression } from './expression' import { genPropKey } from './prop' -import { createSimpleExpression } from '@vue/compiler-dom' +import { + type SimpleExpressionNode, + createForLoopParams, + createSimpleExpression, +} from '@vue/compiler-dom' import { genEventHandler } from './event' import { genDirectiveModifiers, genDirectivesForElement } from './directive' import { genModelHandler } from './modelValue' @@ -158,13 +162,50 @@ function genDynamicSlots( ) { 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)], - ), - ), + ...dynamicSlots.map(slot => { + const { name, fn, forResult } = slot + return forResult + ? genForSlot(slot, context) + : genMulti( + SEGMENTS_OBJECT_NEWLINE, + ['name: ', ...genExpression(name, context)], + ['fn: ', ...genBlock(fn, context)], + ) + }), ) return ['() => ', ...slotsExpr] } + +function genForSlot(slot: ComponentDynamicSlot, context: CodegenContext) { + const { name, fn, forResult } = slot + const { value, key, index, source } = forResult! + const rawValue = value && value.content + const rawKey = key && key.content + const rawIndex = index && index.content + + const idMap: Record = {} + if (rawValue) idMap[rawValue] = rawValue + if (rawKey) idMap[rawKey] = rawKey + if (rawIndex) idMap[rawIndex] = rawIndex + const slotExpr = genMulti( + SEGMENTS_OBJECT_NEWLINE, + ['name: ', ...context.withId(() => genExpression(name, context), idMap)], + ['fn: ', ...context.withId(() => genBlock(fn, context), idMap)], + ) + return [ + ...genCall( + context.vaporHelper('createForSlots'), + ['() => (', ...genExpression(source, context), ')'], + [ + `(`, + [value, key, index] + .filter(Boolean) + .map(exp => exp?.content) + .join(', '), + ') => (', + ...slotExpr, + ')', + ], + ), + ] +} diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 23e3114ec..08a19a21e 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -71,6 +71,14 @@ export interface IfIRNode extends BaseIRNode { negative?: BlockIRNode | IfIRNode } +export type VaporForParseResult = { + source: SimpleExpressionNode + value: SimpleExpressionNode | undefined + key: SimpleExpressionNode | undefined + index: SimpleExpressionNode | undefined + finalized: boolean +} + export interface ForIRNode extends BaseIRNode { type: IRNodeTypes.FOR id: number @@ -207,6 +215,7 @@ export interface ComponentDynamicSlot { name: SimpleExpressionNode fn: ComponentSlotBlockIRNode key?: string + forResult?: VaporForParseResult } export interface CreateComponentIRNode extends BaseIRNode { diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index 8d4554d9e..4a4a1b42b 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -10,7 +10,12 @@ import { } from '@vue/compiler-core' import type { NodeTransform, TransformContext } from '../transform' import { newBlock } from './utils' -import { type BlockIRNode, DynamicFlag, type VaporDirectiveNode } from '../ir' +import { + type BlockIRNode, + DynamicFlag, + type VaporDirectiveNode, + type VaporForParseResult, +} from '../ir' import { findDir, resolveExpression } from '../utils' // TODO dynamic slots @@ -93,9 +98,11 @@ export const transformVSlot: NodeTransform = (node, context) => { slots[slotName] = block } } else { + const vFor = findDir(node, 'for') dynamicSlots.push({ name: arg, fn: block, + forResult: vFor?.forParseResult as VaporForParseResult, }) } return () => onExit() diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 362ac762c..d65cba55d 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -5,6 +5,7 @@ import { renderEffect } from './renderEffect' import { type Block, type Fragment, fragmentKey } from './apiRender' import { warn } from './warning' import { componentKey } from './component' +import type { DynamicSlot } from './componentSlots' interface ForBlock extends Fragment { scope: EffectScope @@ -299,44 +300,58 @@ export const createFor = ( remove(nodes, parent!) scope.stop() } +} - function getLength(source: any): number { - if (isArray(source) || isString(source)) { - return source.length - } else if (typeof source === 'number') { - if (__DEV__ && !Number.isInteger(source)) { - warn(`The v-for range expect an integer value but got ${source}.`) - } - return source - } else if (isObject(source)) { - if (source[Symbol.iterator as any]) { - return Array.from(source as Iterable).length - } else { - return Object.keys(source).length - } +export const createForSlots = ( + src: () => any[] | Record | number | Set | Map, + getSlot: (item: any, key: any, index?: number) => DynamicSlot, +) => { + const source = src() + const sourceLength = getLength(source) + const slots = new Array(sourceLength) + for (let i = 0; i < sourceLength; i++) { + const [item, key, index] = getItem(source, i) + slots[i] = getSlot(item, key, index) + } + return slots +} + +function getLength(source: any): number { + if (isArray(source) || isString(source)) { + return source.length + } else if (typeof source === 'number') { + if (__DEV__ && !Number.isInteger(source)) { + warn(`The v-for range expect an integer value but got ${source}.`) + } + return source + } else if (isObject(source)) { + if (source[Symbol.iterator as any]) { + return Array.from(source as Iterable).length + } else { + return Object.keys(source).length } - return 0 } + return 0 +} - function getItem( - source: any, - idx: number, - ): [item: any, key: any, index?: number] { - if (isArray(source) || isString(source)) { +function getItem( + source: any, + idx: number, +): [item: any, key: any, index?: number] { + if (isArray(source) || isString(source)) { + return [source[idx], idx, undefined] + } else if (typeof source === 'number') { + return [idx + 1, idx, undefined] + } else if (isObject(source)) { + if (source && source[Symbol.iterator as any]) { + source = Array.from(source as Iterable) return [source[idx], idx, undefined] - } else if (typeof source === 'number') { - return [idx + 1, idx, undefined] - } else if (isObject(source)) { - if (source && source[Symbol.iterator as any]) { - source = Array.from(source as Iterable) - return [source[idx], idx, undefined] - } else { - const key = Object.keys(source)[idx] - return [source[key], key, idx] - } + } else { + const key = Object.keys(source)[idx] + return [source[key], key, idx] } - return null! } + return null! } function normalizeAnchor(node: Block): Node { diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index b5f4235c4..29c103983 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -123,7 +123,7 @@ export { type AppContext, } from './apiCreateVaporApp' export { createIf } from './apiCreateIf' -export { createFor } from './apiCreateFor' +export { createFor, createForSlots } from './apiCreateFor' export { createComponent } from './apiCreateComponent' export { resolveComponent, resolveDirective } from './helpers/resolveAssets' From fd0a7733c2bdc2f74202567db8f6748c3c7aebb1 Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Sat, 11 May 2024 15:45:17 +0800 Subject: [PATCH 07/16] feat(compiler-vapor): remove unused import --- packages/compiler-vapor/src/generators/component.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 89e583d46..5ae9a2dac 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -21,11 +21,7 @@ import { } from './utils' import { genExpression } from './expression' import { genPropKey } from './prop' -import { - type SimpleExpressionNode, - createForLoopParams, - createSimpleExpression, -} from '@vue/compiler-dom' +import { createSimpleExpression } from '@vue/compiler-dom' import { genEventHandler } from './event' import { genDirectiveModifiers, genDirectivesForElement } from './directive' import { genModelHandler } from './modelValue' From 7297caed089a8f832bda67e77b0e18e97144c6ec Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Sat, 11 May 2024 16:41:32 +0800 Subject: [PATCH 08/16] test(compiler-vapor): add test case --- .../__snapshots__/vSlot.spec.ts.snap | 17 ++++++++++ .../__tests__/transforms/vSlot.spec.ts | 33 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap index ff692bbf6..e0d1c5ec3 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap @@ -17,6 +17,23 @@ export function render(_ctx) { }" `; +exports[`compiler: transform slot > dynamic slots name w/ v-for 1`] = ` +"import { resolveComponent as _resolveComponent, createComponent as _createComponent, createForSlots as _createForSlots, 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, () => [_createForSlots(() => (_ctx.list), (item) => ({ + name: _ctx.i, + 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("
") diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts index c98b75538..2c86e6ba9 100644 --- a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -126,6 +126,39 @@ describe('compiler: transform slot', () => { ]) }) + test('dynamic slots name w/ v-for', () => { + 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: 'i', + isStatic: false, + }, + fn: { type: IRNodeTypes.BLOCK }, + forResult: { + source: { content: 'list' }, + value: { content: 'item' }, + key: undefined, + index: undefined, + }, + }, + ], + }, + ]) + }) + describe('errors', () => { test('error on extraneous children w/ named default slot', () => { const onError = vi.fn() From bc1b230d8a96720a562b374585f946dedc3dab4f Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Sun, 12 May 2024 21:57:13 +0800 Subject: [PATCH 09/16] feat(compiler-vapor, runtime-vapor): implement v-slot + v-if --- .../__snapshots__/vSlot.spec.ts.snap | 40 +++++++++- .../__tests__/transforms/vSlot.spec.ts | 39 +++++++++ .../src/generators/component.ts | 69 +++++++++++++--- packages/compiler-vapor/src/ir.ts | 32 +++++++- .../compiler-vapor/src/transforms/vSlot.ts | 79 +++++++++++++++++-- packages/runtime-vapor/src/apiCreateFor.ts | 3 +- 6 files changed, 237 insertions(+), 25 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap index e0d1c5ec3..fa75a68e8 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap @@ -23,7 +23,7 @@ const t0 = _template("foo") export function render(_ctx) { const _component_Comp = _resolveComponent("Comp") - const n2 = _createComponent(_component_Comp, null, null, () => [_createForSlots(() => (_ctx.list), (item) => ({ + const n2 = _createComponent(_component_Comp, null, null, () => [_createForSlots(_ctx.list, (item) => ({ name: _ctx.i, fn: () => { const n0 = t0() @@ -34,6 +34,44 @@ export function render(_ctx) { }" `; +exports[`compiler: transform slot > dynamic slots name w/ v-if / v-else[-if] 1`] = ` +"import { resolveComponent as _resolveComponent, createComponent as _createComponent, template as _template } from 'vue/vapor'; +const t0 = _template("condition slot") +const t1 = _template("another condition") +const t2 = _template("else condition") + +export function render(_ctx) { + const _component_Comp = _resolveComponent("Comp") + const n6 = _createComponent(_component_Comp, null, null, () => [_ctx.condition + ? { + name: "condition", + fn: () => { + const n0 = t0() + return n0 + }, + key: "0" + } + : _ctx.anotherCondition + ? { + name: "condition", + fn: () => { + const n2 = t1() + return n2 + }, + key: "1" + } + : { + name: "condition", + fn: () => { + const n4 = t2() + return n4 + }, + key: "2" + }], true) + return n6 +}" +`; + exports[`compiler: transform slot > implicit default slot 1`] = ` "import { resolveComponent as _resolveComponent, createComponent as _createComponent, template as _template } from 'vue/vapor'; const t0 = _template("
") diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts index 2c86e6ba9..5a47c08bd 100644 --- a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -1,5 +1,6 @@ import { ErrorCodes, NodeTypes } from '@vue/compiler-core' import { + DynamicSlotType, IRNodeTypes, transformChildren, transformElement, @@ -159,6 +160,44 @@ describe('compiler: transform slot', () => { ]) }) + test('dynamic slots name w/ v-if / v-else[-if]', () => { + 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: [ + { + slotType: DynamicSlotType.CONDITIONAL, + condition: { content: 'condition' }, + positive: { + slotType: DynamicSlotType.BASIC, + key: 0, + }, + negative: { + slotType: DynamicSlotType.CONDITIONAL, + condition: { content: 'anotherCondition' }, + positive: { + slotType: DynamicSlotType.BASIC, + key: 1, + }, + negative: { slotType: DynamicSlotType.BASIC, key: 2 }, + }, + }, + ], + }, + ]) + }) + describe('errors', () => { test('error on extraneous children w/ named default slot', () => { const onError = vi.fn() diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 5ae9a2dac..fab04402d 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -1,9 +1,13 @@ import { camelize, extend, isArray } from '@vue/shared' import type { CodegenContext } from '../generate' import { + type ComponentBasicDynamicSlot, + type ComponentConditionalDynamicSlot, type ComponentDynamicSlot, + type ComponentLoopDynamicSlot, type ComponentSlots, type CreateComponentIRNode, + DynamicSlotType, IRDynamicPropsKind, type IRProp, type IRProps, @@ -11,6 +15,8 @@ import { } from '../ir' import { type CodeFragment, + INDENT_END, + INDENT_START, NEWLINE, SEGMENTS_ARRAY, SEGMENTS_ARRAY_NEWLINE, @@ -158,21 +164,42 @@ function genDynamicSlots( ) { const slotsExpr = genMulti( dynamicSlots.length > 1 ? SEGMENTS_ARRAY_NEWLINE : SEGMENTS_ARRAY, - ...dynamicSlots.map(slot => { - const { name, fn, forResult } = slot - return forResult - ? genForSlot(slot, context) - : genMulti( - SEGMENTS_OBJECT_NEWLINE, - ['name: ', ...genExpression(name, context)], - ['fn: ', ...genBlock(fn, context)], - ) - }), + ...dynamicSlots.map(slot => genDynamicSlot(slot, context)), ) return ['() => ', ...slotsExpr] } -function genForSlot(slot: ComponentDynamicSlot, context: CodegenContext) { +function genDynamicSlot( + slot: ComponentDynamicSlot, + context: CodegenContext, +): CodeFragment[] { + switch (slot.slotType) { + case DynamicSlotType.BASIC: + return genBasicDynamicSlot(slot, context) + case DynamicSlotType.LOOP: + return genLoopSlot(slot, context) + case DynamicSlotType.CONDITIONAL: + return genConditionalSlot(slot, context) + } +} + +function genBasicDynamicSlot( + slot: ComponentBasicDynamicSlot, + context: CodegenContext, +): CodeFragment[] { + const { name, fn, key } = slot + return genMulti( + SEGMENTS_OBJECT_NEWLINE, + ['name: ', ...genExpression(name, context)], + ['fn: ', ...genBlock(fn, context)], + ...(key !== undefined ? [`key: "${key}"`] : []), + ) +} + +function genLoopSlot( + slot: ComponentLoopDynamicSlot, + context: CodegenContext, +): CodeFragment[] { const { name, fn, forResult } = slot const { value, key, index, source } = forResult! const rawValue = value && value.content @@ -191,7 +218,7 @@ function genForSlot(slot: ComponentDynamicSlot, context: CodegenContext) { return [ ...genCall( context.vaporHelper('createForSlots'), - ['() => (', ...genExpression(source, context), ')'], + genExpression(source, context), [ `(`, [value, key, index] @@ -205,3 +232,21 @@ function genForSlot(slot: ComponentDynamicSlot, context: CodegenContext) { ), ] } + +function genConditionalSlot( + slot: ComponentConditionalDynamicSlot, + context: CodegenContext, +): CodeFragment[] { + const { condition, positive, negative } = slot + return [ + ...genExpression(condition, context), + INDENT_START, + NEWLINE, + '? ', + ...genDynamicSlot(positive, context), + NEWLINE, + ': ', + ...(negative ? [...genDynamicSlot(negative, context)] : ['void 0']), + INDENT_END, + ] +} diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 08a19a21e..46d2e1503 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -211,13 +211,39 @@ export interface ComponentSlotBlockIRNode extends BlockIRNode { // TODO slot props } export type ComponentSlots = Record -export interface ComponentDynamicSlot { + +export enum DynamicSlotType { + BASIC, + LOOP, + CONDITIONAL, +} + +export type ComponentBasicDynamicSlot = { + slotType: DynamicSlotType.BASIC name: SimpleExpressionNode fn: ComponentSlotBlockIRNode - key?: string - forResult?: VaporForParseResult + key?: number } +export type ComponentLoopDynamicSlot = { + slotType: DynamicSlotType.LOOP + name: SimpleExpressionNode + fn: ComponentSlotBlockIRNode + forResult: VaporForParseResult +} + +export interface ComponentConditionalDynamicSlot { + slotType: DynamicSlotType.CONDITIONAL + condition: SimpleExpressionNode + positive: ComponentBasicDynamicSlot + negative?: ComponentBasicDynamicSlot | ComponentConditionalDynamicSlot +} + +export type ComponentDynamicSlot = + | ComponentBasicDynamicSlot + | ComponentLoopDynamicSlot + | ComponentConditionalDynamicSlot + export interface CreateComponentIRNode extends BaseIRNode { type: IRNodeTypes.CREATE_COMPONENT_NODE id: number diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index 4a4a1b42b..2f7620d45 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -12,7 +12,10 @@ import type { NodeTransform, TransformContext } from '../transform' import { newBlock } from './utils' import { type BlockIRNode, + type ComponentBasicDynamicSlot, + type ComponentConditionalDynamicSlot, DynamicFlag, + DynamicSlotType, type VaporDirectiveNode, type VaporForParseResult, } from '../ir' @@ -74,6 +77,9 @@ export const transformVSlot: NodeTransform = (node, context) => { context.dynamic.flags |= DynamicFlag.NON_TEMPLATE + const vFor = findDir(node, 'for') + const vIf = findDir(node, 'if') + const vElse = findDir(node, /^else(-if)?$/, true /* allowEmpty */) const slots = context.slots! const dynamicSlots = context.dynamicSlots! @@ -84,7 +90,7 @@ export const transformVSlot: NodeTransform = (node, context) => { arg &&= resolveExpression(arg) - if (!arg || arg.isStatic) { + if ((!arg || arg.isStatic) && !vFor && !vIf && !vElse) { const slotName = arg ? arg.content : 'default' if (slots[slotName]) { @@ -98,12 +104,71 @@ export const transformVSlot: NodeTransform = (node, context) => { slots[slotName] = block } } else { - const vFor = findDir(node, 'for') - dynamicSlots.push({ - name: arg, - fn: block, - forResult: vFor?.forParseResult as VaporForParseResult, - }) + if (vIf) { + dynamicSlots.push({ + slotType: DynamicSlotType.CONDITIONAL, + condition: vIf.exp!, + positive: { + slotType: DynamicSlotType.BASIC, + name: arg!, + fn: block, + key: 0, + }, + }) + } else if (vElse) { + const vIfIR = dynamicSlots[dynamicSlots.length - 1] + if (vIfIR.slotType === DynamicSlotType.CONDITIONAL) { + let ifNode = vIfIR + while (ifNode.negative?.slotType === DynamicSlotType.CONDITIONAL) + ifNode = ifNode.negative + const negative: + | ComponentBasicDynamicSlot + | ComponentConditionalDynamicSlot = vElse.exp + ? { + slotType: DynamicSlotType.CONDITIONAL, + condition: vElse.exp, + positive: { + slotType: DynamicSlotType.BASIC, + name: arg!, + fn: block, + key: ifNode.positive.key! + 1, + }, + } + : { + slotType: DynamicSlotType.BASIC, + name: arg!, + fn: block, + key: ifNode.positive.key! + 1, + } + ifNode.negative = negative + } else { + context.options.onError( + createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc), + ) + } + } else if (vFor) { + if (vFor.forParseResult) { + dynamicSlots.push({ + slotType: DynamicSlotType.LOOP, + name: arg!, + fn: block, + forResult: vFor.forParseResult as VaporForParseResult, + }) + } else { + context.options.onError( + createCompilerError( + ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, + vFor.loc, + ), + ) + } + } else { + dynamicSlots.push({ + slotType: DynamicSlotType.BASIC, + name: arg!, + fn: block, + }) + } } return () => onExit() } diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index d65cba55d..75e901a40 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -303,10 +303,9 @@ export const createFor = ( } export const createForSlots = ( - src: () => any[] | Record | number | Set | Map, + source: any[] | Record | number | Set | Map, getSlot: (item: any, key: any, index?: number) => DynamicSlot, ) => { - const source = src() const sourceLength = getLength(source) const slots = new Array(sourceLength) for (let i = 0; i < sourceLength; i++) { From 77e6755c42d332b20fcff05e03007a2bcf89abc0 Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Sun, 12 May 2024 22:07:01 +0800 Subject: [PATCH 10/16] chore(compiler-vapor): use interface instead of type --- packages/compiler-vapor/src/ir.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 221b500f0..732c0ba0a 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -219,14 +219,14 @@ export enum DynamicSlotType { CONDITIONAL, } -export type ComponentBasicDynamicSlot = { +export interface ComponentBasicDynamicSlot { slotType: DynamicSlotType.BASIC name: SimpleExpressionNode fn: ComponentSlotBlockIRNode key?: number } -export type ComponentLoopDynamicSlot = { +export interface ComponentLoopDynamicSlot { slotType: DynamicSlotType.LOOP name: SimpleExpressionNode fn: ComponentSlotBlockIRNode From c780263d69d69b17a6e64e08812ca89aca722320 Mon Sep 17 00:00:00 2001 From: Doctorwu Date: Sun, 12 May 2024 22:11:09 +0800 Subject: [PATCH 11/16] chore(compiler-vapor): simplify implement --- .../compiler-vapor/src/transforms/transformChildren.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/compiler-vapor/src/transforms/transformChildren.ts b/packages/compiler-vapor/src/transforms/transformChildren.ts index 4d65ed1d9..3c3e885d8 100644 --- a/packages/compiler-vapor/src/transforms/transformChildren.ts +++ b/packages/compiler-vapor/src/transforms/transformChildren.ts @@ -7,9 +7,6 @@ import { import { DynamicFlag, type IRDynamicInfo, IRNodeTypes } from '../ir' export const transformChildren: NodeTransform = (node, context) => { - const isComponent = - node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.COMPONENT - const isFragment = node.type === NodeTypes.ROOT || (node.type === NodeTypes.ELEMENT && @@ -22,7 +19,7 @@ export const transformChildren: NodeTransform = (node, context) => { const childContext = context.create(child, i) transformNode(childContext) - if (isFragment || isComponent) { + if (isFragment) { childContext.reference() childContext.registerTemplate() @@ -39,7 +36,7 @@ export const transformChildren: NodeTransform = (node, context) => { context.dynamic.children[i] = childContext.dynamic } - if (!(isFragment || isComponent)) { + if (!isFragment) { processDynamicChildren(context as TransformContext) } } From 2f4af7e4471a69efe92277da6b8b618c3afa850a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Sun, 19 May 2024 21:11:08 +0800 Subject: [PATCH 12/16] refactor --- .../compiler-vapor/src/generators/component.ts | 4 ++-- packages/compiler-vapor/src/ir.ts | 17 ++++++----------- packages/compiler-vapor/src/transforms/vSlot.ts | 4 ++-- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 3670be48c..1e05b03fe 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -197,8 +197,8 @@ function genLoopSlot( slot: ComponentLoopDynamicSlot, context: CodegenContext, ): CodeFragment[] { - const { name, fn, forResult } = slot - const { value, key, index, source } = forResult! + const { name, fn, loop } = slot + const { value, key, index, source } = loop const rawValue = value && value.content const rawKey = key && key.content const rawIndex = index && index.content diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 53a5c94ca..83e03126f 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -73,21 +73,16 @@ export interface IfIRNode extends BaseIRNode { once?: boolean } -export type VaporForParseResult = { +export interface IRFor { source: SimpleExpressionNode - value: SimpleExpressionNode | undefined - key: SimpleExpressionNode | undefined - index: SimpleExpressionNode | undefined - finalized: boolean + value?: SimpleExpressionNode + key?: SimpleExpressionNode + index?: SimpleExpressionNode } -export interface ForIRNode extends BaseIRNode { +export interface ForIRNode extends BaseIRNode, IRFor { type: IRNodeTypes.FOR id: number - source: SimpleExpressionNode - value?: SimpleExpressionNode - key?: SimpleExpressionNode - index?: SimpleExpressionNode keyProp?: SimpleExpressionNode render: BlockIRNode once: boolean @@ -234,7 +229,7 @@ export interface ComponentLoopDynamicSlot { slotType: DynamicSlotType.LOOP name: SimpleExpressionNode fn: ComponentSlotBlockIRNode - forResult: VaporForParseResult + loop: IRFor } export interface ComponentConditionalDynamicSlot { diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index 72abe67cd..a5f46ca28 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -16,8 +16,8 @@ import { type ComponentConditionalDynamicSlot, DynamicFlag, DynamicSlotType, + type IRFor, type VaporDirectiveNode, - type VaporForParseResult, } from '../ir' import { findDir, resolveExpression } from '../utils' @@ -152,7 +152,7 @@ export const transformVSlot: NodeTransform = (node, context) => { slotType: DynamicSlotType.LOOP, name: arg!, fn: block, - forResult: vFor.forParseResult as VaporForParseResult, + loop: vFor.forParseResult as IRFor, }) } else { context.options.onError( From e5ccc795bbc95ec2e7d5b8217a1aed493f52cfbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Sun, 19 May 2024 21:14:15 +0800 Subject: [PATCH 13/16] test: update snap --- packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts index 5a47c08bd..00db5b054 100644 --- a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -148,7 +148,7 @@ describe('compiler: transform slot', () => { isStatic: false, }, fn: { type: IRNodeTypes.BLOCK }, - forResult: { + loop: { source: { content: 'list' }, value: { content: 'item' }, key: undefined, From 7ddd1d352ecda84fc056c2add7a55548c74169e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Sun, 19 May 2024 22:04:41 +0800 Subject: [PATCH 14/16] refactor --- .../src/generators/component.ts | 2 +- .../compiler-vapor/src/transforms/vSlot.ts | 114 +++++++++--------- packages/runtime-vapor/src/apiCreateFor.ts | 6 +- packages/runtime-vapor/src/componentSlots.ts | 11 +- 4 files changed, 68 insertions(+), 65 deletions(-) diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 1e05b03fe..33a2c5a14 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -220,7 +220,7 @@ function genLoopSlot( `(`, [value, key, index] .filter(Boolean) - .map(exp => exp?.content) + .map(exp => exp!.content) .join(', '), ') => (', ...slotExpr, diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index a5f46ca28..7aeb2abc9 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -103,73 +103,75 @@ export const transformVSlot: NodeTransform = (node, context) => { } else { slots[slotName] = block } - } else { - if (vIf) { - dynamicSlots.push({ - slotType: DynamicSlotType.CONDITIONAL, - condition: vIf.exp!, - positive: { - slotType: DynamicSlotType.BASIC, - name: arg!, - fn: block, - key: 0, - }, - }) - } else if (vElse) { - const vIfIR = dynamicSlots[dynamicSlots.length - 1] - if (vIfIR.slotType === DynamicSlotType.CONDITIONAL) { - let ifNode = vIfIR - while (ifNode.negative?.slotType === DynamicSlotType.CONDITIONAL) - ifNode = ifNode.negative - const negative: - | ComponentBasicDynamicSlot - | ComponentConditionalDynamicSlot = vElse.exp - ? { - slotType: DynamicSlotType.CONDITIONAL, - condition: vElse.exp, - positive: { - slotType: DynamicSlotType.BASIC, - name: arg!, - fn: block, - key: ifNode.positive.key! + 1, - }, - } - : { + } else if (vIf) { + dynamicSlots.push({ + slotType: DynamicSlotType.CONDITIONAL, + condition: vIf.exp!, + positive: { + slotType: DynamicSlotType.BASIC, + name: arg!, + fn: block, + key: 0, + }, + }) + } else if (vElse) { + const vIfIR = dynamicSlots[dynamicSlots.length - 1] + if (vIfIR.slotType === DynamicSlotType.CONDITIONAL) { + let ifNode = vIfIR + while ( + ifNode.negative && + ifNode.negative.slotType === DynamicSlotType.CONDITIONAL + ) + ifNode = ifNode.negative + const negative: + | ComponentBasicDynamicSlot + | ComponentConditionalDynamicSlot = vElse.exp + ? { + slotType: DynamicSlotType.CONDITIONAL, + condition: vElse.exp, + positive: { slotType: DynamicSlotType.BASIC, name: arg!, fn: block, key: ifNode.positive.key! + 1, - } - ifNode.negative = negative - } else { - context.options.onError( - createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc), - ) - } - } else if (vFor) { - if (vFor.forParseResult) { - dynamicSlots.push({ - slotType: DynamicSlotType.LOOP, - name: arg!, - fn: block, - loop: vFor.forParseResult as IRFor, - }) - } else { - context.options.onError( - createCompilerError( - ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, - vFor.loc, - ), - ) - } + }, + } + : { + slotType: DynamicSlotType.BASIC, + name: arg!, + fn: block, + key: ifNode.positive.key! + 1, + } + ifNode.negative = negative } else { + context.options.onError( + createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc), + ) + } + } else if (vFor) { + if (vFor.forParseResult) { dynamicSlots.push({ - slotType: DynamicSlotType.BASIC, + slotType: DynamicSlotType.LOOP, name: arg!, fn: block, + loop: vFor.forParseResult as IRFor, }) + } else { + context.options.onError( + createCompilerError( + ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, + vFor.loc, + ), + ) } + } else { + dynamicSlots.push({ + slotType: DynamicSlotType.BASIC, + name: arg!, + fn: block, + }) } + return () => onExit() } } diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 84eb05be3..fd7274c56 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -304,12 +304,12 @@ export const createFor = ( } } -export const createForSlots = ( +export function createForSlots( source: any[] | Record | number | Set | Map, getSlot: (item: any, key: any, index?: number) => DynamicSlot, -) => { +): DynamicSlot[] { const sourceLength = getLength(source) - const slots = new Array(sourceLength) + const slots = new Array(sourceLength) for (let i = 0; i < sourceLength; i++) { const [item, key, index] = getItem(source, i) slots[i] = getSlot(item, key, index) diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index b702ff526..6b0db6cd4 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -53,11 +53,12 @@ export function initSlots( slots = shallowReactive(slots) const dynamicSlotKeys: Record = {} firstEffect(instance, () => { - const _dynamicSlots = callWithAsyncErrorHandling( - dynamicSlots, - instance, - VaporErrorCodes.RENDER_FUNCTION, - ) + const _dynamicSlots: (DynamicSlot | DynamicSlot[])[] = + callWithAsyncErrorHandling( + dynamicSlots, + instance, + VaporErrorCodes.RENDER_FUNCTION, + ) for (let i = 0; i < _dynamicSlots.length; i++) { const slot = _dynamicSlots[i] // array of dynamic slot generated by