daiwei 1 mês atrás
pai
commit
e5ea9045c6

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

@@ -6570,4 +6570,90 @@ describe('VDOM interop', () => {
     `,
     )
   })
+
+  test('hydrate multi-root VDOM via mountVNode as non-first child', async () => {
+    const MultiRootVDOM = {
+      setup() {
+        return () => [
+          runtimeDom.h('span', 'Hello'),
+          runtimeDom.h('span', 'World'),
+        ]
+      },
+    }
+
+    const { container } = await testWithVaporApp(
+      `<script setup>
+        import { h } from 'vue'
+        const MultiRootVDOM = _data.MultiRootVDOM
+        const vnode = h(MultiRootVDOM)
+      </script>
+      <template>
+        <div>Before</div>
+        <component :is="vnode" />
+        <div>After</div>
+      </template>`,
+      {},
+      { MultiRootVDOM },
+    )
+
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><div>Before</div>
+      <!--[--><span>Hello</span><span>World</span><!--]-->
+      <!--dynamic-component--><div>After</div><!--]-->
+      "
+    `,
+    )
+  })
+
+  test('hydrate Fragment VNode as first child of multi-root Vapor via createDynamicComponent', async () => {
+    const { container } = await testWithVaporApp(
+      `<script setup>
+        import { Fragment, h } from 'vue'
+        const FragmentChunk = h(Fragment, null, [
+          h('div', null, 'first fragment'),
+          h('div', null, 'second fragment'),
+        ])
+      </script>
+      <template>
+        <component :is="FragmentChunk" />
+        <div>After</div>
+      </template>`,
+    )
+
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[-->
+      <!--[--><div>first fragment</div><div>second fragment</div><!--]-->
+      <!--dynamic-component--><div>After</div><!--]-->
+      "
+    `,
+    )
+  })
+
+  test('hydrate Element VNode as first child of multi-root Vapor via createDynamicComponent', async () => {
+    const { container } = await testWithVaporApp(
+      `<script setup>
+        import { h } from 'vue'
+        const elementVNode = h('span', null, 'hello')
+      </script>
+      <template>
+        <component :is="elementVNode" />
+        <div>After</div>
+      </template>`,
+    )
+
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><span>hello</span><!--dynamic-component--><div>After</div><!--]-->
+      "
+    `,
+    )
+  })
 })

+ 11 - 2
packages/runtime-vapor/src/component.ts

