]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: hydration for slots
authordaiwei <daiwei521@126.com>
Sun, 27 Apr 2025 04:01:42 +0000 (12:01 +0800)
committerdaiwei <daiwei521@126.com>
Mon, 28 Apr 2025 01:36:04 +0000 (09:36 +0800)
15 files changed:
packages/compiler-ssr/__tests__/ssrComponent.spec.ts
packages/compiler-ssr/__tests__/ssrVFor.spec.ts
packages/compiler-ssr/__tests__/ssrVIf.spec.ts
packages/compiler-ssr/__tests__/ssrVModel.spec.ts
packages/compiler-ssr/src/ssrCodegenTransform.ts
packages/compiler-ssr/src/transforms/ssrVFor.ts
packages/runtime-core/src/hydration.ts
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/dom/hydration.ts
packages/runtime-vapor/src/dom/node.ts
packages/runtime-vapor/src/insertionState.ts
packages/server-renderer/src/helpers/ssrRenderSlot.ts

index 1bae37c0f6569b3002a1f2ece409239d5f82f153..fb2fff86574a1cf4d75ab74c560c59652706714d 100644 (file)
@@ -246,7 +246,7 @@ describe('ssr: components', () => {
                   _ssrRenderList(list, (i) => {
                     _push(\`<span\${_scopeId}></span>\`)
                   })
-                  _push(\`<!--]--></div>\`)
+                  _push(\`<!--]--><!--for--></div>\`)
                   _push(\`<!--if-->\`)
                 } else {
                   _push(\`<!---->\`)
@@ -270,7 +270,7 @@ describe('ssr: components', () => {
                   _ssrRenderList(_ctx.list, (i) => {
                     _push(\`<span\${_scopeId}></span>\`)
                   })
-                  _push(\`<!--]--></div>\`)
+                  _push(\`<!--]--><!--for--></div>\`)
                   _push(\`<!--if-->\`)
                 } else {
                   _push(\`<!---->\`)
index 0d957265120986165b35cf80b9247fa1b3f23ac1..dad426de04ceb5590db2cbe5317c5e9eb86e5f99 100644 (file)
@@ -10,7 +10,7 @@ describe('ssr: v-for', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<div></div>\`)
         })
-        _push(\`<!--]-->\`)
+        _push(\`<!--]--><!--for-->\`)
       }"
     `)
   })
@@ -25,7 +25,7 @@ describe('ssr: v-for', () => {
           _ssrRenderList(_ctx.list, (i) => {
             _push(\`<div>foo<span>bar</span></div>\`)
           })
-          _push(\`<!--]-->\`)
+          _push(\`<!--]--><!--for-->\`)
         }"
       `)
   })
@@ -51,9 +51,9 @@ describe('ssr: v-for', () => {
               _ssrInterpolate(j)
             }</div>\`)
           })
-          _push(\`<!--]--></div>\`)
+          _push(\`<!--]--><!--for--></div>\`)
         })
-        _push(\`<!--]-->\`)
+        _push(\`<!--]--><!--for-->\`)
       }"
     `)
   })
@@ -68,7 +68,7 @@ describe('ssr: v-for', () => {
           _ssrRenderList(_ctx.list, (i) => {
             _push(\`<!--[-->\${_ssrInterpolate(i)}<!--]-->\`)
           })
-          _push(\`<!--]-->\`)
+          _push(\`<!--]--><!--for-->\`)
         }"
       `)
   })
@@ -85,7 +85,7 @@ describe('ssr: v-for', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<span>\${_ssrInterpolate(i)}</span>\`)
         })
-        _push(\`<!--]-->\`)
+        _push(\`<!--]--><!--for-->\`)
       }"
     `)
   })
@@ -107,7 +107,7 @@ describe('ssr: v-for', () => {
             _ssrInterpolate(i + 1)
           }</span><!--]-->\`)
         })
-        _push(\`<!--]-->\`)
+        _push(\`<!--]--><!--for-->\`)
       }"
     `)
   })
