]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: slotScopeIds
authordaiwei <daiwei521@126.com>
Sun, 8 Jun 2025 08:22:30 +0000 (16:22 +0800)
committerdaiwei <daiwei521@126.com>
Mon, 9 Jun 2025 07:59:55 +0000 (15:59 +0800)
packages/compiler-vapor/src/generators/component.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-vapor/__tests__/scopeId.spec.ts
packages/runtime-vapor/src/apiCreateApp.ts
packages/runtime-vapor/src/apiCreateDynamicComponent.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 7c232db754be5c6574fd04b8098eeff1e2cb368f..05b16077b08b34296c7576da2d47c5ce63ca7174 100644 (file)
@@ -47,7 +47,7 @@ export function genCreateComponent(
   const { helper } = context
 
   const tag = genTag()
-  const { root, props, slots, once } = operation
+  const { root, props, slots, once, scopeId } = operation
   const rawSlots = genRawSlots(slots, context)
   const [ids, handlers] = processInlineHandlers(props, context)
   const rawProps = context.withId(() => genRawProps(props, context), ids)
@@ -75,6 +75,7 @@ export function genCreateComponent(
       rawSlots,
       root ? 'true' : false,
       once && 'true',
+      scopeId && JSON.stringify(scopeId),
     ),
     ...genDirectivesForElement(operation.id, context),
   ]
index 086f77ca61246d92ffbad30cf6e87cc4a72e9373..e6394ad4661a567400caaa58e0bffe927364d9af 100644 (file)
@@ -197,6 +197,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
   dynamic?: SimpleExpressionNode
   parent?: number
   anchor?: number
