]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-vapor): more accurate fallthrough attr support (#13972)
authorAlex Snezhko <alexsnezhko89@gmail.com>
Tue, 18 Nov 2025 03:55:54 +0000 (19:55 -0800)
committerGitHub <noreply@github.com>
Tue, 18 Nov 2025 03:55:54 +0000 (11:55 +0800)
Co-authored-by: daiwei <daiwei521@126.com>
23 files changed:
packages/compiler-core/src/utils.ts
packages/compiler-ssr/src/transforms/ssrInjectFallthroughAttrs.ts
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
packages/compiler-vapor/__tests__/compile.spec.ts
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts
packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
packages/compiler-vapor/__tests__/transforms/vIf.spec.ts
packages/compiler-vapor/src/generate.ts
packages/compiler-vapor/src/generators/prop.ts
packages/compiler-vapor/src/generators/template.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transform.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/runtime-vapor/__tests__/componentAttrs.spec.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/dom/prop.ts

index d4de245b0524e64e7fe18edc1d49897777c23184..4ec8a7788642d45b091dbc6e4c0a50314c129d64 100644 (file)
@@ -12,6 +12,7 @@ import {
   type MemoExpression,
   NodeTypes,
   type ObjectExpression,
+  type ParentNode,
   type Position,
   type Property,
   type RenderSlotCall,
@@ -568,4 +569,38 @@ export function getMemoedVNodeCall(
   }
 }
 
+export function filterNonCommentChildren(
+  node: ParentNode,
+): TemplateChildNode[] {
+  return node.children.filter(n => n.type !== NodeTypes.COMMENT)
+}
+
+export function hasSingleChild(node: ParentNode): boolean {
+  return filterNonCommentChildren(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 filterNonCommentChildren(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]*)/
index b1aac0d74c2fd8b044fc30c6f4b4694f97d61c4b..8622421242ee5dcb49e540c7e32fe07acf7229a0 100644 (file)
@@ -2,20 +2,16 @@ import {
   ElementTypes,
   type NodeTransform,
   NodeTypes,
-  type ParentNode,
   type RootNode,
   type TemplateChildNode,
   createSimpleExpression,
+  filterNonCommentChildren,
   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 = filterNonCommentChildren(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)
   }
index 20c4e446dbff71e520f2ffb188248d4dcb879773..9527a9dbd6901f3b2640520a6a0d94f9746261e7 100644 (file)
@@ -284,7 +284,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
 "
index 32070300b9c4cd69395e2a24d60fd874740c9699..7514cde1b6da06736b28fd099b180582fad7e1c3 100644 (file)
@@ -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]() }])',
       )
     })
 
index 1befe5482f801e792a39c734835943b18111119f..69ce4b9b28ac629bd15248105eb60e98634c3db4 100644 (file)
@@ -483,7 +483,7 @@ const t0 = _template("<div></div>", true)
 
 export function render(_ctx) {
   const n0 = t0()
-  _renderEffect(() => _setDynamicProps(n0, [_ctx.obj], true))
+  _renderEffect(() => _setDynamicProps(n0, [_ctx.obj]))
   return n0
 }"
 `;
@@ -494,7 +494,7 @@ const t0 = _template("<div></div>", 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
 }"
 `;
@@ -505,7 +505,7 @@ const t0 = _template("<div></div>", 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
 }"
 `;
@@ -516,7 +516,7 @@ const t0 = _template("<div></div>", 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
 }"
 `;
index 851c529bd0632bb112d79ea020dc9aa40eca1688..73420681511f117ac1e50d6cfe7a963bab5b1e50 100644 (file)
@@ -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("<div></div>", true)
+const t0 = _template("<div></div>")
 
 export function render(_ctx) {
   const _setTemplateRef = _createTemplateRefSetter()
index e2579c9ff7fa92cb32d32af51bedd83b8825f340..e19adce62af56b33efc94cbec35a1d60995cffea 100644 (file)
@@ -37,7 +37,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
 }"
@@ -109,7 +109,7 @@ const t0 = _template("<div></div>", 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
 }"
 `;
@@ -335,7 +335,7 @@ const t0 = _template("<div></div>", 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
 }"
 `;