@@ -127,7 +127,7 @@ describe('ssr: v-for', () => {
         _ssrRenderList(_ctx.list, ({ foo }, index) => {
           _push(\`<div>\${_ssrInterpolate(foo + _ctx.bar + index)}</div>\`)
         })
-        _push(\`<!--]-->\`)
+        _push(\`<!--]--><!--for-->\`)
       }"
     `)
   })
index e8909a82842dcc15b2c232ccf08f3e4be865fd97..840d485088ba0fe196db35937a12f260cb7137a5 100644 (file)
@@ -147,7 +147,7 @@ describe('ssr: v-if', () => {
           _ssrRenderList(_ctx.list, (i) => {
             _push(\`<div></div>\`)
           })
-          _push(\`<!--]-->\`)
+          _push(\`<!--]--><!--for-->\`)
           _push(\`<!--if-->\`)
         } else {
           _push(\`<!---->\`)
index db4af81d01ec9e000ab1a0433b4a06d6ed47d2d8..c88f6ba3182bab47d3cd1108096d9fd81e57918c 100644 (file)
@@ -70,7 +70,7 @@ describe('ssr: v-model', () => {
               : _ssrLooseEqual(_ctx.model, i))) ? " selected" : ""
           }></option>\`)
         })
-        _push(\`<!--]--></select></div>\`)
+        _push(\`<!--]--><!--for--></select></div>\`)
       }"
     `)
 
index 56f38b9e5852494c3a5e62cfd036c0b71d566a0f..29f198370aa5587084cea67320273286050c3388 100644 (file)
@@ -381,6 +381,7 @@ function processChildrenDynamicInfo(
  *  <Comp/>     // Dynamic node -> should be wrapped
  *  <Comp/>     // Dynamic node -> should NOT be wrapped
  *  <element/>  // Static node
+ * </element>
  */
 function shouldProcessChildAsDynamic(
   parent: { tag?: string; children: TemplateChildNode[] },
index 8276507850f452ca0efc68d298ec1e3df3ed090f..251b1fef5c9505b5a2907d2c2984fc086a6193cd 100644 (file)
@@ -49,8 +49,7 @@ export function ssrProcessFor(
   )
   if (!disableNestedFragments) {
     context.pushStringPart(`<!--]-->`)
-  } else {
-    // add anchor for non-fragment v-for
-    context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`)
   }
+  // v-for anchor for vapor hydration
+  context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`)
 }
index 4c8be2281d372525987fb45a742e2ca205820af8..6c702f683832c15cdf37ad9874441956d29c9176 100644 (file)
@@ -125,7 +125,7 @@ export function createHydrationFunctions(
     let n = next(node)
     // skip if:
     // - dynamic anchors (`<!--[[-->`, `<!--][-->`)
-    // - dynamic fragment end anchors (e.g. `<!--if-->`, `<!--for-->`)
+    // - vapor fragment end anchors (e.g. `<!--if-->`, `<!--for-->`)
     if (n && (isDynamicAnchor(n) || isVaporFragmentEndAnchor(n))) {
       n = next(n)
     }
index 14faf569c005f3b0c2d7b07ef82899decd902732..b0878e13919d439c0a4305cf653ce25cc7bc323c 100644 (file)
@@ -4,7 +4,12 @@ import { compileScript, parse } from '@vue/compiler-sfc'
 import * as runtimeVapor from '../src'
 import * as runtimeDom from '@vue/runtime-dom'
 import * as VueServerRenderer from '@vue/server-renderer'
-import { DYNAMIC_COMPONENT_ANCHOR_LABEL, IF_ANCHOR_LABEL } from '@vue/shared'
+import {
+  DYNAMIC_COMPONENT_ANCHOR_LABEL,
+  FOR_ANCHOR_LABEL,
+  IF_ANCHOR_LABEL,
+  SLOT_ANCHOR_LABEL,
+} from '@vue/shared'
 
 const Vue = { ...runtimeDom, ...runtimeVapor }
 
@@ -1438,6 +1443,9 @@ describe('Vapor Mode hydration', () => {
   })
 
   describe('for', () => {
+    const forAnchorLabel = FOR_ANCHOR_LABEL
+    const slotAnchorLabel = SLOT_ANCHOR_LABEL
+
     test('basic v-for', async () => {
       const { container, data } = await testHydration(
         `<template>
@@ -1454,7 +1462,7 @@ describe('Vapor Mode hydration', () => {
           `<span>a</span>` +
           `<span>b</span>` +
           `<span>c</span>` +
-          `<!--]-->` +
+          `<!--]--><!--${forAnchorLabel}-->` +
           `</div>`,
       )
 
@@ -1466,8 +1474,9 @@ describe('Vapor Mode hydration', () => {
           `<span>a</span>` +
           `<span>b</span>` +
           `<span>c</span>` +
-          `<span>d</span>` +
           `<!--]-->` +
+          `<span>d</span>` +
+          `<!--${forAnchorLabel}-->` +
           `</div>`,
       )
     })
@@ -1483,13 +1492,23 @@ describe('Vapor Mode hydration', () => {
         ref(['a', 'b', 'c']),
       )
       expect(container.innerHTML).toBe(
-        `<div><!--[--><span>a</span><span>b</span><span>c</span><!--]--></div>`,
+        `<div>` +
+          `<!--[-->` +
+          `<span>a</span><span>b</span><span>c</span>` +
+          `<!--]--><!--${forAnchorLabel}-->` +
+          `</div>`,
       )
 
       data.value.push('d')
       await nextTick()
       expect(container.innerHTML).toBe(
-        `<div><!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]--></div>`,
+        `<div>` +
+          `<!--[-->` +
+          `<span>a</span><span>b</span><span>c</span>` +
+          `<!--]-->` +
+          `<span>d</span>` +
+          `<!--${forAnchorLabel}-->` +
+          `</div>`,
       )
     })
 
@@ -1512,7 +1531,7 @@ describe('Vapor Mode hydration', () => {
           `<span>a</span>` +
           `<span>b</span>` +
           `<span>c</span>` +
-          `<!--]-->` +
+          `<!--]--><!--${forAnchorLabel}-->` +
           `<span></span>` +
           `</div>`,
       )
@@ -1526,8 +1545,9 @@ describe('Vapor Mode hydration', () => {
           `<span>a</span>` +
           `<span>b</span>` +
           `<span>c</span>` +
-          `<span>d</span>` +
           `<!--]-->` +
+          `<span>d</span>` +
+          `<!--${forAnchorLabel}-->` +
           `<span></span>` +
           `</div>`,
       )
@@ -1540,8 +1560,9 @@ describe('Vapor Mode hydration', () => {
           `<!--[-->` +
           `<span>b</span>` +
           `<span>c</span>` +
-          `<span>d</span>` +
           `<!--]-->` +
+          `<span>d</span>` +
+          `<!--${forAnchorLabel}-->` +
           `<span></span>` +
           `</div>`,
       )
@@ -1567,12 +1588,12 @@ describe('Vapor Mode hydration', () => {
           `<span>a</span>` +
           `<span>b</span>` +
           `<span>c</span>` +
-          `<!--]-->` +
+          `<!--]--><!--${forAnchorLabel}-->` +
           `<!--[-->` +
           `<span>a</span>` +
           `<span>b</span>` +
           `<span>c</span>` +
-          `<!--]-->` +
+          `<!--]--><!--${forAnchorLabel}-->` +
           `<span></span>` +
           `</div>`,
       )
@@ -1586,14 +1607,16 @@ describe('Vapor Mode hydration', () => {
           `<span>a</span>` +
           `<span>b</span>` +
           `<span>c</span>` +
-          `<span>d</span>` +
           `<!--]-->` +
+          `<span>d</span>` +
+          `<!--${forAnchorLabel}-->` +
           `<!--[-->` +
           `<span>a</span>` +
           `<span>b</span>` +
           `<span>c</span>` +
-          `<span>d</span>` +
           `<!--]-->` +
+          `<span>d</span>` +
+          `<!--${forAnchorLabel}-->` +
           `<span></span>` +
           `</div>`,
       )
@@ -1605,12 +1628,14 @@ describe('Vapor Mode hydration', () => {
           `<span></span>` +
           `<!--[-->` +
           `<span>c</span>` +
-          `<span>d</span>` +
           `<!--]-->` +
+          `<span>d</span>` +
+          `<!--${forAnchorLabel}-->` +
           `<!--[-->` +
           `<span>c</span>` +
-          `<span>d</span>` +
           `<!--]-->` +
+          `<span>d</span>` +
+          `<!--${forAnchorLabel}-->` +
           `<span></span>` +
           `</div>`,
       )
@@ -1635,7 +1660,7 @@ describe('Vapor Mode hydration', () => {
           `<div>comp</div>` +
           `<div>comp</div>` +
           `<div>comp</div>` +
-          `<!--]-->` +
+          `<!--]--><!--${forAnchorLabel}-->` +
           `</div>`,
       )
 
@@ -1647,8 +1672,9 @@ describe('Vapor Mode hydration', () => {
           `<div>comp</div>` +
           `<div>comp</div>` +
           `<div>comp</div>` +
-          `<div>comp</div>` +
           `<!--]-->` +
+          `<div>comp</div>` +
+          `<!--${forAnchorLabel}-->` +
           `</div>`,
       )
     })
@@ -1670,10 +1696,10 @@ describe('Vapor Mode hydration', () => {
       expect(container.innerHTML).toBe(
         `<div>` +
           `<!--[-->` +
-          `<!--[--><span>a</span><!--]--><!--slot-->` +
-          `<!--[--><span>b</span><!--]--><!--slot-->` +
-          `<!--[--><span>c</span><!--]--><!--slot-->` +
-          `<!--]-->` +
+          `<!--[--><span>a</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><span>b</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><span>c</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--]--><!--${forAnchorLabel}-->` +
           `</div>`,
       )
 
@@ -1682,11 +1708,12 @@ describe('Vapor Mode hydration', () => {
       expect(container.innerHTML).toBe(
         `<div>` +
           `<!--[-->` +
-          `<!--[--><span>a</span><!--]--><!--slot-->` +
-          `<!--[--><span>b</span><!--]--><!--slot-->` +
-          `<!--[--><span>c</span><!--]--><!--slot-->` +
-          `<span>d</span><!--slot-->` +
+          `<!--[--><span>a</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><span>b</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><span>c</span><!--]--><!--${slotAnchorLabel}-->` +
           `<!--]-->` +
+          `<span>d</span><!--${slotAnchorLabel}-->` +
+          `<!--${forAnchorLabel}-->` +
           `</div>`,
       )
     })
@@ -1709,7 +1736,7 @@ describe('Vapor Mode hydration', () => {
           `<!--[--><div>foo</div>-bar-<!--]-->` +
           `<!--[--><div>foo</div>-bar-<!--]-->` +
           `<!--[--><div>foo</div>-bar-<!--]-->` +
-          `<!--]-->` +
+          `<!--]--><!--${forAnchorLabel}-->` +
           `</div>`,
       )
 
@@ -1721,18 +1748,17 @@ describe('Vapor Mode hydration', () => {
           `<!--[--><div>foo</div>-bar-<!--]-->` +
           `<!--[--><div>foo</div>-bar-<!--]-->` +
           `<!--[--><div>foo</div>-bar-<!--]-->` +
-          `<div>foo</div>-bar-` +
           `<!--]-->` +
+          `<div>foo</div>-bar-` +
+          `<!--${forAnchorLabel}-->` +
           `</div>`,
       )
     })
-
-    // TODO wait for vapor TransitionGroup support
-    // v-for inside TransitionGroup does not render as a fragment
-    test.todo('v-for in TransitionGroup', async () => {})
   })
 
   describe('slots', () => {
+    const slotAnchorLabel = SLOT_ANCHOR_LABEL
+    const forAnchorLabel = FOR_ANCHOR_LABEL
     test('basic slot', async () => {
       const { data, container } = await testHydration(
         `<template>
