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