Skip to content

feat(runtime-core): add unwrapFragment to flatten nested Fragment nodes in vnode arrays #13546

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
48 changes: 48 additions & 0 deletions packages/runtime-core/__tests__/unwrapFragment.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest'
import { Fragment, type VNode, h, unwrapFragment } from '../src/index'

describe('unwrapFragment', () => {
it('returns empty array if input is undefined or empty', () => {
expect(unwrapFragment(undefined)).toEqual([])
expect(unwrapFragment([])).toEqual([])
})

it('returns same array if no Fragment present', () => {
const vnode1 = h('div')
const vnode2 = h('span')
const input = [vnode1, vnode2]
const result = unwrapFragment(input)
expect(result).toEqual(input)
})

it('unwraps single level Fragment', () => {
const children = [h('div', 'a'), h('div', 'b')]
const fragmentVNode: VNode = h(Fragment, null, children)
const input = [fragmentVNode]
const result = unwrapFragment(input)
expect(result).toHaveLength(2)
expect(result).toEqual(children)
})

it('unwraps nested Fragments recursively', () => {
const innerChildren = [h('span', 'x'), h('span', 'y')]
const innerFragment = h(Fragment, null, innerChildren)
const outerChildren = [innerFragment, h('div', 'z')]
const outerFragment = h(Fragment, null, outerChildren)
const input = [outerFragment]
const result = unwrapFragment(input)
// Should flatten all fragments recursively
expect(result).toHaveLength(3)
expect(result).toEqual([...innerChildren, outerChildren[1]])
})

it('unwraps mixed array with Fragment and non-Fragment vnode', () => {
const children = [h('li', 'item1'), h('li', 'item2')]
const fragmentVNode = h(Fragment, null, children)
const nonFragmentVNode = h('p', 'paragraph')
const input = [fragmentVNode, nonFragmentVNode]
const result = unwrapFragment(input)
expect(result).toHaveLength(3)
expect(result).toEqual([...children, nonFragmentVNode])
})
})
2 changes: 2 additions & 0 deletions packages/runtime-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export { withDirectives } from './directives'
// SSR context
export { useSSRContext, ssrContextKey } from './helpers/useSsrContext'

export { unwrapFragment } from './unwrapFragment'

// Custom Renderer API ---------------------------------------------------------

export { createRenderer, createHydrationRenderer } from './renderer'
Expand Down
17 changes: 17 additions & 0 deletions packages/runtime-core/src/unwrapFragment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type VNode, isFragmentVNode } from './vnode'

/**
* 展开 vnode 数组中所有 Fragment 节点,将它们的子节点平铺出来。
*/
export const unwrapFragment = (vnodes: VNode[] | undefined): VNode[] => {
if (!vnodes) return []
const result: VNode[] = []
for (const vnode of vnodes) {
if (isFragmentVNode(vnode)) {
result.push(...unwrapFragment(vnode.children as VNode[]))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix unsafe type assertion.

The type assertion vnode.children as VNode[] is unsafe. Fragment children can be of type VNodeNormalizedChildren which includes string | VNodeArrayChildren | RawSlots | null. This could cause runtime errors if fragment children are not an array.

Apply this fix to handle the type safely:

-      result.push(...unwrapFragment(vnode.children as VNode[]))
+      const children = vnode.children
+      if (Array.isArray(children)) {
+        result.push(...unwrapFragment(children))
+      }

This ensures we only recurse when children is actually an array, preventing runtime errors.

🤖 Prompt for AI Agents
In packages/runtime-core/src/unwrapFragment.ts at line 11, the code unsafely
asserts vnode.children as VNode[], which can cause runtime errors since children
may be other types like string or null. Fix this by adding a type check to
confirm vnode.children is an array before spreading and recursing, ensuring
unwrapFragment is only called on arrays to prevent errors.

} else {
result.push(vnode)
}
}
return result
}
4 changes: 4 additions & 0 deletions packages/runtime-core/src/vnode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ export function isVNode(value: any): value is VNode {
return value ? value.__v_isVNode === true : false
}

export function isFragmentVNode(vnode: VNode): vnode is VNode {
return !!vnode && vnode.type === Fragment
}

export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
if (__DEV__ && n2.shapeFlag & ShapeFlags.COMPONENT && n1.component) {
const dirtyInstances = hmrDirtyComponents.get(n2.type as ConcreteComponent)
Expand Down