Prechádzať zdrojové kódy

fix(runtime-vapor): handle Static/Fragment vnode ranges in interop block nodes (#14510)

edison 1 mesiac pred
rodič
commit
8ec6b64318

+ 88 - 0
packages/runtime-vapor/__tests__/hydration.spec.ts

@@ -5377,6 +5377,94 @@ describe('VDOM interop', () => {
     )
   })
 
+  test('hydrate static VNode chunk rendered via createDynamicComponent', async () => {
+    const data = ref('foo')
+    const { container } = await testWithVaporApp(
+      `<script setup>
+        import { createStaticVNode } from 'vue'
+        const data = _data
+        const StaticChunk = createStaticVNode(
+          '<div>first static</div><div>second static</div>',
+          2,
+        )
+      </script>
+      <template>
+        <div>
+          <component :is="StaticChunk" />
+        </div>
+        <span>{{ data }}</span>
+      </template>`,
+      undefined,
+      data,
+    )
+
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><div><div>first static</div><div>second static</div><!--dynamic-component--></div><span>foo</span><!--]-->
+      "
+    `,
+    )
+
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+
+    data.value = 'bar'
+    await nextTick()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><div><div>first static</div><div>second static</div><!--dynamic-component--></div><span>bar</span><!--]-->
+      "
+    `,
+    )
+  })
+
+  test('hydrate Fragment VNode rendered via createDynamicComponent', async () => {
+    const data = ref('foo')
+    const { container } = await testWithVaporApp(
+      `<script setup>
+        import { Fragment, h } from 'vue'
+        const data = _data
+        const FragmentChunk = h(Fragment, null, [
+          h('div', null, 'first fragment'),
+          h('div', null, 'second fragment'),
+        ])
+      </script>
+      <template>
+        <div>
+          <component :is="FragmentChunk" />
+        </div>
+        <span>{{ data }}</span>
+      </template>`,
+      undefined,
+      data,
+    )
+
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><div>
+      <!--[--><div>first fragment</div><div>second fragment</div><!--]-->
+      <!--dynamic-component--></div><span>foo</span><!--]-->
+      "
+    `,
+    )
+
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+
+    data.value = 'bar'
+    await nextTick()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><div>
+      <!--[--><div>first fragment</div><div>second fragment</div><!--]-->
+      <!--dynamic-component--></div><span>bar</span><!--]-->
+      "
+    `,
+    )
+  })
+
   test('hydrate VDOM slot content', async () => {
     const data = ref('foo')
     const { container } = await testWithVaporApp(

+ 27 - 6
packages/runtime-vapor/src/vdomInterop.ts

@@ -2,6 +2,7 @@ import {
   type App,
   type ComponentInternalInstance,
   type ConcreteComponent,
+  Fragment,
   type FunctionalComponent,
   type HydrationRenderer,
   type KeepAliveContext,
@@ -12,6 +13,7 @@ import {
   type RendererNode,
   type ShallowRef,
   type Slots,
+  Static,
   type SuspenseBoundary,
   type TransitionHooks,
   type VNode,
@@ -383,6 +385,25 @@ const vaporSlotsProxyHandler: ProxyHandler<any> = {
 
 let vdomHydrateNode: HydrationRenderer['hydrateNode'] | undefined
 
+function resolveVNodeNodes(vnode: VNode): Block {
+  // Static/Fragment VNodes represent a contiguous node range [el..anchor].
+  // Return the full range so Vapor block helpers (insert/remove/move)
+  // operate on the same boundaries as runtime-core. Single-node VNodes
+  // fall back to `el` below.
+  const { type, el, anchor } = vnode
+  if ((type === Static || type === Fragment) && el && anchor && anchor !== el) {
+    const range: Node[] = []
+    let n: Node | null = el as Node
+    while (n) {
+      range.push(n)
+      if (n === anchor) break
+      n = n.nextSibling
+    }
+    return range
+  }
+  return el as Block
+}
+
 /**
  * Mount VNode in vapor
  */
@@ -393,7 +414,7 @@ function mountVNode(
 ): VaporFragment {
   const suspense =
     currentParentSuspense || (parentComponent && parentComponent.suspense)
-  const frag = new VaporFragment([])
+  const frag = new VaporFragment<Block>([])
   frag.vnode = vnode
 
   let isMounted = false
@@ -424,7 +445,7 @@ function mountVNode(
     hydrateVNode(vnode, parentComponent as any)
     onScopeDispose(unmount, true)
     isMounted = true
-    frag.nodes = vnode.el as any
+    frag.nodes = resolveVNodeNodes(vnode)
   }
 
   frag.insert = (parentNode, anchor, transition) => {
@@ -474,7 +495,7 @@ function mountVNode(
       }
       simpleSetCurrentInstance(prev)
     }
-    frag.nodes = vnode.el as any
+    frag.nodes = resolveVNodeNodes(vnode)
     if (isMounted && frag.onUpdated) frag.onUpdated.forEach(m => m())
   }
 
@@ -497,7 +518,7 @@ function createVDOMComponent(
     currentParentSuspense || (parentComponent && parentComponent.suspense)
   const useBridge = shouldUseRendererBridge(component)
   const comp = useBridge ? ensureRendererBridge(component) : component
-  const frag = new VaporFragment([])
+  const frag = new VaporFragment<Block>([])
   const vnode = (frag.vnode = createVNode(
     comp,
     rawProps && extend({}, new Proxy(rawProps, rawPropsProxyHandlers)),
@@ -564,7 +585,7 @@ function createVDOMComponent(
     )
     onScopeDispose(unmount, true)
     isMounted = true
-    frag.nodes = vnode.el as any
+    frag.nodes = resolveVNodeNodes(vnode)
   }
 
   vnode.scopeId = getCurrentScopeId() || null
@@ -614,7 +635,7 @@ function createVDOMComponent(
       simpleSetCurrentInstance(prev)
     }
 
-    frag.nodes = vnode.el as any
+    frag.nodes = resolveVNodeNodes(vnode)
     if (isMounted && frag.onUpdated) frag.onUpdated.forEach(m => m())
   }