Skip to content

fix(compiler-core): prevent root comments from blocking static node hoisting #13345

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions packages/compiler-core/__tests__/transforms/cacheStatic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,33 @@ 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 },
},
},
])
console.log(generate(root).code)
expect(generate(root).code).toMatchSnapshot()
})

describe('prefixIdentifiers', () => {
test('cache nested static tree with static interpolation', () => {
const root = transformWithCache(
Expand Down
10 changes: 5 additions & 5 deletions packages/compiler-core/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
}
Expand All @@ -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.
Expand Down
19 changes: 9 additions & 10 deletions packages/compiler-core/src/transforms/cacheStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +48 to 57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Key implementation change to filter out comment nodes.

This is the core change that fixes issue #13344. The new getSingleElementRoot function:

  1. Filters out comment nodes from the children array
  2. Checks if there's exactly one element child that's not a slot outlet
  3. Returns the actual element node or null (instead of a boolean)

This ensures that static prop hoisting works correctly even when comment nodes are present, which addresses the fallthrough attributes issue in the PR description.

While thoroughly filtering out all comment nodes is correct for this specific use case (hoisting static props), ensure that this doesn't break any other functionality that might rely on comments being processed. The test case appropriately verifies that the comment node is still present in the final output.

The implementation is clean and well-focused on the specific issue being fixed.

🤖 Prompt for AI Agents
In packages/compiler-core/src/transforms/cacheStatic.ts around lines 48 to 57,
update the getSingleElementRoot function to filter out comment nodes from the
root's children before checking if there is exactly one element child that is
not a slot outlet. Return the element node if conditions are met, otherwise
return null. This change ensures static prop hoisting works correctly when
comment nodes are present without affecting other functionality that relies on
comments.


function walk(
Expand Down