From 26dbd3c902cb34f064b9af726ea66b1b9eefe779 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 19 Jun 2025 16:52:37 +0800 Subject: [PATCH 1/4] fix(vapor): component emits vdom interop --- .../__tests__/componentEmits.spec.ts | 33 ++++++++++++++++++- packages/runtime-vapor/src/componentEmits.ts | 6 +++- packages/runtime-vapor/src/componentProps.ts | 2 +- packages/runtime-vapor/src/vdomInterop.ts | 2 +- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/runtime-vapor/__tests__/componentEmits.spec.ts b/packages/runtime-vapor/__tests__/componentEmits.spec.ts index 8c8a56085ba..b07125bc4a7 100644 --- a/packages/runtime-vapor/__tests__/componentEmits.spec.ts +++ b/packages/runtime-vapor/__tests__/componentEmits.spec.ts @@ -4,12 +4,18 @@ // ./rendererAttrsFallthrough.spec.ts. import { + createApp, + h, isEmitListener, nextTick, onBeforeUnmount, toHandlers, } from '@vue/runtime-dom' -import { createComponent, defineVaporComponent } from '../src' +import { + createComponent, + defineVaporComponent, + vaporInteropPlugin, +} from '../src' import { makeRender } from './_utils' const define = makeRender() @@ -425,3 +431,28 @@ describe('component: emit', () => { expect(fn).not.toHaveBeenCalled() }) }) + +describe('vdom interop', () => { + test('vdom parent > vapor child', () => { + const VaporChild = defineVaporComponent({ + emits: ['click'], + setup(_, { emit }) { + emit('click') + return [] + }, + }) + + const fn = vi.fn() + const App = { + setup() { + return () => h(VaporChild as any, { onClick: fn }) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + + // fn should be called once + expect(fn).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/runtime-vapor/src/componentEmits.ts b/packages/runtime-vapor/src/componentEmits.ts index 68b7cfbeb21..beec548b39a 100644 --- a/packages/runtime-vapor/src/componentEmits.ts +++ b/packages/runtime-vapor/src/componentEmits.ts @@ -46,7 +46,11 @@ function propGetter(rawProps: Record, key: string) { let i = dynamicSources.length while (i--) { const source = resolveSource(dynamicSources[i]) - if (hasOwn(source, key)) return resolveSource(source[key]) + if (hasOwn(source, key)) + // for props passed from VDOM component, no need to resolve + return dynamicSources.__interop + ? source[key] + : resolveSource(source[key]) } } return rawProps[key] && resolveSource(rawProps[key]) diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index a5e9daad229..b088b68c1c0 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -26,7 +26,7 @@ import { renderEffect } from './renderEffect' export type RawProps = Record unknown> & { // generated by compiler for :[key]="x" or v-bind="x" - $?: DynamicPropsSource[] + $?: DynamicPropsSource[] & { __interop?: boolean } } export type DynamicPropsSource = diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 77228fd72a0..0d2290cc062 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -52,7 +52,7 @@ const vaporInteropImpl: Omit< const instance = (vnode.component = createComponent( vnode.type as any as VaporComponent, { - $: [() => propsRef.value], + $: extend([() => propsRef.value], { __interop: true }), } as RawProps, { _: slotsRef, // pass the slots ref From 56cb3b0d46a199c89dd1cb42e4e5757f9324f0af Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 3 Jul 2025 14:32:22 +0800 Subject: [PATCH 2/4] chore: tweaks --- packages/runtime-vapor/src/componentEmits.ts | 3 ++- packages/runtime-vapor/src/vdomInterop.ts | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/runtime-vapor/src/componentEmits.ts b/packages/runtime-vapor/src/componentEmits.ts index beec548b39a..6734a2fba29 100644 --- a/packages/runtime-vapor/src/componentEmits.ts +++ b/packages/runtime-vapor/src/componentEmits.ts @@ -2,6 +2,7 @@ import { type ObjectEmitsOptions, baseEmit } from '@vue/runtime-dom' import type { VaporComponent, VaporComponentInstance } from './component' import { EMPTY_OBJ, hasOwn, isArray } from '@vue/shared' import { resolveSource } from './componentProps' +import { interopKey } from './vdomInterop' /** * The logic from core isn't too reusable so it's better to duplicate here @@ -48,7 +49,7 @@ function propGetter(rawProps: Record, key: string) { const source = resolveSource(dynamicSources[i]) if (hasOwn(source, key)) // for props passed from VDOM component, no need to resolve - return dynamicSources.__interop + return dynamicSources[interopKey] ? source[key] : resolveSource(source[key]) } diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 0d2290cc062..68215da6dd8 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -34,6 +34,8 @@ import { renderEffect } from './renderEffect' import { createTextNode } from './dom/node' import { optimizePropertyLookup } from './dom/prop' +export const interopKey: unique symbol = Symbol(`interop`) + // mounting vapor components and slots in vdom const vaporInteropImpl: Omit< VaporInteropInterface, @@ -48,11 +50,16 @@ const vaporInteropImpl: Omit< const propsRef = shallowRef(vnode.props) const slotsRef = shallowRef(vnode.children) + const dynamicPropSource: (() => any)[] & { [interopKey]?: boolean } = [ + () => propsRef.value, + ] + // mark as interop props + dynamicPropSource[interopKey] = true // @ts-expect-error const instance = (vnode.component = createComponent( vnode.type as any as VaporComponent, { - $: extend([() => propsRef.value], { __interop: true }), + $: dynamicPropSource, } as RawProps, { _: slotsRef, // pass the slots ref From 035d9350ff94c9efdd0059a422b6d01a88f620d1 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 4 Jul 2025 07:57:12 +0800 Subject: [PATCH 3/4] fix: update RawProps type to use interopKey for dynamic props --- packages/runtime-vapor/src/componentEmits.ts | 4 ++-- packages/runtime-vapor/src/componentProps.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/runtime-vapor/src/componentEmits.ts b/packages/runtime-vapor/src/componentEmits.ts index 6734a2fba29..daee4d59fe7 100644 --- a/packages/runtime-vapor/src/componentEmits.ts +++ b/packages/runtime-vapor/src/componentEmits.ts @@ -1,7 +1,7 @@ import { type ObjectEmitsOptions, baseEmit } from '@vue/runtime-dom' import type { VaporComponent, VaporComponentInstance } from './component' import { EMPTY_OBJ, hasOwn, isArray } from '@vue/shared' -import { resolveSource } from './componentProps' +import { type RawProps, resolveSource } from './componentProps' import { interopKey } from './vdomInterop' /** @@ -41,7 +41,7 @@ export function emit( ) } -function propGetter(rawProps: Record, key: string) { +function propGetter(rawProps: RawProps, key: string) { const dynamicSources = rawProps.$ if (dynamicSources) { let i = dynamicSources.length diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index b088b68c1c0..86c3e06c5c8 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -23,10 +23,11 @@ import { } from '@vue/runtime-dom' import { normalizeEmitsOptions } from './componentEmits' import { renderEffect } from './renderEffect' +import type { interopKey } from './vdomInterop' export type RawProps = Record unknown> & { // generated by compiler for :[key]="x" or v-bind="x" - $?: DynamicPropsSource[] & { __interop?: boolean } + $?: DynamicPropsSource[] & { [interopKey]?: boolean } } export type DynamicPropsSource = From 26ab5d177772ab5cbf74e5f8a5e0cf66e47e0ac1 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 4 Jul 2025 11:36:56 +0800 Subject: [PATCH 4/4] test: move test into vdomInterop.spec.ts --- .../__tests__/componentEmits.spec.ts | 33 +------------------ .../__tests__/vdomInterop.spec.ts | 22 ++++++++++++- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/packages/runtime-vapor/__tests__/componentEmits.spec.ts b/packages/runtime-vapor/__tests__/componentEmits.spec.ts index b07125bc4a7..8c8a56085ba 100644 --- a/packages/runtime-vapor/__tests__/componentEmits.spec.ts +++ b/packages/runtime-vapor/__tests__/componentEmits.spec.ts @@ -4,18 +4,12 @@ // ./rendererAttrsFallthrough.spec.ts. import { - createApp, - h, isEmitListener, nextTick, onBeforeUnmount, toHandlers, } from '@vue/runtime-dom' -import { - createComponent, - defineVaporComponent, - vaporInteropPlugin, -} from '../src' +import { createComponent, defineVaporComponent } from '../src' import { makeRender } from './_utils' const define = makeRender() @@ -431,28 +425,3 @@ describe('component: emit', () => { expect(fn).not.toHaveBeenCalled() }) }) - -describe('vdom interop', () => { - test('vdom parent > vapor child', () => { - const VaporChild = defineVaporComponent({ - emits: ['click'], - setup(_, { emit }) { - emit('click') - return [] - }, - }) - - const fn = vi.fn() - const App = { - setup() { - return () => h(VaporChild as any, { onClick: fn }) - }, - } - - const root = document.createElement('div') - createApp(App).use(vaporInteropPlugin).mount(root) - - // fn should be called once - expect(fn).toHaveBeenCalledTimes(1) - }) -}) diff --git a/packages/runtime-vapor/__tests__/vdomInterop.spec.ts b/packages/runtime-vapor/__tests__/vdomInterop.spec.ts index 08326d4d5d9..8b26f5d0f00 100644 --- a/packages/runtime-vapor/__tests__/vdomInterop.spec.ts +++ b/packages/runtime-vapor/__tests__/vdomInterop.spec.ts @@ -7,7 +7,27 @@ const define = makeInteropRender() describe('vdomInterop', () => { describe.todo('props', () => {}) - describe.todo('emit', () => {}) + describe('emit', () => { + test('emit from vapor child to vdom parent', () => { + const VaporChild = defineVaporComponent({ + emits: ['click'], + setup(_, { emit }) { + emit('click') + return [] + }, + }) + + const fn = vi.fn() + define({ + setup() { + return () => h(VaporChild as any, { onClick: fn }) + }, + }).render() + + // fn should be called once + expect(fn).toHaveBeenCalledTimes(1) + }) + }) describe.todo('slots', () => {})