From 228b43064b4abe4146158fd6bdc19fc676b76244 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 21 Oct 2025 16:48:05 +0800 Subject: [PATCH 1/7] feat(vapor): set scopeId --- packages/runtime-core/src/index.ts | 6 +- packages/runtime-core/src/renderer.ts | 78 ++- .../runtime-vapor/__tests__/scopeId.spec.ts | 616 ++++++++++++++++++ packages/runtime-vapor/src/block.ts | 39 ++ packages/runtime-vapor/src/component.ts | 17 +- packages/runtime-vapor/src/componentSlots.ts | 82 ++- packages/runtime-vapor/src/vdomInterop.ts | 7 + 7 files changed, 813 insertions(+), 32 deletions(-) create mode 100644 packages/runtime-vapor/__tests__/scopeId.spec.ts diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index b15fe1e6960..9444c2efddf 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -516,7 +516,11 @@ export { type VaporInteropInterface } from './apiCreateApp' /** * @internal */ -export { type RendererInternals, MoveType, invalidateMount } 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 0cf7c351d0e..9cdc571921c 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -777,30 +777,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]) } } @@ -2792,3 +2771,54 @@ export 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 new file mode 100644 index 00000000000..5947e702625 --- /dev/null +++ b/packages/runtime-vapor/__tests__/scopeId.spec.ts @@ -0,0 +1,616 @@ +import { createApp, h } from '@vue/runtime-dom' +import { + createComponent, + createDynamicComponent, + createSlot, + defineVaporComponent, + forwardedSlotCreator, + 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('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() + + // slot content should have: + // - scopeId from parent + // - slotted scopeId (with `-s` postfix) from child (the tree owner) + expect(html()).toBe( + `
` + + `
` + + // component inside slot should have: + // - scopeId from template context + // - slotted scopeId from slot owner + // - its own scopeId + `` + + `` + + `
`, + ) + }) + + test(':slotted on forwarded slots', async () => { + const Wrapper = defineVaporComponent({ + __scopeId: 'wrapper', + setup() { + //
+ const n1 = template('
', true)() as any + setInsertionState(n1) + createSlot('default', null, undefined, undefined, true /* noSlotted */) + 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', () => { + test('vdom parent > vapor child', () => { + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return template('', true)() + }, + }) + + const VdomParent = { + __scopeId: 'vdom-parent', + setup() { + return () => h(VaporChild as any) + }, + } + + const App = { + setup() { + return () => h(VdomParent) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vdom parent > vapor > 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 VdomParent = { + __scopeId: 'vdom-parent', + setup() { + return () => h(VaporChild as any) + }, + } + + const App = { + setup() { + return () => h(VdomParent) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vdom parent > vapor > vapor > vdom child', () => { + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h('button') + }, + } + + const NestedVaporChild = defineVaporComponent({ + __scopeId: 'nested-vapor-child', + setup() { + return createComponent(VdomChild as any, null, null, true) + }, + }) + + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return createComponent(NestedVaporChild as any, null, null, true) + }, + }) + + const VdomParent = { + __scopeId: 'vdom-parent', + setup() { + return () => h(VaporChild as any) + }, + } + + const App = { + setup() { + return () => h(VdomParent) + }, + } + + 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 VdomParent = { + __scopeId: 'vdom-parent', + setup() { + return () => h(VaporChild as any) + }, + } + + const App = { + setup() { + return () => h(VdomParent) + }, + } + + 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 VaporParent = defineVaporComponent({ + __scopeId: 'vapor-parent', + setup() { + return createComponent(VdomChild as any, null, null, true) + }, + }) + + const App = { + setup() { + return () => h(VaporParent as any) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vapor parent > vdom > vapor child', () => { + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return template('', true)() + }, + }) + + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h(VaporChild as any) + }, + } + + const VaporParent = defineVaporComponent({ + __scopeId: 'vapor-parent', + setup() { + return createComponent(VdomChild as any, null, null, true) + }, + }) + + const App = { + setup() { + return () => h(VaporParent as any) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + ``, + ) + }) + + test('vapor parent > vdom > vdom > vapor child', () => { + const VaporChild = defineVaporComponent({ + __scopeId: 'vapor-child', + setup() { + return template('', true)() + }, + }) + + const VdomChild = { + __scopeId: 'vdom-child', + setup() { + return () => h(VaporChild as any) + }, + } + + const VdomParent = { + __scopeId: 'vdom-parent', + setup() { + return () => h(VdomChild as any) + }, + } + + const VaporParent = defineVaporComponent({ + __scopeId: 'vapor-parent', + setup() { + return createComponent(VdomParent as any, null, null, true) + }, + }) + + const App = { + setup() { + 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) + return [n0, n1] + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => h(VaporParent as any) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + expect(root.innerHTML).toBe( + `
` + + `
` + + `` + + `` + + `
`, + ) + }) +}) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index c4c2f0e188a..fcda9d1b50d 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -11,6 +11,7 @@ import { type TransitionHooks, type TransitionProps, type TransitionState, + getInheritedScopeIds, performTransitionEnter, performTransitionLeave, } from '@vue/runtime-dom' @@ -220,3 +221,41 @@ export function isFragmentBlock(block: Block): boolean { } return false } + +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) + } + + // 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) + } + } +} diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 1952b31048d..7614e5f395c 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -28,7 +28,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, @@ -382,8 +389,6 @@ export function setupComponent( } } - // TODO: scopeid - setActiveSub(prevSub) setCurrentInstance(...prevInstance) @@ -640,6 +645,11 @@ export function createComponentWithFallback( // mark single root ;(el as any).$root = isSingleRoot + if (!isHydrating) { + const scopeId = currentInstance!.type.__scopeId + if (scopeId) setScopeId(el, scopeId) + } + if (rawProps) { const setFn = () => setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)]) @@ -690,6 +700,7 @@ export function mountComponent( if (instance.bm) invokeArrayFns(instance.bm) if (!isHydrating) { insert(instance.block, parent, anchor) + setComponentScopeId(instance) } if (instance.m) queuePostFlushCb(instance.m!) if ( diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index aa0651658c0..d9eaeb679e8 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -1,7 +1,7 @@ import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared' -import { type Block, type BlockFn, insert } from './block' +import { type Block, type BlockFn, insert, setScopeId } from './block' import { rawPropsProxyHandlers } from './componentProps' -import { currentInstance, isRef } from '@vue/runtime-dom' +import { currentInstance, isRef, setCurrentInstance } from '@vue/runtime-dom' import type { LooseRawProps, VaporComponentInstance } from './component' import { renderEffect } from './renderEffect' import { @@ -17,6 +17,23 @@ import { } from './dom/hydration' import { DynamicFragment, type VaporFragment } from './fragment' +/** + * Current slot scopeIds for vdom interop + * @internal + */ +export let currentSlotScopeIds: string[] | null = null + +/** + * @internal + */ +export function setCurrentSlotScopeIds( + scopeIds: string[] | null, +): string[] | null { + const prev = currentSlotScopeIds + currentSlotScopeIds = scopeIds + return prev +} + export type RawSlots = Record & { $?: DynamicSlotSource[] } @@ -104,7 +121,7 @@ export function forwardedSlotCreator(): ( ) => Block { const instance = currentInstance as VaporComponentInstance return (name, rawProps, fallback) => - createSlot(name, rawProps, fallback, instance) + createSlot(name, rawProps, fallback, instance, false /* noSlotted */) } export function createSlot( @@ -112,6 +129,7 @@ export function createSlot( rawProps?: LooseRawProps | null, fallback?: VaporSlot, i?: VaporComponentInstance, + noSlotted?: boolean, ): Block { const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor @@ -146,7 +164,37 @@ export function createSlot( fragment.fallback = fallback // create and cache bound version of the slot to make it stable // so that we avoid unnecessary updates if it resolves to the same slot - fragment.update(slot._bound || (slot._bound = () => slot(slotProps))) + // Note: slot content should be rendered in the context where it was defined (parent), + // not the slot owner (current instance), so the components inside don't inherit + // the slot owner's scopeId + const slotContext = instance.parent + + // Calculate slotScopeIds for vdom interop + const slotScopeIds: string[] = [] + if (!noSlotted) { + const slotOwnerForScopeId = i || instance + const scopeId = slotOwnerForScopeId!.type.__scopeId + if (scopeId) { + slotScopeIds.push(`${scopeId}-s`) + } + } + + fragment.update( + slot._bound || + (slot._bound = () => { + // Temporarily switch to parent context for slot content + const [prevInstance] = setCurrentInstance(slotContext as any) + const prevSlotScopeIds = setCurrentSlotScopeIds( + slotScopeIds.length > 0 ? slotScopeIds : null, + ) + try { + return slot(slotProps) + } finally { + setCurrentInstance(prevInstance) + setCurrentSlotScopeIds(prevSlotScopeIds) + } + }), + ) } else { fragment.update(fallback) } @@ -161,6 +209,32 @@ export function createSlot( } if (!isHydrating) { + // Apply scopeId based on vdom slot scopeId rules: + // 1. Apply slotted scopeId (-s) unless noSlotted is true + // - If noSlotted is true, this slot is just forwarding and should not add its own -s + // - If this is a forwarded slot (i !== undefined), use i's scopeId + // (the component that first receives the slot content) + // - Otherwise, use instance's scopeId (the component rendering ) + if (!noSlotted) { + const slotOwnerForScopeId = i || instance + const scopeId = slotOwnerForScopeId!.type.__scopeId + if (scopeId) { + setScopeId(fragment, `${scopeId}-s`) + } + } + + // 2. Apply parent component's scopeId + // This represents the template context where slot content is defined + // For forwarded slots, this should be the parent of the forwarding component (i) + // Skip this if noSlotted is true (already handled by inner createSlot) + if (!noSlotted) { + const contextParent = i ? i.parent : instance.parent + if (contextParent && contextParent.type.__scopeId) { + const parentScopeId = contextParent.type.__scopeId + setScopeId(fragment, `${parentScopeId}`) + } + } + if (_insertionParent) insert(fragment, _insertionParent, _insertionAnchor) } else { if (fragment.insert) { diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index d3cb8d243a8..a44c078e6aa 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -59,6 +59,7 @@ import { } from '@vue/shared' import { type RawProps, rawPropsProxyHandlers } from './componentProps' import type { RawSlots, VaporSlot } from './componentSlots' +import { currentSlotScopeIds } from './componentSlots' import { renderEffect } from './renderEffect' import { _next, createTextNode } from './dom/node' import { optimizePropertyLookup } from './dom/prop' @@ -331,6 +332,9 @@ function createVDOMComponent( frag.nodes = vnode.el as any } + vnode.scopeId = parentInstance && parentInstance.type.__scopeId! + vnode.slotScopeIds = currentSlotScopeIds + frag.insert = (parentNode, anchor, transition) => { if (isHydrating) return if (vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { @@ -485,6 +489,9 @@ function renderVDOMSlot( parentNode!, anchor, parentComponent as any, + null, // parentSuspense + undefined, // namespace + vnode!.slotScopeIds, // pass slotScopeIds for :slotted styles ) oldVNode = vnode! frag.nodes = vnode!.el as any From 8a2f065826236c20af65724dc96d6cf4e82758eb Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 21 Oct 2025 18:08:48 +0800 Subject: [PATCH 2/7] wip: save --- .../transformSlotOutlet.spec.ts.snap | 27 +++++++++ .../transforms/transformSlotOutlet.spec.ts | 60 +++++++++++++++++++ .../src/generators/slotOutlet.ts | 4 +- packages/compiler-vapor/src/ir/index.ts | 1 + .../src/transforms/transformSlotOutlet.ts | 1 + .../runtime-vapor/__tests__/scopeId.spec.ts | 8 +-- packages/runtime-vapor/src/componentSlots.ts | 32 ++++------ 7 files changed, 106 insertions(+), 27 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap index caac138dcef..8a495056a70 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap @@ -111,6 +111,33 @@ export function render(_ctx) { }" `; +exports[`compiler: transform outlets > slot outlet with scopeId and slotted=false should generate noSlotted 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot("default", null, null, undefined, true) + return n0 +}" +`; + +exports[`compiler: transform outlets > slot outlet with scopeId and slotted=true should not generate noSlotted 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot("default", null, null, undefined) + return n0 +}" +`; + +exports[`compiler: transform outlets > slot outlet without scopeId should not generate noSlotted 1`] = ` +"import { createSlot as _createSlot } from 'vue'; + +export function render(_ctx) { + const n0 = _createSlot("default", null, null, undefined) + return n0 +}" +`; + exports[`compiler: transform outlets > statically named slot outlet 1`] = ` "import { createSlot as _createSlot } from 'vue'; diff --git a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts index 389c665a12f..39b2bdcc8c3 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformSlotOutlet.spec.ts @@ -277,4 +277,64 @@ describe('compiler: transform outlets', () => { }, }) }) + + test('slot outlet with scopeId and slotted=false should generate noSlotted', () => { + const { ir, code } = compileWithSlotsOutlet(``, { + scopeId: 'test-scope', + slotted: false, + }) + expect(code).toMatchSnapshot() + expect(code).toContain('true') + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'default', + isStatic: true, + }, + props: [], + fallback: undefined, + noSlotted: true, + }) + }) + + test('slot outlet with scopeId and slotted=true should not generate noSlotted', () => { + const { ir, code } = compileWithSlotsOutlet(``, { + scopeId: 'test-scope', + slotted: true, + }) + expect(code).toMatchSnapshot() + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'default', + isStatic: true, + }, + props: [], + fallback: undefined, + noSlotted: false, + }) + }) + + test('slot outlet without scopeId should not generate noSlotted', () => { + const { ir, code } = compileWithSlotsOutlet(``, { + slotted: false, + }) + expect(code).toMatchSnapshot() + expect(ir.block.dynamic.children[0].operation).toMatchObject({ + type: IRNodeTypes.SLOT_OUTLET_NODE, + id: 0, + name: { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'default', + isStatic: true, + }, + props: [], + fallback: undefined, + noSlotted: false, + }) + }) }) diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts index dc992ae2334..6935d681b23 100644 --- a/packages/compiler-vapor/src/generators/slotOutlet.ts +++ b/packages/compiler-vapor/src/generators/slotOutlet.ts @@ -12,7 +12,7 @@ export function genSlotOutlet( context: CodegenContext, ): CodeFragment[] { const { helper } = context - const { id, name, fallback, forwarded } = oper + const { id, name, fallback, forwarded, noSlotted } = oper const [frag, push] = buildCodeFragment() const nameExpr = name.isStatic @@ -32,6 +32,8 @@ export function genSlotOutlet( nameExpr, genRawProps(oper.props, context) || 'null', fallbackArg, + `undefined`, + noSlotted && 'true', // noSlotted ), ) diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index 76ef7c53c49..6e6b11aa35f 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -221,6 +221,7 @@ export interface SlotOutletIRNode extends BaseIRNode { props: IRProps[] fallback?: BlockIRNode forwarded?: boolean + noSlotted?: boolean parent?: number anchor?: number append?: boolean diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 75d0c26f4af..2b1b27c6313 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -108,6 +108,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { props: irProps, fallback, forwarded: context.inSlot, + noSlotted: !!(context.options.scopeId && !context.options.slotted), } } } diff --git a/packages/runtime-vapor/__tests__/scopeId.spec.ts b/packages/runtime-vapor/__tests__/scopeId.spec.ts index 5947e702625..6374fa8d8c2 100644 --- a/packages/runtime-vapor/__tests__/scopeId.spec.ts +++ b/packages/runtime-vapor/__tests__/scopeId.spec.ts @@ -343,7 +343,7 @@ describe('vdom interop', () => { ) }) - test('vdom parent > vapor > vdom child', () => { + test('vdom parent > vapor child > vdom child', () => { const VdomChild = { __scopeId: 'vdom-child', setup() { @@ -379,7 +379,7 @@ describe('vdom interop', () => { ) }) - test('vdom parent > vapor > vapor > vdom child', () => { + test('vdom parent > vapor child > vapor child > vdom child', () => { const VdomChild = { __scopeId: 'vdom-child', setup() { @@ -480,7 +480,7 @@ describe('vdom interop', () => { ) }) - test('vapor parent > vdom > vapor child', () => { + test('vapor parent > vdom child > vapor child', () => { const VaporChild = defineVaporComponent({ __scopeId: 'vapor-child', setup() { @@ -516,7 +516,7 @@ describe('vdom interop', () => { ) }) - test('vapor parent > vdom > vdom > vapor child', () => { + test('vapor parent > vdom child > vdom child > vapor child', () => { const VaporChild = defineVaporComponent({ __scopeId: 'vapor-child', setup() { diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index d9eaeb679e8..d98a19463e4 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -172,8 +172,7 @@ export function createSlot( // Calculate slotScopeIds for vdom interop const slotScopeIds: string[] = [] if (!noSlotted) { - const slotOwnerForScopeId = i || instance - const scopeId = slotOwnerForScopeId!.type.__scopeId + const scopeId = instance!.type.__scopeId if (scopeId) { slotScopeIds.push(`${scopeId}-s`) } @@ -183,14 +182,14 @@ export function createSlot( slot._bound || (slot._bound = () => { // Temporarily switch to parent context for slot content - const [prevInstance] = setCurrentInstance(slotContext as any) + const prev = setCurrentInstance(slotContext) const prevSlotScopeIds = setCurrentSlotScopeIds( slotScopeIds.length > 0 ? slotScopeIds : null, ) try { return slot(slotProps) } finally { - setCurrentInstance(prevInstance) + setCurrentInstance(...prev) setCurrentSlotScopeIds(prevSlotScopeIds) } }), @@ -209,29 +208,18 @@ export function createSlot( } if (!isHydrating) { - // Apply scopeId based on vdom slot scopeId rules: - // 1. Apply slotted scopeId (-s) unless noSlotted is true - // - If noSlotted is true, this slot is just forwarding and should not add its own -s - // - If this is a forwarded slot (i !== undefined), use i's scopeId - // (the component that first receives the slot content) - // - Otherwise, use instance's scopeId (the component rendering ) if (!noSlotted) { - const slotOwnerForScopeId = i || instance - const scopeId = slotOwnerForScopeId!.type.__scopeId + // apply slotted scopeId (-s) + const scopeId = instance!.type.__scopeId if (scopeId) { setScopeId(fragment, `${scopeId}-s`) } - } - // 2. Apply parent component's scopeId - // This represents the template context where slot content is defined - // For forwarded slots, this should be the parent of the forwarding component (i) - // Skip this if noSlotted is true (already handled by inner createSlot) - if (!noSlotted) { - const contextParent = i ? i.parent : instance.parent - if (contextParent && contextParent.type.__scopeId) { - const parentScopeId = contextParent.type.__scopeId - setScopeId(fragment, `${parentScopeId}`) + // apply parent component's scopeId + const parent = i ? i.parent : instance.parent + if (parent) { + const parentScopeId = parent.type.__scopeId + if (parentScopeId) setScopeId(fragment, `${parentScopeId}`) } } From 81d30c1e8468a34f119cfb18e1910bf27e2366fe Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 21 Oct 2025 21:20:23 +0800 Subject: [PATCH 3/7] wip: save --- .../transformSlotOutlet.spec.ts.snap | 4 +-- .../src/generators/slotOutlet.ts | 2 +- packages/runtime-vapor/src/block.ts | 29 ++++++++++++------- packages/runtime-vapor/src/component.ts | 2 +- packages/runtime-vapor/src/componentSlots.ts | 12 ++------ 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap index 8a495056a70..f53323247dc 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap @@ -124,7 +124,7 @@ exports[`compiler: transform outlets > slot outlet with scopeId and slott "import { createSlot as _createSlot } from 'vue'; export function render(_ctx) { - const n0 = _createSlot("default", null, null, undefined) + const n0 = _createSlot("default", null) return n0 }" `; @@ -133,7 +133,7 @@ exports[`compiler: transform outlets > slot outlet without scopeId should "import { createSlot as _createSlot } from 'vue'; export function render(_ctx) { - const n0 = _createSlot("default", null, null, undefined) + const n0 = _createSlot("default", null) return n0 }" `; diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts index 6935d681b23..17ed92d946a 100644 --- a/packages/compiler-vapor/src/generators/slotOutlet.ts +++ b/packages/compiler-vapor/src/generators/slotOutlet.ts @@ -32,7 +32,7 @@ export function genSlotOutlet( nameExpr, genRawProps(oper.props, context) || 'null', fallbackArg, - `undefined`, + noSlotted && 'undefined', // instance noSlotted && 'true', // noSlotted ), ) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index fcda9d1b50d..729ebee16fc 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -222,28 +222,33 @@ export function isFragmentBlock(block: Block): boolean { return false } -export function setScopeId(block: Block, scopeId: string): void { +export function setScopeId(block: Block, scopeIds: string[]): void { if (block instanceof Element) { - block.setAttribute(scopeId, '') + for (const id of scopeIds) { + block.setAttribute(id, '') + } } else if (isVaporComponent(block)) { - setScopeId(block.block, scopeId) + setScopeId(block.block, scopeIds) } else if (isArray(block)) { for (const b of block) { - setScopeId(b, scopeId) + setScopeId(b, scopeIds) } } else if (isFragment(block)) { - setScopeId(block.nodes, scopeId) + setScopeId(block.nodes, scopeIds) } } export function setComponentScopeId(instance: VaporComponentInstance): void { const parent = instance.parent if (!parent) return + // prevent setting scopeId on multi-root fragments if (isArray(instance.block) && instance.block.length > 1) return + const scopeIds: string[] = [] + const scopeId = parent.type.__scopeId if (scopeId) { - setScopeId(instance.block, scopeId) + scopeIds.push(scopeId) } // inherit scopeId from vdom parent @@ -252,10 +257,12 @@ export function setComponentScopeId(instance: VaporComponentInstance): void { (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) - } + scopeIds.push(parent.vnode!.scopeId) + const inheritedScopeIds = getInheritedScopeIds(parent.vnode!, parent.parent) + scopeIds.push(...inheritedScopeIds) + } + + if (scopeIds.length > 0) { + setScopeId(instance.block, scopeIds) } } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 7614e5f395c..e9e288031f4 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -647,7 +647,7 @@ export function createComponentWithFallback( if (!isHydrating) { const scopeId = currentInstance!.type.__scopeId - if (scopeId) setScopeId(el, 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 d98a19463e4..a9906b2ac42 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -209,17 +209,9 @@ export function createSlot( if (!isHydrating) { if (!noSlotted) { - // apply slotted scopeId (-s) - const scopeId = instance!.type.__scopeId + const scopeId = instance.type.__scopeId if (scopeId) { - setScopeId(fragment, `${scopeId}-s`) - } - - // apply parent component's scopeId - const parent = i ? i.parent : instance.parent - if (parent) { - const parentScopeId = parent.type.__scopeId - if (parentScopeId) setScopeId(fragment, `${parentScopeId}`) + setScopeId(fragment, [`${scopeId}-s`]) } } From 9051e9d61c40c1c7b6f0afd2aae661fe2cea0f1b Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 21 Oct 2025 22:53:06 +0800 Subject: [PATCH 4/7] wip: save --- packages/runtime-vapor/src/component.ts | 26 ++++++++++ packages/runtime-vapor/src/componentSlots.ts | 54 +++++++++++++------- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index e9e288031f4..0ef810c9252 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -166,6 +166,25 @@ export type LooseRawSlots = Record & { $?: DynamicSlotSource[] } +/** + * Attach slot definition context to all slots + * This allows slot content to be rendered in the correct parent context + */ +function attachSlotContext( + rawSlots: LooseRawSlots, + ctx: VaporComponentInstance, +): void { + // Attach context to static slots + for (const key in rawSlots) { + if (key === '$') continue + const slot = rawSlots[key] + if (typeof slot === 'function' && !slot._ctx) { + slot._ctx = ctx + } + } + // Dynamic slots are handled in getSlot when they are resolved +} + export function createComponent( component: VaporComponent, rawProps?: LooseRawProps | null, @@ -250,6 +269,13 @@ export function createComponent( return frag as any } + // Attach slot definition context to slots + // This context will be used when rendering slot content to ensure + // components created inside slots have the correct parent + if (rawSlots && currentInstance && currentInstance.vapor) { + attachSlotContext(rawSlots, currentInstance as VaporComponentInstance) + } + const instance = new VaporComponentInstance( component, rawProps as RawProps, diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index a9906b2ac42..af4d9e6c415 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -36,11 +36,14 @@ export function setCurrentSlotScopeIds( export type RawSlots = Record & { $?: DynamicSlotSource[] + _ctx?: VaporComponentInstance | null // slot definition context } export type StaticSlots = Record -export type VaporSlot = BlockFn +export type VaporSlot = BlockFn & { + _ctx?: VaporComponentInstance | null // slot definition context +} export type DynamicSlot = { name: string; fn: VaporSlot } export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[] export type DynamicSlotSource = StaticSlots | DynamicSlotFn @@ -98,10 +101,22 @@ export function getSlot( if (slot) { if (isArray(slot)) { for (const s of slot) { - if (String(s.name) === key) return s.fn + if (String(s.name) === key) { + // Attach context to dynamic slot if not already attached + const fn = s.fn + if (!fn._ctx && target._ctx) { + fn._ctx = target._ctx + } + return fn + } } } else if (String(slot.name) === key) { - return slot.fn + // Attach context to dynamic slot if not already attached + const fn = slot.fn + if (!fn._ctx && target._ctx) { + fn._ctx = target._ctx + } + return fn } } } else if (hasOwn(source, key)) { @@ -158,30 +173,33 @@ export function createSlot( ? new DynamicFragment('slot') : new DynamicFragment() const isDynamicName = isFunction(name) + + // Calculate slotScopeIds once (for vdom interop) + const slotScopeIds: string[] = [] + if (!noSlotted) { + const scopeId = instance!.type.__scopeId + if (scopeId) { + slotScopeIds.push(`${scopeId}-s`) + } + } + const renderSlot = () => { const slot = getSlot(rawSlots, isFunction(name) ? name() : name) if (slot) { fragment.fallback = fallback - // create and cache bound version of the slot to make it stable + // Create and cache bound version of the slot to make it stable // so that we avoid unnecessary updates if it resolves to the same slot - // Note: slot content should be rendered in the context where it was defined (parent), - // not the slot owner (current instance), so the components inside don't inherit - // the slot owner's scopeId - const slotContext = instance.parent - - // Calculate slotScopeIds for vdom interop - const slotScopeIds: string[] = [] - if (!noSlotted) { - const scopeId = instance!.type.__scopeId - if (scopeId) { - slotScopeIds.push(`${scopeId}-s`) - } - } fragment.update( slot._bound || (slot._bound = () => { - // Temporarily switch to parent context for slot content + // Temporarily switch currentInstance to the slot's definition context. + // This ensures components created inside the slot use the correct parent + // (where the slot was defined), not the slot owner. + // Falls back to instance.parent if slot._ctx is not set. + const slotContext = slot._ctx || instance.parent + + // TODO: create withVaporCtx helper to simplify this const prev = setCurrentInstance(slotContext) const prevSlotScopeIds = setCurrentSlotScopeIds( slotScopeIds.length > 0 ? slotScopeIds : null, From 26a1fce6cfd7ec6d53fce28b388ea87d00c163b9 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 22 Oct 2025 09:21:52 +0800 Subject: [PATCH 5/7] feat(runtime-vapor): add withVaporCtx function to manage currentInstance context in slot blocks --- .../__snapshots__/compile.spec.ts.snap | 6 +- .../TransformTransition.spec.ts.snap | 36 ++--- .../__snapshots__/vSlot.spec.ts.snap | 152 +++++++++--------- .../__tests__/transforms/vSlot.spec.ts | 14 +- .../src/generators/component.ts | 3 + packages/runtime-vapor/src/componentSlots.ts | 14 +- packages/runtime-vapor/src/index.ts | 6 +- 7 files changed, 125 insertions(+), 106 deletions(-) diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index 0120f5f4878..62465315737 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -26,7 +26,7 @@ export function render(_ctx) { `; exports[`compile > custom directive > component 1`] = ` -"import { resolveComponent as _resolveComponent, resolveDirective as _resolveDirective, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, withVaporDirectives as _withVaporDirectives, createIf as _createIf, template as _template } from 'vue'; +"import { resolveComponent as _resolveComponent, resolveDirective as _resolveDirective, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, withVaporDirectives as _withVaporDirectives, createIf as _createIf, withVaporCtx as _withVaporCtx, template as _template } from 'vue'; const t0 = _template("
") export function render(_ctx) { @@ -35,7 +35,7 @@ export function render(_ctx) { const _directive_hello = _resolveDirective("hello") const _directive_test = _resolveDirective("test") const n4 = _createComponentWithFallback(_component_Comp, null, { - "default": () => { + "default": _withVaporCtx(() => { const n0 = _createIf(() => (true), () => { const n3 = t0() _setInsertionState(n3, null, true) @@ -44,7 +44,7 @@ export function render(_ctx) { return n3 }) return n0 - } + }) }, true) _withVaporDirectives(n4, [[_directive_test]]) return n4 diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap index a621f5a6ec0..9a1a369c294 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap @@ -1,46 +1,46 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`compiler: transition > basic 1`] = ` -"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, createComponent as _createComponent, template as _template } from 'vue'; +"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, withVaporCtx as _withVaporCtx, createComponent as _createComponent, template as _template } from 'vue'; const t0 = _template("

foo

") export function render(_ctx) { const n1 = _createComponent(_VaporTransition, { persisted: () => ("") }, { - "default": () => { + "default": _withVaporCtx(() => { const n0 = t0() _applyVShow(n0, () => (_ctx.show)) return n0 - } + }) }, true) return n1 }" `; exports[`compiler: transition > inject persisted when child has v-show 1`] = ` -"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, createComponent as _createComponent, template as _template } from 'vue'; +"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, withVaporCtx as _withVaporCtx, createComponent as _createComponent, template as _template } from 'vue'; const t0 = _template("
") export function render(_ctx) { const n1 = _createComponent(_VaporTransition, { persisted: () => ("") }, { - "default": () => { + "default": _withVaporCtx(() => { const n0 = t0() _applyVShow(n0, () => (_ctx.ok)) return n0 - } + }) }, true) return n1 }" `; exports[`compiler: transition > the v-if/else-if/else branches in Transition should ignore comments 1`] = ` -"import { VaporTransition as _VaporTransition, setInsertionState as _setInsertionState, createIf as _createIf, createComponent as _createComponent, template as _template } from 'vue'; +"import { VaporTransition as _VaporTransition, setInsertionState as _setInsertionState, createIf as _createIf, withVaporCtx as _withVaporCtx, createComponent as _createComponent, template as _template } from 'vue'; const t0 = _template("
hey
") const t1 = _template("

") const t2 = _template("
") export function render(_ctx) { const n16 = _createComponent(_VaporTransition, null, { - "default": () => { + "default": _withVaporCtx(() => { const n0 = _createIf(() => (_ctx.a), () => { const n2 = t0() n2.$key = 2 @@ -63,14 +63,14 @@ export function render(_ctx) { return n14 })) return [n0, n3, n7] - } + }) }, true) return n16 }" `; exports[`compiler: transition > v-show + appear 1`] = ` -"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, createComponent as _createComponent, template as _template } from 'vue'; +"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, withVaporCtx as _withVaporCtx, createComponent as _createComponent, template as _template } from 'vue'; const t0 = _template("

foo

") export function render(_ctx) { @@ -79,11 +79,11 @@ export function render(_ctx) { appear: () => (""), persisted: () => ("") }, { - "default": () => { + "default": _withVaporCtx(() => { const n0 = t0() deferredApplyVShows.push(() => _applyVShow(n0, () => (_ctx.show))) return n0 - } + }) }, true) deferredApplyVShows.forEach(fn => fn()) return n1 @@ -91,37 +91,37 @@ export function render(_ctx) { `; exports[`compiler: transition > work with dynamic keyed children 1`] = ` -"import { VaporTransition as _VaporTransition, createKeyedFragment as _createKeyedFragment, createComponent as _createComponent, template as _template } from 'vue'; +"import { VaporTransition as _VaporTransition, createKeyedFragment as _createKeyedFragment, withVaporCtx as _withVaporCtx, createComponent as _createComponent, template as _template } from 'vue'; const t0 = _template("

foo

") export function render(_ctx) { const n1 = _createComponent(_VaporTransition, null, { - "default": () => { + "default": _withVaporCtx(() => { return _createKeyedFragment(() => _ctx.key, () => { const n0 = t0() n0.$key = _ctx.key return n0 }) - } + }) }, true) return n1 }" `; exports[`compiler: transition > work with v-if 1`] = ` -"import { VaporTransition as _VaporTransition, createIf as _createIf, createComponent as _createComponent, template as _template } from 'vue'; +"import { VaporTransition as _VaporTransition, createIf as _createIf, withVaporCtx as _withVaporCtx, createComponent as _createComponent, template as _template } from 'vue'; const t0 = _template("

foo

") export function render(_ctx) { const n3 = _createComponent(_VaporTransition, null, { - "default": () => { + "default": _withVaporCtx(() => { const n0 = _createIf(() => (_ctx.show), () => { const n2 = t0() n2.$key = 2 return n2 }) return n0 - } + }) }, true) return n3 }" 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 e7d34070ea3..811ebf638ef 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap @@ -1,7 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`compiler: transform slot > dynamic slots name 1`] = ` -"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +"import { resolveComponent as _resolveComponent, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template("foo") export function render(_ctx) { @@ -10,10 +10,10 @@ export function render(_ctx) { $: [ () => ({ name: _ctx.name, - fn: () => { + fn: _withVaporCtx(() => { const n0 = t0() return n0 - } + }) }) ] }, true) @@ -22,7 +22,7 @@ export function render(_ctx) { `; exports[`compiler: transform slot > dynamic slots name w/ v-for 1`] = ` -"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createForSlots as _createForSlots, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, withVaporCtx as _withVaporCtx, createForSlots as _createForSlots, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template(" ") export function render(_ctx) { @@ -31,11 +31,11 @@ export function render(_ctx) { $: [ () => (_createForSlots(_ctx.list, (item) => ({ name: item, - fn: (_slotProps0) => { + fn: _withVaporCtx((_slotProps0) => { const n0 = t0() _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["bar"]))) return n0 - } + }) }))) ] }, true) @@ -44,7 +44,7 @@ export function render(_ctx) { `; exports[`compiler: transform slot > dynamic slots name w/ v-for and provide absent key 1`] = ` -"import { resolveComponent as _resolveComponent, createForSlots as _createForSlots, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +"import { resolveComponent as _resolveComponent, withVaporCtx as _withVaporCtx, createForSlots as _createForSlots, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template("foo") export function render(_ctx) { @@ -53,10 +53,10 @@ export function render(_ctx) { $: [ () => (_createForSlots(_ctx.list, (_, __, index) => ({ name: index, - fn: () => { + fn: _withVaporCtx(() => { const n0 = t0() return n0 - } + }) }))) ] }, true) @@ -65,7 +65,7 @@ export function render(_ctx) { `; exports[`compiler: transform slot > dynamic slots name w/ v-if / v-else[-if] 1`] = ` -"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +"import { resolveComponent as _resolveComponent, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template("condition slot") const t1 = _template("another condition") const t2 = _template("else condition") @@ -77,25 +77,25 @@ export function render(_ctx) { () => (_ctx.condition ? { name: "condition", - fn: () => { + fn: _withVaporCtx(() => { const n0 = t0() return n0 - } + }) } : _ctx.anotherCondition ? { name: "condition", - fn: (_slotProps0) => { + fn: _withVaporCtx((_slotProps0) => { const n2 = t1() return n2 - } + }) } : { name: "condition", - fn: () => { + fn: _withVaporCtx(() => { const n4 = t2() return n4 - } + }) }) ] }, true) @@ -104,114 +104,114 @@ export function render(_ctx) { `; exports[`compiler: transform slot > forwarded slots > 1`] = ` -"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback } from 'vue'; export function render(_ctx) { const _createForwardedSlot = _forwardedSlotCreator() const _component_Comp = _resolveComponent("Comp") const n2 = _createComponentWithFallback(_component_Comp, null, { - "default": () => { + "default": _withVaporCtx(() => { const n1 = _createComponentWithFallback(_component_Comp, null, { - "default": () => { + "default": _withVaporCtx(() => { 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'; +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback } from 'vue'; export function render(_ctx) { const _createForwardedSlot = _forwardedSlotCreator() const _component_Comp = _resolveComponent("Comp") const n1 = _createComponentWithFallback(_component_Comp, null, { - "default": () => { + "default": _withVaporCtx(() => { 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'; +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback } from 'vue'; export function render(_ctx) { const _createForwardedSlot = _forwardedSlotCreator() const _component_Comp = _resolveComponent("Comp") const n2 = _createComponentWithFallback(_component_Comp, null, { - "default": () => { + "default": _withVaporCtx(() => { 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'; +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createFor as _createFor, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback } from 'vue'; export function render(_ctx) { const _createForwardedSlot = _forwardedSlotCreator() const _component_Comp = _resolveComponent("Comp") const n3 = _createComponentWithFallback(_component_Comp, null, { - "default": () => { + "default": _withVaporCtx(() => { 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'; +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createIf as _createIf, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback } from 'vue'; export function render(_ctx) { const _createForwardedSlot = _forwardedSlotCreator() const _component_Comp = _resolveComponent("Comp") const n3 = _createComponentWithFallback(_component_Comp, null, { - "default": () => { + "default": _withVaporCtx(() => { 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'; +"import { resolveComponent as _resolveComponent, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template("
") export function render(_ctx) { const _component_Comp = _resolveComponent("Comp") const n1 = _createComponentWithFallback(_component_Comp, null, { - "default": () => { + "default": _withVaporCtx(() => { const n0 = t0() return n0 - } + }) }, true) return n1 }" `; exports[`compiler: transform slot > named slots w/ implicit default slot 1`] = ` -"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +"import { resolveComponent as _resolveComponent, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template("foo") const t1 = _template("bar") const t2 = _template("") @@ -219,63 +219,63 @@ const t2 = _template("") export function render(_ctx) { const _component_Comp = _resolveComponent("Comp") const n4 = _createComponentWithFallback(_component_Comp, null, { - "one": () => { + "one": _withVaporCtx(() => { const n0 = t0() return n0 - }, - "default": () => { + }), + "default": _withVaporCtx(() => { const n2 = t1() const n3 = t2() return [n2, n3] - } + }) }, true) return n4 }" `; exports[`compiler: transform slot > nested component slot 1`] = ` -"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; +"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, withVaporCtx as _withVaporCtx } from 'vue'; export function render(_ctx) { const _component_B = _resolveComponent("B") const _component_A = _resolveComponent("A") const n1 = _createComponentWithFallback(_component_A, null, { - "default": () => { + "default": _withVaporCtx(() => { const n0 = _createComponentWithFallback(_component_B) return n0 - } + }) }, true) return n1 }" `; exports[`compiler: transform slot > nested slots scoping 1`] = ` -"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template(" ") export function render(_ctx) { const _component_Inner = _resolveComponent("Inner") const _component_Comp = _resolveComponent("Comp") const n5 = _createComponentWithFallback(_component_Comp, null, { - "default": (_slotProps0) => { + "default": _withVaporCtx((_slotProps0) => { const n1 = _createComponentWithFallback(_component_Inner, null, { - "default": (_slotProps1) => { + "default": _withVaporCtx((_slotProps1) => { const n0 = t0() _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _slotProps1["bar"] + _ctx.baz))) return n0 - } + }) }) const n3 = t0() _renderEffect(() => _setText(n3, " " + _toDisplayString(_slotProps0["foo"] + _ctx.bar + _ctx.baz))) return [n1, n3] - } + }) }, true) return n5 }" `; exports[`compiler: transform slot > on component dynamically named slot 1`] = ` -"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template(" ") export function render(_ctx) { @@ -284,11 +284,11 @@ export function render(_ctx) { $: [ () => ({ name: _ctx.named, - fn: (_slotProps0) => { + fn: _withVaporCtx((_slotProps0) => { const n0 = t0() _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar))) return n0 - } + }) }) ] }, true) @@ -297,48 +297,48 @@ export function render(_ctx) { `; exports[`compiler: transform slot > on component named slot 1`] = ` -"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template(" ") export function render(_ctx) { const _component_Comp = _resolveComponent("Comp") const n1 = _createComponentWithFallback(_component_Comp, null, { - "named": (_slotProps0) => { + "named": _withVaporCtx((_slotProps0) => { const n0 = t0() _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar))) return n0 - } + }) }, true) return n1 }" `; exports[`compiler: transform slot > on-component default slot 1`] = ` -"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template(" ") export function render(_ctx) { const _component_Comp = _resolveComponent("Comp") const n1 = _createComponentWithFallback(_component_Comp, null, { - "default": (_slotProps0) => { + "default": _withVaporCtx((_slotProps0) => { const n0 = t0() _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar))) return n0 - } + }) }, true) return n1 }" `; exports[`compiler: transform slot > quote slot name 1`] = ` -"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; +"import { resolveComponent as _resolveComponent, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback } from 'vue'; export function render(_ctx) { const _component_Comp = _resolveComponent("Comp") const n1 = _createComponentWithFallback(_component_Comp, null, { - "nav-bar-title-before": () => { + "nav-bar-title-before": _withVaporCtx(() => { return null - } + }) }, true) return n1 }" @@ -367,7 +367,7 @@ export function render(_ctx) { `; exports[`compiler: transform slot > with whitespace: 'preserve' > implicit default slot 1`] = ` -"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +"import { resolveComponent as _resolveComponent, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template(" Header ") const t1 = _template(" ") const t2 = _template("

") @@ -375,57 +375,57 @@ const t2 = _template("

") export function render(_ctx) { const _component_Comp = _resolveComponent("Comp") const n4 = _createComponentWithFallback(_component_Comp, null, { - "header": () => { + "header": _withVaporCtx(() => { const n0 = t0() return n0 - }, - "default": () => { + }), + "default": _withVaporCtx(() => { const n2 = t1() const n3 = t2() return [n2, n3] - } + }) }, true) return n4 }" `; exports[`compiler: transform slot > with whitespace: 'preserve' > named default slot + implicit whitespace content 1`] = ` -"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +"import { resolveComponent as _resolveComponent, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template(" Header ") const t1 = _template(" Default ") export function render(_ctx) { const _component_Comp = _resolveComponent("Comp") const n5 = _createComponentWithFallback(_component_Comp, null, { - "header": () => { + "header": _withVaporCtx(() => { const n0 = t0() return n0 - }, - "default": () => { + }), + "default": _withVaporCtx(() => { const n3 = t1() return n3 - } + }) }, true) return n5 }" `; exports[`compiler: transform slot > with whitespace: 'preserve' > should not generate whitespace only default slot 1`] = ` -"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; +"import { resolveComponent as _resolveComponent, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template(" Header ") const t1 = _template(" Footer ") export function render(_ctx) { const _component_Comp = _resolveComponent("Comp") const n5 = _createComponentWithFallback(_component_Comp, null, { - "header": () => { + "header": _withVaporCtx(() => { const n0 = t0() return n0 - }, - "footer": () => { + }), + "footer": _withVaporCtx(() => { const n3 = t1() return n3 - } + }) }, true) return n5 }" diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts index 63b6b00010b..f2e94814c43 100644 --- a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -67,7 +67,7 @@ describe('compiler: transform slot', () => { ) expect(code).toMatchSnapshot() - expect(code).contains(`"default": (_slotProps0) =>`) + expect(code).contains(`"default": _withVaporCtx((_slotProps0) =>`) expect(code).contains(`_slotProps0["foo"] + _ctx.bar`) expect(ir.block.dynamic.children[0].operation).toMatchObject({ @@ -101,7 +101,7 @@ describe('compiler: transform slot', () => { ) expect(code).toMatchSnapshot() - expect(code).contains(`"named": (_slotProps0) =>`) + expect(code).contains(`"named": _withVaporCtx((_slotProps0) =>`) expect(code).contains(`_slotProps0["foo"] + _ctx.bar`) expect(ir.block.dynamic.children[0].operation).toMatchObject({ @@ -130,7 +130,7 @@ describe('compiler: transform slot', () => { ) expect(code).toMatchSnapshot() - expect(code).contains(`fn: (_slotProps0) =>`) + expect(code).contains(`fn: _withVaporCtx((_slotProps0) =>`) expect(code).contains(`_slotProps0["foo"] + _ctx.bar`) expect(ir.block.dynamic.children[0].operation).toMatchObject({ @@ -204,8 +204,8 @@ describe('compiler: transform slot', () => { ) expect(code).toMatchSnapshot() - expect(code).contains(`"default": (_slotProps0) =>`) - expect(code).contains(`"default": (_slotProps1) =>`) + expect(code).contains(`"default": _withVaporCtx((_slotProps0) =>`) + expect(code).contains(`"default": _withVaporCtx((_slotProps1) =>`) expect(code).contains(`_slotProps0["foo"] + _slotProps1["bar"] + _ctx.baz`) expect(code).contains(`_slotProps0["foo"] + _ctx.bar + _ctx.baz`) @@ -282,7 +282,7 @@ describe('compiler: transform slot', () => { ) expect(code).toMatchSnapshot() - expect(code).contains(`fn: (_slotProps0) =>`) + expect(code).contains(`fn: _withVaporCtx((_slotProps0) =>`) expect(code).contains(`_setText(n0, _toDisplayString(_slotProps0["bar"]))`) expect(ir.block.dynamic.children[0].operation).toMatchObject({ @@ -346,7 +346,7 @@ describe('compiler: transform slot', () => { ) expect(code).toMatchSnapshot() - expect(code).contains(`fn: (_slotProps0) =>`) + expect(code).contains(`fn: _withVaporCtx((_slotProps0) =>`) expect(ir.block.dynamic.children[0].operation).toMatchObject({ type: IRNodeTypes.CREATE_COMPONENT_NODE, diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 04df6c5a48a..b1555877540 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -458,5 +458,8 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) { ] } + // wrap with withVaporCtx to ensure correct currentInstance inside slot + blockFn = [`${context.helper('withVaporCtx')}(`, ...blockFn, `)`] + return blockFn } diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index aa0651658c0..346c272b6d5 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -1,7 +1,7 @@ import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared' import { type Block, type BlockFn, insert } from './block' import { rawPropsProxyHandlers } from './componentProps' -import { currentInstance, isRef } from '@vue/runtime-dom' +import { currentInstance, isRef, setCurrentInstance } from '@vue/runtime-dom' import type { LooseRawProps, VaporComponentInstance } from './component' import { renderEffect } from './renderEffect' import { @@ -97,6 +97,18 @@ export function getSlot( } } +export function withVaporCtx(fn: Function): Function { + const instance = currentInstance as VaporComponentInstance + return (...args: any[]) => { + const prev = setCurrentInstance(instance) + try { + return fn(...args) + } finally { + setCurrentInstance(...prev) + } + } +} + export function forwardedSlotCreator(): ( name: string | (() => string), rawProps?: LooseRawProps | null, diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 3e76b6955d3..a9d919f8175 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -16,7 +16,11 @@ export { isVaporComponent, } from './component' export { renderEffect } from './renderEffect' -export { createSlot, forwardedSlotCreator } from './componentSlots' +export { + createSlot, + forwardedSlotCreator, + withVaporCtx, +} from './componentSlots' export { template } from './dom/template' export { createTextNode, child, nthChild, next, txt } from './dom/node' export { From fc5fe06e3f1f0581df9026cfad0b7d27ea61e9e6 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 22 Oct 2025 10:02:44 +0800 Subject: [PATCH 6/7] wip: save --- .../TransformTransition.spec.ts.snap | 36 +++++++++---------- .../src/generators/component.ts | 9 +++-- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap index 9a1a369c294..a621f5a6ec0 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap @@ -1,46 +1,46 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`compiler: transition > basic 1`] = ` -"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, withVaporCtx as _withVaporCtx, createComponent as _createComponent, template as _template } from 'vue'; +"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, createComponent as _createComponent, template as _template } from 'vue'; const t0 = _template("

foo

") export function render(_ctx) { const n1 = _createComponent(_VaporTransition, { persisted: () => ("") }, { - "default": _withVaporCtx(() => { + "default": () => { const n0 = t0() _applyVShow(n0, () => (_ctx.show)) return n0 - }) + } }, true) return n1 }" `; exports[`compiler: transition > inject persisted when child has v-show 1`] = ` -"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, withVaporCtx as _withVaporCtx, createComponent as _createComponent, template as _template } from 'vue'; +"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, createComponent as _createComponent, template as _template } from 'vue'; const t0 = _template("
") export function render(_ctx) { const n1 = _createComponent(_VaporTransition, { persisted: () => ("") }, { - "default": _withVaporCtx(() => { + "default": () => { const n0 = t0() _applyVShow(n0, () => (_ctx.ok)) return n0 - }) + } }, true) return n1 }" `; exports[`compiler: transition > the v-if/else-if/else branches in Transition should ignore comments 1`] = ` -"import { VaporTransition as _VaporTransition, setInsertionState as _setInsertionState, createIf as _createIf, withVaporCtx as _withVaporCtx, createComponent as _createComponent, template as _template } from 'vue'; +"import { VaporTransition as _VaporTransition, setInsertionState as _setInsertionState, createIf as _createIf, createComponent as _createComponent, template as _template } from 'vue'; const t0 = _template("
hey
") const t1 = _template("

") const t2 = _template("
") export function render(_ctx) { const n16 = _createComponent(_VaporTransition, null, { - "default": _withVaporCtx(() => { + "default": () => { const n0 = _createIf(() => (_ctx.a), () => { const n2 = t0() n2.$key = 2 @@ -63,14 +63,14 @@ export function render(_ctx) { return n14 })) return [n0, n3, n7] - }) + } }, true) return n16 }" `; exports[`compiler: transition > v-show + appear 1`] = ` -"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, withVaporCtx as _withVaporCtx, createComponent as _createComponent, template as _template } from 'vue'; +"import { VaporTransition as _VaporTransition, applyVShow as _applyVShow, createComponent as _createComponent, template as _template } from 'vue'; const t0 = _template("

foo

") export function render(_ctx) { @@ -79,11 +79,11 @@ export function render(_ctx) { appear: () => (""), persisted: () => ("") }, { - "default": _withVaporCtx(() => { + "default": () => { const n0 = t0() deferredApplyVShows.push(() => _applyVShow(n0, () => (_ctx.show))) return n0 - }) + } }, true) deferredApplyVShows.forEach(fn => fn()) return n1 @@ -91,37 +91,37 @@ export function render(_ctx) { `; exports[`compiler: transition > work with dynamic keyed children 1`] = ` -"import { VaporTransition as _VaporTransition, createKeyedFragment as _createKeyedFragment, withVaporCtx as _withVaporCtx, createComponent as _createComponent, template as _template } from 'vue'; +"import { VaporTransition as _VaporTransition, createKeyedFragment as _createKeyedFragment, createComponent as _createComponent, template as _template } from 'vue'; const t0 = _template("

foo

") export function render(_ctx) { const n1 = _createComponent(_VaporTransition, null, { - "default": _withVaporCtx(() => { + "default": () => { return _createKeyedFragment(() => _ctx.key, () => { const n0 = t0() n0.$key = _ctx.key return n0 }) - }) + } }, true) return n1 }" `; exports[`compiler: transition > work with v-if 1`] = ` -"import { VaporTransition as _VaporTransition, createIf as _createIf, withVaporCtx as _withVaporCtx, createComponent as _createComponent, template as _template } from 'vue'; +"import { VaporTransition as _VaporTransition, createIf as _createIf, createComponent as _createComponent, template as _template } from 'vue'; const t0 = _template("

foo

") export function render(_ctx) { const n3 = _createComponent(_VaporTransition, null, { - "default": _withVaporCtx(() => { + "default": () => { const n0 = _createIf(() => (_ctx.show), () => { const n2 = t0() n2.$key = 2 return n2 }) return n0 - }) + } }, true) return n3 }" diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index b1555877540..ae213af222b 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -29,6 +29,7 @@ import { import { genExpression, genVarName } from './expression' import { genPropKey, genPropValue } from './prop' import { + NodeTypes, type SimpleExpressionNode, createSimpleExpression, isMemberExpression, @@ -407,7 +408,7 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) { let propsName: string | undefined let exitScope: (() => void) | undefined let depth: number | undefined - const { props, key } = oper + const { props, key, node } = oper const idsOfProps = new Set() if (props) { @@ -458,8 +459,10 @@ function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) { ] } - // wrap with withVaporCtx to ensure correct currentInstance inside slot - blockFn = [`${context.helper('withVaporCtx')}(`, ...blockFn, `)`] + if (node.type === NodeTypes.ELEMENT && !isBuiltInComponent(node.tag)) { + // wrap with withVaporCtx to ensure correct currentInstance inside slot + blockFn = [`${context.helper('withVaporCtx')}(`, ...blockFn, `)`] + } return blockFn } From 8afb3b048bcb7e54e8188c4668ca90e9ce7291ac Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 22 Oct 2025 10:19:52 +0800 Subject: [PATCH 7/7] wip: save --- .../runtime-vapor/__tests__/scopeId.spec.ts | 17 ++++++----- packages/runtime-vapor/src/component.ts | 26 ---------------- packages/runtime-vapor/src/componentSlots.ts | 30 ++----------------- 3 files changed, 12 insertions(+), 61 deletions(-) diff --git a/packages/runtime-vapor/__tests__/scopeId.spec.ts b/packages/runtime-vapor/__tests__/scopeId.spec.ts index 6374fa8d8c2..10bad40af68 100644 --- a/packages/runtime-vapor/__tests__/scopeId.spec.ts +++ b/packages/runtime-vapor/__tests__/scopeId.spec.ts @@ -8,6 +8,7 @@ import { setInsertionState, template, vaporInteropPlugin, + withVaporCtx, } from '../src' import { makeRender } from './_utils' @@ -226,11 +227,11 @@ describe('scopeId', () => { Child, null, { - default: () => { + default: withVaporCtx(() => { const n0 = template('
')() const n1 = createComponent(Child2) return [n0, n1] - }, + }) as any, }, true, ) @@ -275,10 +276,10 @@ describe('scopeId', () => { Wrapper, null, { - default: () => { + default: withVaporCtx(() => { const n0 = _createForwardedSlot('default', null) return n0 - }, + }) as any, }, true, ) @@ -294,9 +295,9 @@ describe('scopeId', () => { Slotted, null, { - default: () => { + default: withVaporCtx(() => { return template('
')() - }, + }) as any, }, true, ) @@ -584,11 +585,11 @@ describe('vdom interop', () => { VaporSlot, null, { - default: () => { + default: withVaporCtx(() => { const n0 = template('
')() const n1 = createComponent(VdomChild) return [n0, n1] - }, + }) as any, }, true, ) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 0ef810c9252..e9e288031f4 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -166,25 +166,6 @@ export type LooseRawSlots = Record & { $?: DynamicSlotSource[] } -/** - * Attach slot definition context to all slots - * This allows slot content to be rendered in the correct parent context - */ -function attachSlotContext( - rawSlots: LooseRawSlots, - ctx: VaporComponentInstance, -): void { - // Attach context to static slots - for (const key in rawSlots) { - if (key === '$') continue - const slot = rawSlots[key] - if (typeof slot === 'function' && !slot._ctx) { - slot._ctx = ctx - } - } - // Dynamic slots are handled in getSlot when they are resolved -} - export function createComponent( component: VaporComponent, rawProps?: LooseRawProps | null, @@ -269,13 +250,6 @@ export function createComponent( return frag as any } - // Attach slot definition context to slots - // This context will be used when rendering slot content to ensure - // components created inside slots have the correct parent - if (rawSlots && currentInstance && currentInstance.vapor) { - attachSlotContext(rawSlots, currentInstance as VaporComponentInstance) - } - const instance = new VaporComponentInstance( component, rawProps as RawProps, diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 931d1890b65..6b29ad1ae0d 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -36,14 +36,11 @@ export function setCurrentSlotScopeIds( export type RawSlots = Record & { $?: DynamicSlotSource[] - _ctx?: VaporComponentInstance | null // slot definition context } export type StaticSlots = Record -export type VaporSlot = BlockFn & { - _ctx?: VaporComponentInstance | null // slot definition context -} +export type VaporSlot = BlockFn export type DynamicSlot = { name: string; fn: VaporSlot } export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[] export type DynamicSlotSource = StaticSlots | DynamicSlotFn @@ -101,22 +98,10 @@ export function getSlot( if (slot) { if (isArray(slot)) { for (const s of slot) { - if (String(s.name) === key) { - // Attach context to dynamic slot if not already attached - const fn = s.fn - if (!fn._ctx && target._ctx) { - fn._ctx = target._ctx - } - return fn - } + if (String(s.name) === key) return s.fn } } else if (String(slot.name) === key) { - // Attach context to dynamic slot if not already attached - const fn = slot.fn - if (!fn._ctx && target._ctx) { - fn._ctx = target._ctx - } - return fn + return slot.fn } } } else if (hasOwn(source, key)) { @@ -205,21 +190,12 @@ export function createSlot( fragment.update( slot._bound || (slot._bound = () => { - // Temporarily switch currentInstance to the slot's definition context. - // This ensures components created inside the slot use the correct parent - // (where the slot was defined), not the slot owner. - // Falls back to instance.parent if slot._ctx is not set. - const slotContext = slot._ctx || instance.parent - - // TODO: create withVaporCtx helper to simplify this - const prev = setCurrentInstance(slotContext) const prevSlotScopeIds = setCurrentSlotScopeIds( slotScopeIds.length > 0 ? slotScopeIds : null, ) try { return slot(slotProps) } finally { - setCurrentInstance(...prev) setCurrentSlotScopeIds(prevSlotScopeIds) } }),