@@ -434,7 +434,7 @@ const t0 = _template("<div></div>", 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
 }"
 `;
@@ -609,7 +609,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
 }"
@@ -623,7 +623,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
 }"
@@ -677,7 +677,7 @@ const t0 = _template("<svg></svg>", true, 1)
 
 export function render(_ctx) {
   const n0 = t0()
-  _renderEffect(() => _setDynamicProps(n0, [_ctx.obj], true, true))
+  _renderEffect(() => _setDynamicProps(n0, [_ctx.obj], true))
   return n0
 }"
 `;
index 54f0e5a9b0cdb25c3c8021f61a5542f204adce4c..29cacf2bff3d1f277b2615052f988bcb69ef25eb 100644 (file)
@@ -2,7 +2,7 @@
 
 exports[`compiler: v-for > array de-structured value (with rest) 1`] = `
 "import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
-const t0 = _template("<div> </div>", true)
+const t0 = _template("<div> </div>")
 
 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 { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
-const t0 = _template("<div> </div>", true)
+const t0 = _template("<div> </div>")
 
 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 { txt as _txt, createInvoker as _createInvoker, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, delegateEvents as _delegateEvents, template as _template } from 'vue';
-const t0 = _template("<div> </div>", true)
+const t0 = _template("<div> </div>")
 _delegateEvents("click")
 
 export function render(_ctx) {
@@ -49,7 +49,7 @@ export function render(_ctx) {
 
 exports[`compiler: v-for > key only binding pattern 1`] = `
 "import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue';
-const t0 = _template("<tr> </tr>", true)
+const t0 = _template("<tr> </tr>")
 
 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("<div></div>", true)
+const t0 = _template("<div></div>")
 
 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, txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<span> </span>")
-const t1 = _template("<div></div>", true)
+const t1 = _template("<div></div>")
 
 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, txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
