]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-core): prevent comments from blocking static node hoisting (#13345)
authorAlex Snezhko <alexsnezhko89@gmail.com>
Thu, 5 Jun 2025 02:23:00 +0000 (22:23 -0400)
committerGitHub <noreply@github.com>
Thu, 5 Jun 2025 02:23:00 +0000 (10:23 +0800)
 close #13344

packages/compiler-core/__tests__/transforms/__snapshots__/cacheStatic.spec.ts.snap
packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/cacheStatic.ts
packages/compiler-dom/__tests__/transforms/__snapshots__/stringifyStatic.spec.ts.snap
packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts

index 884727cfb402c5a37956b52a8efd896f613af50a..daa0b42245d133a8d3fb25ccf429666063af14ba 100644 (file)
@@ -410,6 +410,32 @@ return function render(_ctx, _cache) {
 }"
 `;
 
+exports[`compiler: cacheStatic transform > should hoist props for root with single element excluding comments 1`] = `
+"const _Vue = Vue
+const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue
+
+const _hoisted_1 = { id: "a" }
+
+return function render(_ctx, _cache) {
+  with (_ctx) {
+    const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+    return (_openBlock(), _createElementBlock(_Fragment, null, [
+      _createCommentVNode("comment"),
+      _createElementVNode("div", _hoisted_1, _cache[0] || (_cache[0] = [
+        _createElementVNode("div", { id: "b" }, [
+          _createElementVNode("div", { id: "c" }, [
+            _createElementVNode("div", { id: "d" }, [
+              _createElementVNode("div", { id: "e" }, "hello")
+            ])
+          ])
+        ], -1 /* HOISTED */)
+      ]))
+    ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
+  }
+}"
+`;
+
 exports[`compiler: cacheStatic transform > should hoist v-for children if static 1`] = `
 "const _Vue = Vue
 const { createElementVNode: _createElementVNode } = _Vue
index 358c0e31c3dc398c42cfff2a8655a1159c3fa7a7..74f6caca328831ddf9d5534d02dd602b31f7a723 100644 (file)
@@ -543,6 +543,32 @@ describe('compiler: cacheStatic transform', () => {
     expect(generate(root).code).toMatchSnapshot()
   })
 
+  test('should hoist props for root with single element excluding comments', () => {
+    // deeply nested div to trigger stringification condition
+    const root = transformWithCache(
+      `<!--comment--><div id="a"><div id="b"><div id="c"><div id="d"><div id="e">hello</div></div></div></div></div>`,
+    )
+    expect(root.cached.length).toBe(1)
+    expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'a' })])
+
+    expect((root.codegenNode as VNodeCall).children).toMatchObject([
+      {
+        type: NodeTypes.COMMENT,
+        content: 'comment',
+      },
+      {
+        type: NodeTypes.ELEMENT,
+        codegenNode: {
+          type: NodeTypes.VNODE_CALL,
+          tag: `"div"`,
+          props: { content: `_hoisted_1` },
+          children: { type: NodeTypes.JS_CACHE_EXPRESSION },
+        },
+      },
+    ])
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
   describe('prefixIdentifiers', () => {
     test('cache nested static tree with static interpolation', () => {
       const root = transformWithCache(
index aeb96cc2b4adc1cb0b07a85f6caed251fd4daa2c..9d8fd842935ecd8198cc40da400e20c90f82ea31 100644 (file)
@@ -37,7 +37,7 @@ import {
   helperNameMap,
 } from './runtimeHelpers'
 import { isVSlot } from './utils'
-import { cacheStatic, isSingleElementRoot } from './transforms/cacheStatic'
+import { cacheStatic, getSingleElementRoot } from './transforms/cacheStatic'
 import type { CompilerCompatOptions } from './compat/compatConfig'
 
 // There are two types of transforms:
@@ -356,12 +356,12 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
   const { helper } = context
   const { children } = root
   if (children.length === 1) {
-    const child = children[0]
+    const singleElementRootChild = getSingleElementRoot(root)
     // if the single child is an element, turn it into a block.
-    if (isSingleElementRoot(root, child) && child.codegenNode) {
+    if (singleElementRootChild && singleElementRootChild.codegenNode) {
       // single element root is never hoisted so codegenNode will never be
       // SimpleExpressionNode
-      const codegenNode = child.codegenNode
+      const codegenNode = singleElementRootChild.codegenNode
       if (codegenNode.type === NodeTypes.VNODE_CALL) {
         convertToBlock(codegenNode, context)
       }
@@ -370,7 +370,7 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
       // - single <slot/>, IfNode, ForNode: already blocks.
       // - single text node: always patched.
       // root codegen falls through via genNode()
-      root.codegenNode = child
+      root.codegenNode = children[0]
     }
   } else if (children.length > 1) {
     // root has multiple nodes - return a fragment block.
index e5d67380640a68327a5414caaed49effba2f97cd..239ee689a9fd6141b91f557bd1811c8e2c3fabee 100644 (file)
@@ -41,20 +41,19 @@ export function cacheStatic(root: RootNode, context: TransformContext): void {
     context,
     // Root node is unfortunately non-hoistable due to potential parent
     // fallthrough attributes.
-    isSingleElementRoot(root, root.children[0]),
+    !!getSingleElementRoot(root),
   )
 }
 
-export function isSingleElementRoot(
+export function getSingleElementRoot(
   root: RootNode,
-  child: TemplateChildNode,
-): child is PlainElementNode | ComponentNode | TemplateNode {
-  const { children } = root
-  return (
-    children.length === 1 &&
-    child.type === NodeTypes.ELEMENT &&
-    !isSlotOutlet(child)
-  )
+): PlainElementNode | ComponentNode | TemplateNode | null {
+  const children = root.children.filter(x => x.type !== NodeTypes.COMMENT)
+  return children.length === 1 &&
+    children[0].type === NodeTypes.ELEMENT &&
+    !isSlotOutlet(children[0])
+    ? children[0]
+    : null
 }
 
 function walk(
index 6d77797aaca2505f1df68fb0fe283f2aeae510c8..5bc40d3fab5181d51847a9eb2d5dd2a94d320a22 100644 (file)
@@ -75,6 +75,22 @@ return function render(_ctx, _cache) {
 }"
 `;
 
+exports[`stringify static html > should bail for comments 1`] = `
+"const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
+
+const _hoisted_1 = { class: "a" }
+
+return function render(_ctx, _cache) {
+  return (_openBlock(), _createElementBlock(_Fragment, null, [
+    _createCommentVNode(" Comment 1 "),
+    _createElementVNode("div", _hoisted_1, [
+      _createCommentVNode(" Comment 2 "),
+      _cache[0] || (_cache[0] = _createStaticVNode("<span class=\\"b\\"></span><span class=\\"b\\"></span><span class=\\"b\\"></span><span class=\\"b\\"></span><span class=\\"b\\"></span>", 5))
+    ])
+  ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
+}"
+`;
+
 exports[`stringify static html > should bail on bindings that are cached but not stringifiable 1`] = `
 "const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
 
index 79e6fc9c6e85e1697417d285c4190c3ada56a266..f58e207d6cfedf8aac1cb4deefc8f52c1634ab38 100644 (file)
@@ -491,6 +491,16 @@ describe('stringify static html', () => {
     expect(code).toMatchSnapshot()
   })
 
+  test('should bail for comments', () => {
+    const { code } = compileWithStringify(
+      `<!-- Comment 1 --><div class="a"><!-- Comment 2 -->${repeat(
+        `<span class="b"/>`,
+        StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
+      )}</div>`,
+    )
+    expect(code).toMatchSnapshot()
+  })
+
   test('should bail for <option> elements with null values', () => {
     const { ast, code } = compileWithStringify(
       `<div><select><option :value="null" />${repeat(