Browse Source

chore: Merge branch 'edison/feat/setScopeId' into edison/testVapor

daiwei 1 year ago
parent
commit
6795f5a3e3

+ 2 - 1
packages/compiler-vapor/src/generators/component.ts

@@ -49,7 +49,7 @@ export function genCreateComponent(
   const { helper } = context
 
   const tag = genTag()
-  const { root, props, slots, once } = operation
+  const { root, props, slots, once, scopeId } = operation
   const rawSlots = genRawSlots(slots, context)
   const [ids, handlers] = processInlineHandlers(props, context)
   const rawProps = context.withId(() => genRawProps(props, context), ids)
@@ -80,6 +80,7 @@ export function genCreateComponent(
       rawSlots,
       root ? 'true' : false,
       once && 'true',
+      scopeId && JSON.stringify(scopeId),
     ),
     ...genDirectivesForElement(operation.id, context),
   ]

+ 1 - 0
packages/compiler-vapor/src/ir/index.ts

@@ -200,6 +200,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
   dynamic?: SimpleExpressionNode
   parent?: number
   anchor?: number
+  scopeId?: string | null
 }
 
 export interface DeclareOldRefIRNode extends BaseIRNode {

+ 1 - 0
packages/compiler-vapor/src/transforms/transformElement.ts

@@ -159,6 +159,7 @@ function transformComponentElement(
     root: singleRoot && !context.inVFor,
     slots: [...context.slots],
     once: context.inVOnce,
+    scopeId: context.inSlot ? context.options.scopeId : undefined,
     dynamic: dynamicComponent,
   }
   context.slots = []

+ 6 - 1
packages/runtime-core/src/apiCreateApp.ts

@@ -192,7 +192,12 @@ export interface VaporInteropInterface {
   ): void
   hydrate(node: Node, fn: () => void): void
 
-  vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any
+  vdomMount: (
+    component: ConcreteComponent,
+    props?: any,
+    slots?: any,
+    scopeId?: string,
+  ) => any
   vdomUnmount: UnmountComponentFn
   vdomSlot: (
     slots: any,

+ 5 - 1
packages/runtime-core/src/index.ts

@@ -510,7 +510,11 @@ export { type VaporInteropInterface } from './apiCreateApp'
 /**
  * @internal
  */
-export { type RendererInternals, MoveType } from './renderer'
+export {
+  type RendererInternals,
+  MoveType,
+  getInheritedScopeIds,
+} from './renderer'
 /**
  * @internal
  */

+ 54 - 24
packages/runtime-core/src/renderer.ts

@@ -766,30 +766,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])
     }
   }
 
@@ -2719,3 +2698,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
+}

+ 329 - 43
packages/runtime-vapor/__tests__/scopeId.spec.ts

@@ -4,6 +4,7 @@ import {
   createDynamicComponent,
   createSlot,
   defineVaporComponent,
+  forwardedSlotCreator,
   setInsertionState,
   template,
   vaporInteropPlugin,
@@ -21,6 +22,23 @@ describe('scopeId', () => {
       },
     })
 