+  scopeId?: string | null
 }
 
 export interface DeclareOldRefIRNode extends BaseIRNode {
index dceb3fd6121f33da80dfd7121ede5ddbb9ad05bf..fbb48d8201824dafa2d8888ebd2526de501beea6 100644 (file)
@@ -133,6 +133,7 @@ function transformComponentElement(
     root: singleRoot,
     slots: [...context.slots],
     once: context.inVOnce,
+    scopeId: context.inSlot ? context.options.scopeId : undefined,
     dynamic: dynamicComponent,
   }
   context.slots = []
index 5bdd204cfad5948ee4992b9eb4d2eb66a50c1c09..531496fb7433583694a7afceaa21e8320580df6e 100644 (file)
@@ -188,7 +188,12 @@ export interface VaporInteropInterface {
   move(vnode: VNode, container: any, anchor: any): void
   slot(n1: VNode | null, n2: VNode, container: any, anchor: any): void
 
-  vdomMount: (component: ConcreteComponent, props?: any, slots?: any) => any
+  vdomMount: (
+    component: ConcreteComponent,
+    props?: any,
+    slots?: any,
+    scopeId?: string,
+  ) => any
   vdomUnmount: UnmountComponentFn
   vdomSlot: (
     slots: any,
index 827ea501f2327174ac8064d428c7d2ac03985f2e..48051b1497f0d7efb1638d4a59dc6e5d6bf7bccf 100644 (file)
@@ -4,6 +4,7 @@ import {
   createDynamicComponent,
   createSlot,
   defineVaporComponent,
+  forwardedSlotCreator,
   setInsertionState,
   template,
   vaporInteropPlugin,
@@ -200,7 +201,7 @@ describe('scopeId', () => {
   test.todo('should attach scopeId to suspense content', async () => {})
 
   // :slotted basic
-  test.todo('should work on slots', () => {
+  test('should work on slots', () => {
     const Child = defineVaporComponent({
       __scopeId: 'child',
       setup() {
@@ -227,7 +228,14 @@ describe('scopeId', () => {
           {
             default: () => {
               const n0 = template('<div parent></div>')()
-              const n1 = createComponent(Child2)
+              const n1 = createComponent(
+                Child2,
+                null,
+                null,
+                undefined,
+                undefined,
+                'parent',
+              )
               return [n0, n1]
             },
           },
@@ -244,13 +252,69 @@ describe('scopeId', () => {
         // - scopeId from template context
         // - slotted scopeId from slot owner
         // - its own scopeId
-        `<span child2="" child="" parent="" child-s=""></span>` +
+        `<span child2="" parent="" child="" child-s=""></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', () => {
@@ -262,17 +326,16 @@ describe('vdom interop', () => {
       },
     })
 
-    const VdomChild = {
-      __scopeId: 'vdom-child',
+    const VdomParent = {
+      __scopeId: 'vdom-parent',
       setup() {
         return () => h(VaporChild as any)
       },
     }
 
     const App = {
-      __scopeId: 'parent',
       setup() {
-        return () => h(VdomChild)
+        return () => h(VdomParent)
       },
     }
 
@@ -280,13 +343,13 @@ describe('vdom interop', () => {
     createApp(App).use(vaporInteropPlugin).mount(root)
 
     expect(root.innerHTML).toBe(
-      `<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')
       },
@@ -295,21 +358,20 @@ describe('vdom interop', () => {
     const VaporChild = defineVaporComponent({
       __scopeId: 'vapor-child',
       setup() {
-        return createComponent(InnerVdomChild as any, null, null, true)
+        return createComponent(VdomChild as any, null, null, true)
       },
     })
 
-    const VdomChild = {
-      __scopeId: 'vdom-child',
+    const VdomParent = {
+      __scopeId: 'vdom-parent',
       setup() {
         return () => h(VaporChild as any)
       },
     }
 
     const App = {
-      __scopeId: 'parent',
       setup() {
-        return () => h(VdomChild)
+        return () => h(VdomParent)
       },
     }
 
@@ -317,43 +379,42 @@ describe('vdom interop', () => {
     createApp(App).use(vaporInteropPlugin).mount(root)
 
     expect(root.innerHTML).toBe(
-      `<button inner-vdom-child="" vapor-child="" vdom-child="" parent=""></button>`,
+      `<button vdom-child="" vapor-child="" vdom-parent=""></button>`,
     )
   })
 
   test('vdom parent > vapor > vapor > vdom child', () => {
-    const InnerVdomChild = {
-      __scopeId: 'inner-vdom-child',
+    const VdomChild = {
+      __scopeId: 'vdom-child',
       setup() {
         return () => h('button')
       },
     }
 
-    const VaporChild = defineVaporComponent({
-      __scopeId: 'vapor-child',
+    const NestedVaporChild = defineVaporComponent({
+      __scopeId: 'nested-vapor-child',
       setup() {
-        return createComponent(InnerVdomChild as any, null, null, true)
+        return createComponent(VdomChild as any, null, null, true)
       },
     })
 
-    const VaporChild2 = defineVaporComponent({
-      __scopeId: 'vapor-child2',
+    const VaporChild = defineVaporComponent({
+      __scopeId: 'vapor-child',
       setup() {
-        return createComponent(VaporChild as any, null, null, true)
+        return createComponent(NestedVaporChild as any, null, null, true)
       },
     })
 
-    const VdomChild = {
-      __scopeId: 'vdom-child',
+    const VdomParent = {
+      __scopeId: 'vdom-parent',
       setup() {
-        return () => h(VaporChild2 as any)
+        return () => h(VaporChild as any)
       },
     }
 
     const App = {
-      __scopeId: 'parent',
       setup() {
-        return () => h(VdomChild)
+        return () => h(VdomParent)
       },
     }
 
@@ -361,7 +422,7 @@ describe('vdom interop', () => {
     createApp(App).use(vaporInteropPlugin).mount(root)
 
     expect(root.innerHTML).toBe(
-      `<button inner-vdom-child="" vapor-child="" vapor-child2="" vdom-child="" parent=""></button>`,
+      `<button vdom-child="" nested-vapor-child="" vapor-child="" vdom-parent=""></button>`,
     )
   })
 
@@ -373,17 +434,16 @@ describe('vdom interop', () => {
       },
     })
 
-    const VdomChild = {
-      __scopeId: 'vdom-child',
+    const VdomParent = {
+      __scopeId: 'vdom-parent',
       setup() {
         return () => h(VaporChild as any)
       },
     }
 
     const App = {
-      __scopeId: 'parent',
       setup() {
-        return () => h(VdomChild)
+        return () => h(VdomParent)
       },
     }
 
@@ -391,7 +451,7 @@ describe('vdom interop', () => {
     createApp(App).use(vaporInteropPlugin).mount(root)
 
     expect(root.innerHTML).toBe(
-      `<button vapor-child="" vdom-child="" parent=""></button><!--dynamic-component-->`,
+      `<button vapor-child="" vdom-parent=""></button><!--dynamic-component-->`,
     )
   })
 
@@ -403,17 +463,16 @@ describe('vdom interop', () => {
       },
     }
 
-    const VaporChild = defineVaporComponent({
-      __scopeId: 'vapor-child',
+    const VaporParent = defineVaporComponent({
+      __scopeId: 'vapor-parent',
       setup() {
         return createComponent(VdomChild as any, null, null, true)
       },
     })
 
     const App = {
-      __scopeId: 'parent',
       setup() {
-        return () => h(VaporChild as any)
+        return () => h(VaporParent as any)
       },
     }
 
@@ -421,36 +480,35 @@ describe('vdom interop', () => {
     createApp(App).use(vaporInteropPlugin).mount(root)
 
     expect(root.innerHTML).toBe(
-      `<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(VaporChild as any)
+        return () => h(VaporParent as any)
       },
     }
 
@@ -458,43 +516,100 @@ describe('vdom interop', () => {
     createApp(App).use(vaporInteropPlugin).mount(root)
 
     expect(root.innerHTML).toBe(
-      `<button inner-vapor-child="" vdom-child="" vapor-child="" parent=""></button>`,
+      `<button vapor-child="" vdom-child="" vapor-parent=""></button>`,
     )
   })
 
   test('vapor parent > vdom > 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 VdomChild2 = {
-      __scopeId: 'vdom-child2',
+    const VdomParent = {
+      __scopeId: 'vdom-parent',
       setup() {
         return () => h(VdomChild as any)
       },
     }
 
-    const VaporChild = defineVaporComponent({
-      __scopeId: 'vapor-child',
+    const VaporParent = defineVaporComponent({
+      __scopeId: 'vapor-parent',
       setup() {
-        return createComponent(VdomChild2 as any, null, null, true)
+        return createComponent(VdomParent as any, null, null, true)
       },
     })
 
     const App = {
-      __scopeId: 'parent',
       setup() {
-        return () => h(VaporChild as any)
+        return () => h(VaporParent as any)
+      },
+    }
+
+    const root = document.createElement('div')
+    createApp(App).use(vaporInteropPlugin).mount(root)
+
+    expect(root.innerHTML).toBe(
+      `<button vapor-child="" vdom-child="" vdom-parent="" vapor-parent=""></button>`,
+    )
+  })
+
+  test('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)
       },
     }
 
@@ -502,7 +617,11 @@ describe('vdom interop', () => {
     createApp(App).use(vaporInteropPlugin).mount(root)
 
     expect(root.innerHTML).toBe(
-      `<button inner-vapor-child="" vdom-child="" vdom-child2="" vapor-child="" parent=""></button>`,
+      `<div vapor-slot="" vapor-parent="">` +
+        `<div vapor-parent="" vapor-slot-s=""></div>` +
+        `<span vdom-child="" vapor-parent="" vapor-slot-s=""></span>` +
+        `<!--slot-->` +
+        `</div>`,
     )
   })
 })
index 834437ee350b0cd06d3b617ab59afa4bf5d1df8e..1a9ed06a735056f158934a132081f9c284f37ba6 100644 (file)
@@ -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)
index 33697b4ca7672666206024f46f5a84812a49d79d..e436f77e29142e4aade94a2c9c1980b7fb2a6d5e 100644 (file)
@@ -12,6 +12,8 @@ export function createDynamicComponent(
   rawProps?: RawProps | null,
   rawSlots?: RawSlots | null,
   isSingleRoot?: boolean,
+  once?: boolean,
+  scopeId?: string,
 ): VaporFragment {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
@@ -28,6 +30,8 @@ export function createDynamicComponent(
           rawProps,
           rawSlots,
           isSingleRoot,
+          once,
+          scopeId,
         ),
       value,
     )
index a67e43543e99fe2a0a4701bdb0954265333154ec..a5a63807ee14b2a031c5f4df14970405ebea1b25 100644 (file)
@@ -35,6 +35,11 @@ export class DynamicFragment extends VaporFragment {
   scope: EffectScope | undefined
   current?: BlockFn
   fallback?: BlockFn
+  /**
+   * slot only
+   * indicates forwarded slot
+   */
+  forwarded?: boolean
 
   constructor(anchorLabel?: string) {
     super([])
@@ -206,7 +211,6 @@ export function setScopeId(block: Block, scopeId: string): void {
 export function setComponentScopeId(instance: VaporComponentInstance): void {
   const parent = instance.parent
   if (!parent) return
-
   if (isArray(instance.block) && instance.block.length > 1) return
 
   const scopeId = parent.type.__scopeId
index ea01450a119e6db73850d977249cc71af8355b5a..4708db618858e889d720c99182142dbc14ffb93b 100644 (file)
@@ -145,6 +145,8 @@ export function createComponent(
   rawProps?: LooseRawProps | null,
   rawSlots?: LooseRawSlots | null,
   isSingleRoot?: boolean,
+  once?: boolean, // TODO once support
+  scopeId?: string,
   appContext: GenericAppContext = (currentInstance &&
     currentInstance.appContext) ||
     emptyContext,
@@ -163,6 +165,7 @@ export function createComponent(
       component as any,
       rawProps,
       rawSlots,
+      scopeId,
     )
     if (!isHydrating && _insertionParent) {
       insert(frag, _insertionParent, _insertionAnchor)
@@ -282,6 +285,8 @@ export function createComponent(
 
   onScopeDispose(() => unmountComponent(instance), true)
 
+  if (scopeId) setScopeId(instance.block, scopeId)
+
   if (!isHydrating && _insertionParent) {
     mountComponent(instance, _insertionParent, _insertionAnchor)
   }
@@ -477,16 +482,25 @@ export function createComponentWithFallback(
   rawProps?: LooseRawProps | null,
   rawSlots?: LooseRawSlots | null,
   isSingleRoot?: boolean,
+  once?: boolean,
+  scopeId?: string,
 ): HTMLElement | VaporComponentInstance {
   if (!isString(comp)) {
-    return createComponent(comp, rawProps, rawSlots, isSingleRoot)
+    return createComponent(
+      comp,
+      rawProps,
+      rawSlots,
+      isSingleRoot,
+      once,
+      scopeId,
+    )
   }
 
   const el = document.createElement(comp)
   // mark single root
   ;(el as any).$root = isSingleRoot
 
-  const scopeId = currentInstance!.type.__scopeId
+  scopeId = scopeId || currentInstance!.type.__scopeId
   if (scopeId) setScopeId(el, scopeId)
 
   if (rawProps) {
index 19e9b5b6d1a9b51d9e1fa8df7d2dbf0f97c3cbdb..32dd235a0e769ea0f50693475ed9f4d5a04967cf 100644 (file)
@@ -1,5 +1,11 @@
 import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
-import { type Block, type BlockFn, DynamicFragment, insert } from './block'
+import {
+  type Block,
+  type BlockFn,
+  DynamicFragment,
+  insert,
+  setScopeId,
+} from './block'
 import { rawPropsProxyHandlers } from './componentProps'
 import { currentInstance, isRef } from '@vue/runtime-dom'
 import type { LooseRawProps, VaporComponentInstance } from './component'
@@ -156,9 +162,27 @@ export function createSlot(
     }
   }
 
+  if (i) fragment.forwarded = true
+  if (i || !hasForwardedSlot(fragment.nodes)) {
+    const scopeId = instance!.type.__scopeId
+    if (scopeId) setScopeId(fragment, `${scopeId}-s`)
+  }
+
   if (!isHydrating && _insertionParent) {
     insert(fragment, _insertionParent, _insertionAnchor)
   }
 
   return fragment
 }
+
+function isForwardedSlot(block: Block): block is DynamicFragment {
+  return block instanceof DynamicFragment && !!block.forwarded
+}
+
+function hasForwardedSlot(block: Block): block is DynamicFragment {
+  if (isArray(block)) {
+    return block.some(isForwardedSlot)
+  } else {
+    return isForwardedSlot(block)
+  }
+}
index e4d3c8de3b3a99622f8ee7ab3c4abb383f8b0d58..af86b291b279e3f36c864c75151f244d3441da2f 100644 (file)
@@ -155,6 +155,7 @@ function createVDOMComponent(
   component: ConcreteComponent,
   rawProps?: LooseRawProps | null,
   rawSlots?: LooseRawSlots | null,
+  scopeId?: string,
 ): VaporFragment {
   const frag = new VaporFragment([])
   const vnode = createVNode(
@@ -183,7 +184,7 @@ function createVDOMComponent(
     internals.umt(vnode.component!, null, !!parentNode)
   }
 
-  vnode.scopeId = parentInstance.type.__scopeId!
+  vnode.scopeId = scopeId || parentInstance.type.__scopeId!
 
   frag.insert = (parentNode, anchor) => {
     if (!isMounted) {