]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-vapor): preserve correct parent instance for slotted content (#14095)
authoredison <daiwei521@126.com>
Fri, 14 Nov 2025 06:10:42 +0000 (14:10 +0800)
committerGitHub <noreply@github.com>
Fri, 14 Nov 2025 06:10:42 +0000 (14:10 +0800)
13 files changed:
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformSlotOutlet.spec.ts.snap
packages/compiler-vapor/src/generators/slotOutlet.ts
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-vapor/__tests__/apiInject.spec.ts
packages/runtime-vapor/__tests__/apiSetupContext.spec.ts
packages/runtime-vapor/__tests__/componentAttrs.spec.ts
packages/runtime-vapor/__tests__/componentSlots.spec.ts
packages/runtime-vapor/__tests__/customElement.spec.ts
packages/runtime-vapor/__tests__/dom/templateRef.spec.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentSlots.ts
packages/runtime-vapor/src/vdomInterop.ts

index f53323247dca1a81de1fbf772bc4c01def9d51d6..e55d3299b810996cdc1f93272d845408686251c9 100644 (file)
@@ -115,7 +115,7 @@ exports[`compiler: transform <slot> outlets > slot outlet with scopeId and slott
 "import { createSlot as _createSlot } from 'vue';
 
 export function render(_ctx) {
-  const n0 = _createSlot("default", null, null, undefined, true)
+  const n0 = _createSlot("default", null, null, true)
   return n0
 }"
 `;
index 8ba81383658a7ed2066adc5707317932778b3fe8..afacb644888bce9052372767a6ed4e1b0ccbdfbc 100644 (file)
@@ -30,7 +30,6 @@ export function genSlotOutlet(
       nameExpr,
       genRawProps(oper.props, context) || 'null',
       fallbackArg,
-      noSlotted && 'undefined', // instance
       noSlotted && 'true', // noSlotted
     ),
   )
index e3a63bf846d486d361b9283a835604261fb3e18e..2aca5f3ee1e3a91afa72b3fe6195ca875e65bfed 100644 (file)
@@ -212,7 +212,12 @@ export interface VaporInteropInterface {
     transition: TransitionHooks,
   ): void
 
-  vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any
+  vdomMount: (
+    component: ConcreteComponent,
+    parentComponent: any,
+    props?: any,
+    slots?: any,
+  ) => any
   vdomUnmount: UnmountComponentFn
   vdomSlot: (
     slots: any,
index ea185e14acc01defa98c0b2bf1de4f2d6da3eb58..845ec9e015645fa0a02367ca3aacc4d09c73c6bc 100644 (file)
@@ -12,9 +12,12 @@ import {
 } from '@vue/runtime-dom'
 import {
   createComponent,
+  createSlot,
   createTextNode,
   createVaporApp,
+  defineVaporComponent,
   renderEffect,
+  withVaporCtx,
 } from '../src'
 import { makeRender } from './_utils'
 import { setElementText } from '../src/dom/prop'
@@ -368,6 +371,32 @@ describe('api: provide/inject', () => {
     expect(host.innerHTML).toBe('')
   })
 
+  it('should work with slots', () => {
+    const Parent = defineVaporComponent({
+      setup() {
+        provide('test', 'hello')
+        return createSlot('default', null)
+      },
+    })
+
+    const Child = defineVaporComponent({
+      setup() {
+        const test = inject('test')
+        return createTextNode(toDisplayString(test))
+      },
+    })
+
+    const { host } = define({
+      setup() {
+        return createComponent(Parent, null, {
+          default: withVaporCtx(() => createComponent(Child)),
+        })
+      },
+    }).render()
+
+    expect(host.innerHTML).toBe('hello<!--slot-->')
+  })
+
   describe('hasInjectionContext', () => {
     it('should be false outside of setup', () => {
       expect(hasInjectionContext()).toBe(false)
index 5e0d5d98aa0c1870bcacfdb04e31e5a048d3ddc2..d69421432250723a9cf77458e3a7c5d8dc013c15 100644 (file)
@@ -10,6 +10,7 @@ import {
   setDynamicProps,
   setText,
   template,
+  withVaporCtx,
 } from '../src'
 import { nextTick, reactive, ref, watchEffect } from '@vue/runtime-dom'
 import { makeRender } from './_utils'
@@ -113,11 +114,11 @@ describe('api: setup context', () => {
       inheritAttrs: false,
       setup(_: any, { attrs }: any) {
         const n0 = createComponent(Wrapper, null, {
-          default: () => {
+          default: withVaporCtx(() => {
             const n0 = template('<div>')() as HTMLDivElement
             renderEffect(() => setDynamicProps(n0, [attrs]))
             return n0
-          },
+          }),
         })
         return n0
       },
index 1f43ebba8c0e748fa351fe7c7f641656387464c8..7c86c295fbd1ae5819e77614a146b0301ba76e8b 100644 (file)
@@ -10,6 +10,7 @@ import {
   setProp,
   setStyle,
   template,
+  withVaporCtx,
 } from '../src'
 import { makeRender } from './_utils'
 import { stringifyStyle } from '@vue/shared'
@@ -286,10 +287,10 @@ describe('attribute fallthrough', () => {
           () => 'button',
           null,
           {
-            default: () => {
+            default: withVaporCtx(() => {
               const n0 = createSlot('default', null)
               return n0
-            },
+            }),
           },
           true,
         )
index ca0b0b019b6446bc66b79a82f70a79ba2669b33d..69756b02339ef5c8cb6fec6ed518bbec47f1adc7 100644 (file)
@@ -177,7 +177,7 @@ describe('component: slots', () => {
 
       const { host } = define(() => {
         return createComponent(Comp, null, {
-          header: () => template('header')(),
+          header: withVaporCtx(() => template('header')()),
         })
       }).render()
 
@@ -224,7 +224,7 @@ describe('component: slots', () => {
       )
       define(() =>
         createComponent(Comp, null, {
-          default: _props => ((props = _props), []),
+          default: withVaporCtx((_props: any) => ((props = _props), [])),
         }),
       ).render()
 
@@ -252,7 +252,7 @@ describe('component: slots', () => {
       )
       define(() =>
         createComponent(Comp, null, {
-          default: _props => ((props = _props), []),
+          default: withVaporCtx((_props: any) => ((props = _props), [])),
         }),
       ).render()
 
@@ -285,13 +285,13 @@ describe('component: slots', () => {
           $: [
             () => ({
               name: 'header',
-              fn: (props: any) => {
+              fn: withVaporCtx((props: any) => {
                 const el = template('<h1></h1>')()
                 renderEffect(() => {
                   setElementText(el, props.title)
                 })
                 return el
-              },
+              }),
             }),
           ],
         })
@@ -320,8 +320,8 @@ describe('component: slots', () => {
 
       const { host } = define(() => {
         return createComponent(Comp, null, {
-          header: () => template('header')(),
-          footer: () => template('footer')(),
+          header: withVaporCtx(() => template('header')()),
+          footer: withVaporCtx(() => template('footer')()),
         })
       }).render()
 
@@ -368,8 +368,14 @@ describe('component: slots', () => {
           $: [
             () =>
               flag1.value
-                ? { name: 'one', fn: () => template('one content')() }
-                : { name: 'two', fn: () => template('two content')() },
+                ? {
+                    name: 'one',
+                    fn: withVaporCtx(() => template('one content')()),
+                  }
+                : {
+                    name: 'two',
+                    fn: withVaporCtx(() => template('two content')()),
+                  },
           ],
         })
       }).render()
@@ -413,8 +419,8 @@ describe('component: slots', () => {
           Child,
           {},
           {
-            one: () => template('one content')(),
-            two: () => template('two content')(),
+            one: withVaporCtx(() => template('one content')()),
+            two: withVaporCtx(() => template('two content')()),
           },
         )
       }).render()
@@ -461,14 +467,14 @@ describe('component: slots', () => {
       const { html } = define({
         setup() {
           return createComponent(Child, null, {
-            default: () => {
+            default: withVaporCtx(() => {
               return createIf(
                 () => toggle.value,
                 () => {
                   return document.createTextNode('content')
                 },
               )
-            },
+            }),
           })
         },
       }).render()
@@ -498,14 +504,14 @@ describe('component: slots', () => {
       const { html } = define({
         setup() {
           return createComponent(Child, null, {
-            default: () => {
+            default: withVaporCtx(() => {
               return createIf(
                 () => toggle.value,
                 () => {
                   return document.createTextNode('content')
                 },
               )
-            },
+            }),
           })
         },
       }).render()
@@ -539,9 +545,9 @@ describe('component: slots', () => {
               (toggle.value
                 ? {
                     name: val.value,
-                    fn: () => {
+                    fn: withVaporCtx(() => {
                       return template('<h1></h1>')()
-                    },
+                    }),
                   }
                 : void 0) as DynamicSlot,
           ],
@@ -567,9 +573,9 @@ describe('component: slots', () => {
       const { html } = define({
         setup() {
           return createComponent(Child, null, {
-            default: () => {
+            default: withVaporCtx(() => {
               return template('<!--comment-->')()
-            },
+            }),
           })
         },
       }).render()
@@ -591,14 +597,14 @@ describe('component: slots', () => {
       const { html } = define({
         setup() {
           return createComponent(Child, null, {
-            default: () => {
+            default: withVaporCtx(() => {
               return createIf(
                 () => toggle.value,
                 () => {
                   return document.createTextNode('content')
                 },
               )
-            },
+            }),
           })
         },
       }).render()
@@ -629,7 +635,7 @@ describe('component: slots', () => {
       const { html } = define({
         setup() {
           return createComponent(Child, null, {
-            default: () => {
+            default: withVaporCtx(() => {
               return createIf(
                 () => outerShow.value,
                 () => {
@@ -641,7 +647,7 @@ describe('component: slots', () => {
                   )
                 },
               )
-            },
+            }),
           })
         },
       }).render()
@@ -686,7 +692,7 @@ describe('component: slots', () => {
       const { html } = define({
         setup() {
           return createComponent(Child, null, {
-            default: () => {
+            default: withVaporCtx(() => {
               const n2 = createFor(
                 () => items.value,
                 for_item0 => {
@@ -699,7 +705,7 @@ describe('component: slots', () => {
                 },
               )
               return n2
-            },
+            }),
           })
         },
       }).render()
@@ -732,7 +738,7 @@ describe('component: slots', () => {
       const { html } = define({
         setup() {
           return createComponent(Child, null, {
-            default: () => {
+            default: withVaporCtx(() => {
               const n2 = createFor(
                 () => items.value,
                 for_item0 => {
@@ -745,7 +751,7 @@ describe('component: slots', () => {
                 },
               )
               return n2
-            },
+            }),
           })
         },
       }).render()
@@ -800,11 +806,11 @@ describe('component: slots', () => {
             Parent,
             null,
             {
-              foo: () => {
+              foo: withVaporCtx(() => {
                 const n0 = template(' ')() as any
                 renderEffect(() => setText(n0, foo.value))
                 return n0
-              },
+              }),
             },
             true,
           )
@@ -845,16 +851,16 @@ describe('component: slots', () => {
             Parent,
             null,
             {
-              foo: () => {
+              foo: withVaporCtx(() => {
                 const n0 = template(' ')() as any
                 renderEffect(() => setText(n0, foo.value))
                 return n0
-              },
-              default: () => {
+              }),
+              default: withVaporCtx(() => {
                 const n3 = template(' ')() as any
                 renderEffect(() => setText(n3, foo.value))
                 return n3
-              },
+              }),
             },
             true,
           )
@@ -893,7 +899,7 @@ describe('component: slots', () => {
       const { html } = define({
         setup() {
           return createComponent(Parent, null, {
-            default: () => template('<!-- <div>App</div> -->')(),
+            default: withVaporCtx(() => template('<!-- <div>App</div> -->')()),
           })
         },
       }).render()
@@ -933,7 +939,7 @@ describe('component: slots', () => {
       const { html } = define({
         setup() {
           return createComponent(Parent, null, {
-            default: () => template('<!-- <div>App</div> -->')(),
+            default: withVaporCtx(() => template('<!-- <div>App</div> -->')()),
           })
         },
       }).render()
@@ -999,6 +1005,36 @@ describe('component: slots', () => {
       expect(html()).toBe('child fallback<!--for--><!--slot--><!--slot-->')
     })
 
+    test('consecutive slots with insertion state', async () => {
+      const { component: Child } = define({
+        setup() {
+          const n2 = template('<div><div>baz</div></div>', true)() as any
+          setInsertionState(n2, 0)
+          createSlot('default', null)
+          setInsertionState(n2, 0)
+          createSlot('foo', null)
+          return n2
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return createComponent(Child, null, {
+            default: withVaporCtx(() => template('default')()),
+            foo: withVaporCtx(() => template('foo')()),
+          })
+        },
+      }).render()
+
+      expect(html()).toBe(
+        `<div>` +
+          `default<!--slot-->` +
+          `foo<!--slot-->` +
+          `<div>baz</div>` +
+          `</div>`,
+      )
+    })
+
     describe('vdom interop', () => {
       const createVaporSlot = (fallbackText = 'fallback') => {
         return defineVaporComponent({
@@ -1862,35 +1898,5 @@ describe('component: slots', () => {
         expect(root.innerHTML).toBe('<span>bar</span>')
       })
     })
-
-    test('consecutive slots with insertion state', async () => {
-      const { component: Child } = define({
-        setup() {
-          const n2 = template('<div><div>baz</div></div>', true)() as any
-          setInsertionState(n2, 0)
-          createSlot('default', null)
-          setInsertionState(n2, 0)
-          createSlot('foo', null)
-          return n2
-        },
-      })
-
-      const { html } = define({
-        setup() {
-          return createComponent(Child, null, {
-            default: () => template('default')(),
-            foo: () => template('foo')(),
-          })
-        },
-      }).render()
-
-      expect(html()).toBe(
-        `<div>` +
-          `default<!--slot-->` +
-          `foo<!--slot-->` +
-          `<div>baz</div>` +
-          `</div>`,
-      )
-    })
   })
 })
index c109be3738e3786645ccf1cb642f757d52d00679..198ece168cae77d3bd37061556846a3cd69d1292 100644 (file)
@@ -32,6 +32,7 @@ import {
   setValue,
   template,
   txt,
+  withVaporCtx,
 } from '../src'
 
 declare var __VUE_HMR_RUNTIME__: HMRRuntime
@@ -1404,10 +1405,11 @@ describe('defineVaporCustomElement', () => {
       const App = {
         setup() {
           return createPlainElement('my-parent', null, {
-            default: () =>
+            default: withVaporCtx(() =>
               createPlainElement('my-child', null, {
-                default: () => template('<span>default</span>')(),
+                default: withVaporCtx(() => template('<span>default</span>')()),
               }),
+            ),
           })
         },
       }
@@ -1436,7 +1438,7 @@ describe('defineVaporCustomElement', () => {
               VaporTeleport,
               { to: () => target },
               {
-                default: () => createSlot('default'),
+                default: withVaporCtx(() => createSlot('default')),
               },
             )
           },
@@ -1457,10 +1459,11 @@ describe('defineVaporCustomElement', () => {
       const App = {
         setup() {
           return createPlainElement('my-el-teleport-parent', null, {
-            default: () =>
+            default: withVaporCtx(() =>
               createPlainElement('my-el-teleport-child', null, {
-                default: () => template('<span>default</span>')(),
+                default: withVaporCtx(() => template('<span>default</span>')()),
               }),
+            ),
           })
         },
       }
@@ -1482,14 +1485,14 @@ describe('defineVaporCustomElement', () => {
                 VaporTeleport,
                 { to: () => target1 },
                 {
-                  default: () => createSlot('header'),
+                  default: withVaporCtx(() => createSlot('header')),
                 },
               ),
               createComponent(
                 VaporTeleport,
                 { to: () => target2 },
                 {
-                  default: () => createSlot('body'),
+                  default: withVaporCtx(() => createSlot('body')),
                 },
               ),
             ]
@@ -1502,10 +1505,10 @@ describe('defineVaporCustomElement', () => {
       const App = {
         setup() {
           return createPlainElement('my-el-two-teleport-child', null, {
-            default: () => [
+            default: withVaporCtx(() => [
               template('<div slot="header">header</div>')(),
               template('<span slot="body">body</span>')(),
-            ],
+            ]),
           })
         },
       }
@@ -1533,14 +1536,14 @@ describe('defineVaporCustomElement', () => {
                 // with disabled: true
                 { to: () => target1, disabled: () => true },
                 {
-                  default: () => createSlot('header'),
+                  default: withVaporCtx(() => createSlot('header')),
                 },
               ),
               createComponent(
                 VaporTeleport,
                 { to: () => target2 },
                 {
-                  default: () => createSlot('body'),
+                  default: withVaporCtx(() => createSlot('body')),
                 },
               ),
             ]
@@ -1553,10 +1556,10 @@ describe('defineVaporCustomElement', () => {
       const App = {
         setup() {
           return createPlainElement('my-el-two-teleport-child-0', null, {
-            default: () => [
+            default: withVaporCtx(() => [
               template('<div slot="header">header</div>')(),
               template('<span slot="body">body</span>')(),
-            ],
+            ]),
           })
         },
       }
@@ -1588,7 +1591,7 @@ describe('defineVaporCustomElement', () => {
       const ChildWrapper = {
         setup() {
           return createPlainElement('my-el-child-shadow-false', null, {
-            default: () => template('child')(),
+            default: withVaporCtx(() => template('child')()),
           })
         },
       }
@@ -1624,7 +1627,7 @@ describe('defineVaporCustomElement', () => {
             'my-el-parent-shadow-false',
             { isShown: () => props.isShown },
             {
-              default: () => createSlot('default'),
+              default: withVaporCtx(() => createSlot('default')),
             },
           )
         },
@@ -1637,7 +1640,7 @@ describe('defineVaporCustomElement', () => {
             ParentWrapper,
             { isShown: () => isShown.value },
             {
-              default: () => createComponent(ChildWrapper),
+              default: withVaporCtx(() => createComponent(ChildWrapper)),
             },
           )
         },
index a448a1be4ab23ab7aaaa3b10d42ff3e695c05db3..d253d8822dd9a2ddd0fc5393d244fb99fa4f3f2c 100644 (file)
@@ -12,6 +12,7 @@ import {
   insert,
   renderEffect,
   template,
+  withVaporCtx,
 } from '../../src'
 import { compile, makeRender, runtimeDom, runtimeVapor } from '../_utils'
 import {
@@ -612,11 +613,11 @@ describe('api: template ref', () => {
       render() {
         const setRef = createTemplateRefSetter()
         const n0 = createComponent(Child, null, {
-          default: () => {
+          default: withVaporCtx(() => {
             n = document.createElement('div')
             setRef(n, 'foo')
             return n
-          },
+          }),
         })
         return n0
       },
@@ -640,11 +641,11 @@ describe('api: template ref', () => {
       setup() {
         const setRef = createTemplateRefSetter()
         const n0 = createComponent(Child, null, {
-          default: () => {
+          default: withVaporCtx(() => {
             n = document.createElement('div')
             setRef(n, r)
             return n
-          },
+          }),
         })
         return n0
       },
@@ -669,11 +670,11 @@ describe('api: template ref', () => {
         r = useTemplateRef('foo')
         const setRef = createTemplateRefSetter()
         const n0 = createComponent(Child, null, {
-          default: () => {
+          default: withVaporCtx(() => {
             n = document.createElement('div')
             setRef(n, 'foo')
             return n
-          },
+          }),
         })
         return n0
       },
index 76a5edb2f69bc3c4c1826e632050b2e2f232cf30..27c9e333716f24b70917feec120392a51118f6e5 100644 (file)
@@ -255,16 +255,21 @@ export function setScopeId(block: Block, scopeIds: string[]): void {
 }
 
 export function setComponentScopeId(instance: VaporComponentInstance): void {
-  const parent = instance.parent
-  if (!parent) return
+  const { parent, scopeId } = instance
+  if (!parent || !scopeId) 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) {
+  const parentScopeId = parent && parent.type.__scopeId
+  // if parent scopeId is different from scopeId, this means scopeId
+  // is inherited from slot owner, so we need to set it to the component
+  // scopeIds. the `parentScopeId-s` is handled in createSlot
+  if (parentScopeId !== scopeId) {
     scopeIds.push(scopeId)
+  } else {
+    if (parentScopeId) scopeIds.push(parentScopeId)
   }
 
   // inherit scopeId from vdom parent
index ec4f72db2e4a27bf98455268658d53d1bd3caa48..72a068508d578332281b623965a2a5f10b7fdad5 100644 (file)
@@ -70,7 +70,9 @@ import {
   type StaticSlots,
   type VaporSlot,
   dynamicSlotsProxyHandlers,
+  getParentInstance,
   getSlot,
+  setCurrentSlotConsumer,
 } from './componentSlots'
 import { hmrReload, hmrRerender } from './hmr'
 import {
@@ -191,15 +193,17 @@ export function createComponent(
     resetInsertionState()
   }
 
+  const parentInstance = getParentInstance()
+
   if (
     isSingleRoot &&
     component.inheritAttrs !== false &&
-    isVaporComponent(currentInstance) &&
-    currentInstance.hasFallthrough
+    isVaporComponent(parentInstance) &&
+    parentInstance.hasFallthrough
   ) {
     // check if we are the single root of the parent
     // if yes, inject parent attrs as dynamic props source
-    const attrs = currentInstance.attrs
+    const attrs = parentInstance.attrs
     if (rawProps) {
       ;((rawProps as RawProps).$ || ((rawProps as RawProps).$ = [])).push(
         () => attrs,
@@ -210,12 +214,8 @@ export function createComponent(
   }
 
   // keep-alive
-  if (
-    currentInstance &&
-    currentInstance.vapor &&
-    isKeepAlive(currentInstance)
-  ) {
-    const cached = (currentInstance as KeepAliveInstance).getCachedComponent(
+  if (parentInstance && parentInstance.vapor && isKeepAlive(parentInstance)) {
+    const cached = (parentInstance as KeepAliveInstance).getCachedComponent(
       component,
     )
     // @ts-expect-error
@@ -224,12 +224,14 @@ export function createComponent(
 
   // vdom interop enabled and component is not an explicit vapor component
   if (appContext.vapor && !component.__vapor) {
+    const prevSlotConsumer = setCurrentSlotConsumer(null)
     const frag = appContext.vapor.vdomMount(
       component as any,
+      parentInstance as any,
       rawProps,
       rawSlots,
     )
-
+    setCurrentSlotConsumer(prevSlotConsumer)
     if (!isHydrating) {
       if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
     } else {
@@ -262,8 +264,12 @@ export function createComponent(
     rawSlots as RawSlots,
     appContext,
     once,
+    parentInstance,
   )
 
+  // set currentSlotConsumer to null to avoid affecting the child components
+  const prevSlotConsumer = setCurrentSlotConsumer(null)
+
   // HMR
   if (__DEV__ && component.__hmrId) {
     registerHMR(instance)
@@ -322,6 +328,8 @@ export function createComponent(
     setupComponent(instance, component)
   }
 
+  // restore currentSlotConsumer to previous value after setupFn is called
+  setCurrentSlotConsumer(prevSlotConsumer)
   onScopeDispose(() => unmountComponent(instance), true)
 
   if (_insertionParent || isHydrating) {
@@ -331,6 +339,7 @@ export function createComponent(
   if (isHydrating && _insertionAnchor !== undefined) {
     advanceHydrationNode(_insertionParent!)
   }
+
   return instance
 }
 
@@ -475,6 +484,8 @@ export class VaporComponentInstance implements GenericComponentInstance {
 
   slots: StaticSlots
 
+  scopeId?: string | null
+
   // to hold vnode props / slots in vdom interop mode
   rawPropsRef?: ShallowRef<any>
   rawSlotsRef?: ShallowRef<any>
@@ -541,17 +552,18 @@ export class VaporComponentInstance implements GenericComponentInstance {
     rawSlots?: RawSlots | null,
     appContext?: GenericAppContext,
     once?: boolean,
+    parent: GenericComponentInstance | null = currentInstance,
   ) {
     this.vapor = true
     this.uid = nextUid()
     this.type = comp
-    this.parent = currentInstance
-    this.root = currentInstance ? currentInstance.root : this
+    this.parent = parent
+    this.root = parent ? parent.root : this
 
-    if (currentInstance) {
-      this.appContext = currentInstance.appContext
-      this.provides = currentInstance.provides
-      this.ids = currentInstance.ids
+    if (parent) {
+      this.appContext = parent.appContext
+      this.provides = parent.provides
+      this.ids = parent.ids
     } else {
       this.appContext = appContext || emptyContext
       this.provides = Object.create(this.appContext.provides)
@@ -600,6 +612,8 @@ export class VaporComponentInstance implements GenericComponentInstance {
         : rawSlots
       : EMPTY_OBJ
 
+    this.scopeId = currentInstance && currentInstance.type.__scopeId
+
     // apply custom element special handling
     if (comp.ce) {
       comp.ce(this)
index 01b0be5d4dd69e836313dd67f045d9761e357b35..2f83b9f8647032bd65f88318a5b77a7b641f176c 100644 (file)
@@ -27,19 +27,15 @@ import { setDynamicProps } from './dom/prop'
 
 /**
  * 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
+function setCurrentSlotScopeIds(scopeIds: string[] | null): string[] | null {
+  try {
+    return currentSlotScopeIds
+  } finally {
+    currentSlotScopeIds = scopeIds
+  }
 }
 
 export type RawSlots = Record<string, VaporSlot> & {
@@ -122,28 +118,42 @@ export function getSlot(
   }
 }
 
+export let currentSlotConsumer: GenericComponentInstance | null = null
+
+export function setCurrentSlotConsumer(
+  consumer: GenericComponentInstance | null,
+): GenericComponentInstance | null {
+  try {
+    return currentSlotConsumer
+  } finally {
+    currentSlotConsumer = consumer
+  }
+}
+
 /**
- * Wraps a slot function to execute in the parent component's context.
- *
- * This ensures that:
- * 1. Reactive effects created inside the slot (e.g., `renderEffect`) bind to the
- *    parent's instance, so the parent's lifecycle hooks fire when the slot's
- *    reactive dependencies change.
- * 2. Elements created in the slot inherit the parent's scopeId for proper style
- *    scoping in scoped CSS.
- *
- * **Rationale**: Slots are defined in the parent's template, so the parent should
- * own the rendering context and be aware of updates.
- *
+ * use currentSlotConsumer as parent, the currentSlotConsumer will be reset to null
+ * before setupFn call to avoid affecting children and restore to previous value
+ * after setupFn is called
+ */
+export function getParentInstance(): GenericComponentInstance | null {
+  return currentSlotConsumer || currentInstance
+}
+
+/**
+ * Wrap a slot function to memoize currentInstance
+ * 1. ensure correct currentInstance in forwarded slots
+ * 2. elements created in the slot inherit the slot owner's scopeId
  */
 export function withVaporCtx(fn: Function): BlockFn {
-  const instance = currentInstance as VaporComponentInstance
+  const owner = currentInstance
   return (...args: any[]) => {
-    const prev = setCurrentInstance(instance)
+    const prev = setCurrentInstance(owner)
+    const prevConsumer = setCurrentSlotConsumer(prev[0])
     try {
       return fn(...args)
     } finally {
       setCurrentInstance(...prev)
+      setCurrentSlotConsumer(prevConsumer)
     }
   }
 }
@@ -185,7 +195,7 @@ export function createSlot(
     // Calculate slotScopeIds once (for vdom interop)
     const slotScopeIds: string[] = []
     if (!noSlotted) {
-      const scopeId = instance!.type.__scopeId
+      const scopeId = instance.type.__scopeId
       if (scopeId) {
         slotScopeIds.push(`${scopeId}-s`)
       }
@@ -220,7 +230,6 @@ 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 = () => {
index ab232ff8c7878147be37ce2ee8126755e04be2de..f46ae195e003c51b735f6d93543eecb847d73f60 100644 (file)
@@ -273,10 +273,10 @@ let vdomHydrateNode: HydrationRenderer['hydrateNode'] | undefined
 function createVDOMComponent(
   internals: RendererInternals,
   component: ConcreteComponent,
+  parentComponent: VaporComponentInstance | null,
   rawProps?: LooseRawProps | null,
   rawSlots?: LooseRawSlots | null,
 ): VaporFragment {
-  const parentInstance = currentInstance as VaporComponentInstance
   const frag = new VaporFragment([])
   const vnode = (frag.vnode = createVNode(
     component,
@@ -286,6 +286,9 @@ function createVDOMComponent(
     { props: component.props },
     rawProps as RawProps,
     rawSlots as RawSlots,
+    parentComponent ? parentComponent.appContext : undefined,
+    undefined,
+    parentComponent,
   )
 
   // overwrite how the vdom instance handles props
@@ -315,9 +318,9 @@ function createVDOMComponent(
     if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
       vdomDeactivate(
         vnode,
-        findParentKeepAlive(parentInstance)!.getStorageContainer(),
+        findParentKeepAlive(parentComponent!)!.getStorageContainer(),
         internals,
-        parentInstance as any,
+        parentComponent as any,
         null,
       )
       return
@@ -326,13 +329,13 @@ function createVDOMComponent(
   }
 
   frag.hydrate = () => {
-    hydrateVNode(vnode, parentInstance as any)
+    hydrateVNode(vnode, parentComponent as any)
     onScopeDispose(unmount, true)
     isMounted = true
     frag.nodes = vnode.el as any
   }
 
-  vnode.scopeId = parentInstance && parentInstance.type.__scopeId!
+  vnode.scopeId = (currentInstance && currentInstance.type.__scopeId) || null
   vnode.slotScopeIds = currentSlotScopeIds
 
   frag.insert = (parentNode, anchor, transition) => {
@@ -343,21 +346,21 @@ function createVDOMComponent(
         parentNode,
         anchor,
         internals,
-        parentInstance as any,
+        parentComponent as any,
         null,
         undefined,
         false,
       )
     } else {
       const prev = currentInstance
-      simpleSetCurrentInstance(parentInstance)
+      simpleSetCurrentInstance(parentComponent)
       if (!isMounted) {
         if (transition) setVNodeTransitionHooks(vnode, transition)
         internals.mt(
           vnode,
           parentNode,
           anchor,
-          parentInstance as any,
+          parentComponent as any,
           null,
           undefined,
           false,
@@ -373,7 +376,7 @@ function createVDOMComponent(
           parentNode,
           anchor,
           MoveType.REORDER,
-          parentInstance as any,
+          parentComponent as any,
         )
       }
       simpleSetCurrentInstance(prev)