@@ -1745,13 +1771,13 @@ describe('Vapor Mode hydration', () => {
         },
       )
       expect(container.innerHTML).toBe(
-        `<!--[--><span>foo</span><!--]--><!--slot-->`,
+        `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
       )
 
       data.value = 'bar'
       await nextTick()
       expect(container.innerHTML).toBe(
-        `<!--[--><span>bar</span><!--]--><!--slot-->`,
+        `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`,
       )
     })
 
@@ -1769,13 +1795,13 @@ describe('Vapor Mode hydration', () => {
         },
       )
       expect(container.innerHTML).toBe(
-        `<!--[--><span>foo</span><!--]--><!--slot-->`,
+        `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
       )
 
       data.value = 'bar'
       await nextTick()
       expect(container.innerHTML).toBe(
-        `<!--[--><span>bar</span><!--]--><!--slot-->`,
+        `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`,
       )
     })
 
@@ -1793,12 +1819,14 @@ describe('Vapor Mode hydration', () => {
         },
       )
       expect(container.innerHTML).toBe(
-        `<!--[--><span>foo</span><!--]--><!--slot-->`,
+        `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
       )
 
       data.value = false
       await nextTick()
-      expect(container.innerHTML).toBe(`<!--[--><!--]--><!--slot-->`)
+      expect(container.innerHTML).toBe(
+        `<!--[--><!--]--><!--${slotAnchorLabel}-->`,
+      )
     })
 
     test('named slot with v-if and v-for', async () => {
@@ -1821,15 +1849,15 @@ describe('Vapor Mode hydration', () => {
       )
       expect(container.innerHTML).toBe(
         `<!--[-->` +
-          `<!--[--><span>a</span><span>b</span><span>c</span><!--]-->` +
+          `<!--[--><span>a</span><span>b</span><span>c</span><!--]--><!--${forAnchorLabel}-->` +
           `<!--]-->` +
-          `<!--slot-->`,
+          `<!--${slotAnchorLabel}-->`,
       )
 
       data.show = false
       await nextTick()
       expect(container.innerHTML).toBe(
-        `<!--[--><!--[--><!--]--><!--]--><!--slot-->`,
+        `<!--[--><!--[--><!--]--><!--]--><!--${slotAnchorLabel}-->`,
       )
     })
 
@@ -1852,7 +1880,7 @@ describe('Vapor Mode hydration', () => {
           `<span>foo</span>` +
           `<span></span>` +
           `<!--]-->` +
-          `<!--slot-->`,
+          `<!--${slotAnchorLabel}-->`,
       )
 
       data.value = 'bar'
@@ -1863,7 +1891,7 @@ describe('Vapor Mode hydration', () => {
           `<span>bar</span>` +
           `<span></span>` +
           `<!--]-->` +
-          `<!--slot-->`,
+          `<!--${slotAnchorLabel}-->`,
       )
     })
 
@@ -1896,7 +1924,7 @@ describe('Vapor Mode hydration', () => {
           `<span>foo</span>` +
           `<span></span>` +
           `<!--]-->` +
-          `<!--slot-->` +
+          `<!--${slotAnchorLabel}-->` +
           `<div></div>` +
           `<!--]-->`,
       )
@@ -1912,14 +1940,13 @@ describe('Vapor Mode hydration', () => {
           `<span>bar</span>` +
           `<span></span>` +
           `<!--]-->` +
-          `<!--slot-->` +
+          `<!--${slotAnchorLabel}-->` +
           `<div></div>` +
           `<!--]-->`,
       )
     })
 
-    // problem is next child is incorrect after slot
-    test.todo('mixed slot and text node', async () => {
+    test('mixed slot and text node', async () => {
       const data = reactive({
         text: 'foo',
         msg: 'hi',
@@ -1937,11 +1964,11 @@ describe('Vapor Mode hydration', () => {
       )
 
       expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><!--[--><span>foo</span><!--]--><!--slot-->hi</div>"`,
