From dde70761200653e86c27e5cb0e41d54eda99bd02 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 29 May 2025 17:04:49 +0800 Subject: [PATCH 1/7] feat(compiler-vapor): add support for forwarded slots --- packages/compiler-vapor/src/generate.ts | 7 +++++ .../src/generators/slotOutlet.ts | 6 +++-- packages/compiler-vapor/src/ir/index.ts | 2 ++ packages/compiler-vapor/src/transform.ts | 1 + .../src/transforms/transformSlotOutlet.ts | 27 +++++++++++++++++++ packages/runtime-vapor/src/componentSlots.ts | 16 ++++++++++- packages/runtime-vapor/src/index.ts | 2 +- 7 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 193a0f5da77..ff3806611ad 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -18,6 +18,7 @@ import { genCall, } from './generators/utils' import { setTemplateRefIdent } from './generators/templateRef' +import { createForwardedSlotIdent } from './generators/slotOutlet' export type CodegenOptions = Omit @@ -129,6 +130,12 @@ export function generate( `const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`, ) } + if (ir.hasForwardedSlot) { + push( + NEWLINE, + `const ${createForwardedSlotIdent} = ${context.helper('forwardedSlotCreator')}()`, + ) + } push(...genBlockContent(ir.block, context, true)) push(INDENT_END, NEWLINE) diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts index 3221cbbd2c7..dc992ae2334 100644 --- a/packages/compiler-vapor/src/generators/slotOutlet.ts +++ b/packages/compiler-vapor/src/generators/slotOutlet.ts @@ -5,12 +5,14 @@ import { genExpression } from './expression' import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils' import { genRawProps } from './component' +export const createForwardedSlotIdent = `_createForwardedSlot` + export function genSlotOutlet( oper: SlotOutletIRNode, context: CodegenContext, ): CodeFragment[] { const { helper } = context - const { id, name, fallback } = oper + const { id, name, fallback, forwarded } = oper const [frag, push] = buildCodeFragment() const nameExpr = name.isStatic @@ -26,7 +28,7 @@ export function genSlotOutlet( NEWLINE, `const n${id} = `, ...genCall( - helper('createSlot'), + forwarded ? createForwardedSlotIdent : helper('createSlot'), nameExpr, genRawProps(oper.props, context) || 'null', fallbackArg, diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index da636113224..086f77ca612 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -66,6 +66,7 @@ export interface RootIRNode { directive: Set block: BlockIRNode hasTemplateRef: boolean + hasForwardedSlot: boolean } export interface IfIRNode extends BaseIRNode { @@ -209,6 +210,7 @@ export interface SlotOutletIRNode extends BaseIRNode { name: SimpleExpressionNode props: IRProps[] fallback?: BlockIRNode + forwarded?: boolean parent?: number anchor?: number } diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 76563899d2b..93488ae95a1 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -230,6 +230,7 @@ export function transform( directive: new Set(), block: newBlock(node), hasTemplateRef: false, + hasForwardedSlot: false, } const context = new TransformContext(ir, node, options) diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 83b4aa2d2e4..159d70c3814 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -5,6 +5,7 @@ import { ErrorCodes, NodeTypes, type SimpleExpressionNode, + type TemplateChildNode, createCompilerError, createSimpleExpression, isStaticArgOf, @@ -99,6 +100,13 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { } return () => { + let forwarded = false + const slotNode = context.block.node + if (slotNode.type === NodeTypes.ELEMENT) { + forwarded = hasForwardedSlots(slotNode.children) + } + if (forwarded) context.ir.hasForwardedSlot = true + exitBlock && exitBlock() context.dynamic.operation = { type: IRNodeTypes.SLOT_OUTLET_NODE, @@ -106,6 +114,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { name: slotName, props: irProps, fallback, + forwarded, } } } @@ -131,3 +140,21 @@ function createFallback( context.reference() return [fallback, exitBlock] } + +// TODO +function hasForwardedSlots(children: TemplateChildNode[]): boolean { + for (let i = 0; i < children.length; i++) { + const child = children[i] + switch (child.type) { + case NodeTypes.ELEMENT: + if ( + child.tagType === ElementTypes.SLOT || + hasForwardedSlots(child.children) + ) { + return true + } + break + } + } + return false +} diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 74296e09466..00ae4ea29ac 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -87,10 +87,24 @@ export function getSlot( } } +export function forwardedSlotCreator(): ( + name: string | (() => string), + rawProps?: LooseRawProps | null, + fallback?: VaporSlot, +) => Block { + const instance = currentInstance as VaporComponentInstance + return ( + name: string | (() => string), + rawProps?: LooseRawProps | null, + fallback?: VaporSlot, + ) => createSlot(name, rawProps, fallback, instance) +} + export function createSlot( name: string | (() => string), rawProps?: LooseRawProps | null, fallback?: VaporSlot, + i?: VaporComponentInstance, ): Block { const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor @@ -98,7 +112,7 @@ export function createSlot( locateHydrationNode() } - const instance = currentInstance as VaporComponentInstance + const instance = i || (currentInstance as VaporComponentInstance) const rawSlots = instance.rawSlots const slotProps = rawProps ? new Proxy(rawProps, rawPropsProxyHandlers) diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 682532fa4d8..10d0aa63384 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -9,7 +9,7 @@ export { insert, prepend, remove, isFragment, VaporFragment } from './block' export { setInsertionState } from './insertionState' export { createComponent, createComponentWithFallback } from './component' export { renderEffect } from './renderEffect' -export { createSlot } from './componentSlots' +export { createSlot, forwardedSlotCreator } from './componentSlots' export { template } from './dom/template' export { createTextNode, child, nthChild, next } from './dom/node' export { From a952b0335897e18f4bf2dc68c22bfeb8a8993d1f Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 29 May 2025 21:45:00 +0800 Subject: [PATCH 2/7] test: add tests --- .../__snapshots__/vSlot.spec.ts.snap | 91 +++++++++++++++++++ .../__tests__/transforms/vSlot.spec.ts | 29 ++++++ packages/compiler-vapor/src/transform.ts | 1 + .../src/transforms/transformSlotOutlet.ts | 14 +-- .../compiler-vapor/src/transforms/vSlot.ts | 9 +- .../__tests__/componentSlots.spec.ts | 54 ++++++++++- 6 files changed, 190 insertions(+), 8 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 4ecd8c76a7e..d1d80d4d620 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap @@ -103,6 +103,97 @@ export function render(_ctx) { }" `; +exports[`compiler: transform slot > forwarded slots > 1`] = ` +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _createForwardedSlot = _forwardedSlotCreator() + const _component_Comp = _resolveComponent("Comp") + const n2 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n1 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n0 = _createForwardedSlot("default", null) + return n0 + } + }) + return n1 + } + }, true) + return n2 +}" +`; + +exports[`compiler: transform slot > forwarded slots > tag only 1`] = ` +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _createForwardedSlot = _forwardedSlotCreator() + const _component_Comp = _resolveComponent("Comp") + const n1 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n0 = _createForwardedSlot("default", null) + return n0 + } + }, true) + return n1 +}" +`; + +exports[`compiler: transform slot > forwarded slots > tag w/ template 1`] = ` +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _createForwardedSlot = _forwardedSlotCreator() + const _component_Comp = _resolveComponent("Comp") + const n2 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n0 = _createForwardedSlot("default", null) + return n0 + } + }, true) + return n2 +}" +`; + +exports[`compiler: transform slot > forwarded slots > tag w/ v-for 1`] = ` +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createFor as _createFor, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _createForwardedSlot = _forwardedSlotCreator() + const _component_Comp = _resolveComponent("Comp") + const n3 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n0 = _createFor(() => (_ctx.b), (_for_item0) => { + const n2 = _createForwardedSlot("default", null) + return n2 + }) + return n0 + } + }, true) + return n3 +}" +`; + +exports[`compiler: transform slot > forwarded slots > tag w/ v-if 1`] = ` +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createIf as _createIf, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _createForwardedSlot = _forwardedSlotCreator() + const _component_Comp = _resolveComponent("Comp") + const n3 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n0 = _createIf(() => (_ctx.ok), () => { + const n2 = _createForwardedSlot("default", null) + return n2 + }) + return n0 + } + }, true) + return n3 +}" +`; + exports[`compiler: transform slot > implicit default slot 1`] = ` "import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template("
") diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts index 84ddb2e5d04..a7da3d542f7 100644 --- a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -409,6 +409,35 @@ describe('compiler: transform slot', () => { }) }) + describe('forwarded slots', () => { + test(' tag only', () => { + const { code } = compileWithSlots(``) + expect(code).toMatchSnapshot() + }) + + test(' tag w/ v-if', () => { + const { code } = compileWithSlots(``) + expect(code).toMatchSnapshot() + }) + + test(' tag w/ v-for', () => { + const { code } = compileWithSlots(``) + expect(code).toMatchSnapshot() + }) + + test(' tag w/ template', () => { + const { code } = compileWithSlots( + ``, + ) + expect(code).toMatchSnapshot() + }) + + test('', () => { + const { code } = compileWithSlots(``) + expect(code).toMatchSnapshot() + }) + }) + describe('errors', () => { test('error on extraneous children w/ named default slot', () => { const onError = vi.fn() diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 93488ae95a1..6d07ebcaf52 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -76,6 +76,7 @@ export class TransformContext { inVOnce: boolean = false inVFor: number = 0 + inSlot: number = 0 comment: CommentNode[] = [] component: Set = this.ir.component diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 159d70c3814..a281c90a70d 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -100,11 +100,14 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { } return () => { - let forwarded = false - const slotNode = context.block.node - if (slotNode.type === NodeTypes.ELEMENT) { - forwarded = hasForwardedSlots(slotNode.children) - } + const { + block: { node: slotNode }, + inSlot, + } = context + const forwarded = + inSlot !== 0 && + slotNode.type === NodeTypes.ELEMENT && + hasForwardedSlots(slotNode.children) if (forwarded) context.ir.hasForwardedSlot = true exitBlock && exitBlock() @@ -141,7 +144,6 @@ function createFallback( return [fallback, exitBlock] } -// TODO function hasForwardedSlots(children: TemplateChildNode[]): boolean { for (let i = 0; i < children.length; i++) { const child = children[i] diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index d1bf1c6b05f..2e767cb41cd 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -237,7 +237,14 @@ function createSlotBlock( const block: SlotBlockIRNode = newBlock(slotNode) block.props = dir && dir.exp const exitBlock = context.enterBlock(block) - return [block, exitBlock] + context.inSlot++ + return [ + block, + () => { + context.inSlot-- + exitBlock() + }, + ] } function isNonWhitespaceContent(node: TemplateChildNode): boolean { diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 58076fff9ee..46bfc3d938d 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -7,6 +7,7 @@ import { createSlot, createVaporApp, defineVaporComponent, + forwardedSlotCreator, insert, prepend, renderEffect, @@ -15,7 +16,7 @@ import { import { currentInstance, nextTick, ref } from '@vue/runtime-dom' import { makeRender } from './_utils' import type { DynamicSlot } from '../src/componentSlots' -import { setElementText } from '../src/dom/prop' +import { setElementText, setText } from '../src/dom/prop' const define = makeRender() @@ -503,4 +504,55 @@ describe('component: slots', () => { expect(host.innerHTML).toBe('

