]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: refactor
authordaiwei <daiwei521@126.com>
Fri, 6 Jun 2025 07:36:36 +0000 (15:36 +0800)
committerdaiwei <daiwei521@126.com>
Sat, 7 Jun 2025 14:10:33 +0000 (22:10 +0800)
packages/runtime-vapor/__tests__/scopeId.spec.ts [new file with mode: 0644]
packages/runtime-vapor/src/apiCreateDynamicComponent.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/vdomInterop.ts

diff --git a/packages/runtime-vapor/__tests__/scopeId.spec.ts b/packages/runtime-vapor/__tests__/scopeId.spec.ts
new file mode 100644 (file)
index 0000000..6579363
--- /dev/null
@@ -0,0 +1,464 @@
+import { createApp, h } from '@vue/runtime-dom'
+import {
+  createComponent,
+  createDynamicComponent,
+  createSlot,
+  defineVaporComponent,
+  setInsertionState,
+  template,
+  vaporInteropPlugin,
+} from '../src'
+import { makeRender } from './_utils'
+
+const define = makeRender()
+
+describe('scopeId', () => {
+  test('should attach scopeId to child component', () => {
+    const Child = defineVaporComponent({
+      __scopeId: 'child',
+      setup() {
+        return template('<div child></div>', true)()
+      },
+    })
+
+    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() {
+        const t0 = template('<div parent></div>', true)
+        const n1 = t0() as any
+        setInsertionState(n1)
+        createComponent(Child)
+        return n1
+      },
+    }).render()
+    expect(html()).toBe(`<div parent=""><div child="" parent=""></div></div>`)
+  })
+
+  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() {
+        return template('<div child></div>', true)()
+      },
+    })
+
+    const Parent = defineVaporComponent({
+      __scopeId: 'parent',
+      setup() {
+        return createComponent(Child)
+      },
+    })
+
+    const { html } = define({
+      __scopeId: 'app',
+      setup() {
+        const t0 = template('<div app></div>', true)
+        const n1 = t0() as any
+        setInsertionState(n1)
+        createComponent(Parent)
+        return n1
+      },
+    }).render()
+    expect(html()).toBe(
+      `<div app=""><div child="" parent="" app=""></div></div>`,
+    )
+  })
+
+  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() {
+        const t0 = template('<div parent></div>', true)
+        const n1 = t0() as any
+        setInsertionState(n1)
+        createDynamicComponent(() => 'button')
+        return n1
+      },
+    }).render()
+    expect(html()).toBe(
+      `<div parent=""><button parent=""></button><!--dynamic-component--></div>`,
+    )
+  })
+
+  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(
+      `<button child="" parent=""></button><!--dynamic-component-->`,
+    )
+  })
+
+  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('<div parent></div>', true)
+        const n1 = t0() as any
+        setInsertionState(n1)
+        createComponent(Comp, null, null, true)
+        return n1
+      },
+    }).render()
+    expect(html()).toBe(
+      `<div parent=""><button child="" parent=""></button><!--dynamic-component--></div>`,
+    )
+  })
+
+  test.todo('should attach scopeId to suspense content', async () => {})
+
+  // :slotted basic
+  test.todo('should work on slots', () => {
+    const Child = defineVaporComponent({
+      __scopeId: 'child',
+      setup() {
+        const n1 = template('<div child></div>', true)() as any
+        setInsertionState(n1)
+        createSlot('default', null)
+        return n1
+      },
+    })
+
+    const Child2 = defineVaporComponent({
+      __scopeId: 'child2',
+      setup() {
+        return template('<span child2></span>', true)()
+      },
+    })
+
+    const { html } = define({
+      __scopeId: 'parent',
+      setup() {
+        const n2 = createComponent(
+          Child,
+          null,
+          {
+            default: () => {
+              const n0 = template('<div parent></div>')()
+              const n1 = createComponent(Child2)
+              return [n0, n1]
+            },
+          },
+          true,
+        )
+        return n2
+      },
+    }).render()
+
+    expect(html()).toBe(
+      `<div child="" parent="">` +
+        `<div parent="" child-s=""></div>` +
+        // component inside slot should have:
+        // - scopeId from template context
+        // - slotted scopeId from slot owner
+        // - its own scopeId
+        `<span child2="" child="" parent="" child-s=""></span>` +
+        `<!--slot-->` +
+        `</div>`,
+    )
+  })
+
+  test.todo(':slotted on forwarded slots', async () => {})
+})
+
+describe('vdom interop', () => {
+  test('vdom parent > 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 App = {
+      __scopeId: 'parent',
+      setup() {
+        return () => h(VdomChild)
+      },
+    }
+
+    const root = document.createElement('div')
+    createApp(App).use(vaporInteropPlugin).mount(root)
+
+    expect(root.innerHTML).toBe(
+      `<button vapor-child="" vdom-child="" parent=""></button>`,
+    )
+  })
+
+  test('vdom parent > vapor > vdom child', () => {
+    const InnerVdomChild = {
+      __scopeId: 'inner-vdom-child',
+      setup() {
+        return () => h('button')
+      },
+    }
+
+    const VaporChild = defineVaporComponent({
+      __scopeId: 'vapor-child',
+      setup() {
+        return createComponent(InnerVdomChild as any, null, null, true)
+      },
+    })
+
+    const VdomChild = {
+      __scopeId: 'vdom-child',
+      setup() {
+        return () => h(VaporChild as any)
+      },
+    }
+
+    const App = {
+      __scopeId: 'parent',
+      setup() {
+        return () => h(VdomChild)
+      },
+    }
+
+    const root = document.createElement('div')
+    createApp(App).use(vaporInteropPlugin).mount(root)
+
+    expect(root.innerHTML).toBe(
+      `<button inner-vdom-child="" vapor-child="" vdom-child="" parent=""></button>`,
+    )
+  })
+
+  test('vdom parent > vapor dynamic child', () => {
+    const VaporChild = defineVaporComponent({
+      __scopeId: 'vapor-child',
+      setup() {
+        return createDynamicComponent(() => 'button', null, null, true)
+      },
+    })
+
+    const VdomChild = {
+      __scopeId: 'vdom-child',
+      setup() {
+        return () => h(VaporChild as any)
+      },
+    }
+
+    const App = {
+      __scopeId: 'parent',
+      setup() {
+        return () => h(VdomChild)
+      },
+    }
+
+    const root = document.createElement('div')
+    createApp(App).use(vaporInteropPlugin).mount(root)
+
+    expect(root.innerHTML).toBe(
+      `<button vapor-child="" vdom-child="" parent=""></button><!--dynamic-component-->`,
+    )
+  })
+
+  test('vapor parent > vdom child', () => {
+    const VdomChild = {
+      __scopeId: 'vdom-child',
+      setup() {
+        return () => h('button')
+      },
+    }
+
+    const VaporChild = defineVaporComponent({
+      __scopeId: 'vapor-child',
+      setup() {
+        return createComponent(VdomChild as any, null, null, true)
+      },
+    })
+
+    const App = {
+      __scopeId: 'parent',
+      setup() {
+        return () => h(VaporChild as any)
+      },
+    }
+
+    const root = document.createElement('div')
+    createApp(App).use(vaporInteropPlugin).mount(root)
+
+    expect(root.innerHTML).toBe(
+      `<button vdom-child="" vapor-child="" parent=""></button>`,
+    )
+  })
+
+  test('vapor parent > vdom > vapor child', () => {
+    const InnerVaporChild = defineVaporComponent({
+      __scopeId: 'inner-vapor-child',
+      setup() {
+        return template('<button inner-vapor-child></button>', true)()
+      },
+    })
+
+    const VdomChild = {
+      __scopeId: 'vdom-child',
+      setup() {
+        return () => h(InnerVaporChild as any)
+      },
+    }
+
+    const VaporChild = defineVaporComponent({
+      __scopeId: 'vapor-child',
+      setup() {
+        return createComponent(VdomChild as any, null, null, true)
+      },
+    })
+
+    const App = {
+      __scopeId: 'parent',
+      setup() {
+        return () => h(VaporChild as any)
+      },
+    }
+
+    const root = document.createElement('div')
+    createApp(App).use(vaporInteropPlugin).mount(root)
+
+    expect(root.innerHTML).toBe(
+      `<button inner-vapor-child="" vdom-child="" vapor-child="" parent=""></button>`,
+    )
+  })
+
+  test.todo('vapor parent > vdom > vdom > vapor child', () => {
+    const InnerVaporChild = defineVaporComponent({
+      __scopeId: 'inner-vapor-child',
+      setup() {
+        return template('<button inner-vapor-child></button>', true)()
+      },
+    })
+
+    const VdomChild = {
+      __scopeId: 'vdom-child',
+      setup() {
+        return () => h(InnerVaporChild as any)
+      },
+    }
+
+    const VdomChild2 = {
+      __scopeId: 'vdom-child2',
+      setup() {
+        return () => h(VdomChild as any)
+      },
+    }
+
+    const VaporChild = defineVaporComponent({
+      __scopeId: 'vapor-child',
+      setup() {
+        return createComponent(VdomChild2 as any, null, null, true)
+      },
+    })
+
+    const App = {
+      __scopeId: 'parent',
+      setup() {
+        return () => h(VaporChild as any)
+      },
+    }
+
+    const root = document.createElement('div')
+    createApp(App).use(vaporInteropPlugin).mount(root)
+
+    expect(root.innerHTML).toBe(
+      `<button inner-vapor-child="" vdom-child="" vdom-child2="" vapor-child="" parent=""></button>`,
+    )
+  })
+})
index 2126611d7182e1cb548d4179e09dd536c8f3c920..33697b4ca7672666206024f46f5a84812a49d79d 100644 (file)
@@ -1,9 +1,11 @@
 import { resolveDynamicComponent } from '@vue/runtime-dom'
