]> 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>
Fri, 6 Jun 2025 08:55:50 +0000 (16:55 +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..a0ea12e
--- /dev/null
@@ -0,0 +1,341 @@
+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() {
+        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() {
+        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 child dynamic component', () => {
+    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 dynamic component', () => {
+    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() {
+        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>`,
+    )
+  })
+})
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..cd72863cedf46334ba58a7872f18293870eca206 100644 (file)
@@ -1,6 +1,7 @@
 import { isArray } from '@vue/shared'
 import {
   type VaporComponentInstance,
+  currentInstance,
   isVaporComponent,
   mountComponent,
   unmountComponent,
@@ -8,6 +9,7 @@ import {
 import { createComment, createTextNode } from './dom/node'
 import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
 import { isHydrating } from './dom/hydration'
+import { queuePostFlushCb } from '@vue/runtime-dom'
 
 export type Block =
   | Node
@@ -187,3 +189,57 @@ 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, '')
+    }
+  } else if (isVaporComponent(block)) {
+    setComponentScopeId(block, scopeId, true)
+  } else if (isArray(block)) {
+    for (const b of block) {
+      setScopeId(b, scopeId)
+    }
+  } else {
+    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!)
+      }
+    }
+  }
+  if (immediate) {
+    doSet()
+  } else {
+    queuePostFlushCb(doSet)
+  }
+}
index 548babebf8beef2115e31356d50a989e2e1a0112..d9af68539a41b1b1b42dbc20e2fc57654608ad22 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,
@@ -270,7 +277,7 @@ export function createComponent(
   onScopeDispose(() => unmountComponent(instance), true)
 
   if (!isHydrating && _insertionParent) {
-    insert(instance.block, _insertionParent, _insertionAnchor)
+    mountComponent(instance, _insertionParent, _insertionAnchor)
   }
 
   return instance
@@ -474,6 +481,7 @@ export function createComponentWithFallback(
   // mark single root
   ;(el as any).$root = isSingleRoot
 
+  setScopeId(el, currentInstance ? currentInstance.type.__scopeId : undefined)
   if (rawProps) {
     renderEffect(() => {
       setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)])
@@ -501,6 +509,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