From 898964c3688e3593d9aabbb09b4849e58a208ce8 Mon Sep 17 00:00:00 2001 From: Alex Snezhko Date: Mon, 6 Oct 2025 21:23:07 -0700 Subject: [PATCH] fix(runtime-vapor): fallthrough attrs with comments in template root --- .../__tests__/componentAttrs.spec.ts | 125 ++++++++++++++++++ packages/runtime-vapor/src/component.ts | 30 ++++- 2 files changed, 150 insertions(+), 5 deletions(-) diff --git a/packages/runtime-vapor/__tests__/componentAttrs.spec.ts b/packages/runtime-vapor/__tests__/componentAttrs.spec.ts index 1f43ebba8c0..d513a0bb88c 100644 --- a/packages/runtime-vapor/__tests__/componentAttrs.spec.ts +++ b/packages/runtime-vapor/__tests__/componentAttrs.spec.ts @@ -57,6 +57,131 @@ describe('attribute fallthrough', () => { expect(host.innerHTML).toBe('
2
') }) + it('should allow attrs to fallthrough on component with comment at root', async () => { + const t0 = template('') + const t1 = template('
') + const { component: Child } = define({ + props: ['foo'], + setup(props: any) { + const n0 = t0() + const n1 = t1() + renderEffect(() => setElementText(n1, props.foo)) + return [n0, n1] + }, + }) + + const foo = ref(1) + const id = ref('a') + const { host } = define({ + setup() { + return createComponent( + Child, + { + foo: () => foo.value, + id: () => id.value, + }, + null, + true, + ) + }, + }).render() + expect(host.innerHTML).toBe('
1
') + + foo.value++ + await nextTick() + expect(host.innerHTML).toBe('
2
') + + id.value = 'b' + await nextTick() + expect(host.innerHTML).toBe('
2
') + }) + + it('should allow attrs to fallthrough on component with single-element array root', async () => { + const t0 = template('
') + const { component: Child } = define({ + props: ['foo'], + setup(props: any) { + const n0 = t0() + renderEffect(() => setElementText(n0, props.foo)) + return [n0] + }, + }) + + const foo = ref(1) + const id = ref('a') + const { host } = define({ + setup() { + return createComponent( + Child, + { + foo: () => foo.value, + id: () => id.value, + }, + null, + true, + ) + }, + }).render() + expect(host.innerHTML).toBe('
1
') + + foo.value++ + await nextTick() + expect(host.innerHTML).toBe('
2
') + + id.value = 'b' + await nextTick() + expect(host.innerHTML).toBe('
2
') + }) + + it('should not allow attrs to fallthrough on component with multiple roots', async () => { + const t0 = template('') + const t1 = template('
') + const { component: Child } = define({ + props: ['foo'], + setup(props: any) { + const n0 = t0() + const n1 = t1() + renderEffect(() => setElementText(n1, props.foo)) + return [n0, n1] + }, + }) + + const foo = ref(1) + const id = ref('a') + const { host } = define({ + setup() { + return createComponent( + Child, + { + foo: () => foo.value, + id: () => id.value, + }, + null, + true, + ) + }, + }).render() + expect(host.innerHTML).toBe('
1
') + }) + + it('should not allow attrs to fallthrough on component with single comment root', async () => { + const t0 = template('') + const { component: Child } = define({ + setup() { + const n0 = t0() + return [n0] + }, + }) + + const id = ref('a') + const { host } = define({ + setup() { + return createComponent(Child, { id: () => id.value }, null, true) + }, + }).render() + expect(host.innerHTML).toBe('') + }) + it('should not fallthrough if explicitly pass inheritAttrs: false', async () => { const t0 = template('
', true) const { component: Child } = define({ diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 08fd881e959..7067c8f4e6a 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -34,7 +34,13 @@ import { setActiveSub, unref, } from '@vue/reactivity' -import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared' +import { + EMPTY_OBJ, + invokeArrayFns, + isArray, + isFunction, + isString, +} from '@vue/shared' import { type DynamicPropsSource, type RawProps, @@ -255,7 +261,7 @@ export function createComponent( component.inheritAttrs !== false && Object.keys(instance.attrs).length ) { - const el = getRootElement(instance) + const el = getRootElement(instance.block) if (el) { renderEffect(() => { isApplyingFallthroughProps = true @@ -579,9 +585,7 @@ export function getExposed( } } -function getRootElement({ - block, -}: VaporComponentInstance): Element | undefined { +function getRootElement(block: Block): Element | undefined { if (block instanceof Element) { return block } @@ -592,4 +596,20 @@ function getRootElement({ return nodes } } + + if (isArray(block)) { + let singleRoot: Element | undefined + for (const b of block) { + if (b instanceof Comment) { + continue + } + const thisRoot = getRootElement(b) + // only return root if there is exactly one eligible root in the array + if (!thisRoot || singleRoot) { + return + } + singleRoot = thisRoot + } + return singleRoot + } }