-import { DynamicFragment, type VaporFragment } from './block'
+import { DynamicFragment, type VaporFragment, insert } from './block'
 import { createComponentWithFallback } from './component'
 import { renderEffect } from './renderEffect'
 import type { RawProps } from './componentProps'
 import type { RawSlots } from './componentSlots'
+import { isHydrating } from './dom/hydration'
+import { insertionAnchor, insertionParent } from './insertionState'
 
 export function createDynamicComponent(
   getter: () => any,
@@ -11,6 +13,9 @@ export function createDynamicComponent(
   rawSlots?: RawSlots | null,
   isSingleRoot?: boolean,
 ): VaporFragment {
+  const _insertionParent = insertionParent
+  const _insertionAnchor = insertionAnchor
+
   const frag = __DEV__
     ? new DynamicFragment('dynamic-component')
     : new DynamicFragment()
@@ -27,5 +32,9 @@ export function createDynamicComponent(
       value,
     )
   })
+
+  if (!isHydrating && _insertionParent) {
+    insert(frag, _insertionParent, _insertionAnchor)
+  }
   return frag
 }
index b782afd38d35b66c9fed33675d4f4705efbceb3f..c094642dfca1adb37f2e4aec11e55ef2ed43fc37 100644 (file)
@@ -187,3 +187,38 @@ export function normalizeBlock(block: Block): Node[] {
   }
   return nodes
 }
