From a5a08b6d55998264ab32ddb705a25e66343c19ab Mon Sep 17 00:00:00 2001 From: Valentin Yushkevich Date: Thu, 1 Oct 2026 11:34:25 +0300 Subject: [PATCH 1/2] fix(runtime-vapor): pass vdom components plain object vnode props --- .../__tests__/vdomInterop.spec.ts | 27 +++++++++++++++++++ packages/runtime-vapor/src/componentProps.ts | 4 ++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/runtime-vapor/__tests__/vdomInterop.spec.ts b/packages/runtime-vapor/__tests__/vdomInterop.spec.ts index 3493ec117f3..71a13a9e7e3 100644 --- a/packages/runtime-vapor/__tests__/vdomInterop.spec.ts +++ b/packages/runtime-vapor/__tests__/vdomInterop.spec.ts @@ -640,6 +640,33 @@ describe('vdomInterop', () => { '
boolean:false:false
boolean:false:false
', ) }) + + test('vnode props of a vdom component are a plain object', async () => { + // e.g. Vuetify's useProxiedModel checks `vm.vnode.props?.hasOwnProperty()` + const Field = defineComponent({ + props: ['modelValue'], + setup(props) { + const vm = getCurrentInstance()! + return () => + h( + 'i', + `${props.modelValue}:${vm.vnode.props!.hasOwnProperty('modelValue')}`, + ) + }, + }) + const { vdom, vapor } = await renderParity( + { + App: ``, + }, + () => ref({ n: 1 }), + async data => { + data.value.n++ + }, + { Field }, + ) + expect(vdom.after).toBe('2:true') + expect(vapor.after).toBe(vdom.after) + }) }) describe('v-model', () => { diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index c4ceada475d..2fe8b380cd3 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -3,6 +3,7 @@ import { EMPTY_OBJ, NO, camelize, + extend, hasChanged, hasOwn, isArray, @@ -811,7 +812,8 @@ function updateProps( triggerPropsValue(instance, RAW_VALUES_KEY) } const vnode = isInteropEnabled && instance.interopVNode - if (vnode && vnode.vi) vnode.props = rawValues + // vdom code reads vnode props as a plain object (e.g. `props.hasOwnProperty`) + if (vnode && vnode.vi) vnode.props = extend({}, rawValues) const present: Record | undefined = propsToUpdate || isInitial ? undefined : Object.create(null) From e34d541c2693dce7c1e37a096e358b2131a5414c Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 8 Oct 2026 09:30:49 +0800 Subject: [PATCH 2/2] fix(runtime-vapor): preserve own keys in interop vnode props --- .../__tests__/vdomInterop.spec.ts | 45 ++++++++++++++++++- packages/runtime-vapor/src/componentProps.ts | 3 +- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/runtime-vapor/__tests__/vdomInterop.spec.ts b/packages/runtime-vapor/__tests__/vdomInterop.spec.ts index 71a13a9e7e3..7ef9b62a36d 100644 --- a/packages/runtime-vapor/__tests__/vdomInterop.spec.ts +++ b/packages/runtime-vapor/__tests__/vdomInterop.spec.ts @@ -647,10 +647,14 @@ describe('vdomInterop', () => { props: ['modelValue'], setup(props) { const vm = getCurrentInstance()! + expect(vm.vnode.props!.hasOwnProperty('modelValue')).toBe(true) + expect(vm.vnode.props!.hasOwnProperty('onUpdate:modelValue')).toBe( + true, + ) return () => h( 'i', - `${props.modelValue}:${vm.vnode.props!.hasOwnProperty('modelValue')}`, + `${props.modelValue}:${vm.vnode.props!.hasOwnProperty('modelValue')}:${vm.vnode.props!.hasOwnProperty('onUpdate:modelValue')}`, ) }, }) @@ -664,9 +668,46 @@ describe('vdomInterop', () => { }, { Field }, ) - expect(vdom.after).toBe('2:true') + expect(vdom.after).toBe('2:true:true') expect(vapor.after).toBe(vdom.after) }) + + test.each([null, { inherited: true }, 'value'])( + 'preserves an own __proto__ vnode prop with value %j', + async value => { + let vm: ReturnType + const Child = defineComponent({ + inheritAttrs: false, + setup() { + vm = getCurrentInstance()! + return () => null + }, + }) + await renderParity( + { + App: ``, + }, + () => shallowRef({ key: '__proto__', value }), + async data => { + expect(Object.getPrototypeOf(vm!.vnode.props)).toBe( + Object.prototype, + ) + expect(vm!.vnode.props!.hasOwnProperty('__proto__')).toBe(true) + expect(vm!.vnode.props!.__proto__).toBe(value) + + data.value = { key: '__proto__', value: { updated: true } } + await nextTick() + + expect(Object.getPrototypeOf(vm!.vnode.props)).toBe( + Object.prototype, + ) + expect(vm!.vnode.props!.hasOwnProperty('__proto__')).toBe(true) + expect(vm!.vnode.props!.__proto__).toBe(data.value.value) + }, + { Child }, + ) + }, + ) }) describe('v-model', () => { diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index 2fe8b380cd3..d8138aba944 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -3,7 +3,6 @@ import { EMPTY_OBJ, NO, camelize, - extend, hasChanged, hasOwn, isArray, @@ -813,7 +812,7 @@ function updateProps( } const vnode = isInteropEnabled && instance.interopVNode // vdom code reads vnode props as a plain object (e.g. `props.hasOwnProperty`) - if (vnode && vnode.vi) vnode.props = extend({}, rawValues) + if (vnode && vnode.vi) vnode.props = { ...rawValues } const present: Record | undefined = propsToUpdate || isInitial ? undefined : Object.create(null)