Ver Fonte

wip: save

daiwei há 1 mês atrás
pai
commit
68d8fa6b3c

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

@@ -5027,6 +5027,51 @@ describe('VDOM interop', () => {
     )
   })
 
+  test('nested components (VDOM -> Vapor(multi-root) -> VDOM) with preceding sibling', async () => {
+    const data = ref('foo')
+    const { container } = await testWithVDOMApp(
+      `<script setup>const data = _data; const components = _components;</script>
+      <template>
+        <p>before</p>
+        <components.VaporChild/>
+      </template>`,
+      {
+        VaporChild: {
+          code: `<template><components.VdomChild/><div>second</div></template>`,
+          vapor: true,
+        },
+        VdomChild: {
+          code: `<script setup>const data = _data;</script>
+            <template><span>{{ data }}</span></template>`,
+          vapor: false,
+        },
+      },
+      data,
+    )
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><p>before</p>
+      <!--[--><span>foo</span><div>second</div><!--]-->
+      <!--]-->
+      "
+    `,
+    )
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+
+    data.value = 'bar'
+    await nextTick()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><p>before</p>
+      <!--[--><span>bar</span><div>second</div><!--]-->
+      <!--]-->
+      "
+    `,
+    )
+  })
+
   test('nested components (VDOM -> Vapor) should not duplicate', async () => {
     const { container } = await testWithVDOMApp(
       `<script setup>const components = _components;</script>
@@ -6413,4 +6458,116 @@ describe('VDOM interop', () => {
     `,
     )
   })
+
+  test('hydrate handwritten multi-root VDOM component as first child in multi-root Vapor', async () => {
+    const MultiRootVDOM = {
+      setup() {
+        return () => [
+          runtimeDom.h('span', 'Hello'),
+          runtimeDom.h('span', 'World'),
+        ]
+      },
+    }
+
+    const { container } = await testWithVaporApp(
+      `<script setup>
+        const MultiRootVDOM = _data.MultiRootVDOM
+      </script>
+      <template>
+        <MultiRootVDOM />
+        <div>After</div>
+      </template>`,
+      {},
+      { MultiRootVDOM },
+    )
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[-->
+      <!--[--><span>Hello</span><span>World</span><!--]-->
+      <div>After</div><!--]-->
+      "
+    `,
+    )
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  })
+
+  test('hydrate SFC multi-root VDOM component inside multi-root Vapor', async () => {
+    const data = ref('foo')
+    const { container } = await testWithVaporApp(
+      `<script setup>
+        const data = _data; const components = _components;
+      </script>
+      <template>
+        <div>Before</div>
+        <components.VdomMultiRoot />
+        <div>After</div>
+      </template>`,
+      {
+        VdomMultiRoot: {
+          code: `<script setup>const data = _data;</script><template><div>first {{ data }}</div><div>second {{ data }}</div></template>`,
+          vapor: false,
+        },
+      },
+      data,
+    )
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><div>Before</div>
+      <!--[--><div>first foo</div><div>second foo</div><!--]-->
+      <div>After</div><!--]-->
+      "
+    `,
+    )
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+
+    data.value = 'bar'
+    await nextTick()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><div>Before</div>
+      <!--[--><div>first bar</div><div>second bar</div><!--]-->
+      <div>After</div><!--]-->
+      "
+    `,
+    )
+  })
+
+  test('hydrate handwritten multi-root VDOM via createDynamicComponent with siblings', 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>
+        <component :is="vnode" />
+        <div>After</div>
+      </template>`,
+      {},
+      { MultiRootVDOM },
+    )
+
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[-->
+      <!--[--><span>Hello</span><span>World</span><!--]-->
+      <!--dynamic-component--><div>After</div><!--]-->
+      "
+    `,
+    )
+  })
 })

+ 4 - 0
packages/runtime-vapor/src/component.ts

@@ -248,8 +248,10 @@ export function createComponent(
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
   const _isLastInsertion = isLastInsertion
+  let hydrationStartNode: Node | null = null
   if (isHydrating) {
     locateHydrationNode()
+    hydrationStartNode = currentHydrationNode
   } else {
     resetInsertionState()
   }
@@ -334,6 +336,7 @@ export function createComponent(
     appContext,
     once,
   )
+  instance.hydrationStartNode = hydrationStartNode
 
   // handle currentKeepAliveCtx for component boundary isolation
   // AsyncWrapper should NOT clear currentKeepAliveCtx so its internal
@@ -628,6 +631,7 @@ export class VaporComponentInstance<
   propsOptions?: NormalizedPropsOptions
   emitsOptions?: ObjectEmitsOptions | null
   isSingleRoot?: boolean
+  hydrationStartNode?: Node | null
 
   /**
    * dev only flag to track whether $attrs was used during render.

+ 11 - 5
packages/runtime-vapor/src/vdomInterop.ts

@@ -647,14 +647,20 @@ function createVDOMComponent(
 
   frag.hydrate = () => {
     if (!isHydrating) return
+
     hydrateVNode(
       vnode,
       parentComponent as any,
-      // skip fragment start anchor for multi-root component to avoid mismatch
-      // `!previousSibling` means the hydration node is at the first child of
-      // the container (the parent fragment's opening `<!--[-->`), which should
-      // be skipped. Otherwise the `<!--[-->` belongs to this component itself.
-      !isSingleRoot && !currentHydrationNode!.previousSibling,
+      // 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
+          )),
     )
     onScopeDispose(unmount, true)
     isMounted = true