@@ -93,10 +93,12 @@ import {
   adoptTemplate,
   advanceHydrationNode,
   currentHydrationNode,
+  currentHydrationStartNode,
   isHydrating,
   locateHydrationNode,
   locateNextNode,
   setCurrentHydrationNode,
+  setCurrentHydrationStartNode,
 } from './dom/hydration'
 import { createComment, createElement, createTextNode } from './dom/node'
 import {
@@ -329,6 +331,11 @@ export function createComponent(
     return frag as any
   }
 
+  const prevHydrationStartNode = currentHydrationStartNode
+  if (isHydrating) {
+    setCurrentHydrationStartNode(hydrationStartNode)
+  }
+
   const instance = new VaporComponentInstance(
     component,
     rawProps as RawProps,
@@ -336,7 +343,6 @@ export function createComponent(
     appContext,
     once,
   )
-  instance.hydrationStartNode = hydrationStartNode
 
   // handle currentKeepAliveCtx for component boundary isolation
   // AsyncWrapper should NOT clear currentKeepAliveCtx so its internal
@@ -401,6 +407,10 @@ export function createComponent(
     advanceHydrationNode(_insertionParent!)
   }
 
+  if (isHydrating) {
+    setCurrentHydrationStartNode(prevHydrationStartNode)
+  }
+
   return instance
 }
 
@@ -631,7 +641,6 @@ export class VaporComponentInstance<
   propsOptions?: NormalizedPropsOptions
   emitsOptions?: ObjectEmitsOptions | null
   isSingleRoot?: boolean
-  hydrationStartNode?: Node | null
 
   /**
    * dev only flag to track whether $attrs was used during render.

+ 6 - 0
packages/runtime-vapor/src/dom/hydration.ts

@@ -30,6 +30,12 @@ export function setIsHydratingEnabled(value: boolean): void {
 
 export let currentHydrationNode: Node | null = null
 
+export let currentHydrationStartNode: Node | null = null
+
+export function setCurrentHydrationStartNode(node: Node | null): void {
+  currentHydrationStartNode = node
+}
+
 export let isHydrating = false
 function setIsHydrating(value: boolean) {
   if (!isHydratingEnabled && !isVdomHydrating) return false

+ 38 - 36
packages/runtime-vapor/src/vdomInterop.ts

@@ -80,9 +80,9 @@ import { optimizePropertyLookup } from './dom/prop'
 import {
   advanceHydrationNode,
   currentHydrationNode,
+  currentHydrationStartNode,
   isComment,
   isHydrating,
-  locateHydrationNode,
   setCurrentHydrationNode,
   hydrateNode as vaporHydrateNode,
 } from './dom/hydration'
@@ -483,18 +483,21 @@ function mountVNode(
 
   frag.hydrate = () => {
     if (!isHydrating) return
-    hydrateVNode(
-      vnode,
-      parentComponent as any,
-      // In the current hydration cursor, component / Teleport / Suspense / Static
-      // VNodes can be prefixed by an outer fragment start marker (`<!--[-->`).
-      // Skip it so runtime-core hydrateNode() starts from this vnode's first real node.
-      vnode.type === Static ||
-        !!(
-          vnode.shapeFlag &
-          (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT | ShapeFlags.SUSPENSE)
-        ),
-    )
+    // The hydration cursor may be sitting on a fragment start anchor
+    // (`<!--[-->`) from the parent's multi-root wrapper or VDOM slot.
+    // Skip it so runtime-core hydrateNode() receives the actual first
+    // DOM node. For Fragment VNodes (whose own SSR output also starts
+    // with `<!--[-->`), only skip when the next sibling is also
+    // `<!--[-->`, confirming nested anchors (outer = parent's, inner =
+    // Fragment's own).
+    if (
+      isOuterFragmentAnchor() &&
+      (vnode.type !== Fragment ||
+        isComment(currentHydrationNode!.nextSibling!, '['))
+    ) {
+      setCurrentHydrationNode(currentHydrationNode!.nextSibling!)
+    }
+    hydrateVNode(vnode, parentComponent as any)
     onScopeDispose(unmount, true)
     isMounted = true
     frag.nodes = resolveVNodeNodes(vnode)
@@ -647,21 +650,12 @@ function createVDOMComponent(
 
   frag.hydrate = () => {
     if (!isHydrating) return
-
-    hydrateVNode(
-      vnode,
-      parentComponent as any,
-      // Skip fragment start anchor (`<!--[-->`) for multi-root component to avoid
-      // mismatch. For the first child inside a multi-root parent, hydration starts
-      // at the parent fragment anchor (`<!--[-->`). Skip it so VDOM hydration
-      // receives the actual first node of this component.
-      !isSingleRoot &&
-        (!currentHydrationNode!.previousSibling ||
-          !!(
-            parentComponent &&
-            currentHydrationNode === parentComponent.hydrationStartNode
-          )),
-    )
+    // For multi-root components, skip the VDOM fragment start anchor
+    // so that VDOM hydration receives this component's actual first DOM node
+    if (!isSingleRoot && isOuterFragmentAnchor()) {
+      setCurrentHydrationNode(currentHydrationNode!.nextSibling!)
+    }
+    hydrateVNode(vnode, parentComponent as any)
     onScopeDispose(unmount, true)
     isMounted = true
     frag.nodes = resolveVNodeNodes(vnode)
@@ -974,18 +968,26 @@ export const vaporInteropPlugin: Plugin = app => {
   }) satisfies App['mount']
 }
 
+/**
+ * Check if the current hydration node is a VDOM fragment start anchor
+ * (`<!--[-->`) belonging to the parent rather than the VDOM component
+ * itself
+ */
+function isOuterFragmentAnchor(): boolean {
+  return (
+    isComment(currentHydrationNode!, '[') &&
+    // first child of parent
+    (!currentHydrationNode!.previousSibling ||
+      // matches the parent component's hydration start node
+      currentHydrationNode === currentHydrationStartNode)
+  )
+}
+
 function hydrateVNode(
   vnode: VNode,
   parentComponent: ComponentInternalInstance | null,
-  skipFragmentAnchor: boolean = false,
 ) {
-  locateHydrationNode()
-
-  let node = currentHydrationNode!
-  if (skipFragmentAnchor && isComment(node, '[')) {
-    setCurrentHydrationNode((node = node.nextSibling!))
-  }
-
+  const node = currentHydrationNode!
   if (!vdomHydrateNode) vdomHydrateNode = ensureHydrationRenderer().hydrateNode!
   const nextNode = vdomHydrateNode(
     node,