From 898964c3688e3593d9aabbb09b4849e58a208ce8 Mon Sep 17 00:00:00 2001 From: Alex Snezhko Date: Mon, 6 Oct 2025 21:23:07 -0700 Subject: [PATCH 1/2] 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 + } } From 7a7d9957c9bde5ed709f00705004f47400e8f71b Mon Sep 17 00:00:00 2001 From: Alex Snezhko Date: Sat, 11 Oct 2025 17:23:56 -0700 Subject: [PATCH 2/2] fix: fallthrough attrs for v-if at root --- packages/compiler-core/src/utils.ts | 33 +++++ .../transforms/ssrInjectFallthroughAttrs.ts | 33 +---- .../__snapshots__/compile.spec.ts.snap | 2 +- .../compiler-vapor/__tests__/compile.spec.ts | 2 +- .../transformElement.spec.ts.snap | 8 +- .../transformTemplateRef.spec.ts.snap | 2 +- .../__snapshots__/vBind.spec.ts.snap | 12 +- .../__snapshots__/vFor.spec.ts.snap | 30 ++-- .../transforms/__snapshots__/vIf.spec.ts.snap | 131 +++++++++++++++++- .../__snapshots__/vModel.spec.ts.snap | 2 +- .../__snapshots__/vOnce.spec.ts.snap | 6 +- .../transforms/transformElement.spec.ts | 12 +- .../__tests__/transforms/vBind.spec.ts | 8 +- .../__tests__/transforms/vIf.spec.ts | 90 ++++++++++++ packages/compiler-vapor/src/generate.ts | 2 +- .../compiler-vapor/src/generators/prop.ts | 1 - .../compiler-vapor/src/generators/template.ts | 4 +- packages/compiler-vapor/src/ir/index.ts | 4 +- packages/compiler-vapor/src/transform.ts | 1 + .../src/transforms/transformElement.ts | 57 +++++--- .../__tests__/componentAttrs.spec.ts | 46 ++++++ 21 files changed, 383 insertions(+), 103 deletions(-) diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index ddf9b0eed8d..7ab8a07ecea 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -12,6 +12,7 @@ import { type MemoExpression, NodeTypes, type ObjectExpression, + type ParentNode, type Position, type Property, type RenderSlotCall, @@ -568,4 +569,36 @@ export function getMemoedVNodeCall( } } +export function filterCommentChildren(node: ParentNode): TemplateChildNode[] { + return node.children.filter(n => n.type !== NodeTypes.COMMENT) +} + +export function hasSingleChild(node: ParentNode): boolean { + return filterCommentChildren(node).length === 1 +} + +export function isSingleIfBlock(parent: ParentNode): boolean { + // detect cases where the parent v-if is not the only root level node + let hasEncounteredIf = false + for (const c of filterCommentChildren(parent)) { + if ( + c.type === NodeTypes.IF || + (c.type === NodeTypes.ELEMENT && findDir(c, 'if')) + ) { + // multiple root v-if + if (hasEncounteredIf) return false + hasEncounteredIf = true + } else if ( + // node before v-if + !hasEncounteredIf || + // non else nodes + !(c.type === NodeTypes.ELEMENT && findDir(c, /^else(-if)?$/, true)) + ) { + return false + } + } + + return true +} + export const forAliasRE: RegExp = /([\s\S]*?)\s+(?:in|of)\s+(\S[\s\S]*)/ diff --git a/packages/compiler-ssr/src/transforms/ssrInjectFallthroughAttrs.ts b/packages/compiler-ssr/src/transforms/ssrInjectFallthroughAttrs.ts index b1aac0d74c2..a9b8c8f4421 100644 --- a/packages/compiler-ssr/src/transforms/ssrInjectFallthroughAttrs.ts +++ b/packages/compiler-ssr/src/transforms/ssrInjectFallthroughAttrs.ts @@ -2,20 +2,16 @@ import { ElementTypes, type NodeTransform, NodeTypes, - type ParentNode, type RootNode, type TemplateChildNode, createSimpleExpression, + filterCommentChildren, findDir, + hasSingleChild, + isSingleIfBlock, locStub, } from '@vue/compiler-dom' -const filterChild = (node: ParentNode) => - node.children.filter(n => n.type !== NodeTypes.COMMENT) - -const hasSingleChild = (node: ParentNode): boolean => - filterChild(node).length === 1 - export const ssrInjectFallthroughAttrs: NodeTransform = (node, context) => { // _attrs is provided as a function argument. // mark it as a known identifier so that it doesn't get prefixed by @@ -32,7 +28,7 @@ export const ssrInjectFallthroughAttrs: NodeTransform = (node, context) => { node.tag === 'KeepAlive' || node.tag === 'keep-alive') ) { - const rootChildren = filterChild(context.root) + const rootChildren = filterCommentChildren(context.root) if (rootChildren.length === 1 && rootChildren[0] === node) { if (hasSingleChild(node)) { injectFallthroughAttrs(node.children[0]) @@ -47,26 +43,9 @@ export const ssrInjectFallthroughAttrs: NodeTransform = (node, context) => { } if (node.type === NodeTypes.IF_BRANCH && hasSingleChild(node)) { - // detect cases where the parent v-if is not the only root level node - let hasEncounteredIf = false - for (const c of filterChild(parent)) { - if ( - c.type === NodeTypes.IF || - (c.type === NodeTypes.ELEMENT && findDir(c, 'if')) - ) { - // multiple root v-if - if (hasEncounteredIf) return - hasEncounteredIf = true - } else if ( - // node before v-if - !hasEncounteredIf || - // non else nodes - !(c.type === NodeTypes.ELEMENT && findDir(c, /else/, true)) - ) { - return - } + if (isSingleIfBlock(parent)) { + injectFallthroughAttrs(node.children[0]) } - injectFallthroughAttrs(node.children[0]) } else if (hasSingleChild(parent)) { injectFallthroughAttrs(node) } diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap index 9f2183ce83e..3d5467594a7 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap @@ -260,7 +260,7 @@ exports[`compile > expression parsing > v-bind 1`] = ` const n0 = t0() _renderEffect(() => { const _key = key.value - _setDynamicProps(n0, [{ [_key+1]: _unref(foo)[_key+1]() }], true) + _setDynamicProps(n0, [{ [_key+1]: _unref(foo)[_key+1]() }]) }) return n0 " diff --git a/packages/compiler-vapor/__tests__/compile.spec.ts b/packages/compiler-vapor/__tests__/compile.spec.ts index 7963a9e98c2..9f4598181b0 100644 --- a/packages/compiler-vapor/__tests__/compile.spec.ts +++ b/packages/compiler-vapor/__tests__/compile.spec.ts @@ -196,7 +196,7 @@ describe('compile', () => { expect(code).contains('const _key = key.value') expect(code).contains('_key+1') expect(code).contains( - '_setDynamicProps(n0, [{ [_key+1]: _unref(foo)[_key+1]() }], true)', + '_setDynamicProps(n0, [{ [_key+1]: _unref(foo)[_key+1]() }])', ) }) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap index 3188a866070..ea88da22f9d 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap @@ -413,7 +413,7 @@ const t0 = _template("
", true) export function render(_ctx) { const n0 = t0() - _renderEffect(() => _setDynamicProps(n0, [_ctx.obj], true)) + _renderEffect(() => _setDynamicProps(n0, [_ctx.obj])) return n0 }" `; @@ -424,7 +424,7 @@ const t0 = _template("
", true) export function render(_ctx) { const n0 = t0() - _renderEffect(() => _setDynamicProps(n0, [{ id: "foo" }, _ctx.obj], true)) + _renderEffect(() => _setDynamicProps(n0, [{ id: "foo" }, _ctx.obj])) return n0 }" `; @@ -435,7 +435,7 @@ const t0 = _template("
", true) export function render(_ctx) { const n0 = t0() - _renderEffect(() => _setDynamicProps(n0, [_ctx.obj, { id: "foo" }], true)) + _renderEffect(() => _setDynamicProps(n0, [_ctx.obj, { id: "foo" }])) return n0 }" `; @@ -446,7 +446,7 @@ const t0 = _template("
", true) export function render(_ctx) { const n0 = t0() - _renderEffect(() => _setDynamicProps(n0, [{ id: "foo" }, _ctx.obj, { class: "bar" }], true)) + _renderEffect(() => _setDynamicProps(n0, [{ id: "foo" }, _ctx.obj, { class: "bar" }])) return n0 }" `; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap index 7184446fc09..4ca43957a2d 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap @@ -35,7 +35,7 @@ export function render(_ctx) { exports[`compiler: template ref transform > ref + v-for 1`] = ` "import { createTemplateRefSetter as _createTemplateRefSetter, createFor as _createFor, template as _template } from 'vue'; -const t0 = _template("
", true) +const t0 = _template("
") export function render(_ctx) { const _setTemplateRef = _createTemplateRefSetter() diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap index 4ea0db55fe5..cc55b4f6ca9 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap @@ -23,7 +23,7 @@ export function render(_ctx) { const n0 = t0() _renderEffect(() => { const _key = _ctx.key - _setDynamicProps(n0, [{ [_key+1]: _ctx.foo[_key+1]() }], true) + _setDynamicProps(n0, [{ [_key+1]: _ctx.foo[_key+1]() }]) }) return n0 }" @@ -80,7 +80,7 @@ const t0 = _template("
", true) export function render(_ctx) { const n0 = t0() - _renderEffect(() => _setDynamicProps(n0, [{ foo: bar => _ctx.foo = bar }], true)) + _renderEffect(() => _setDynamicProps(n0, [{ foo: bar => _ctx.foo = bar }])) return n0 }" `; @@ -306,7 +306,7 @@ const t0 = _template("
", true) export function render(_ctx) { const n0 = t0() - _renderEffect(() => _setDynamicProps(n0, [{ [_camelize(_ctx.foo)]: _ctx.id }], true)) + _renderEffect(() => _setDynamicProps(n0, [{ [_camelize(_ctx.foo)]: _ctx.id }])) return n0 }" `; @@ -405,7 +405,7 @@ const t0 = _template("
", true) export function render(_ctx) { const n0 = t0() - _renderEffect(() => _setDynamicProps(n0, [{ ["." + _ctx.fooBar]: _ctx.id }], true)) + _renderEffect(() => _setDynamicProps(n0, [{ ["." + _ctx.fooBar]: _ctx.id }])) return n0 }" `; @@ -569,7 +569,7 @@ export function render(_ctx) { _renderEffect(() => { const _id = _ctx.id const _title = _ctx.title - _setDynamicProps(n0, [{ [_id]: _id, [_title]: _title }], true) + _setDynamicProps(n0, [{ [_id]: _id, [_title]: _title }]) }) return n0 }" @@ -583,7 +583,7 @@ export function render(_ctx) { const n0 = t0() _renderEffect(() => { const _id = _ctx.id - _setDynamicProps(n0, [{ [_id]: _id, foo: "bar", checked: "" }], true) + _setDynamicProps(n0, [{ [_id]: _id, foo: "bar", checked: "" }]) }) return n0 }" diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap index 4b1574e5d25..7f797e66130 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap @@ -2,7 +2,7 @@ exports[`compiler: v-for > array de-structured value (with rest) 1`] = ` "import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; -const t0 = _template("
", true) +const t0 = _template("
") export function render(_ctx) { const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => { @@ -17,7 +17,7 @@ export function render(_ctx) { exports[`compiler: v-for > array de-structured value 1`] = ` "import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; -const t0 = _template("
", true) +const t0 = _template("
") export function render(_ctx) { const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => { @@ -32,7 +32,7 @@ export function render(_ctx) { exports[`compiler: v-for > basic v-for 1`] = ` "import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, delegateEvents as _delegateEvents, template as _template } from 'vue'; -const t0 = _template("
", true) +const t0 = _template("
") _delegateEvents("click") export function render(_ctx) { @@ -49,7 +49,7 @@ export function render(_ctx) { exports[`compiler: v-for > key only binding pattern 1`] = ` "import { child as _child, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue'; -const t0 = _template(" ", true) +const t0 = _template(" ") export function render(_ctx) { const n0 = _createFor(() => (_ctx.rows), (_for_item0) => { @@ -64,7 +64,7 @@ export function render(_ctx) { exports[`compiler: v-for > multi effect 1`] = ` "import { setProp as _setProp, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; -const t0 = _template("
", true) +const t0 = _template("
") export function render(_ctx) { const n0 = _createFor(() => (_ctx.items), (_for_item0, _for_key0) => { @@ -82,7 +82,7 @@ export function render(_ctx) { exports[`compiler: v-for > nested v-for 1`] = ` "import { setInsertionState as _setInsertionState, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; const t0 = _template(" ") -const t1 = _template("
", true) +const t1 = _template("
") export function render(_ctx) { const n0 = _createFor(() => (_ctx.list), (_for_item0) => { @@ -102,7 +102,7 @@ export function render(_ctx) { exports[`compiler: v-for > object de-structured value (with rest) 1`] = ` "import { getRestElement as _getRestElement, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; -const t0 = _template("
", true) +const t0 = _template("
") export function render(_ctx) { const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => { @@ -117,7 +117,7 @@ export function render(_ctx) { exports[`compiler: v-for > object de-structured value 1`] = ` "import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; -const t0 = _template(" ", true) +const t0 = _template(" ") export function render(_ctx) { const n0 = _createFor(() => (_ctx.items), (_for_item0) => { @@ -132,7 +132,7 @@ export function render(_ctx) { exports[`compiler: v-for > object value, key and index 1`] = ` "import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; -const t0 = _template("
", true) +const t0 = _template("
") export function render(_ctx) { const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0, _for_index0) => { @@ -147,7 +147,7 @@ export function render(_ctx) { exports[`compiler: v-for > selector pattern 1`] = ` "import { child as _child, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue'; -const t0 = _template(" ", true) +const t0 = _template(" ") export function render(_ctx) { let _selector0_0 @@ -167,7 +167,7 @@ export function render(_ctx) { exports[`compiler: v-for > selector pattern 2`] = ` "import { setClass as _setClass, createFor as _createFor, template as _template } from 'vue'; -const t0 = _template("", true) +const t0 = _template("") export function render(_ctx) { let _selector0_0 @@ -186,7 +186,7 @@ export function render(_ctx) { exports[`compiler: v-for > selector pattern 3`] = ` "import { setClass as _setClass, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; -const t0 = _template("", true) +const t0 = _template("") export function render(_ctx) { const n0 = _createFor(() => (_ctx.rows), (_for_item0) => { @@ -203,7 +203,7 @@ export function render(_ctx) { exports[`compiler: v-for > selector pattern 4`] = ` "import { setClass as _setClass, createFor as _createFor, template as _template } from 'vue'; -const t0 = _template("", true) +const t0 = _template("") export function render(_ctx) { let _selector0_0 @@ -222,7 +222,7 @@ export function render(_ctx) { exports[`compiler: v-for > v-for aliases w/ complex expressions 1`] = ` "import { getDefaultValue as _getDefaultValue, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; -const t0 = _template("
", true) +const t0 = _template("
") export function render(_ctx) { const n0 = _createFor(() => (_ctx.list), (_for_item0) => { @@ -269,7 +269,7 @@ export function render(_ctx) { exports[`compiler: v-for > w/o value 1`] = ` "import { createFor as _createFor, template as _template } from 'vue'; -const t0 = _template("
item
", true) +const t0 = _template("
item
") export function render(_ctx) { const n0 = _createFor(() => (_ctx.items), (_for_item0) => { diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap index c41dc9226c5..5d2113cbe86 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap @@ -61,11 +61,109 @@ export function render(_ctx) { }" `; +exports[`compiler: v-if > multiple v-if at root 1`] = ` +"import { createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
foo
") +const t1 = _template("
bar
") +const t2 = _template("
baz
") + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.foo), () => { + const n2 = t0() + return n2 + }, () => _createIf(() => (_ctx.bar), () => { + const n4 = t1() + return n4 + })) + const n6 = _createIf(() => (_ctx.baz), () => { + const n8 = t2() + return n8 + }) + return [n0, n6] +}" +`; + +exports[`compiler: v-if > template v-if (multiple element) 1`] = ` +"import { createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
hi
") +const t1 = _template("
ho
") + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.foo), () => { + const n2 = t0() + const n3 = t1() + return [n2, n3] + }) + return n0 +}" +`; + +exports[`compiler: v-if > template v-if (single element) 1`] = ` +"import { createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
hi
", true) + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.foo), () => { + const n2 = t0() + return n2 + }) + return n0 +}" +`; + +exports[`compiler: v-if > template v-if (text) 1`] = ` +"import { createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("hello") + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.foo), () => { + const n2 = t0() + return n2 + }) + return n0 +}" +`; + +exports[`compiler: v-if > template v-if (with v-for inside) 1`] = ` +"import { createFor as _createFor, createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
") + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.foo), () => { + const n2 = _createFor(() => (_ctx.list), (_for_item0) => { + const n4 = t0() + return n4 + }) + return n2 + }) + return n0 +}" +`; + +exports[`compiler: v-if > template v-if + normal v-else 1`] = ` +"import { createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
hi
") +const t1 = _template("
ho
") +const t2 = _template("
", true) + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.foo), () => { + const n2 = t0() + const n3 = t1() + return [n2, n3] + }, () => { + const n5 = t2() + return n5 + }) + return n0 +}" +`; + exports[`compiler: v-if > template v-if 1`] = ` "import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue'; const t0 = _template("
") const t1 = _template("hello") -const t2 = _template("

", true) +const t2 = _template("

") export function render(_ctx) { const n0 = _createIf(() => (_ctx.ok), () => { @@ -82,8 +180,8 @@ export function render(_ctx) { exports[`compiler: v-if > v-if + v-else 1`] = ` "import { createIf as _createIf, template as _template } from 'vue'; -const t0 = _template("
") -const t1 = _template("

") +const t0 = _template("
", true) +const t1 = _template("

", true) export function render(_ctx) { const n0 = _createIf(() => (_ctx.ok), () => { @@ -99,8 +197,8 @@ export function render(_ctx) { exports[`compiler: v-if > v-if + v-else-if + v-else 1`] = ` "import { createIf as _createIf, template as _template } from 'vue'; -const t0 = _template("
") -const t1 = _template("

") +const t0 = _template("
", true) +const t1 = _template("

", true) const t2 = _template("fine") export function render(_ctx) { @@ -120,8 +218,8 @@ export function render(_ctx) { exports[`compiler: v-if > v-if + v-else-if 1`] = ` "import { createIf as _createIf, template as _template } from 'vue'; -const t0 = _template("
") -const t1 = _template("

") +const t0 = _template("
", true) +const t1 = _template("

", true) export function render(_ctx) { const n0 = _createIf(() => (_ctx.ok), () => { @@ -160,3 +258,22 @@ export function render(_ctx) { return n8 }" `; + +exports[`compiler: v-if > v-if and extra at root 1`] = ` +"import { createIf as _createIf, template as _template } from 'vue'; +const t0 = _template("
foo
") +const t1 = _template("
bar
") +const t2 = _template("
baz
") + +export function render(_ctx) { + const n0 = _createIf(() => (_ctx.foo), () => { + const n2 = t0() + return n2 + }, () => _createIf(() => (_ctx.bar), () => { + const n4 = t1() + return n4 + })) + const n6 = t2() + return [n0, n6] +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap index 5ef064974c0..5443464a2d9 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap @@ -225,7 +225,7 @@ const t0 = _template("", true) export function render(_ctx) { const n0 = t0() _applyDynamicModel(n0, () => (_ctx.model), _value => (_ctx.model = _value)) - _renderEffect(() => _setDynamicProps(n0, [_ctx.obj], true)) + _renderEffect(() => _setDynamicProps(n0, [_ctx.obj])) return n0 }" `; diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap index b6107d5a1a1..bbda721b7cc 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap @@ -62,7 +62,7 @@ export function render(_ctx) { exports[`compiler: v-once > with v-for 1`] = ` "import { createFor as _createFor, template as _template } from 'vue'; -const t0 = _template("
", true) +const t0 = _template("
") export function render(_ctx) { const n0 = _createFor(() => (_ctx.list), (_for_item0) => { @@ -88,8 +88,8 @@ export function render(_ctx) { exports[`compiler: v-once > with v-if/else 1`] = ` "import { createIf as _createIf, template as _template } from 'vue'; -const t0 = _template("
") -const t1 = _template("

") +const t0 = _template("
", true) +const t1 = _template("

", true) export function render(_ctx) { const n0 = _createIf(() => (_ctx.expr), () => { diff --git a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts index a693db4ad39..9149fda1939 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts @@ -625,7 +625,7 @@ describe('compiler: element transform', () => { ], }, ]) - expect(code).contains('_setDynamicProps(n0, [_ctx.obj], true)') + expect(code).contains('_setDynamicProps(n0, [_ctx.obj])') }) test('v-bind="obj" after static prop', () => { @@ -661,9 +661,7 @@ describe('compiler: element transform', () => { ], }, ]) - expect(code).contains( - '_setDynamicProps(n0, [{ id: "foo" }, _ctx.obj], true)', - ) + expect(code).contains('_setDynamicProps(n0, [{ id: "foo" }, _ctx.obj])') }) test('v-bind="obj" before static prop', () => { @@ -689,9 +687,7 @@ describe('compiler: element transform', () => { ], }, ]) - expect(code).contains( - '_setDynamicProps(n0, [_ctx.obj, { id: "foo" }], true)', - ) + expect(code).contains('_setDynamicProps(n0, [_ctx.obj, { id: "foo" }])') }) test('v-bind="obj" between static props', () => { @@ -719,7 +715,7 @@ describe('compiler: element transform', () => { }, ]) expect(code).contains( - '_setDynamicProps(n0, [{ id: "foo" }, _ctx.obj, { class: "bar" }], true)', + '_setDynamicProps(n0, [{ id: "foo" }, _ctx.obj, { class: "bar" }])', ) }) diff --git a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts index e96186c275c..24289d47678 100644 --- a/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vBind.spec.ts @@ -171,7 +171,7 @@ describe('compiler v-bind', () => { ], }) expect(code).contains( - '_setDynamicProps(n0, [{ [_id]: _id, [_title]: _title }], true)', + '_setDynamicProps(n0, [{ [_id]: _id, [_title]: _title }])', ) }) @@ -224,7 +224,7 @@ describe('compiler v-bind', () => { ], }) expect(code).contains( - '_setDynamicProps(n0, [{ [_id]: _id, foo: "bar", checked: "" }], true)', + '_setDynamicProps(n0, [{ [_id]: _id, foo: "bar", checked: "" }])', ) }) @@ -341,7 +341,7 @@ describe('compiler v-bind', () => { expect(code).matchSnapshot() expect(code).contains('renderEffect') expect(code).contains( - `_setDynamicProps(n0, [{ [_camelize(_ctx.foo)]: _ctx.id }], true)`, + `_setDynamicProps(n0, [{ [_camelize(_ctx.foo)]: _ctx.id }])`, ) }) @@ -422,7 +422,7 @@ describe('compiler v-bind', () => { }) expect(code).contains('renderEffect') expect(code).contains( - `_setDynamicProps(n0, [{ ["." + _ctx.fooBar]: _ctx.id }], true)`, + `_setDynamicProps(n0, [{ ["." + _ctx.fooBar]: _ctx.id }])`, ) }) diff --git a/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts b/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts index e5fd61add2e..7a7ce771b6e 100644 --- a/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vIf.spec.ts @@ -6,6 +6,7 @@ import { transformComment, transformElement, transformText, + transformVFor, transformVIf, transformVOnce, transformVText, @@ -16,6 +17,7 @@ const compileWithVIf = makeCompile({ nodeTransforms: [ transformVOnce, transformVIf, + transformVFor, transformText, transformElement, transformComment, @@ -62,6 +64,38 @@ describe('compiler: v-if', () => { expect(code).matchSnapshot() }) + test('multiple v-if at root', () => { + const { code, ir } = compileWithVIf( + `
foo
bar
baz
`, + ) + + expect(code).toMatchSnapshot() + expect(code).contains(`_template("
foo
")`) + expect(code).contains(`_template("
bar
")`) + expect(code).contains(`_template("
baz
")`) + expect(ir.template).toMatchObject([ + '
foo
', + '
bar
', + '
baz
', + ]) + }) + + test('v-if and extra at root', () => { + const { code, ir } = compileWithVIf( + `
foo
bar
baz
`, + ) + + expect(code).toMatchSnapshot() + expect(code).contains(`_template("
foo
")`) + expect(code).contains(`_template("
bar
")`) + expect(code).contains(`_template("
baz
")`) + expect(ir.template).toMatchObject([ + '
foo
', + '
bar
', + '
baz
', + ]) + }) + test('template v-if', () => { const { code, ir } = compileWithVIf( ``, @@ -98,6 +132,62 @@ describe('compiler: v-if', () => { }) }) + test('template v-if (text)', () => { + const { code, ir } = compileWithVIf(``) + + expect(code).toMatchSnapshot() + expect(code).toContain('_template("hello")') + expect(ir.template).toMatchObject(['hello']) + }) + + test('template v-if (single element)', () => { + // single element should not wrap with fragment + const { code, ir } = compileWithVIf( + ``, + ) + + expect(code).toMatchSnapshot() + expect(code).toContain('_template("
hi
", true)') + expect(ir.template).toMatchObject(['
hi
']) + }) + + test('template v-if (multiple element)', () => { + const { code, ir } = compileWithVIf( + ``, + ) + + expect(code).toMatchSnapshot() + expect(code).toContain('_template("
hi
")') + expect(code).toContain('_template("
ho
")') + expect(ir.template).toMatchObject(['
hi
', '
ho
']) + }) + + test('template v-if (with v-for inside)', () => { + const { code, ir } = compileWithVIf( + ``, + ) + + expect(code).toMatchSnapshot() + expect(code).toContain('_template("
")') + expect(ir.template).toMatchObject(['
']) + }) + + test('template v-if + normal v-else', () => { + const { code, ir } = compileWithVIf( + `
`, + ) + + expect(code).toMatchSnapshot() + expect(code).toContain('_template("
hi
")') + expect(code).toContain('_template("
ho
")') + expect(code).toContain('_template("
", true)') + expect(ir.template).toMatchObject([ + '
hi
', + '
ho
', + '
', + ]) + }) + test('dedupe same template', () => { const { code, ir } = compileWithVIf( `
hello
hello
`, diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 193a0f5da77..cd2d162448a 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -137,7 +137,7 @@ export function generate( } const delegates = genDelegates(context) - const templates = genTemplates(ir.template, ir.rootTemplateIndex, context) + const templates = genTemplates(ir.template, ir.rootTemplateIndexes, context) const imports = genHelperImports(context) const preamble = imports + templates + delegates diff --git a/packages/compiler-vapor/src/generators/prop.ts b/packages/compiler-vapor/src/generators/prop.ts index 42f063331fc..4bf6f195c40 100644 --- a/packages/compiler-vapor/src/generators/prop.ts +++ b/packages/compiler-vapor/src/generators/prop.ts @@ -89,7 +89,6 @@ export function genDynamicProps( helper('setDynamicProps'), `n${oper.element}`, genMulti(DELIMITERS_ARRAY, ...values), - oper.root && 'true', ), ] } diff --git a/packages/compiler-vapor/src/generators/template.ts b/packages/compiler-vapor/src/generators/template.ts index 2b8a9ea0e04..4e4afc39241 100644 --- a/packages/compiler-vapor/src/generators/template.ts +++ b/packages/compiler-vapor/src/generators/template.ts @@ -6,7 +6,7 @@ import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils' export function genTemplates( templates: string[], - rootIndex: number | undefined, + rootIndexes: Set, { helper }: CodegenContext, ): string { return templates @@ -14,7 +14,7 @@ export function genTemplates( (template, i) => `const t${i} = ${helper('template')}(${JSON.stringify( template, - )}${i === rootIndex ? ', true' : ''})\n`, + )}${rootIndexes.has(i) ? ', true' : ''})\n`, ) .join('') } diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index 18f0139ab56..237958ad6e3 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -60,7 +60,7 @@ export interface RootIRNode { node: RootNode source: string template: string[] - rootTemplateIndex?: number + rootTemplateIndexes: Set component: Set directive: Set block: BlockIRNode @@ -101,7 +101,6 @@ export interface SetPropIRNode extends BaseIRNode { type: IRNodeTypes.SET_PROP element: number prop: IRProp - root: boolean tag: string } @@ -109,7 +108,6 @@ export interface SetDynamicPropsIRNode extends BaseIRNode { type: IRNodeTypes.SET_DYNAMIC_PROPS element: number props: IRProps[] - root: boolean } export interface SetDynamicEventsIRNode extends BaseIRNode { diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 946c89b734a..cbebedb1c79 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -215,6 +215,7 @@ export function transform( node, source: node.source, template: [], + rootTemplateIndexes: new Set(), component: new Set(), directive: new Set(), block: newBlock(node), diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 05153e729af..9032754b2dc 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -1,4 +1,10 @@ -import { isValidHTMLNesting } from '@vue/compiler-dom' +import { + type RootNode, + type TemplateChildNode, + hasSingleChild, + isSingleIfBlock, + isValidHTMLNesting, +} from '@vue/compiler-dom' import { type AttributeNode, type ComponentNode, @@ -67,19 +73,7 @@ export const transformElement: NodeTransform = (node, context) => { getEffectIndex, ) - let { parent } = context - while ( - parent && - parent.parent && - parent.node.type === NodeTypes.ELEMENT && - parent.node.tagType === ElementTypes.TEMPLATE - ) { - parent = parent.parent - } - const singleRoot = - context.root === parent && - parent.node.children.filter(child => child.type !== NodeTypes.COMMENT) - .length === 1 + const singleRoot = isSingleRoot(context) if (isComponent) { transformComponentElement( @@ -101,6 +95,35 @@ export const transformElement: NodeTransform = (node, context) => { } } +function isSingleRoot( + context: TransformContext, +): boolean { + if (context.inVFor) { + return false + } + + let { parent } = context + if ( + parent && + !(hasSingleChild(parent.node) || isSingleIfBlock(parent.node)) + ) { + return false + } + while ( + parent && + parent.parent && + parent.node.type === NodeTypes.ELEMENT && + parent.node.tagType === ElementTypes.TEMPLATE + ) { + parent = parent.parent + if (!(hasSingleChild(parent.node) || isSingleIfBlock(parent.node))) { + return false + } + } + + return context.root === parent +} + function transformComponentElement( node: ComponentNode, propsResult: PropsResult, @@ -150,7 +173,7 @@ function transformComponentElement( tag, props: propsResult[0] ? propsResult[1] : [propsResult[1]], asset, - root: singleRoot && !context.inVFor, + root: singleRoot, slots: [...context.slots], once: context.inVOnce, dynamic: dynamicComponent, @@ -216,7 +239,6 @@ function transformNativeElement( type: IRNodeTypes.SET_DYNAMIC_PROPS, element: context.reference(), props: dynamicArgs, - root: singleRoot, }, getEffectIndex, ) @@ -234,7 +256,6 @@ function transformNativeElement( type: IRNodeTypes.SET_PROP, element: context.reference(), prop, - root: singleRoot, tag, }, getEffectIndex, @@ -250,7 +271,7 @@ function transformNativeElement( } if (singleRoot) { - context.ir.rootTemplateIndex = context.ir.template.length + context.ir.rootTemplateIndexes.add(context.ir.template.length) } if ( diff --git a/packages/runtime-vapor/__tests__/componentAttrs.spec.ts b/packages/runtime-vapor/__tests__/componentAttrs.spec.ts index d513a0bb88c..9c57a38423d 100644 --- a/packages/runtime-vapor/__tests__/componentAttrs.spec.ts +++ b/packages/runtime-vapor/__tests__/componentAttrs.spec.ts @@ -2,6 +2,7 @@ import { type Ref, nextTick, ref } from '@vue/runtime-dom' import { createComponent, createDynamicComponent, + createIf, createSlot, defineVaporComponent, renderEffect, @@ -96,6 +97,51 @@ describe('attribute fallthrough', () => { expect(host.innerHTML).toBe('
2
') }) + it('if block', async () => { + const t0 = template('
foo
', true) + const t1 = template('
bar
', true) + const t2 = template('
baz
', true) + const { component: Child } = define({ + setup() { + const n0 = createIf( + () => true, + () => { + const n2 = t0() + return n2 + }, + () => + createIf( + () => false, + () => { + const n4 = t1() + return n4 + }, + () => { + const n7 = t2() + return n7 + }, + ), + ) + return n0 + }, + }) + + const id = ref('a') + const { host } = define({ + setup() { + return createComponent( + Child, + { + id: () => id.value, + }, + null, + true, + ) + }, + }).render() + expect(host.innerHTML).toBe('
foo
') + }) + it('should allow attrs to fallthrough on component with single-element array root', async () => { const t0 = template('
') const { component: Child } = define({