') }) }) + + describe('forwarded slot', () => { + test('should work', async () => { + const Child = defineVaporComponent({ + setup() { + return createSlot('foo', null) + }, + }) + const Parent = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + Child, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const foo = ref('foo') + const { host } = define({ + setup() { + const n2 = createComponent( + Parent, + null, + { + foo: () => { + const n0 = template(' ')() as any + renderEffect(() => setText(n0, foo.value)) + return n0 + }, + }, + true, + ) + return n2 + }, + }).render() + + expect(host.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(host.innerHTML).toBe('bar') + }) + }) }) From c23d63582e3ddc569b84102b22757549b97ea8f0 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 30 May 2025 09:17:04 +0800 Subject: [PATCH 3/7] chore: update --- packages/compiler-vapor/src/transform.ts | 2 +- .../src/transforms/transformSlotOutlet.ts | 31 +---------- .../compiler-vapor/src/transforms/vSlot.ts | 4 +- .../__tests__/componentSlots.spec.ts | 51 +++++++++++++++++++ packages/runtime-vapor/src/componentSlots.ts | 7 +-- 5 files changed, 58 insertions(+), 37 deletions(-) diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 6d07ebcaf52..763e9612cdc 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -76,7 +76,7 @@ export class TransformContext { inVOnce: boolean = false inVFor: number = 0 - inSlot: number = 0 + inSlot: boolean = false comment: CommentNode[] = [] component: Set = this.ir.component diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index a281c90a70d..dc2b620ddb2 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -5,7 +5,6 @@ import { ErrorCodes, NodeTypes, type SimpleExpressionNode, - type TemplateChildNode, createCompilerError, createSimpleExpression, isStaticArgOf, @@ -100,16 +99,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { } return () => { - const { - block: { node: slotNode }, - inSlot, - } = context - const forwarded = - inSlot !== 0 && - slotNode.type === NodeTypes.ELEMENT && - hasForwardedSlots(slotNode.children) - if (forwarded) context.ir.hasForwardedSlot = true - + if (context.inSlot) context.ir.hasForwardedSlot = true exitBlock && exitBlock() context.dynamic.operation = { type: IRNodeTypes.SLOT_OUTLET_NODE, @@ -117,7 +107,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { name: slotName, props: irProps, fallback, - forwarded, + forwarded: context.inSlot, } } } @@ -143,20 +133,3 @@ function createFallback( context.reference() return [fallback, exitBlock] } - -function hasForwardedSlots(children: TemplateChildNode[]): boolean { - for (let i = 0; i < children.length; i++) { - const child = children[i] - switch (child.type) { - case NodeTypes.ELEMENT: - if ( - child.tagType === ElementTypes.SLOT || - hasForwardedSlots(child.children) - ) { - return true - } - break - } - } - return false -} diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index 2e767cb41cd..525fa323d3a 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -237,11 +237,11 @@ function createSlotBlock( const block: SlotBlockIRNode = newBlock(slotNode) block.props = dir && dir.exp const exitBlock = context.enterBlock(block) - context.inSlot++ + context.inSlot = true return [ block, () => { - context.inSlot-- + context.inSlot = false exitBlock() }, ] diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 46bfc3d938d..bdbd960363d 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -554,5 +554,56 @@ describe('component: slots', () => { await nextTick() expect(host.innerHTML).toBe('bar') }) + + test('mixed with non-forwarded slot', async () => { + const Child = defineVaporComponent({ + setup() { + return [createSlot('foo', null)] + }, + }) + const Parent = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent(Child, null, { + foo: () => { + const n0 = createForwardedSlot('foo', null) + return n0 + }, + }) + const n3 = createSlot('default', null) + return [n2, n3] + }, + }) + + const foo = ref('foo') + const { host } = define({ + setup() { + const n2 = createComponent( + Parent, + null, + { + foo: () => { + const n0 = template(' ')() as any + renderEffect(() => setText(n0, foo.value)) + return n0 + }, + default: () => { + const n3 = template(' ')() as any + renderEffect(() => setText(n3, foo.value)) + return n3 + }, + }, + true, + ) + return n2 + }, + }).render() + + expect(host.innerHTML).toBe('foofoo') + + foo.value = 'bar' + await nextTick() + expect(host.innerHTML).toBe('barbar') + }) }) }) diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 00ae4ea29ac..19e9b5b6d1a 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -93,11 +93,8 @@ export function forwardedSlotCreator(): ( fallback?: VaporSlot, ) => Block { const instance = currentInstance as VaporComponentInstance - return ( - name: string | (() => string), - rawProps?: LooseRawProps | null, - fallback?: VaporSlot, - ) => createSlot(name, rawProps, fallback, instance) + return (name, rawProps, fallback) => + createSlot(name, rawProps, fallback, instance) } export function createSlot( From dcf927ff8c1a7662771b30b7f76cbb63afa8ebe7 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 30 May 2025 16:27:02 +0800 Subject: [PATCH 4/7] fix(vdomInterop): handle forwarded vapor slots during render VDOM slot --- packages/runtime-vapor/src/componentProps.ts | 3 ++- packages/runtime-vapor/src/vdomInterop.ts | 28 ++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index a5e9daad229..7a0e9ed9286 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -210,7 +210,8 @@ export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean { if (dynamicSources) { let i = dynamicSources.length while (i--) { - if (hasOwn(resolveSource(dynamicSources[i]), key)) { + const source = resolveSource(dynamicSources[i]) + if (source && hasOwn(source, key)) { return true } } diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 77228fd72a0..e7c7e02e0bd 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -26,7 +26,14 @@ import { mountComponent, unmountComponent, } from './component' -import { type Block, VaporFragment, insert, remove } from './block' +import { + type Block, + VaporFragment, + insert, + isFragment, + isValidBlock, + remove, +} from './block' import { EMPTY_OBJ, extend, isFunction } from '@vue/shared' import { type RawProps, rawPropsProxyHandlers } from './componentProps' import type { RawSlots, VaporSlot } from './componentSlots' @@ -230,7 +237,24 @@ function renderVDOMSlot( isFunction(name) ? name() : name, props, ) - if ((vnode.children as any[]).length) { + let isValidSlotContent + let children = vnode.children as any[] + + // TODO add tests + // handle forwarded vapor slot + let vaporSlot + if (children.length === 1 && (vaporSlot = children[0].vs)) { + const block = vaporSlot.slot(props) + isValidSlotContent = + isValidBlock(block) || + // if block is a vapor fragment with insert, it indicates a forwarded VDOM slot + (isFragment(block) && block.insert) + } + // vnode children + else { + isValidSlotContent = children.length > 0 + } + if (isValidSlotContent) { if (fallbackNodes) { remove(fallbackNodes, parentNode) fallbackNodes = undefined From b5f6f01b4d76899491fe73e81e84423fa538b0bc Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 6 Jun 2025 15:36:36 +0800 Subject: [PATCH 5/7] wip: refactor --- .../runtime-vapor/__tests__/scopeId.spec.ts | 464 ++++++++++++++++++ .../src/apiCreateDynamicComponent.ts | 11 +- packages/runtime-vapor/src/block.ts | 35 ++ packages/runtime-vapor/src/component.ts | 24 +- packages/runtime-vapor/src/vdomInterop.ts | 6 + 5 files changed, 535 insertions(+), 5 deletions(-) create mode 100644 packages/runtime-vapor/__tests__/scopeId.spec.ts diff --git a/packages/runtime-vapor/__tests__/scopeId.spec.ts b/packages/runtime-vapor/__tests__/scopeId.spec.ts new file mode 100644 index 00000000000..657936352d3 --- /dev/null +++ b/packages/runtime-vapor/__tests__/scopeId.spec.ts @@ -0,0 +1,464 @@ +import { createApp, h } from '@vue/runtime-dom' +import { + createComponent, + createDynamicComponent, + createSlot, + defineVaporComponent, + setInsertionState, + template, + vaporInteropPlugin, +} from '../src' +import { makeRender } from './_utils' + +const define = makeRender() + +describe('scopeId', () => { + test('should attach scopeId to child component', () => { + const Child = defineVaporComponent({ + __scopeId: 'child', + setup() { + return template('
', true)() + }, + }) + + const { html } = define({ + __scopeId: 'parent', + setup() { + return createComponent(Child) + }, + }).render() + expect(html()).toBe(`
`) + }) + + test('should attach scopeId to child component with insertion state', () => { + const Child = defineVaporComponent({ + __scopeId: 'child', + setup() { + return template('
', true)() + }, + }) + + const { html } = define({ + __scopeId: 'parent', + setup() { + const t0 = template('
', true) + const n1 = t0() as any + setInsertionState(n1) + createComponent(Child) + return n1 + }, + }).render() + expect(html()).toBe(`
`) + }) + + test('should attach scopeId to nested child component', () => { + const Child = defineVaporComponent({ + __scopeId: 'child', + setup() { + return template('
', true)() + }, + }) + + const Parent = defineVaporComponent({ + __scopeId: 'parent', + setup() { + return createComponent(Child) + }, + }) + + const { html } = define({ + __scopeId: 'app', + setup() { + return createComponent(Parent) + }, + }).render() + expect(html()).toBe(`
`) + }) + + test('should not attach scopeId to nested multiple root components', () => { + const Child = defineVaporComponent({ + __scopeId: 'child', + setup() { + return template('
', true)() + }, + }) + + const Parent = defineVaporComponent({ + __scopeId: 'parent', + setup() { + const n0 = template('
')() + const n1 = createComponent(Child) + return [n0, n1] + }, + }) + + const { html } = define({ + __scopeId: 'app', + setup() { + return createComponent(Parent) + }, + }).render() + expect(html()).toBe(`
`) + }) + + test('should attach scopeId to nested child component with insertion state', () => { + const Child = defineVaporComponent({ + __scopeId: 'child', + setup() { + return template('
', true)() + }, + }) + + const Parent = defineVaporComponent({ + __scopeId: 'parent', + setup() { + return createComponent(Child) + }, + }) + + const { html } = define({ + __scopeId: 'app', + setup() { + const t0 = template('
', true) + const n1 = t0() as any + setInsertionState(n1) + createComponent(Parent) + return n1 + }, + }).render() + expect(html()).toBe( + `
`, + ) + }) + + test('should attach scopeId to dynamic component', () => { + const { html } = define({ + __scopeId: 'parent', + setup() { + return createDynamicComponent(() => 'button') + }, + }).render() + expect(html()).toBe(``) + }) + + test('should attach scopeId to dynamic component with insertion state', () => { + const { html } = define({ + __scopeId: 'parent', + setup() { + const t0 = template('
', true) + const n1 = t0() as any + setInsertionState(n1) + createDynamicComponent(() => 'button') + return n1 + }, + }).render() + expect(html()).toBe( + `
`, + ) + }) + + test('should attach scopeId to nested dynamic component', () => { + const Comp = defineVaporComponent({ + __scopeId: 'child', + setup() { + return createDynamicComponent(() => 'button', null, null, true) + }, + }) + const { html } = define({ + __scopeId: 'parent', + setup() { + return createComponent(Comp, null, null, true) + }, + }).render() + expect(html()).toBe( + ``, + ) + }) + + test('should attach scopeId to nested dynamic component with insertion state', () => { + const Comp = defineVaporComponent({ + __scopeId: 'child', + setup() { + return createDynamicComponent(() => 'button', null, null, true) + }, + }) + const { html } = define({ + __scopeId: 'parent', + setup() { + const t0 = template('
', true) + const n1 = t0() as any + setInsertionState(n1) + createComponent(Comp, null, null, true) + return n1 + }, + }).render() + expect(html()).toBe( + `
`, + ) + }) + + test.todo('should attach scopeId to suspense content', async () => {}) + + // :slotted basic + test.todo('should work on slots', () => { + const Child = defineVaporComponent({ + __scopeId: 'child', + setup() { + const n1 = template('
', true)() as any + setInsertionState(n1) + createSlot('default', null) + return n1 + }, + }) + + const Child2 = defineVaporComponent({ + __scopeId: 'child2', + setup() { + return template('', true)() + }, + }) + + const { html } = define({ + __scopeId: 'parent', + setup() { + const n2 = createComponent( + Child, + null, + { + default: () => { + const n0 = template('
')() + const n1 = createComponent(Child2) + return [n0, n1] + }, + }, + true, + ) + return n2 + }, + }).render() + + expect(html()).toBe( + `
` + + `
` + + // component inside slot should have: + // - scopeId from template context + // - slotted scopeId from slot owner + // - its own scopeId + `` + + `` + + `
`, + ) + }) + + test.todo(':slotted on forwarded slots', async () => {}) +}) + +describe('vdom interop', () => { + test('vdom parent > vapor child', () => { + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return template('', true)() + }, + }) + + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h(VaporChild as any) + }, + } + + const App = { + __scopeId: 'parent', + setup() { + return () => h(VdomChild) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vdom parent > vapor > vdom child', () => { + const InnerVdomChild = { + __scopeId: 'inner-vdom-child', + setup() { + return () => h('button') + }, + } + + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return createComponent(InnerVdomChild as any, null, null, true) + }, + }) + + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h(VaporChild as any) + }, + } + + const App = { + __scopeId: 'parent', + setup() { + return () => h(VdomChild) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vdom parent > vapor dynamic child', () => { + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return createDynamicComponent(() => 'button', null, null, true) + }, + }) + + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h(VaporChild as any) + }, + } + + const App = { + __scopeId: 'parent', + setup() { + return () => h(VdomChild) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vapor parent > vdom child', () => { + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h('button') + }, + } + + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return createComponent(VdomChild as any, null, null, true) + }, + }) + + const App = { + __scopeId: 'parent', + setup() { + return () => h(VaporChild as any) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vapor parent > vdom > vapor child', () => { + const InnerVaporChild = defineVaporComponent({ + __scopeId: 'inner-vapor-child', + setup() { + return template('', true)() + }, + }) + + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h(InnerVaporChild as any) + }, + } + + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return createComponent(VdomChild as any, null, null, true) + }, + }) + + const App = { + __scopeId: 'parent', + setup() { + return () => h(VaporChild as any) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test.todo('vapor parent > vdom > vdom > vapor child', () => { + const InnerVaporChild = defineVaporComponent({ + __scopeId: 'inner-vapor-child', + setup() { + return template('', true)() + }, + }) + + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h(InnerVaporChild as any) + }, + } + + const VdomChild2 = { + __scopeId: 'vdom-child2', + setup() { + return () => h(VdomChild as any) + }, + } + + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return createComponent(VdomChild2 as any, null, null, true) + }, + }) + + const App = { + __scopeId: 'parent', + setup() { + return () => h(VaporChild as any) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) +}) diff --git a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts index 2126611d718..33697b4ca76 100644 --- a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts +++ b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts @@ -1,9 +1,11 @@ import { resolveDynamicComponent } from '@vue/runtime-dom' -import { DynamicFragment, type VaporFragment } from './block' +import { DynamicFragment, type VaporFragment, insert } from './block' import { createComponentWithFallback } from './component' import { renderEffect } from './renderEffect' import type { RawProps } from './componentProps' import type { RawSlots } from './componentSlots' +import { isHydrating } from './dom/hydration' +import { insertionAnchor, insertionParent } from './insertionState' export function createDynamicComponent( getter: () => any, @@ -11,6 +13,9 @@ export function createDynamicComponent( rawSlots?: RawSlots | null, isSingleRoot?: boolean, ): VaporFragment { + const _insertionParent = insertionParent + const _insertionAnchor = insertionAnchor + const frag = __DEV__ ? new DynamicFragment('dynamic-component') : new DynamicFragment() @@ -27,5 +32,9 @@ export function createDynamicComponent( value, ) }) + + if (!isHydrating && _insertionParent) { + insert(frag, _insertionParent, _insertionAnchor) + } return frag } diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index b782afd38d3..c094642dfca 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -187,3 +187,38 @@ export function normalizeBlock(block: Block): Node[] { } return nodes } + +export function setScopeId(block: Block, scopeId: string): void { + if (block instanceof Element) { + block.setAttribute(scopeId, '') + } else if (isVaporComponent(block)) { + setScopeId(block.block, scopeId) + } else if (isArray(block)) { + for (const b of block) { + setScopeId(b, scopeId) + } + } else if (isFragment(block)) { + setScopeId(block.nodes, scopeId) + } +} + +export function setComponentScopeId(instance: VaporComponentInstance): void { + const parent = instance.parent + if (!parent) return + + if (isArray(instance.block) && instance.block.length > 1) return + + const scopeId = parent.type.__scopeId + if (scopeId) { + setScopeId(instance.block, scopeId) + } + + // vdom parent + if ( + parent.subTree && + (parent.subTree.component as any) === instance && + parent.vnode!.scopeId + ) { + setScopeId(instance.block, parent.vnode!.scopeId) + } +} diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 548babebf8b..ea01450a119 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -25,7 +25,14 @@ import { unregisterHMR, warn, } from '@vue/runtime-dom' -import { type Block, insert, isBlock, remove } from './block' +import { + type Block, + insert, + isBlock, + remove, + setComponentScopeId, + setScopeId, +} from './block' import { type ShallowRef, markRaw, @@ -59,7 +66,11 @@ import { } from './componentSlots' import { hmrReload, hmrRerender } from './hmr' import { isHydrating, locateHydrationNode } from './dom/hydration' -import { insertionAnchor, insertionParent } from './insertionState' +import { + insertionAnchor, + insertionParent, + resetInsertionState, +} from './insertionState' export { currentInstance } from '@vue/runtime-dom' @@ -142,6 +153,8 @@ export function createComponent( const _insertionAnchor = insertionAnchor if (isHydrating) { locateHydrationNode() + } else { + resetInsertionState() } // vdom interop enabled and component is not an explicit vapor component @@ -270,9 +283,8 @@ export function createComponent( onScopeDispose(() => unmountComponent(instance), true) if (!isHydrating && _insertionParent) { - insert(instance.block, _insertionParent, _insertionAnchor) + mountComponent(instance, _insertionParent, _insertionAnchor) } - return instance } @@ -474,6 +486,9 @@ export function createComponentWithFallback( // mark single root ;(el as any).$root = isSingleRoot + const scopeId = currentInstance!.type.__scopeId + if (scopeId) setScopeId(el, scopeId) + if (rawProps) { renderEffect(() => { setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)]) @@ -501,6 +516,7 @@ export function mountComponent( } if (instance.bm) invokeArrayFns(instance.bm) insert(instance.block, parent, anchor) + setComponentScopeId(instance) if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!)) instance.isMounted = true if (__DEV__) { diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 77228fd72a0..0e4e1c492ad 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -61,6 +61,7 @@ const vaporInteropImpl: Omit< instance.rawPropsRef = propsRef instance.rawSlotsRef = slotsRef mountComponent(instance, container, selfAnchor) + vnode.el = instance.block simpleSetCurrentInstance(prev) return instance }, @@ -175,6 +176,8 @@ function createVDOMComponent( internals.umt(vnode.component!, null, !!parentNode) } + vnode.scopeId = parentInstance.type.__scopeId! + frag.insert = (parentNode, anchor) => { if (!isMounted) { internals.mt( @@ -198,6 +201,9 @@ function createVDOMComponent( parentInstance as any, ) } + + // update the fragment nodes + frag.nodes = vnode.el as Block } frag.remove = unmount From 4aaa69ae496cfb16b90570f27e12d149a50cafb1 Mon Sep 17 00:00:00 2001 From: daiwei Date: Sun, 8 Jun 2025 09:04:58 +0800 Subject: [PATCH 6/7] wip: save --- packages/runtime-core/src/index.ts | 6 +- packages/runtime-core/src/renderer.ts | 78 +++++++++++++------ .../runtime-vapor/__tests__/scopeId.spec.ts | 46 ++++++++++- packages/runtime-vapor/src/block.ts | 7 +- 4 files changed, 110 insertions(+), 27 deletions(-) diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index e309554f2f6..527bead3ecc 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -505,7 +505,11 @@ export { type VaporInteropInterface } from './apiCreateApp' /** * @internal */ -export { type RendererInternals, MoveType } from './renderer' +export { + type RendererInternals, + MoveType, + getInheritedScopeIds, +} from './renderer' /** * @internal */ diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 5a18d62a8e1..cdf7462449d 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -764,30 +764,9 @@ function baseCreateRenderer( hostSetScopeId(el, slotScopeIds[i]) } } - let subTree = parentComponent && parentComponent.subTree - if (subTree) { - if ( - __DEV__ && - subTree.patchFlag > 0 && - subTree.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT - ) { - subTree = - filterSingleRoot(subTree.children as VNodeArrayChildren) || subTree - } - if ( - vnode === subTree || - (isSuspense(subTree.type) && - (subTree.ssContent === vnode || subTree.ssFallback === vnode)) - ) { - const parentVNode = parentComponent!.vnode! - setScopeId( - el, - parentVNode, - parentVNode.scopeId, - parentVNode.slotScopeIds, - parentComponent!.parent, - ) - } + const inheritedScopeIds = getInheritedScopeIds(vnode, parentComponent) + for (let i = 0; i < inheritedScopeIds.length; i++) { + hostSetScopeId(el, inheritedScopeIds[i]) } } @@ -2656,3 +2635,54 @@ function getVaporInterface( } return res! } + +/** + * shared between vdom and vapor + */ +export function getInheritedScopeIds( + vnode: VNode, + parentComponent: GenericComponentInstance | null, +): string[] { + const inheritedScopeIds: string[] = [] + + let currentParent = parentComponent + let currentVNode = vnode + + while (currentParent) { + let subTree = currentParent.subTree + if (!subTree) break + + if ( + __DEV__ && + subTree.patchFlag > 0 && + subTree.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT + ) { + subTree = + filterSingleRoot(subTree.children as VNodeArrayChildren) || subTree + } + + if ( + currentVNode === subTree || + (isSuspense(subTree.type) && + (subTree.ssContent === currentVNode || + subTree.ssFallback === currentVNode)) + ) { + const parentVNode = currentParent.vnode! + + if (parentVNode.scopeId) { + inheritedScopeIds.push(parentVNode.scopeId) + } + + if (parentVNode.slotScopeIds) { + inheritedScopeIds.push(...parentVNode.slotScopeIds) + } + + currentVNode = parentVNode + currentParent = currentParent.parent + } else { + break + } + } + + return inheritedScopeIds +} diff --git a/packages/runtime-vapor/__tests__/scopeId.spec.ts b/packages/runtime-vapor/__tests__/scopeId.spec.ts index 657936352d3..827ea501f23 100644 --- a/packages/runtime-vapor/__tests__/scopeId.spec.ts +++ b/packages/runtime-vapor/__tests__/scopeId.spec.ts @@ -321,6 +321,50 @@ describe('vdom interop', () => { ) }) + test('vdom parent > vapor > vapor > vdom child', () => { + const InnerVdomChild = { + __scopeId: 'inner-vdom-child', + setup() { + return () => h('button') + }, + } + + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return createComponent(InnerVdomChild as any, null, null, true) + }, + }) + + const VaporChild2 = defineVaporComponent({ + __scopeId: 'vapor-child2', + setup() { + return createComponent(VaporChild as any, null, null, true) + }, + }) + + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h(VaporChild2 as any) + }, + } + + const App = { + __scopeId: 'parent', + setup() { + return () => h(VdomChild) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + test('vdom parent > vapor dynamic child', () => { const VaporChild = defineVaporComponent({ __scopeId: 'vapor-child', @@ -418,7 +462,7 @@ describe('vdom interop', () => { ) }) - test.todo('vapor parent > vdom > vdom > vapor child', () => { + test('vapor parent > vdom > vdom > vapor child', () => { const InnerVaporChild = defineVaporComponent({ __scopeId: 'inner-vapor-child', setup() { diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index c094642dfca..a67e43543e9 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -8,6 +8,7 @@ import { import { createComment, createTextNode } from './dom/node' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' import { isHydrating } from './dom/hydration' +import { getInheritedScopeIds } from '@vue/runtime-dom' export type Block = | Node @@ -213,12 +214,16 @@ export function setComponentScopeId(instance: VaporComponentInstance): void { setScopeId(instance.block, scopeId) } - // vdom parent + // inherit scopeId from vdom parent if ( parent.subTree && (parent.subTree.component as any) === instance && parent.vnode!.scopeId ) { setScopeId(instance.block, parent.vnode!.scopeId) + const scopeIds = getInheritedScopeIds(parent.vnode!, parent.parent) + for (const id of scopeIds) { + setScopeId(instance.block, id) + } } } From 9772a4c6c4ac869c5c4d47d4fdb2b2c859ddac94 Mon Sep 17 00:00:00 2001 From: daiwei Date: Sun, 8 Jun 2025 16:22:30 +0800 Subject: [PATCH 7/7] wip: slotScopeIds --- .../src/generators/component.ts | 3 +- packages/compiler-vapor/src/ir/index.ts | 1 + .../src/transforms/transformElement.ts | 1 + packages/runtime-core/src/apiCreateApp.ts | 7 +- .../runtime-vapor/__tests__/scopeId.spec.ts | 243 +++++++++++++----- packages/runtime-vapor/src/apiCreateApp.ts | 4 + .../src/apiCreateDynamicComponent.ts | 4 + packages/runtime-vapor/src/block.ts | 6 +- packages/runtime-vapor/src/component.ts | 18 +- packages/runtime-vapor/src/componentSlots.ts | 26 +- packages/runtime-vapor/src/vdomInterop.ts | 3 +- 11 files changed, 247 insertions(+), 69 deletions(-) diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 7c232db754b..05b16077b08 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -47,7 +47,7 @@ export function genCreateComponent( const { helper } = context const tag = genTag() - const { root, props, slots, once } = operation + const { root, props, slots, once, scopeId } = operation const rawSlots = genRawSlots(slots, context) const [ids, handlers] = processInlineHandlers(props, context) const rawProps = context.withId(() => genRawProps(props, context), ids) @@ -75,6 +75,7 @@ export function genCreateComponent( rawSlots, root ? 'true' : false, once && 'true', + scopeId && JSON.stringify(scopeId), ), ...genDirectivesForElement(operation.id, context), ] diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index 086f77ca612..e6394ad4661 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -197,6 +197,7 @@ export interface CreateComponentIRNode extends BaseIRNode { dynamic?: SimpleExpressionNode parent?: number anchor?: number + scopeId?: string | null } export interface DeclareOldRefIRNode extends BaseIRNode { diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index dceb3fd6121..fbb48d82018 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -133,6 +133,7 @@ function transformComponentElement( root: singleRoot, slots: [...context.slots], once: context.inVOnce, + scopeId: context.inSlot ? context.options.scopeId : undefined, dynamic: dynamicComponent, } context.slots = [] diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 5bdd204cfad..531496fb743 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -188,7 +188,12 @@ export interface VaporInteropInterface { move(vnode: VNode, container: any, anchor: any): void slot(n1: VNode | null, n2: VNode, container: any, anchor: any): void - vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any + vdomMount: ( + component: ConcreteComponent, + props?: any, + slots?: any, + scopeId?: string, + ) => any vdomUnmount: UnmountComponentFn vdomSlot: ( slots: any, diff --git a/packages/runtime-vapor/__tests__/scopeId.spec.ts b/packages/runtime-vapor/__tests__/scopeId.spec.ts index 827ea501f23..48051b1497f 100644 --- a/packages/runtime-vapor/__tests__/scopeId.spec.ts +++ b/packages/runtime-vapor/__tests__/scopeId.spec.ts @@ -4,6 +4,7 @@ import { createDynamicComponent, createSlot, defineVaporComponent, + forwardedSlotCreator, setInsertionState, template, vaporInteropPlugin, @@ -200,7 +201,7 @@ describe('scopeId', () => { test.todo('should attach scopeId to suspense content', async () => {}) // :slotted basic - test.todo('should work on slots', () => { + test('should work on slots', () => { const Child = defineVaporComponent({ __scopeId: 'child', setup() { @@ -227,7 +228,14 @@ describe('scopeId', () => { { default: () => { const n0 = template('
')() - const n1 = createComponent(Child2) + const n1 = createComponent( + Child2, + null, + null, + undefined, + undefined, + 'parent', + ) return [n0, n1] }, }, @@ -244,13 +252,69 @@ describe('scopeId', () => { // - scopeId from template context // - slotted scopeId from slot owner // - its own scopeId - `` + + `` + `` + ``, ) }) - test.todo(':slotted on forwarded slots', async () => {}) + test(':slotted on forwarded slots', async () => { + const Wrapper = defineVaporComponent({ + __scopeId: 'wrapper', + setup() { + //
+ const n1 = template('
', true)() as any + setInsertionState(n1) + createSlot('default', null) + return n1 + }, + }) + + const Slotted = defineVaporComponent({ + __scopeId: 'slotted', + setup() { + // + const _createForwardedSlot = forwardedSlotCreator() + const n1 = createComponent( + Wrapper, + null, + { + default: () => { + const n0 = _createForwardedSlot('default', null) + return n0 + }, + }, + true, + ) + return n1 + }, + }) + + const { html } = define({ + __scopeId: 'root', + setup() { + //
+ const n2 = createComponent( + Slotted, + null, + { + default: () => { + return template('
')() + }, + }, + true, + ) + return n2 + }, + }).render() + + expect(html()).toBe( + `
` + + `
` + + `` + + `
`, + ) + }) }) describe('vdom interop', () => { @@ -262,17 +326,16 @@ describe('vdom interop', () => { }, }) - const VdomChild = { - __scopeId: 'vdom-child', + const VdomParent = { + __scopeId: 'vdom-parent', setup() { return () => h(VaporChild as any) }, } const App = { - __scopeId: 'parent', setup() { - return () => h(VdomChild) + return () => h(VdomParent) }, } @@ -280,13 +343,13 @@ describe('vdom interop', () => { createApp(App).use(vaporInteropPlugin).mount(root) expect(root.innerHTML).toBe( - ``, + ``, ) }) test('vdom parent > vapor > vdom child', () => { - const InnerVdomChild = { - __scopeId: 'inner-vdom-child', + const VdomChild = { + __scopeId: 'vdom-child', setup() { return () => h('button') }, @@ -295,21 +358,20 @@ describe('vdom interop', () => { const VaporChild = defineVaporComponent({ __scopeId: 'vapor-child', setup() { - return createComponent(InnerVdomChild as any, null, null, true) + return createComponent(VdomChild as any, null, null, true) }, }) - const VdomChild = { - __scopeId: 'vdom-child', + const VdomParent = { + __scopeId: 'vdom-parent', setup() { return () => h(VaporChild as any) }, } const App = { - __scopeId: 'parent', setup() { - return () => h(VdomChild) + return () => h(VdomParent) }, } @@ -317,43 +379,42 @@ describe('vdom interop', () => { createApp(App).use(vaporInteropPlugin).mount(root) expect(root.innerHTML).toBe( - ``, + ``, ) }) test('vdom parent > vapor > vapor > vdom child', () => { - const InnerVdomChild = { - __scopeId: 'inner-vdom-child', + const VdomChild = { + __scopeId: 'vdom-child', setup() { return () => h('button') }, } - const VaporChild = defineVaporComponent({ - __scopeId: 'vapor-child', + const NestedVaporChild = defineVaporComponent({ + __scopeId: 'nested-vapor-child', setup() { - return createComponent(InnerVdomChild as any, null, null, true) + return createComponent(VdomChild as any, null, null, true) }, }) - const VaporChild2 = defineVaporComponent({ - __scopeId: 'vapor-child2', + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', setup() { - return createComponent(VaporChild as any, null, null, true) + return createComponent(NestedVaporChild as any, null, null, true) }, }) - const VdomChild = { - __scopeId: 'vdom-child', + const VdomParent = { + __scopeId: 'vdom-parent', setup() { - return () => h(VaporChild2 as any) + return () => h(VaporChild as any) }, } const App = { - __scopeId: 'parent', setup() { - return () => h(VdomChild) + return () => h(VdomParent) }, } @@ -361,7 +422,7 @@ describe('vdom interop', () => { createApp(App).use(vaporInteropPlugin).mount(root) expect(root.innerHTML).toBe( - ``, + ``, ) }) @@ -373,17 +434,16 @@ describe('vdom interop', () => { }, }) - const VdomChild = { - __scopeId: 'vdom-child', + const VdomParent = { + __scopeId: 'vdom-parent', setup() { return () => h(VaporChild as any) }, } const App = { - __scopeId: 'parent', setup() { - return () => h(VdomChild) + return () => h(VdomParent) }, } @@ -391,7 +451,7 @@ describe('vdom interop', () => { createApp(App).use(vaporInteropPlugin).mount(root) expect(root.innerHTML).toBe( - ``, + ``, ) }) @@ -403,17 +463,16 @@ describe('vdom interop', () => { }, } - const VaporChild = defineVaporComponent({ - __scopeId: 'vapor-child', + const VaporParent = defineVaporComponent({ + __scopeId: 'vapor-parent', setup() { return createComponent(VdomChild as any, null, null, true) }, }) const App = { - __scopeId: 'parent', setup() { - return () => h(VaporChild as any) + return () => h(VaporParent as any) }, } @@ -421,36 +480,35 @@ describe('vdom interop', () => { createApp(App).use(vaporInteropPlugin).mount(root) expect(root.innerHTML).toBe( - ``, + ``, ) }) test('vapor parent > vdom > vapor child', () => { - const InnerVaporChild = defineVaporComponent({ - __scopeId: 'inner-vapor-child', + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', setup() { - return template('', true)() + return template('', true)() }, }) const VdomChild = { __scopeId: 'vdom-child', setup() { - return () => h(InnerVaporChild as any) + return () => h(VaporChild as any) }, } - const VaporChild = defineVaporComponent({ - __scopeId: 'vapor-child', + const VaporParent = defineVaporComponent({ + __scopeId: 'vapor-parent', setup() { return createComponent(VdomChild as any, null, null, true) }, }) const App = { - __scopeId: 'parent', setup() { - return () => h(VaporChild as any) + return () => h(VaporParent as any) }, } @@ -458,43 +516,100 @@ describe('vdom interop', () => { createApp(App).use(vaporInteropPlugin).mount(root) expect(root.innerHTML).toBe( - ``, + ``, ) }) test('vapor parent > vdom > vdom > vapor child', () => { - const InnerVaporChild = defineVaporComponent({ - __scopeId: 'inner-vapor-child', + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', setup() { - return template('', true)() + return template('', true)() }, }) const VdomChild = { __scopeId: 'vdom-child', setup() { - return () => h(InnerVaporChild as any) + return () => h(VaporChild as any) }, } - const VdomChild2 = { - __scopeId: 'vdom-child2', + const VdomParent = { + __scopeId: 'vdom-parent', setup() { return () => h(VdomChild as any) }, } - const VaporChild = defineVaporComponent({ - __scopeId: 'vapor-child', + const VaporParent = defineVaporComponent({ + __scopeId: 'vapor-parent', setup() { - return createComponent(VdomChild2 as any, null, null, true) + return createComponent(VdomParent as any, null, null, true) }, }) const App = { - __scopeId: 'parent', setup() { - return () => h(VaporChild as any) + return () => h(VaporParent as any) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vapor parent > vapor slot > vdom child', () => { + const VaporSlot = defineVaporComponent({ + __scopeId: 'vapor-slot', + setup() { + const n1 = template('
', true)() as any + setInsertionState(n1) + createSlot('default', null) + return n1 + }, + }) + + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h('span') + }, + } + + const VaporParent = defineVaporComponent({ + __scopeId: 'vapor-parent', + setup() { + const n2 = createComponent( + VaporSlot, + null, + { + default: () => { + const n0 = template('
')() + const n1 = createComponent( + VdomChild, + undefined, + undefined, + undefined, + undefined, + 'vapor-parent', + ) + return [n0, n1] + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => h(VaporParent as any) }, } @@ -502,7 +617,11 @@ describe('vdom interop', () => { createApp(App).use(vaporInteropPlugin).mount(root) expect(root.innerHTML).toBe( - ``, + `
` + + `
` + + `` + + `` + + `
`, ) }) }) diff --git a/packages/runtime-vapor/src/apiCreateApp.ts b/packages/runtime-vapor/src/apiCreateApp.ts index 834437ee350..1a9ed06a735 100644 --- a/packages/runtime-vapor/src/apiCreateApp.ts +++ b/packages/runtime-vapor/src/apiCreateApp.ts @@ -41,6 +41,8 @@ const mountApp: AppMountFn = (app, container) => { app._props as RawProps, null, false, + false, + undefined, app._context, ) mountComponent(instance, container) @@ -61,6 +63,8 @@ const hydrateApp: AppMountFn = (app, container) => { app._props as RawProps, null, false, + false, + undefined, app._context, ) mountComponent(instance, container) diff --git a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts index 33697b4ca76..e436f77e291 100644 --- a/packages/runtime-vapor/src/apiCreateDynamicComponent.ts +++ b/packages/runtime-vapor/src/apiCreateDynamicComponent.ts @@ -12,6 +12,8 @@ export function createDynamicComponent( rawProps?: RawProps | null, rawSlots?: RawSlots | null, isSingleRoot?: boolean, + once?: boolean, + scopeId?: string, ): VaporFragment { const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor @@ -28,6 +30,8 @@ export function createDynamicComponent( rawProps, rawSlots, isSingleRoot, + once, + scopeId, ), value, ) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index a67e43543e9..a5a63807ee1 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -35,6 +35,11 @@ export class DynamicFragment extends VaporFragment { scope: EffectScope | undefined current?: BlockFn fallback?: BlockFn + /** + * slot only + * indicates forwarded slot + */ + forwarded?: boolean constructor(anchorLabel?: string) { super([]) @@ -206,7 +211,6 @@ export function setScopeId(block: Block, scopeId: string): void { export function setComponentScopeId(instance: VaporComponentInstance): void { const parent = instance.parent if (!parent) return - if (isArray(instance.block) && instance.block.length > 1) return const scopeId = parent.type.__scopeId diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index ea01450a119..4708db61885 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -145,6 +145,8 @@ export function createComponent( rawProps?: LooseRawProps | null, rawSlots?: LooseRawSlots | null, isSingleRoot?: boolean, + once?: boolean, // TODO once support + scopeId?: string, appContext: GenericAppContext = (currentInstance && currentInstance.appContext) || emptyContext, @@ -163,6 +165,7 @@ export function createComponent( component as any, rawProps, rawSlots, + scopeId, ) if (!isHydrating && _insertionParent) { insert(frag, _insertionParent, _insertionAnchor) @@ -282,6 +285,8 @@ export function createComponent( onScopeDispose(() => unmountComponent(instance), true) + if (scopeId) setScopeId(instance.block, scopeId) + if (!isHydrating && _insertionParent) { mountComponent(instance, _insertionParent, _insertionAnchor) } @@ -477,16 +482,25 @@ export function createComponentWithFallback( rawProps?: LooseRawProps | null, rawSlots?: LooseRawSlots | null, isSingleRoot?: boolean, + once?: boolean, + scopeId?: string, ): HTMLElement | VaporComponentInstance { if (!isString(comp)) { - return createComponent(comp, rawProps, rawSlots, isSingleRoot) + return createComponent( + comp, + rawProps, + rawSlots, + isSingleRoot, + once, + scopeId, + ) } const el = document.createElement(comp) // mark single root ;(el as any).$root = isSingleRoot - const scopeId = currentInstance!.type.__scopeId + scopeId = scopeId || currentInstance!.type.__scopeId if (scopeId) setScopeId(el, scopeId) if (rawProps) { diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 19e9b5b6d1a..32dd235a0e7 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -1,5 +1,11 @@ import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared' -import { type Block, type BlockFn, DynamicFragment, insert } from './block' +import { + type Block, + type BlockFn, + DynamicFragment, + insert, + setScopeId, +} from './block' import { rawPropsProxyHandlers } from './componentProps' import { currentInstance, isRef } from '@vue/runtime-dom' import type { LooseRawProps, VaporComponentInstance } from './component' @@ -156,9 +162,27 @@ export function createSlot( } } + if (i) fragment.forwarded = true + if (i || !hasForwardedSlot(fragment.nodes)) { + const scopeId = instance!.type.__scopeId + if (scopeId) setScopeId(fragment, `${scopeId}-s`) + } + if (!isHydrating && _insertionParent) { insert(fragment, _insertionParent, _insertionAnchor) } return fragment } + +function isForwardedSlot(block: Block): block is DynamicFragment { + return block instanceof DynamicFragment && !!block.forwarded +} + +function hasForwardedSlot(block: Block): block is DynamicFragment { + if (isArray(block)) { + return block.some(isForwardedSlot) + } else { + return isForwardedSlot(block) + } +} diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index e4d3c8de3b3..af86b291b27 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -155,6 +155,7 @@ function createVDOMComponent( component: ConcreteComponent, rawProps?: LooseRawProps | null, rawSlots?: LooseRawSlots | null, + scopeId?: string, ): VaporFragment { const frag = new VaporFragment([]) const vnode = createVNode( @@ -183,7 +184,7 @@ function createVDOMComponent( internals.umt(vnode.component!, null, !!parentNode) } - vnode.scopeId = parentInstance.type.__scopeId! + vnode.scopeId = scopeId || parentInstance.type.__scopeId! frag.insert = (parentNode, anchor) => { if (!isMounted) {