+
+export function setScopeId(block: Block, scopeId: string): void {
+  if (block instanceof Element) {
+    block.setAttribute(scopeId, '')
+  } else if (isVaporComponent(block)) {
+    setScopeId(block.block, scopeId)
+  } else if (isArray(block)) {
+    for (const b of block) {
+      setScopeId(b, scopeId)
+    }
+  } else if (isFragment(block)) {
+    setScopeId(block.nodes, scopeId)
+  }
+}
+
+export function setComponentScopeId(instance: VaporComponentInstance): void {
+  const parent = instance.parent
+  if (!parent) return
+
+  if (isArray(instance.block) && instance.block.length > 1) return
+
+  const scopeId = parent.type.__scopeId
+  if (scopeId) {
+    setScopeId(instance.block, scopeId)
+  }
+
+  // vdom parent
+  if (
+    parent.subTree &&
+    (parent.subTree.component as any) === instance &&
+    parent.vnode!.scopeId
+  ) {
+    setScopeId(instance.block, parent.vnode!.scopeId)
+  }
+}
index 548babebf8beef2115e31356d50a989e2e1a0112..ea01450a119e6db73850d977249cc71af8355b5a 100644 (file)
@@ -25,7 +25,14 @@ import {
   unregisterHMR,
   warn,
 } from '@vue/runtime-dom'