+        `"<div><!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->hi</div>"`,
       )
     })
 
-    test.todo('mixed slot and element', async () => {
+    test('mixed slot and element', async () => {
       const data = reactive({
         text: 'foo',
         msg: 'hi',
@@ -1959,14 +1986,272 @@ describe('Vapor Mode hydration', () => {
       )
 
       expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><!--hi--><span>foo</span><!--]--><!--slot--><div>hi</div></div>"`,
+        `"<div><!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}--><div>hi</div></div>"`,
+      )
+    })
+
+    test('mixed slot and component', async () => {
+      const data = reactive({
+        msg1: 'foo',
+        msg2: 'bar',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span>{{data.msg1}}</span>
+          </components.Child>
+        </template>`,
+        {
+          Child: `
+          <template>
+            <div>
+              <components.Child2/>
+              <slot/>
+              <components.Child2/>
+            </div>
+          </template>`,
+          Child2: `
+          <template>
+            <div>{{data.msg2}}</div>
+          </template>`,
+        },
+        data,
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<div>bar</div>` +
+          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<div>bar</div>` +
+          `</div>`,
+      )
+      data.msg2 = 'hello'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<div>hello</div>` +
+          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<div>hello</div>` +
+          `</div>`,
+      )
+    })
+
+    test('mixed slot and fragment component', async () => {
+      const data = reactive({
+        msg1: 'foo',
+        msg2: 'bar',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span>{{data.msg1}}</span>
+          </components.Child>
+        </template>`,
+        {
+          Child: `
+          <template>
+            <div>
+              <components.Child2/>
+              <slot/>
+              <components.Child2/>
+            </div>
+          </template>`,
+          Child2: `
+          <template>
+            <div>{{data.msg1}}</div> {{data.msg2}}
+          </template>`,
+        },
+        data,
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<!--[--><div>foo</div> bar<!--]-->` +
+          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><div>foo</div> bar<!--]-->` +
+          `</div>`,
+      )
+
+      data.msg1 = 'hello'
+      data.msg2 = 'vapor'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<!--[--><div>hello</div> vapor<!--]-->` +
+          `<!--[--><span>hello</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><div>hello</div> vapor<!--]-->` +
+          `</div>`,
       )
     })
 
-    // mixed slot and component
-    // mixed slot and fragment component
-    // mixed slot and v-if
-    // mixed slot and v-for
+    test('mixed slot and v-if', async () => {
+      const data = reactive({
+        show: true,
+        msg: 'foo',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span>{{data.msg}}</span>
+          </components.Child>
+        </template>`,
+        {
+          Child: `
+          <template>
+            <div v-if="data.show">{{data.msg}}</div>
+            <slot/>
+            <div v-if="data.show">{{data.msg}}</div>
+          </template>`,
+        },
+        data,
+      )
+
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<div>foo</div><!--if-->` +
+          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<div>foo</div><!--if-->` +
+          `<!--]-->`,
+      )
+
+      data.show = false
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<!--if-->` +
+          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--if-->` +
+          `<!--]-->`,
+      )
+    })
+
+    test('mixed slot and v-for', async () => {
+      const data = reactive({
+        items: ['a', 'b', 'c'],
+        msg: 'foo',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span>{{data.msg}}</span>
+          </components.Child>
+        </template>`,
+        {
+          Child: `
+          <template>
+            <div v-for="item in data.items" :key="item">{{item}}</div>
+            <slot/>
+            <div v-for="item in data.items" :key="item">{{item}}</div>
+          </template>`,
+        },
+        data,
+      )
+
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<!--[--><div>a</div><div>b</div><div>c</div><!--]--><!--${forAnchorLabel}-->` +
+          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><div>a</div><div>b</div><div>c</div><!--]--><!--${forAnchorLabel}-->` +
+          `<!--]-->`,
+      )
+
+      data.items.push('d')
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<!--[--><div>a</div><div>b</div><div>c</div><!--]--><div>d</div><!--${forAnchorLabel}-->` +
+          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><div>a</div><div>b</div><div>c</div><!--]--><div>d</div><!--${forAnchorLabel}-->` +
+          `<!--]-->`,
+      )
+    })
+
+    test('consecutive slots', async () => {
+      const data = reactive({
+        msg1: 'foo',
+        msg2: 'bar',
+      })
+
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span>{{data.msg1}}</span>
+            <template #bar>
+              <span>{{data.msg2}}</span>
+            </template>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><slot/><slot name="bar"/></template>`,
+        },
+        data,
+      )
+
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--]-->`,
+      )
+
+      data.msg1 = 'hello'
+      data.msg2 = 'vapor'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--[-->` +
+          `<!--[--><span>hello</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><span>vapor</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--]-->`,
+      )
+    })
+
+    test('consecutive slots with anchor insertion', async () => {
+      const data = reactive({
+        msg1: 'foo',
+        msg2: 'bar',
+      })
+
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span>{{data.msg1}}</span>
+            <template #bar>
+              <span>{{data.msg2}}</span>
+            </template>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template>
+            <div>
+              <span/>
+              <slot/>
+              <slot name="bar"/>
+              <span/>
+            </div>
+          </template>`,
+        },
+        data,
+      )
+
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+
+      data.msg1 = 'hello'
+      data.msg2 = 'vapor'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span></span>` +
+          `<!--[--><span>hello</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><span>vapor</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<span></span>` +
+          `</div>`,
+      )
+    })
   })
 
   // test('element with ref', () => {