+    const { html } = define({
+      __scopeId: 'parent',
+      setup() {
+        return createComponent(Child)
+      },
+    }).render()
+    expect(html()).toBe(`<div child="" parent=""></div>`)
+  })
+
+  test('should attach scopeId to child component with insertion state', () => {
+    const Child = defineVaporComponent({
+      __scopeId: 'child',
+      setup() {
+        return template('<div child></div>', true)()
+      },
+    })
+
     const { html } = define({
       __scopeId: 'parent',
       setup() {
@@ -34,7 +52,57 @@ describe('scopeId', () => {
     expect(html()).toBe(`<div parent=""><div child="" parent=""></div></div>`)
   })
 
-  test.todo('should attach scopeId to nested child component', () => {
+  test('should attach scopeId to nested child component', () => {
+    const Child = defineVaporComponent({
+      __scopeId: 'child',
+      setup() {
+        return template('<div child></div>', true)()
+      },
+    })
+
+    const Parent = defineVaporComponent({
+      __scopeId: 'parent',
+      setup() {
+        return createComponent(Child)
+      },
+    })
+
+    const { html } = define({
+      __scopeId: 'app',
+      setup() {
+        return createComponent(Parent)
+      },
+    }).render()
+    expect(html()).toBe(`<div child="" parent="" app=""></div>`)
+  })
+
+  test('should not attach scopeId to nested multiple root components', () => {
+    const Child = defineVaporComponent({
+      __scopeId: 'child',
+      setup() {
+        return template('<div child></div>', true)()
+      },
+    })
+
+    const Parent = defineVaporComponent({
+      __scopeId: 'parent',
+      setup() {
+        const n0 = template('<div parent></div>')()
+        const n1 = createComponent(Child)
+        return [n0, n1]
+      },
+    })
+
+    const { html } = define({
+      __scopeId: 'app',
+      setup() {
+        return createComponent(Parent)
+      },
+    }).render()
+    expect(html()).toBe(`<div parent=""></div><div child="" parent=""></div>`)
+  })
+
+  test('should attach scopeId to nested child component with insertion state', () => {
     const Child = defineVaporComponent({
       __scopeId: 'child',
       setup() {
@@ -64,7 +132,17 @@ describe('scopeId', () => {
     )
   })
 
-  test('should attach scopeId to child dynamic component', () => {
+  test('should attach scopeId to dynamic component', () => {
+    const { html } = define({
+      __scopeId: 'parent',
+      setup() {
+        return createDynamicComponent(() => 'button')
+      },
+    }).render()
+    expect(html()).toBe(`<button parent=""></button><!--dynamic-component-->`)
+  })
+
+  test('should attach scopeId to dynamic component with insertion state', () => {
     const { html } = define({
       __scopeId: 'parent',
       setup() {
@@ -80,23 +158,25 @@ describe('scopeId', () => {
     )
   })
 
-  test('should attach scopeId to dynamic component', () => {
+  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() {
-        const t0 = template('<div parent></div>', true)
-        const n1 = t0() as any
-        setInsertionState(n1)
-        createDynamicComponent(() => 'button')
-        return n1
+        return createComponent(Comp, null, null, true)
       },
     }).render()
     expect(html()).toBe(
-      `<div parent=""><button parent=""></button><!--dynamic-component--></div>`,
+      `<button child="" parent=""></button><!--dynamic-component-->`,
     )
   })
 
-  test('should attach scopeId to nested dynamic component', () => {
+  test('should attach scopeId to nested dynamic component with insertion state', () => {
     const Comp = defineVaporComponent({
       __scopeId: 'child',
       setup() {
@@ -121,7 +201,7 @@ describe('scopeId', () => {
   test.todo('should attach scopeId to suspense content', async () => {})
 
   // :slotted basic
-  test.todo('should work on slots', () => {
+  test('should work on slots', () => {
     const Child = defineVaporComponent({
       __scopeId: 'child',
       setup() {
@@ -148,7 +228,14 @@ describe('scopeId', () => {
           {
             default: () => {
               const n0 = template('<div parent></div>')()
-              const n1 = createComponent(Child2)
+              const n1 = createComponent(
+                Child2,
+                null,
+                null,
+                undefined,
+                undefined,
+                'parent',
+              )
               return [n0, n1]
             },
           },
@@ -165,13 +252,69 @@ describe('scopeId', () => {
         // - scopeId from template context
         // - slotted scopeId from slot owner
         // - its own scopeId
-        `<span child2="" child="" parent="" child-s=""></span>` +
+        `<span child2="" parent="" child-s="" child=""></span>` +
         `<!--slot-->` +
         `</div>`,
     )
   })
 
-  test.todo(':slotted on forwarded slots', async () => {})
+  test(':slotted on forwarded slots', async () => {
+    const Wrapper = defineVaporComponent({
+      __scopeId: 'wrapper',
+      setup() {
+        // <div><slot/></div>
+        const n1 = template('<div wrapper></div>', true)() as any
+        setInsertionState(n1)
+        createSlot('default', null)
+        return n1
+      },
+    })
+
+    const Slotted = defineVaporComponent({
+      __scopeId: 'slotted',
+      setup() {
+        // <Wrapper><slot/></Wrapper>
+        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() {
+        // <Slotted><div></div></Slotted>
+        const n2 = createComponent(
+          Slotted,
+          null,
+          {
+            default: () => {
+              return template('<div root></div>')()
+            },
+          },
+          true,
+        )
+        return n2
+      },
+    }).render()
+
+    expect(html()).toBe(
+      `<div wrapper="" slotted="" root="">` +
+        `<div root="" slotted-s=""></div>` +
+        `<!--slot--><!--slot-->` +
+        `</div>`,
+    )
+  })
 })
 
 describe('vdom interop', () => {
@@ -183,17 +326,16 @@ describe('vdom interop', () => {
       },
     })
 
-    const VdomChild = {
-      __scopeId: 'vdom-child',
+    const VdomParent = {
+      __scopeId: 'vdom-parent',
       setup() {
         return () => h(VaporChild as any)
       },
     }
 
     const App = {
-      __scopeId: 'parent',
       setup() {
-        return () => h(VdomChild)
+        return () => h(VdomParent)
       },
     }
 
@@ -201,13 +343,13 @@ describe('vdom interop', () => {
     createApp(App).use(vaporInteropPlugin).mount(root)
 
     expect(root.innerHTML).toBe(
-      `<button vapor-child="" vdom-child="" parent=""></button>`,
+      `<button vapor-child="" vdom-parent=""></button>`,
     )
   })
 
   test('vdom parent > vapor > vdom child', () => {
-    const InnerVdomChild = {
-      __scopeId: 'inner-vdom-child',
+    const VdomChild = {
+      __scopeId: 'vdom-child',
       setup() {
         return () => h('button')
       },
@@ -216,21 +358,63 @@ describe('vdom interop', () => {
     const VaporChild = defineVaporComponent({
       __scopeId: 'vapor-child',
       setup() {
-        return createComponent(InnerVdomChild as any, null, null, true)
+        return createComponent(VdomChild as any, null, null, true)
       },
     })
 
+    const 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(
+      `<button vdom-child="" vapor-child="" vdom-parent=""></button>`,
+    )
+  })
+
+  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 = {
-      __scopeId: 'parent',
       setup() {
-        return () => h(VdomChild)
+        return () => h(VdomParent)
       },
     }
 
@@ -238,7 +422,7 @@ describe('vdom interop', () => {
     createApp(App).use(vaporInteropPlugin).mount(root)
 
     expect(root.innerHTML).toBe(
-      `<button inner-vdom-child="" vapor-child="" vdom-child="" parent=""></button>`,
+      `<button vdom-child="" nested-vapor-child="" vapor-child="" vdom-parent=""></button>`,
     )
   })
 
@@ -250,17 +434,16 @@ describe('vdom interop', () => {
       },
     })
 
-    const VdomChild = {
-      __scopeId: 'vdom-child',
+    const VdomParent = {
+      __scopeId: 'vdom-parent',
       setup() {
         return () => h(VaporChild as any)
       },
     }
 
     const App = {
-      __scopeId: 'parent',
       setup() {
-        return () => h(VdomChild)
+        return () => h(VdomParent)
       },
     }
 
@@ -268,7 +451,7 @@ describe('vdom interop', () => {
     createApp(App).use(vaporInteropPlugin).mount(root)
 
     expect(root.innerHTML).toBe(
-      `<button vapor-child="" vdom-child="" parent=""></button><!--dynamic-component-->`,
+      `<button vapor-child="" vdom-parent=""></button><!--dynamic-component-->`,
     )
   })
 
@@ -280,17 +463,16 @@ describe('vdom interop', () => {
       },
     }
 
-    const VaporChild = defineVaporComponent({
-      __scopeId: 'vapor-child',
+    const VaporParent = defineVaporComponent({
+      __scopeId: 'vapor-parent',
       setup() {
         return createComponent(VdomChild as any, null, null, true)
       },
     })
 
     const App = {
-      __scopeId: 'parent',
       setup() {
-        return () => h(VaporChild as any)
+        return () => h(VaporParent as any)
       },
     }
 
@@ -298,44 +480,148 @@ describe('vdom interop', () => {
     createApp(App).use(vaporInteropPlugin).mount(root)
 
     expect(root.innerHTML).toBe(
-      `<button vdom-child="" vapor-child="" parent=""></button>`,
+      `<button vdom-child="" vapor-parent=""></button>`,
     )
   })
 
   test('vapor parent > vdom > vapor child', () => {
-    const InnerVaporChild = defineVaporComponent({
-      __scopeId: 'inner-vapor-child',
+    const VaporChild = defineVaporComponent({
+      __scopeId: 'vapor-child',
       setup() {
-        return template('<button inner-vapor-child></button>', true)()
+        return template('<button vapor-child></button>', true)()
       },
     })
 
     const VdomChild = {
       __scopeId: 'vdom-child',
       setup() {
-        return () => h(InnerVaporChild as any)
+        return () => h(VaporChild as any)
       },
     }
 
-    const VaporChild = defineVaporComponent({
-      __scopeId: 'vapor-child',
+    const VaporParent = defineVaporComponent({
+      __scopeId: 'vapor-parent',
       setup() {
         return createComponent(VdomChild as any, null, null, true)
       },
     })
 
     const App = {
-      __scopeId: 'parent',
+      setup() {
+        return () => h(VaporParent as any)
+      },
+    }
+
+    const root = document.createElement('div')
+    createApp(App).use(vaporInteropPlugin).mount(root)
+
+    expect(root.innerHTML).toBe(
+      `<button vapor-child="" vdom-child="" vapor-parent=""></button>`,
+    )
+  })
+
+  test('vapor parent > vdom > vdom > vapor child', () => {
+    const VaporChild = defineVaporComponent({
+      __scopeId: 'vapor-child',
+      setup() {
+        return template('<button vapor-child></button>', 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(
-      `<button inner-vapor-child="" vdom-child="" vapor-child="" parent=""></button>`,
+      `<button vapor-child="" vdom-child="" vdom-parent="" vapor-parent=""></button>`,
+    )
+  })
+
+  test.todo('vapor parent > vapor slot > vdom child', () => {
+    const VaporSlot = defineVaporComponent({
+      __scopeId: 'vapor-slot',
+      setup() {
+        const n1 = template('<div vapor-slot></div>', 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('<div vapor-parent></div>')()
+              const n1 = createComponent(
+                VdomChild,
+                undefined,
+                undefined,
+                undefined,
+                undefined,
+                'vapor-parent',
+              )
+              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(
+      `<div vapor-slot="" vapor-parent="">` +
+        `<div vapor-parent="" vapor-slot-s=""></div>` +
+        `<span vdom-child="" vapor-parent="" vapor-slot-s=""></span>` +
+        `<!--slot-->` +
+        `</div>`,
     )
   })
 })

+ 4 - 0
packages/runtime-vapor/src/apiCreateApp.ts

@@ -41,6 +41,8 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
     app._props as RawProps,
     null,
     false,
+    false,
+    undefined,
     app._context,
   )
   mountComponent(instance, container)
@@ -61,6 +63,8 @@ const hydrateApp: AppMountFn<ParentNode> = (app, container) => {
       app._props as RawProps,
       null,
       false,
+      false,
+      undefined,
       app._context,
     )
     mountComponent(instance, container)

+ 4 - 0
packages/runtime-vapor/src/apiCreateDynamicComponent.ts

@@ -17,6 +17,8 @@ export function createDynamicComponent(
   rawProps?: RawProps | null,
   rawSlots?: RawSlots | null,
   isSingleRoot?: boolean,
+  once?: boolean,
+  scopeId?: string,
 ): VaporFragment {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
@@ -42,6 +44,8 @@ export function createDynamicComponent(
           rawProps,
           rawSlots,
           isSingleRoot,
+          once,
+          scopeId,
           appContext,
         ),
       value,

+ 33 - 44
packages/runtime-vapor/src/block.ts

@@ -1,7 +1,6 @@
 import { isArray } from '@vue/shared'
 import {
   type VaporComponentInstance,
-  currentInstance,
   isVaporComponent,
   mountComponent,
   unmountComponent,
@@ -11,11 +10,9 @@ import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
 import {
   currentHydrationNode,
   isComment,
-  isHydrating,
   locateHydrationNode,
   locateVaporFragmentAnchor,
 } from './dom/hydration'
-import { queuePostFlushCb } from '@vue/runtime-dom'
 import {
   type TransitionHooks,
   type TransitionProps,
@@ -32,6 +29,8 @@ export interface TransitionOptions {
   $key?: any
   $transition?: VaporTransitionHooks
 }
+import { isHydrating } from './dom/hydration'
+import { getInheritedScopeIds } from '@vue/runtime-dom'
 
 export interface VaporTransitionHooks extends TransitionHooks {
   state: TransitionState
@@ -73,6 +72,12 @@ export class DynamicFragment extends VaporFragment {
   anchor!: Node
   scope: EffectScope | undefined
   current?: BlockFn
+  fallback?: BlockFn
+  /**
+   * slot only
+   * indicates forwarded slot
+   */
+  forwarded?: boolean
 
   constructor(anchorLabel?: string) {
     super([])
@@ -290,56 +295,40 @@ export function normalizeBlock(block: Block): Node[] {
   return nodes
 }
 
-export function setScopeId(block: Block, scopeId?: string): void {
-  if (block instanceof Node) {
-    if (scopeId && block instanceof Element) {
-      block.setAttribute(scopeId, '')
-    }
+export function setScopeId(block: Block, scopeId: string): void {
+  if (block instanceof Element) {
+    block.setAttribute(scopeId, '')
   } else if (isVaporComponent(block)) {
-    setComponentScopeId(block, scopeId, true)
+    setScopeId(block.block, scopeId)
   } else if (isArray(block)) {
     for (const b of block) {
       setScopeId(b, scopeId)
     }
-  } else {
+  } else if (isFragment(block)) {
     setScopeId(block.nodes, scopeId)
   }
 }
 
-export function setComponentScopeId(
-  instance: VaporComponentInstance,
-  scopeId: string | undefined = currentInstance
-    ? currentInstance.type.__scopeId
-    : undefined,
-  immediate: boolean = false,
-): void {
-  function doSet() {
-    if (scopeId) {
-      setScopeId(instance.block, scopeId)
-    }
-    // inherit scopeId from parent component. this requires initial rendering
-    // to be finished, due to `parent.block` is null during initial rendering
-    const parent = instance.parent
-    if (parent && parent.type.__scopeId) {
-      // vapor parent
-      if (
-        parent.vapor &&
-        (parent as VaporComponentInstance).block === instance
-      ) {
-        setScopeId(instance.block, parent.type.__scopeId)
-      }
-      // vdom parent
-      else if (
-        parent.subTree &&
-        (parent.subTree.component as any) === instance
-      ) {
-        setScopeId(instance.block, parent.vnode!.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)
   }
-  if (immediate) {
-    doSet()
-  } else {
-    queuePostFlushCb(doSet)
+
+  // 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)
+    }
   }
 }

+ 19 - 3
packages/runtime-vapor/src/component.ts

@@ -147,6 +147,8 @@ export function createComponent(
   rawProps?: LooseRawProps | null,
   rawSlots?: LooseRawSlots | null,
   isSingleRoot?: boolean,
+  once?: boolean, // TODO once support
+  scopeId?: string,
   appContext: GenericAppContext = (currentInstance &&
     currentInstance.appContext) ||
     emptyContext,
@@ -165,6 +167,7 @@ export function createComponent(
       component as any,
       rawProps,
       rawSlots,
+      scopeId,
     )
 
     // `frag.insert` handles both hydration and mounting
@@ -284,10 +287,11 @@ export function createComponent(
 
   onScopeDispose(() => unmountComponent(instance), true)
 
+  if (scopeId) setScopeId(instance.block, scopeId)
+
   if (!isHydrating && _insertionParent) {
     mountComponent(instance, _insertionParent, _insertionAnchor)
   }
-
   return instance
 }
 
@@ -489,10 +493,20 @@ export function createComponentWithFallback(
   rawProps?: LooseRawProps | null,
   rawSlots?: LooseRawSlots | null,
   isSingleRoot?: boolean,
+  once?: boolean,
+  scopeId?: string,
   appContext?: GenericAppContext,
 ): HTMLElement | VaporComponentInstance {
   if (!isString(comp)) {
-    return createComponent(comp, rawProps, rawSlots, isSingleRoot, appContext)
+    return createComponent(
+      comp,
+      rawProps,
+      rawSlots,
+      isSingleRoot,
+      once,
+      scopeId,
+      appContext,
+    )
   }
 
   const _insertionParent = insertionParent
@@ -507,7 +521,9 @@ export function createComponentWithFallback(
   // mark single root
   ;(el as any).$root = isSingleRoot
 
-  setScopeId(el, currentInstance ? currentInstance.type.__scopeId : undefined)
+  scopeId = scopeId || currentInstance!.type.__scopeId
+  if (scopeId) setScopeId(el, scopeId)
+
   if (rawProps) {
     renderEffect(() => {
       setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)])

+ 19 - 0
packages/runtime-vapor/src/componentSlots.ts

@@ -13,6 +13,7 @@ import {
   type VaporFragment,
   insert,
   isFragment,
+  setScopeId,
 } from './block'
 import { rawPropsProxyHandlers } from './componentProps'
 import { currentInstance, isRef } from '@vue/runtime-dom'
@@ -197,6 +198,12 @@ export function createSlot(
     }
   }
 
+  if (i) fragment.forwarded = true
+  if (i || !hasForwardedSlot(fragment.nodes)) {
+    const scopeId = instance!.type.__scopeId
+    if (scopeId) setScopeId(fragment, `${scopeId}-s`)
+  }
+
   if (
     _insertionParent &&
     (!isHydrating ||
@@ -209,6 +216,18 @@ export function createSlot(
   return fragment
 }
 
+function isForwardedSlot(block: Block): block is DynamicFragment {
+  return block instanceof DynamicFragment && !!block.forwarded
+}
+
+function hasForwardedSlot(block: Block): block is DynamicFragment {
+  if (isArray(block)) {
+    return block.some(isForwardedSlot)
+  } else {
+    return isForwardedSlot(block)
+  }
+}
+
 function ensureVaporSlotFallback(
   block: VaporFragment,
   fallback?: VaporSlot,

+ 1 - 0
packages/runtime-vapor/src/vdomInterop.ts

@@ -215,6 +215,7 @@ function createVDOMComponent(
   component: ConcreteComponent,
   rawProps?: LooseRawProps | null,
   rawSlots?: LooseRawSlots | null,
+  scopeId?: string,
 ): VaporFragment {
   const frag = new VaporFragment([])
   const vnode = createVNode(