diff --git a/packages/runtime-vapor/__tests__/scopeId.spec.ts b/packages/runtime-vapor/__tests__/scopeId.spec.ts
new file mode 100644
index 000000000..a0ea12e70
--- /dev/null
+++ b/packages/runtime-vapor/__tests__/scopeId.spec.ts
@@ -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('
', true)()
+ },
+ })
+
+ const { html } = define({
+ __scopeId: 'parent',
+ setup() {
+ const t0 = template('', true)
+ const n1 = t0() as any
+ setInsertionState(n1)
+ createComponent(Child)
+ return n1
+ },
+ }).render()
+ expect(html()).toBe(``)
+ })
+
+ test('should attach scopeId to nested child component', () => {
+ const Child = defineVaporComponent({
+ __scopeId: 'child',
+ setup() {
+ return template('', true)()
+ },
+ })
+
+ const Parent = defineVaporComponent({
+ __scopeId: 'parent',
+ setup() {
+ return createComponent(Child)
+ },
+ })
+
+ const { html } = define({
+ __scopeId: 'app',
+ setup() {
+ const t0 = template('', true)
+ const n1 = t0() as any
+ setInsertionState(n1)
+ createComponent(Parent)
+ return n1
+ },
+ }).render()
+ expect(html()).toBe(
+ ``,
+ )
+ })
+
+ test('should attach scopeId to child dynamic component', () => {
+ const { html } = define({
+ __scopeId: 'parent',
+ setup() {
+ const t0 = template('', true)
+ const n1 = t0() as any
+ setInsertionState(n1)
+ createDynamicComponent(() => 'button')
+ return n1
+ },
+ }).render()
+ expect(html()).toBe(
+ ``,
+ )
+ })
+
+ test('should attach scopeId to dynamic component', () => {
+ const { html } = define({
+ __scopeId: 'parent',
+ setup() {
+ const t0 = template('', true)
+ const n1 = t0() as any
+ setInsertionState(n1)
+ createDynamicComponent(() => 'button')
+ return n1
+ },
+ }).render()
+ expect(html()).toBe(
+ ``,
+ )
+ })
+
+ 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('', true)
+ const n1 = t0() as any
+ setInsertionState(n1)
+ createComponent(Comp, null, null, true)
+ return n1
+ },
+ }).render()
+ expect(html()).toBe(
+ ``,
+ )
+ })
+
+ 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('', true)() as any
+ setInsertionState(n1)
+ createSlot('default', null)
+ return n1
+ },
+ })
+
+ const Child2 = defineVaporComponent({
+ __scopeId: 'child2',
+ setup() {
+ return template('', true)()
+ },
+ })
+
+ const { html } = define({
+ __scopeId: 'parent',
+ setup() {
+ const n2 = createComponent(
+ Child,
+ null,
+ {
+ default: () => {
+ const n0 = template('')()
+ const n1 = createComponent(Child2)
+ return [n0, n1]
+ },
+ },
+ true,
+ )
+ return n2
+ },
+ }).render()
+
+ expect(html()).toBe(
+ `` +
+ `
` +
+ // component inside slot should have:
+ // - scopeId from template context
+ // - slotted scopeId from slot owner
+ // - its own scopeId
+ `
` +
+ `` +
+ `
`,
+ )
+ })
+
+ test.todo(':slotted on forwarded slots', async () => {})
+})
+
+describe('vdom interop', () => {
+ test('vdom parent > vapor child', () => {
+ const VaporChild = defineVaporComponent({
+ __scopeId: 'vapor-child',
+ setup() {
+ return template('', 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(
+ ``,
+ )
+ })
+
+ 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(
+ ``,
+ )
+ })
+
+ 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(
+ ``,
+ )
+ })
+
+ 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(
+ ``,
+ )
+ })
+
+ test('vapor parent > vdom > vapor child', () => {
+ const InnerVaporChild = defineVaporComponent({
+ __scopeId: 'inner-vapor-child',
+ setup() {
+ return template('', 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(
+ ``,
+ )
+ })
+})
diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts
index d6be89efc..31b71848d 100644
--- a/packages/runtime-vapor/src/block.ts
+++ b/packages/runtime-vapor/src/block.ts
@@ -1,6 +1,7 @@
import { isArray } from '@vue/shared'
import {
type VaporComponentInstance,
+ currentInstance,
isVaporComponent,
mountComponent,
unmountComponent,
@@ -14,6 +15,7 @@ import {
locateHydrationNode,
locateVaporFragmentAnchor,
} from './dom/hydration'
+import { queuePostFlushCb } from '@vue/runtime-dom'
export type Block =
| Node
@@ -213,3 +215,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)
+ }
+}
diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts
index e0396aa2e..46322ac0e 100644
--- a/packages/runtime-vapor/src/component.ts
+++ b/packages/runtime-vapor/src/component.ts
@@ -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,
@@ -482,6 +489,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)])
@@ -509,6 +517,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__) {
diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts
index 33f6d4ca8..ee8799039 100644
--- a/packages/runtime-vapor/src/vdomInterop.ts
+++ b/packages/runtime-vapor/src/vdomInterop.ts
@@ -77,6 +77,7 @@ const vaporInteropImpl: Omit<
instance.rawPropsRef = propsRef
instance.rawSlotsRef = slotsRef
mountComponent(instance, container, selfAnchor)
+ vnode.el = instance.block
simpleSetCurrentInstance(prev)
return instance
},
@@ -203,6 +204,8 @@ function createVDOMComponent(
internals.umt(vnode.component!, null, !!parentNode)
}
+ vnode.scopeId = parentInstance.type.__scopeId!
+
frag.insert = (parentNode, anchor) => {
if (!isMounted || isHydrating) {
if (isHydrating) {
@@ -240,6 +243,9 @@ function createVDOMComponent(
parentInstance as any,
)
}
+
+ // update the fragment nodes
+ frag.nodes = vnode.el as Block
}
frag.remove = unmount