index 54b65d50bdf6c89ca2e61173501d04094207e4b8..704b01973d8d357210e171f2df0e7870f9957bb6 100644 (file)
@@ -19,7 +19,7 @@ import {
 import {
   createComment,
   createTextNode,
-  nextVaporFragmentAnchor,
+  findVaporFragmentAnchor,
 } from './dom/node'
 import {
   type Block,
@@ -34,7 +34,6 @@ import { renderEffect } from './renderEffect'
 import { VaporVForFlags } from '../../shared/src/vaporFlags'
 import {
   currentHydrationNode,
-  isComment,
   isHydrating,
   locateHydrationNode,
 } from './dom/hydration'
@@ -99,15 +98,20 @@ export const createFor = (
   let oldBlocks: ForBlock[] = []
   let newBlocks: ForBlock[]
   let parent: ParentNode | undefined | null
-  const parentAnchor = isHydrating
-    ? // Use fragment end anchor if available, otherwise use the specific for anchor.
-      nextVaporFragmentAnchor(
-        currentHydrationNode!,
-        isComment(currentHydrationNode!, '[') ? ']' : FOR_ANCHOR_LABEL,
-      )!
-    : __DEV__
-      ? createComment('for')
-      : createTextNode()
+  let parentAnchor: Node
+  if (isHydrating) {
+    parentAnchor = findVaporFragmentAnchor(
+      currentHydrationNode!,
+      FOR_ANCHOR_LABEL,
+    )!
+    if (__DEV__ && !parentAnchor) {
+      // TODO warn, should not happen
+      warn(`createFor anchor not found...`)
+    }
+  } else {
+    parentAnchor = __DEV__ ? createComment('for') : createTextNode()
+  }
+
   const frag = new VaporFragment(oldBlocks)
   const instance = currentInstance!
   const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE
index c846cc87247f225e72edb19ea7a60bad8c87c397..987949646266fa9530200578c978bfdf400d8198 100644 (file)
@@ -8,7 +8,7 @@ import {
 import {
   createComment,
   createTextNode,
-  nextVaporFragmentAnchor,
+  findVaporFragmentAnchor,
 } from './dom/node'
 import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
 import {
@@ -99,7 +99,7 @@ export class DynamicFragment extends VaporFragment {
       this.anchor = currentHydrationNode
     } else {
       // find next sibling dynamic fragment end anchor
-      const anchor = nextVaporFragmentAnchor(currentHydrationNode!, label)!
+      const anchor = findVaporFragmentAnchor(currentHydrationNode!, label)!
       if (anchor) {
         this.anchor = anchor
       } else if (__DEV__) {
index 548babebf8beef2115e31356d50a989e2e1a0112..03675475b8d981c4c5ca757fa2c0b804c16ec7ed 100644 (file)
@@ -59,7 +59,11 @@ import {
 } from './componentSlots'
 import { hmrReload, hmrRerender } from './hmr'
 import { isHydrating, locateHydrationNode } from './dom/hydration'
-import { insertionAnchor, insertionParent } from './insertionState'
+import {
+  insertionAnchor,
+  insertionParent,
+  resetInsertionState,
+} from './insertionState'
 
 export { currentInstance } from '@vue/runtime-dom'
 
@@ -142,6 +146,8 @@ export function createComponent(
   const _insertionAnchor = insertionAnchor
   if (isHydrating) {
     locateHydrationNode()
+  } else {
+    resetInsertionState()
   }
 
   // vdom interop enabled and component is not an explicit vapor component
index 2506a5c8fb25aed2df655f718df20701ccb084b6..3ed3ee2b8671f579d99c854b5189bafb83467958 100644 (file)
@@ -6,7 +6,7 @@ import {
   setInsertionState,
 } from '../insertionState'
 import {
-  child,
+  _child,
   disableHydrationNodeLookup,
   enableHydrationNodeLookup,
   next,
@@ -28,6 +28,7 @@ export function withHydration(container: ParentNode, fn: () => void): void {
   if (!isOptimized) {
     // optimize anchor cache lookup
     ;(Comment.prototype as any).$fs = undefined
+    ;(Node.prototype as any).$nc = undefined
     isOptimized = true
   }
   enableHydrationNodeLookup()
@@ -87,19 +88,17 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
   let node: Node | null
   // prepend / firstChild
   if (insertionAnchor === 0) {
-    node = child(insertionParent!)
+    node = _child(insertionParent!)
   } else if (insertionAnchor) {
     // for dynamic children, use insertionAnchor as the node
     node = insertionAnchor
   } else {
-    node = insertionParent ? insertionParent.lastChild : currentHydrationNode
+    node = insertionParent
+      ? insertionParent.$nc || insertionParent.lastChild
+      : currentHydrationNode
 
-    // if current node is fragment start anchor, find the next one
-    if (node && isComment(node, '[')) {
-      node = node.nextSibling
-    }
     // if the last child is a vapor fragment end anchor, find the previous one
-    else if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) {
+    if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) {
       node = node.previousSibling
       if (__DEV__ && !node) {
         // TODO warning, should not happen
@@ -135,6 +134,10 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
         }
       }
     }
+
+    if (insertionParent && node) {
+      insertionParent.$nc = node!.previousSibling
+    }
   }
 
   if (__DEV__ && !node) {
index 18cfd3fc903e304dc5ee37463a001b9d95be0660..b1922944537ccd7bee586f59b80bac624f8d68ed 100644 (file)
@@ -22,10 +22,42 @@ export function querySelector(selectors: string): Element | null {
 }
 
 /*! #__NO_SIDE_EFFECTS__ */
-export function child(node: ParentNode): Node {
+export function _child(node: ParentNode): Node {
   return node.firstChild!
 }
 
+/*! #__NO_SIDE_EFFECTS__ */
+export function __child(node: ParentNode): Node {
+  /**
+   * During hydration, the first child of a node not be the expected
+   * if the first child is slot
+   *
+   * for template code: `div><slot />{{ data }}</div>`
+   * - slot: 'slot',
+   * - data: 'hi',
+   *
+   * client side:
+   * const n2 = _template("<div> </div>")()
+   * const n1 = _child(n2) -> the text node
+   * _setInsertionState(n2, 0) -> slot fragment
+   *
+   * during hydration:
+   * const n2 = _template("<div><!--[-->slot<!--]--><!--slot-->Hi</div>")()
+   * const n1 = _child(n2) -> should be `Hi` instead of the slot fragment
+   * _setInsertionState(n2, 0) -> slot fragment
+   */
+  let n = node.firstChild!
+
+  if (isComment(n, '[')) {
+    n = locateEndAnchor(n)!.nextSibling!
+  }
+
+  while (n && isVaporFragmentEndAnchor(n)) {
+    n = n.nextSibling!
+  }
+  return n
+}
+
 /*! #__NO_SIDE_EFFECTS__ */
 export function _nthChild(node: Node, i: number): Node {
   return node.childNodes[i]
@@ -56,9 +88,13 @@ export function __next(node: Node): Node {
   return n
 }
 
+type ChildFn = (node: ParentNode) => Node
 type NextFn = (node: Node) => Node
 type NthChildFn = (node: Node, i: number) => Node
 
+interface DelegatedChildFunction extends ChildFn {
+  impl: ChildFn
+}
 interface DelegatedNextFunction extends NextFn {
   impl: NextFn
 }
@@ -66,6 +102,12 @@ interface DelegatedNthChildFunction extends NthChildFn {
   impl: NthChildFn
 }
 
+/*! #__NO_SIDE_EFFECTS__ */
+export const child: DelegatedChildFunction = node => {
+  return child.impl(node)
+}
+child.impl = _child
+
 /*! #__NO_SIDE_EFFECTS__ */
 export const next: DelegatedNextFunction = node => {
   return next.impl(node)
@@ -90,11 +132,13 @@ nthChild.impl = _nthChild
 // of `next` and `nthChild`. After hydration is complete, their implementations
 // are restored to the original versions.
 export function enableHydrationNodeLookup(): void {
+  child.impl = __child
   next.impl = __next
   nthChild.impl = __nthChild
 }
 
 export function disableHydrationNodeLookup(): void {
+  child.impl = _child
   next.impl = _next
   nthChild.impl = _nthChild
 }
@@ -112,15 +156,10 @@ function isNonHydrationNode(node: Node) {
   )
 }
 
-export function nextVaporFragmentAnchor(
+export function findVaporFragmentAnchor(
   node: Node,
   anchorLabel: string,
 ): Comment | null {
-  node = handleWrappedNode(node)
-  if (isComment(node, anchorLabel)) {
-    return node as Comment
-  }
-
   let n = node.nextSibling
   while (n) {
     if (isComment(n, anchorLabel)) return n
index c8c7ffbcd1de3b1000cbc9fa62f50ee9c8c3a0b3..b33c820e0132c1cb5ab17a7c40715129926bff61 100644 (file)
@@ -1,4 +1,9 @@
-export let insertionParent: ParentNode | undefined
+export let insertionParent:
+  | (ParentNode & {
+      // the next child node to be hydrated
+      $nc?: Node | null
+    })
+  | undefined
 export let insertionAnchor: Node | 0 | undefined
 
 /**
index b8a57ae8d96a0b860df4a019964353ca97e69ccb..0733c8233907c5e6883358b5f377a0a3d265b293 100644 (file)
@@ -104,7 +104,7 @@ export function ssrRenderSlotInner(
         if (
           transition &&
           slotBuffer[0] === '<!--[-->' &&
-          slotBuffer[end - 1] === '<!--]-->'
+          (slotBuffer[end - 1] as string).startsWith('<!--]-->')
         ) {
           start++
           end--