-import { type Block, insert, isBlock, remove } from './block'
+import {
+  type Block,
+  insert,
+  isBlock,
+  remove,
+  setComponentScopeId,
+  setScopeId,
+} from './block'
 import {
   type ShallowRef,
   markRaw,
@@ -59,7 +66,11 @@ import {
 } from './componentSlots'
 import { hmrReload, hmrRerender } from './hmr'
 import { isHydrating, locateHydrationNode } from './dom/hydration'
-import { insertionAnchor, insertionParent } from './insertionState'
+import {
+  insertionAnchor,
+  insertionParent,
+  resetInsertionState,
+} from './insertionState'
 
 export { currentInstance } from '@vue/runtime-dom'
 
@@ -142,6 +153,8 @@ export function createComponent(
   const _insertionAnchor = insertionAnchor
   if (isHydrating) {
     locateHydrationNode()
+  } else {
+    resetInsertionState()
   }
 
   // vdom interop enabled and component is not an explicit vapor component
@@ -270,9 +283,8 @@ export function createComponent(
   onScopeDispose(() => unmountComponent(instance), true)
 
   if (!isHydrating && _insertionParent) {
-    insert(instance.block, _insertionParent, _insertionAnchor)
+    mountComponent(instance, _insertionParent, _insertionAnchor)
   }
-
   return instance
 }
 
@@ -474,6 +486,9 @@ export function createComponentWithFallback(
   // mark single root
   ;(el as any).$root = isSingleRoot
 
+  const scopeId = currentInstance!.type.__scopeId
+  if (scopeId) setScopeId(el, scopeId)
+
   if (rawProps) {
     renderEffect(() => {
       setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)])
@@ -501,6 +516,7 @@ export function mountComponent(
   }
   if (instance.bm) invokeArrayFns(instance.bm)
   insert(instance.block, parent, anchor)
+  setComponentScopeId(instance)
   if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
   instance.isMounted = true
   if (__DEV__) {
index 77228fd72a02fe85a5496daf7d89bc37e197a4d2..0e4e1c492ad05c85ee60523e7b17170c7b818c32 100644 (file)
@@ -61,6 +61,7 @@ const vaporInteropImpl: Omit<
     instance.rawPropsRef = propsRef
     instance.rawSlotsRef = slotsRef
     mountComponent(instance, container, selfAnchor)
+    vnode.el = instance.block
     simpleSetCurrentInstance(prev)
     return instance
   },
@@ -175,6 +176,8 @@ function createVDOMComponent(
     internals.umt(vnode.component!, null, !!parentNode)
   }
 
+  vnode.scopeId = parentInstance.type.__scopeId!
+
   frag.insert = (parentNode, anchor) => {
     if (!isMounted) {
       internals.mt(
@@ -198,6 +201,9 @@ function createVDOMComponent(
         parentInstance as any,
       )
     }
+
+    // update the fragment nodes
+    frag.nodes = vnode.el as Block
   }
 
   frag.remove = unmount