-const t0 = _template("<div> </div>", true)
+const t0 = _template("<div> </div>")
 
 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 { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
-const t0 = _template("<span> </span>", true)
+const t0 = _template("<span> </span>")
 
 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 { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
-const t0 = _template("<div> </div>", true)
+const t0 = _template("<div> </div>")
 
 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 { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue';
-const t0 = _template("<tr> </tr>", true)
+const t0 = _template("<tr> </tr>")
 
 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("<tr></tr>", true)
+const t0 = _template("<tr></tr>")
 
 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("<tr></tr>", true)
+const t0 = _template("<tr></tr>")
 
 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("<tr></tr>", true)
+const t0 = _template("<tr></tr>")
 
 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, txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
-const t0 = _template("<div> </div>", true)
+const t0 = _template("<div> </div>")
 
 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("<div>item</div>", true)
+const t0 = _template("<div>item</div>")
 
 export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
index 0d6e13301c841787dfcc697e3847923c1dc6778e..1bb82cc48262377a306ee9106a5b0464da738c58 100644 (file)
@@ -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("<div>foo</div>")
+const t1 = _template("<div>bar</div>")
+const t2 = _template("<div>baz</div>")
+
+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("<div>hi</div>")
+const t1 = _template("<div>ho</div>")
+
+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("<div>hi</div>", 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("<div></div>")
+
+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("<div>hi</div>")
+const t1 = _template("<div>ho</div>")
+const t2 = _template("<div></div>", 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 { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue';
 const t0 = _template("<div></div>")
 const t1 = _template("hello")
-const t2 = _template("<p> </p>", true)
+const t2 = _template("<p> </p>")
 
 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("<div></div>")
-const t1 = _template("<p></p>")
+const t0 = _template("<div></div>", true)
+const t1 = _template("<p></p>", true)
 
 export function render(_ctx) {
   const n0 = _createIf(() => (_ctx.ok), () => {
@@ -99,9 +197,9 @@ 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("<div></div>")
-const t1 = _template("<p></p>")
-const t2 = _template("fine")
+const t0 = _template("<div></div>", true)
+const t1 = _template("<p></p>", true)
+const t2 = _template("fine", true)
 
 export function render(_ctx) {
   const n0 = _createIf(() => (_ctx.ok), () => {
@@ -123,8 +221,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("<div></div>")
-const t1 = _template("<p></p>")
+const t0 = _template("<div></div>", true)
+const t1 = _template("<p></p>", true)
 
 export function render(_ctx) {
   const n0 = _createIf(() => (_ctx.ok), () => {
@@ -163,3 +261,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("<div>foo</div>")
+const t1 = _template("<div>bar</div>")
+const t2 = _template("<div>baz</div>")
+
+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]
+}"
+`;
index 4f177e27e40269feb0240ca076e4b97d2bf05a34..4d6b9d5b406db0bbbda54f962c04830dd2ab32da 100644 (file)
@@ -237,7 +237,7 @@ const t0 = _template("<input>", 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
 }"
 `;
index ae5e9df743a66761aea71b10341f9d223b6b01f1..7f76c3cd7c11c0d15388e326ad4e06029b6cc682 100644 (file)
@@ -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("<div></div>", true)
+const t0 = _template("<div></div>")
 
 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("<div></div>")
-const t1 = _template("<p></p>")
+const t0 = _template("<div></div>", true)
+const t1 = _template("<p></p>", true)
 
 export function render(_ctx) {
   const n0 = _createIf(() => (_ctx.expr), () => {
index 3847567cf371dd4f3332daf85b5bbe910f4720b7..ffedee627fc7fb2e521cfc8edf17c5ba941650b6 100644 (file)
@@ -634,7 +634,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', () => {
@@ -670,9 +670,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', () => {
@@ -698,9 +696,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', () => {
@@ -728,7 +724,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" }])',
     )
   })
 
index 63744c2ad783aeb3e3c26ae7afc2fba68a6bdb94..0d60c02fd54e94d994b2c2a53862ade72c310017 100644 (file)
@@ -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 }])`,
     )
   })
 
@@ -669,7 +669,7 @@ describe('compiler v-bind', () => {
       <svg v-bind="obj"/>
     `)
     expect(code).matchSnapshot()
-    expect(code).contains('_setDynamicProps(n0, [_ctx.obj], true, true))')
+    expect(code).contains('_setDynamicProps(n0, [_ctx.obj], true))')
   })
 
   test('number value', () => {
index cfac122885efed55b081deef6ae49404a765b89a..88fb6d39c92dabc99c840b730ab6718996558572 100644 (file)
@@ -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(
+      `<div v-if="foo">foo</div><div v-else-if="bar">bar</div><div v-if="baz">baz</div>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`_template("<div>foo</div>")`)
+    expect(code).contains(`_template("<div>bar</div>")`)
+    expect(code).contains(`_template("<div>baz</div>")`)
+    expect([...ir.template.keys()]).toMatchObject([
+      '<div>foo</div>',
+      '<div>bar</div>',
+      '<div>baz</div>',
+    ])
+  })
+
+  test('v-if and extra at root', () => {
+    const { code, ir } = compileWithVIf(
+      `<div v-if="foo">foo</div><div v-else-if="bar">bar</div><div>baz</div>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`_template("<div>foo</div>")`)
+    expect(code).contains(`_template("<div>bar</div>")`)
+    expect(code).contains(`_template("<div>baz</div>")`)
+    expect([...ir.template.keys()]).toMatchObject([
+      '<div>foo</div>',
+      '<div>bar</div>',
+      '<div>baz</div>',
+    ])
+  })
+
   test('template v-if', () => {
     const { code, ir } = compileWithVIf(
       `<template v-if="ok"><div/>hello<p v-text="msg"/></template>`,
@@ -102,6 +136,65 @@ describe('compiler: v-if', () => {
     })
   })
 
+  test('template v-if (text)', () => {
+    const { code, ir } = compileWithVIf(`<template v-if="foo">hello</template>`)
+
+    expect(code).toMatchSnapshot()
+    expect(code).toContain('_template("hello")')
+    expect([...ir.template.keys()]).toMatchObject(['hello'])
+  })
+
+  test('template v-if (single element)', () => {
+    // single element should not wrap with fragment
+    const { code, ir } = compileWithVIf(
+      `<template v-if="foo"><div>hi</div></template>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).toContain('_template("<div>hi</div>", true)')
+    expect([...ir.template.keys()]).toMatchObject(['<div>hi</div>'])
+  })
+
+  test('template v-if (multiple element)', () => {
+    const { code, ir } = compileWithVIf(
+      `<template v-if="foo"><div>hi</div><div>ho</div></template>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).toContain('_template("<div>hi</div>")')
+    expect(code).toContain('_template("<div>ho</div>")')
+    expect([...ir.template.keys()]).toMatchObject([
+      '<div>hi</div>',
+      '<div>ho</div>',
+    ])
+  })
+
+  test('template v-if (with v-for inside)', () => {
+    const { code, ir } = compileWithVIf(
+      `<template v-if="foo"><div v-for="i in list"/></template>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).toContain('_template("<div></div>")')
+    expect([...ir.template.keys()]).toMatchObject(['<div></div>'])
+  })
+
+  test('template v-if + normal v-else', () => {
+    const { code, ir } = compileWithVIf(
+      `<template v-if="foo"><div>hi</div><div>ho</div></template><div v-else/>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).toContain('_template("<div>hi</div>")')
+    expect(code).toContain('_template("<div>ho</div>")')
+    expect(code).toContain('_template("<div></div>", true)')
+    expect([...ir.template.keys()]).toMatchObject([
+      '<div>hi</div>',
+      '<div>ho</div>',
+      '<div></div>',
+    ])
+  })
+
   test('dedupe same template', () => {
     const { code, ir } = compileWithVIf(
       `<div v-if="ok">hello</div><div v-if="ok">hello</div>`,
index 63b010be974f474492783d7cac7ee6b9d0a849e3..8f6d03b9acf80d485bfe98e2fa22f55e2cc2aae7 100644 (file)
@@ -209,7 +209,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) + genAssetImports(context)
   const preamble = imports + templates + delegates
 
index 6b679da99f224093037f8293aaee1d350f3bf2a7..63c6471f1f4343f12370ef4e8338ed58ea4558dd 100644 (file)
@@ -92,7 +92,6 @@ export function genDynamicProps(
       helper('setDynamicProps'),
       `n${oper.element}`,
       genMulti(DELIMITERS_ARRAY, ...values),
-      oper.root && 'true',
       isSVG && 'true',
     ),
   ]
index d6f16f7014f2f2864ce1d8166b5096068a319ea3..08c589a3e11733dd121dea8ab256d7caa9c6029a 100644 (file)
@@ -16,7 +16,7 @@ import {
 
 export function genTemplates(
   templates: Map<string, number>,
-  rootIndex: number | undefined,
+  rootIndexes: Set<number>,
   context: CodegenContext,
 ): string {
   const result: string[] = []
@@ -29,7 +29,7 @@ export function genTemplates(
         // replace import expressions with string concatenation
         IMPORT_EXPR_RE,
         `" + $1 + "`,
-      )}${i === rootIndex ? ', true' : ns ? ', false' : ''}${ns ? `, ${ns}` : ''})\n`,
+      )}${rootIndexes.has(i) ? ', true' : ns ? ', false' : ''}${ns ? `, ${ns}` : ''})\n`,
     )
     i++
   })
index 9a4415e410386250ca660aae59a40769c72685d9..13fd5e1e6960f2e3ff22a648f65bf7f4a2b24a4f 100644 (file)
@@ -63,7 +63,7 @@ export interface RootIRNode {
   source: string
   template: Map<string, Namespace>
   templateIndexMap: Map<string, number>
-  rootTemplateIndex?: number
+  rootTemplateIndexes: Set<number>
   component: Set<string>
   directive: Set<string>
   block: BlockIRNode
@@ -108,7 +108,6 @@ export interface SetPropIRNode extends BaseIRNode {
   type: IRNodeTypes.SET_PROP
   element: number
   prop: IRProp
-  root: boolean
   tag: string
 }
 
@@ -116,7 +115,6 @@ export interface SetDynamicPropsIRNode extends BaseIRNode {
   type: IRNodeTypes.SET_DYNAMIC_PROPS
   element: number
   props: IRProps[]
-  root: boolean
   tag: string
 }
 
index 3d311e5cd8ac396efea89e201dd20d3f2e523983..be0efa6547169bde170e942109f7b0878863ff00 100644 (file)
@@ -251,6 +251,7 @@ export function transform(
     source: node.source,
     template: new Map<string, number>(),
     templateIndexMap: new Map<string, number>(),
+    rootTemplateIndexes: new Set(),
     component: new Set(),
     directive: new Set(),
     block: newBlock(node),
index db3943f2ad83487d011ab1420edd3352c0d23501..587bcf997ca1749e49cdcc8925ec52ebda1cc28b 100644 (file)
@@ -6,9 +6,13 @@ import {
   ErrorCodes,
   NodeTypes,
   type PlainElementNode,
+  type RootNode,
   type SimpleExpressionNode,
+  type TemplateChildNode,
   createCompilerError,
   createSimpleExpression,
+  hasSingleChild,
+  isSingleIfBlock,
   isStaticArgOf,
   isValidHTMLNesting,
 } from '@vue/compiler-dom'
@@ -73,21 +77,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) ||
-      isCustomElement
-
+    const singleRoot = isSingleRoot(context)
     if (isComponent) {
       transformComponentElement(
         node as ComponentNode,
@@ -109,6 +99,35 @@ export const transformElement: NodeTransform = (node, context) => {
   }
 }
 
+function isSingleRoot(
+  context: TransformContext<RootNode | TemplateChildNode>,
+): 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,
@@ -165,7 +184,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,
@@ -235,7 +254,6 @@ function transformNativeElement(
         type: IRNodeTypes.SET_DYNAMIC_PROPS,
         element: context.reference(),
         props: dynamicArgs,
-        root: singleRoot,
         tag,
       },
       getEffectIndex,
@@ -269,7 +287,6 @@ function transformNativeElement(
             type: IRNodeTypes.SET_PROP,
             element: context.reference(),
             prop,
-            root: singleRoot,
             tag,
           },
           getEffectIndex,
@@ -285,7 +302,7 @@ function transformNativeElement(
   }
 
   if (singleRoot) {
-    context.ir.rootTemplateIndex = context.ir.template.size
+    context.ir.rootTemplateIndexes.add(context.ir.template.size)
   }
 
   if (
index 7c86c295fbd1ae5819e77614a146b0301ba76e8b..7fd99b88fadf162914b8206989c5dc551935cbcb 100644 (file)
@@ -2,6 +2,7 @@ import { type Ref, nextTick, ref } from '@vue/runtime-dom'
 import {
   createComponent,
   createDynamicComponent,
+  createIf,
   createSlot,
   defineVaporComponent,
   renderEffect,
@@ -58,6 +59,139 @@ describe('attribute fallthrough', () => {
     expect(host.innerHTML).toBe('<div id="b">2</div>')
   })
 
+  it('should allow attrs to fallthrough on component with comment at root', async () => {
+    const t0 = template('<!--comment-->')
+    const t1 = template('<div>')
+    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('<!--comment--><div id="a">1</div>')
+
+    foo.value++
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--comment--><div id="a">2</div>')
+
+    id.value = 'b'
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--comment--><div id="b">2</div>')
+  })
+
+  it('if block', async () => {
+    const t0 = template('<div>foo</div>', true)
+    const t1 = template('<div>bar</div>', true)
+    const t2 = template('<div>baz</div>', 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('<div id="a">foo</div><!--if-->')
+  })
+
+  it('should not allow attrs to fallthrough on component with multiple roots', async () => {
+    const t0 = template('<span>')
+    const t1 = template('<div>')
+    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('<span></span><div>1</div>')
+  })
+
+  it('should not allow attrs to fallthrough on component with single comment root', async () => {
+    const t0 = template('<!--comment-->')
+    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('<!--comment-->')
+  })
+
   it('should not fallthrough if explicitly pass inheritAttrs: false', async () => {
     const t0 = template('<div>', true)
     const { component: Child } = define({
index 72a068508d578332281b623965a2a5f10b7fdad5..cae2bd7860de1a51d1f4aa288a4f4b0c2c7af1e0 100644 (file)
@@ -48,6 +48,7 @@ import {
   EMPTY_OBJ,
   ShapeFlags,
   invokeArrayFns,
+  isArray,
   isFunction,
   isString,
 } from '@vue/shared'
@@ -832,4 +833,25 @@ function getRootElement(block: Block): Element | undefined {
       return nodes
     }
   }
+
+  // The root node contains comments. It is necessary to filter out
+  // the comment nodes and return a single root node.
+  // align with vdom behavior
+  if (isArray(block)) {
+    let singleRoot: Element | undefined
+    let hasComment = false
+    for (const b of block) {
+      if (b instanceof Comment) {
+        hasComment = true
+        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 hasComment ? singleRoot : undefined
+  }
 }
index c7f9631509f22dfdcb352d98d7df1987f36a3bf7..9167d2b9b3b484049e59d33aad5537ecf5dbd81c 100644 (file)
@@ -430,16 +430,10 @@ function setHtmlToBlock(block: Block, value: any): void {
   }
 }
 
-export function setDynamicProps(
-  el: any,
-  args: any[],
-  root?: boolean,
-  isSVG?: boolean,
-): void {
+export function setDynamicProps(el: any, args: any[], isSVG?: boolean): void {
   const props = args.length > 1 ? mergeProps(...args) : args[0]
   const cacheKey = `$dprops${isApplyingFallthroughProps ? '$' : ''}`
   const prevKeys = el[cacheKey] as string[]
-  if (root) el.$root = root
 
   if (prevKeys) {
     for (const key of prevKeys) {