]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(vapor): hydration (#13226)
authoredison <daiwei521@126.com>
Tue, 21 Oct 2025 00:29:08 +0000 (08:29 +0800)
committerGitHub <noreply@github.com>
Tue, 21 Oct 2025 00:29:08 +0000 (08:29 +0800)
48 files changed:
package.json
packages/compiler-sfc/src/compileScript.ts
packages/compiler-ssr/__tests__/ssrVIf.spec.ts
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
packages/compiler-vapor/__tests__/compile.spec.ts
packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/expression.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformElement.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vText.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts
packages/compiler-vapor/__tests__/transforms/transformElement.spec.ts
packages/compiler-vapor/src/generators/operation.ts
packages/compiler-vapor/src/generators/template.ts
packages/compiler-vapor/src/generators/text.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/transformChildren.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/compiler-vapor/src/transforms/vIf.ts
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-dom/src/index.ts
packages/runtime-vapor/__tests__/dom/prop.spec.ts
packages/runtime-vapor/__tests__/dom/template.spec.ts
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/apiCreateDynamicComponent.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/apiCreateIf.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentSlots.ts
packages/runtime-vapor/src/directives/vShow.ts
packages/runtime-vapor/src/dom/hydration.ts
packages/runtime-vapor/src/dom/node.ts
packages/runtime-vapor/src/dom/prop.ts
packages/runtime-vapor/src/dom/template.ts
packages/runtime-vapor/src/fragment.ts
packages/runtime-vapor/src/index.ts
packages/runtime-vapor/src/insertionState.ts
packages/runtime-vapor/src/vdomInterop.ts
packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts
pnpm-lock.yaml

index e94789865e9be4f15a509cf303ead4bb7fefa9ee..26e4f5f6ca64c8a3846e38fc59753a6da33e7a20 100644 (file)
@@ -98,7 +98,7 @@
     "pug": "^3.0.3",
     "puppeteer": "~24.16.2",
     "rimraf": "^6.0.1",
-    "rollup": "4.50.1",
+    "rollup": "^4.52.5",
     "rollup-plugin-dts": "^6.2.3",
     "rollup-plugin-esbuild": "^6.2.1",
     "rollup-plugin-polyfill-node": "^0.13.0",
index 36ac6547709377b52f1a92b7ed534d675f4dec05..fe413753548007294c871ea0108ad362eace37e6 100644 (file)
@@ -1021,6 +1021,11 @@ export function compileScript(
     : ''
   // wrap setup code with function.
   if (ctx.isTS) {
+    // in SSR, always use defineComponent, so __vapor flag is required
+    if (ssr && vapor) {
+      runtimeOptions += `\n  __vapor: true,`
+    }
+
     // for TS, make sure the exported type is still valid type with
     // correct props information
     // we have to use object spread for types to be merged properly
index b544adadcf3a77293cbf91b35b48f3305707ea0d..7867a4b5fb50724117a0ac01ffec5353d54fbac9 100644 (file)
@@ -82,15 +82,15 @@ describe('ssr: v-if', () => {
   test('<template v-if> (text)', () => {
     expect(compile(`<template v-if="foo">hello</template>`).code)
       .toMatchInlineSnapshot(`
-      "
-      return function ssrRender(_ctx, _push, _parent, _attrs) {
-        if (_ctx.foo) {
-          _push(\`<!--[-->hello<!--]-->\`)
-        } else {
-          _push(\`<!---->\`)
-        }
-      }"
-    `)
+        "
+        return function ssrRender(_ctx, _push, _parent, _attrs) {
+          if (_ctx.foo) {
+            _push(\`<!--[-->hello<!--]-->\`)
+          } else {
+            _push(\`<!---->\`)
+          }
+        }"
+      `)
   })
 
   test('<template v-if> (single element)', () => {
index 9f2183ce83ea698ac9b1fb7f3d30aca19cde8a1d..0120f5f487827e4f3cbfdbe53d7cb94a24381b37 100644 (file)
@@ -1,12 +1,12 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
 exports[`compile > bindings 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div> </div>", true)
 
 export function render(_ctx, $props, $emit, $attrs, $slots) {
   const n0 = t0()
-  const x0 = _child(n0)
+  const x0 = _txt(n0)
   _renderEffect(() => _setText(x0, "count is " + _toDisplayString(_ctx.count) + "."))
   return n0
 }"
@@ -38,7 +38,7 @@ export function render(_ctx) {
     "default": () => {
       const n0 = _createIf(() => (true), () => {
         const n3 = t0()
-        _setInsertionState(n3)
+        _setInsertionState(n3, null, true)
         const n2 = _createComponentWithFallback(_component_Bar)
         _withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]])
         return n3
@@ -157,8 +157,8 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
   const _component_Comp = _resolveComponent("Comp")
   const n0 = t0()
   const n3 = t1()
-  const n2 = _child(n3)
-  _setInsertionState(n3, 0)
+  const n2 = _child(n3, 1)
+  _setInsertionState(n3, 0, true)
   const n1 = _createComponentWithFallback(_component_Comp)
   _renderEffect(() => {
     _setProp(n3, "id", _ctx.foo)
@@ -180,13 +180,13 @@ export function render(_ctx) {
 `;
 
 exports[`compile > dynamic root nodes and interpolation 1`] = `
-"import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, delegateEvents as _delegateEvents, template as _template } from 'vue';
+"import { txt as _txt, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, delegateEvents as _delegateEvents, template as _template } from 'vue';
 const t0 = _template("<button> </button>", true)
 _delegateEvents("click")
 
 export function render(_ctx) {
   const n0 = t0()
-  const x0 = _child(n0)
+  const x0 = _txt(n0)
   n0.$evtclick = e => _ctx.handleClick(e)
   _renderEffect(() => {
     const _count = _ctx.count
@@ -198,12 +198,12 @@ export function render(_ctx) {
 `;
 
 exports[`compile > execution order > basic 1`] = `
-"import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { txt as _txt, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div> </div>", true)
 
 export function render(_ctx) {
   const n0 = t0()
-  const x0 = _child(n0)
+  const x0 = _txt(n0)
   _renderEffect(() => {
     _setProp(n0, "id", _ctx.foo)
     _setText(x0, _toDisplayString(_ctx.bar))
@@ -212,6 +212,30 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compile > execution order > setInsertionState > next, child and nthChild should be above the setInsertionState 1`] = `
+"import { resolveComponent as _resolveComponent, child as _child, next as _next, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, nthChild as _nthChild, createIf as _createIf, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+const t1 = _template("<div><div></div><!><div></div><!><div><button></button></div></div>", true)
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n6 = t1()
+  const n5 = _next(_child(n6), 1)
+  const n7 = _nthChild(n6, 3, 3)
+  const p0 = _next(n7, 4)
+  const n4 = _child(p0, 0)
+  _setInsertionState(n6, n5)
+  const n0 = _createComponentWithFallback(_component_Comp)
+  _setInsertionState(n6, n7, true)
+  const n1 = _createIf(() => (true), () => {
+    const n3 = t0()
+    return n3
+  })
+  _renderEffect(() => _setProp(n4, "disabled", _ctx.foo))
+  return n6
+}"
+`;
+
 exports[`compile > execution order > with insertionState 1`] = `
 "import { resolveComponent as _resolveComponent, child as _child, setInsertionState as _setInsertionState, createSlot as _createSlot, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
 const t0 = _template("<div><div></div></div>", true)
@@ -219,25 +243,25 @@ const t0 = _template("<div><div></div></div>", true)
 export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
   const n3 = t0()
-  const n1 = _child(n3)
-  _setInsertionState(n1)
+  const n1 = _child(n3, 0)
+  _setInsertionState(n1, null, true)
   const n0 = _createSlot("default", null)
-  _setInsertionState(n3)
+  _setInsertionState(n3, 1, true)
   const n2 = _createComponentWithFallback(_component_Comp)
   return n3
 }"
 `;
 
 exports[`compile > execution order > with v-once 1`] = `
-"import { child as _child, next as _next, nthChild as _nthChild, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { child as _child, next as _next, nthChild as _nthChild, txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div><span> </span> <br> </div>", true)
 
 export function render(_ctx) {
   const n3 = t0()
-  const n0 = _child(n3)
-  const n1 = _next(n0)
-  const n2 = _nthChild(n3, 3)
-  const x0 = _child(n0)
+  const n0 = _child(n3, 0)
+  const n1 = _next(n0, 1)
+  const n2 = _nthChild(n3, 3, 3)
+  const x0 = _txt(n0)
   _setText(x0, _toDisplayString(_ctx.foo))
   _renderEffect(() => {
     _setText(n1, " " + _toDisplayString(_ctx.bar))
@@ -280,30 +304,6 @@ export function render(_ctx) {
 }"
 `;
 
-exports[`compile > setInsertionState > next, child and nthChild should be above the setInsertionState 1`] = `
-"import { resolveComponent as _resolveComponent, child as _child, next as _next, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, nthChild as _nthChild, createIf as _createIf, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
-const t0 = _template("<div></div>")
-const t1 = _template("<div><div></div><!><div></div><!><div><button></button></div></div>", true)
-
-export function render(_ctx) {
-  const _component_Comp = _resolveComponent("Comp")
-  const n6 = t1()
-  const n5 = _next(_child(n6))
-  const n7 = _nthChild(n6, 3)
-  const p0 = _next(n7)
-  const n4 = _child(p0)
-  _setInsertionState(n6, n5)
-  const n0 = _createComponentWithFallback(_component_Comp)
-  _setInsertionState(n6, n7)
-  const n1 = _createIf(() => (true), () => {
-    const n3 = t0()
-    return n3
-  })
-  _renderEffect(() => _setProp(n4, "disabled", _ctx.foo))
-  return n6
-}"
-`;
-
 exports[`compile > static + dynamic root 1`] = `
 "import { toDisplayString as _toDisplayString, setText as _setText, template as _template } from 'vue';
 const t0 = _template(" ")
index 7963a9e98c2c63a96ec47ee79a8ac1cfe0cfa413..6de93bd320c2c86a8d9dd4163b0b7246f0cc6e2a 100644 (file)
@@ -221,9 +221,19 @@ describe('compile', () => {
     })
   })
 
-  describe('setInsertionState', () => {
-    test('next, child and nthChild should be above the setInsertionState', () => {
-      const code = compile(`
+  describe('execution order', () => {
+    test('basic', () => {
+      const code = compile(`<div :id="foo">{{ bar }}</div>`)
+      expect(code).matchSnapshot()
+      expect(code).contains(
+        `_setProp(n0, "id", _ctx.foo)
+    _setText(x0, _toDisplayString(_ctx.bar))`,
+      )
+    })
+
+    describe('setInsertionState', () => {
+      test('next, child and nthChild should be above the setInsertionState', () => {
+        const code = compile(`
       <div>
         <div />
         <Comp />
@@ -234,18 +244,8 @@ describe('compile', () => {
         </div>
       </div>
       `)
-      expect(code).toMatchSnapshot()
-    })
-  })
-
-  describe('execution order', () => {
-    test('basic', () => {
-      const code = compile(`<div :id="foo">{{ bar }}</div>`)
-      expect(code).matchSnapshot()
-      expect(code).contains(
-        `_setProp(n0, "id", _ctx.foo)
-    _setText(x0, _toDisplayString(_ctx.bar))`,
-      )
+        expect(code).toMatchSnapshot()
+      })
     })
 
     test('with v-once', () => {
index 12a3f2a8e7df741f846cbf6fc5f3f678a6373de4..a621f5a6ec03e8c896e8db5803f32f46d911bc48 100644 (file)
@@ -51,7 +51,7 @@ export function render(_ctx) {
         return n5
       }, () => {
         const n14 = t2()
-        _setInsertionState(n14, 0)
+        _setInsertionState(n14, 0, true)
         const n9 = _createIf(() => (_ctx.c), () => {
           const n11 = t1()
           return n11
index fda0121d632294ccde069e362a6d2af3a59d091b..9dc329f2120c6e896efcaf8853d279f9af280620 100644 (file)
@@ -42,13 +42,13 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: expression > empty interpolation 4`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { child as _child, txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div> </div>", true)
 
 export function render(_ctx) {
   const n1 = t0()
-  const n0 = _child(n1)
-  const x1 = _child(n1)
+  const n0 = _child(n1, 0)
+  const x1 = _txt(n1)
   _renderEffect(() => {
     const _foo = _ctx.foo
     _setText(n0, _toDisplayString(_foo))
@@ -81,13 +81,13 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
 `;
 
 exports[`compiler: expression > update expression 1`] = `
-"import { child as _child, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { child as _child, txt as _txt, setProp as _setProp, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div> </div>", true)
 
 export function render(_ctx) {
   const n1 = t0()
-  const n0 = _child(n1)
-  const x1 = _child(n1)
+  const n0 = _child(n1, 0)
+  const x1 = _txt(n1)
   _renderEffect(() => {
     const _String = String
     const _foo = _ctx.foo
index 6a353b771ff36a9da1d4d8231d6d7f9870e0cfc8..c75f67c36df82dcfd1e876164fa67b4c511e0a1d 100644 (file)
@@ -7,8 +7,8 @@ const t1 = _template("<div><div></div><!><div></div></div>", true)
 
 export function render(_ctx) {
   const n4 = t1()
-  const n3 = _next(_child(n4))
-  _setInsertionState(n4, n3)
+  const n3 = _next(_child(n4), 1)
+  _setInsertionState(n4, n3, true)
   const n0 = _createIf(() => (1), () => {
     const n2 = t0()
     return n2
@@ -18,16 +18,16 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: children transform > children & sibling references 1`] = `
-"import { child as _child, next as _next, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { child as _child, next as _next, txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div><p> </p> <p> </p></div>", true)
 
 export function render(_ctx) {
   const n3 = t0()
-  const n0 = _child(n3)
-  const n1 = _next(n0)
-  const n2 = _next(n1)
-  const x0 = _child(n0)
-  const x2 = _child(n2)
+  const n0 = _child(n3, 0)
+  const n1 = _next(n0, 1)
+  const n2 = _next(n1, 2)
+  const x0 = _txt(n0)
+  const x2 = _txt(n2)
   _renderEffect(() => {
     _setText(x0, _toDisplayString(_ctx.first))
     _setText(n1, " " + _toDisplayString(_ctx.second) + " " + _toDisplayString(_ctx.third) + " ")
@@ -38,33 +38,33 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: children transform > efficient find 1`] = `
-"import { child as _child, nthChild as _nthChild, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { child as _child, nthChild as _nthChild, txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div><div>x</div><div>x</div><div> </div></div>", true)
 
 export function render(_ctx) {
   const n1 = t0()
-  const n0 = _nthChild(n1, 2)
-  const x0 = _child(n0)
+  const n0 = _nthChild(n1, 2, 2)
+  const x0 = _txt(n0)
   _renderEffect(() => _setText(x0, _toDisplayString(_ctx.msg)))
   return n1
 }"
 `;
 
 exports[`compiler: children transform > efficient traversal 1`] = `
-"import { child as _child, next as _next, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { child as _child, next as _next, txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div><div>x</div><div><span> </span></div><div><span> </span></div><div><span> </span></div></div>", true)
 
 export function render(_ctx) {
   const n3 = t0()
-  const p0 = _next(_child(n3))
-  const n0 = _child(p0)
-  const p1 = _next(p0)
-  const n1 = _child(p1)
-  const p2 = _next(p1)
-  const n2 = _child(p2)
-  const x0 = _child(n0)
-  const x1 = _child(n1)
-  const x2 = _child(n2)
+  const p0 = _next(_child(n3), 1)
+  const n0 = _child(p0, 0)
+  const p1 = _next(p0, 2)
+  const n1 = _child(p1, 0)
+  const p2 = _next(p1, 3)
+  const n2 = _child(p2, 0)
+  const x0 = _txt(n0)
+  const x1 = _txt(n1)
+  const x2 = _txt(n2)
   _renderEffect(() => {
     const _msg = _ctx.msg
     _setText(x0, _toDisplayString(_msg))
index 6f92a4c1bf7b0c9a9c0593702d565e7b6c811877..8d9df60dfa131cda9cad23b18cfa14affa1d2e3d 100644 (file)
@@ -1,5 +1,16 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
+exports[`compiler: element transform > checkbox with static indeterminate 1`] = `
+"import { setProp as _setProp, template as _template } from 'vue';
+const t0 = _template("<input type=\\"checkbox\\">", true)
+
+export function render(_ctx) {
+  const n0 = t0()
+  _setProp(n0, "indeterminate", "")
+  return n0
+}"
+`;
+
 exports[`compiler: element transform > component > cache v-on expression with unique handler name 1`] = `
 "import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
 
index 4b1574e5d2587b51d054d43aef30910bd0fb6be0..6b75644630ab50a826ce1710dca47bfdeaf40a97 100644 (file)
@@ -1,13 +1,13 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
 exports[`compiler: v-for > array de-structured value (with rest) 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<div> </div>", true)
 
 export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => {
     const n2 = t0()
-    const x2 = _child(n2)
+    const x2 = _txt(n2)
     _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value[0] + _for_item0.value.slice(1) + _for_key0.value)))
     return n2
   }, ([id, ...other], index) => (id))
@@ -16,13 +16,13 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-for > array de-structured value 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<div> </div>", true)
 
 export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => {
     const n2 = t0()
-    const x2 = _child(n2)
+    const x2 = _txt(n2)
     _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value[0] + _for_item0.value[1] + _for_key0.value)))
     return n2
   }, ([id, other], index) => (id))
@@ -31,14 +31,14 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-for > basic v-for 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, delegateEvents as _delegateEvents, template as _template } from 'vue';
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, delegateEvents as _delegateEvents, template as _template } from 'vue';
 const t0 = _template("<div> </div>", true)
 _delegateEvents("click")
 
 export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
     const n2 = t0()
-    const x2 = _child(n2)
+    const x2 = _txt(n2)
     n2.$evtclick = () => (_ctx.remove(_for_item0.value))
     _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value)))
     return n2
@@ -48,13 +48,13 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-for > key only binding pattern 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue';
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<tr> </tr>", true)
 
 export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.rows), (_for_item0) => {
     const n2 = t0()
-    const x2 = _child(n2)
+    const x2 = _txt(n2)
     _setText(x2, _toDisplayString(_for_item0.value.id + _for_item0.value.id))
     return n2
   }, (row) => (row.id))
@@ -80,17 +80,17 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-for > nested v-for 1`] = `
-"import { setInsertionState as _setInsertionState, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+"import { setInsertionState as _setInsertionState, txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<span> </span>")
 const t1 = _template("<div></div>", true)
 
 export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
     const n5 = t1()
-    _setInsertionState(n5)
+    _setInsertionState(n5, null, true)
     const n2 = _createFor(() => (_for_item0.value), (_for_item1) => {
       const n4 = t0()
-      const x4 = _child(n4)
+      const x4 = _txt(n4)
       _renderEffect(() => _setText(x4, _toDisplayString(_for_item1.value+_for_item0.value)))
       return n4
     }, undefined, 1)
@@ -101,13 +101,13 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-for > object de-structured value (with rest) 1`] = `
-"import { getRestElement as _getRestElement, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+"import { getRestElement as _getRestElement, txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<div> </div>", true)
 
 export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => {
     const n2 = t0()
-    const x2 = _child(n2)
+    const x2 = _txt(n2)
     _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value.id + _getRestElement(_for_item0.value, ["id"]) + _for_key0.value)))
     return n2
   }, ({ id, ...other }, index) => (id))
@@ -116,13 +116,13 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-for > object de-structured value 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<span> </span>", true)
 
 export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
     const n2 = t0()
-    const x2 = _child(n2)
+    const x2 = _txt(n2)
     _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value.id) + _toDisplayString(_for_item0.value.value)))
     return n2
   }, ({ id, value }) => (id))
@@ -131,13 +131,13 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-for > object value, key and index 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<div> </div>", true)
 
 export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0, _for_index0) => {
     const n2 = t0()
-    const x2 = _child(n2)
+    const x2 = _txt(n2)
     _renderEffect(() => _setText(x2, _toDisplayString(_for_item0.value + _for_key0.value + _for_index0.value)))
     return n2
   }, (value, key, index) => (key))
@@ -146,14 +146,14 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-for > selector pattern 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue';
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<tr> </tr>", true)
 
 export function render(_ctx) {
   let _selector0_0
   const n0 = _createFor(() => (_ctx.rows), (_for_item0) => {
     const n2 = t0()
-    const x2 = _child(n2)
+    const x2 = _txt(n2)
     _selector0_0(() => {
       _setText(x2, _toDisplayString(_ctx.selected === _for_item0.value.id ? 'danger' : ''))
     })
@@ -221,13 +221,13 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-for > v-for aliases w/ complex expressions 1`] = `
-"import { getDefaultValue as _getDefaultValue, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+"import { getDefaultValue as _getDefaultValue, txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<div> </div>", true)
 
 export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
     const n2 = t0()
-    const x2 = _child(n2)
+    const x2 = _txt(n2)
     _renderEffect(() => _setText(x2, _toDisplayString(_getDefaultValue(_for_item0.value.foo, _ctx.bar) + _ctx.bar + _ctx.baz + _getDefaultValue(_for_item0.value.baz[0], _ctx.quux) + _ctx.quux)))
     return n2
   })
@@ -243,7 +243,7 @@ export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
   const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
     const n3 = _createComponentWithFallback(_component_Comp)
-    const n2 = _child(n3)
+    const n2 = _child(n3, 0)
     _renderEffect(() => _setText(n2, _toDisplayString(_for_item0.value)))
     return [n2, n3]
   }, undefined, 2)
@@ -259,7 +259,7 @@ export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
   const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
     const n3 = _createComponentWithFallback(_component_Comp)
-    const n2 = _child(n3)
+    const n2 = _child(n3, 0)
     _renderEffect(() => _setText(n2, _toDisplayString(_for_item0.value)))
     return [n2, n3]
   }, undefined, 2)
index c41dc9226c59f1c1c46dfd244e7f228cfc73cdbc..a8dc5aa459006a79826e40d3f3895537bfd3ff46 100644 (file)
@@ -1,13 +1,13 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
 exports[`compiler: v-if > basic v-if 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue';
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue';
 const t0 = _template("<div> </div>", true)
 
 export function render(_ctx) {
   const n0 = _createIf(() => (_ctx.ok), () => {
     const n2 = t0()
-    const x2 = _child(n2)
+    const x2 = _txt(n2)
     _renderEffect(() => _setText(x2, _toDisplayString(_ctx.msg)))
     return n2
   })
@@ -16,7 +16,7 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-if > comment between branches 1`] = `
-"import { createIf as _createIf, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { createIf as _createIf, txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div></div>")
 const t1 = _template("<!--foo-->")
 const t2 = _template("<p></p>")
@@ -38,7 +38,7 @@ export function render(_ctx) {
     return [n10, n11]
   }))
   const n13 = t5()
-  const x13 = _child(n13)
+  const x13 = _txt(n13)
   _renderEffect(() => _setText(x13, _toDisplayString(_ctx.text)))
   return [n0, n13]
 }"
@@ -62,7 +62,7 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-if > template v-if 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue';
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createIf as _createIf, template as _template } from 'vue';
 const t0 = _template("<div></div>")
 const t1 = _template("hello")
 const t2 = _template("<p> </p>", true)
@@ -72,7 +72,7 @@ export function render(_ctx) {
     const n2 = t0()
     const n3 = t1()
     const n4 = t2()
-    const x4 = _child(n4)
+    const x4 = _txt(n4)
     _renderEffect(() => _setText(x4, _toDisplayString(_ctx.msg)))
     return [n2, n3, n4]
   })
@@ -144,12 +144,12 @@ const t3 = _template("<div></div>", true)
 
 export function render(_ctx) {
   const n8 = t3()
-  _setInsertionState(n8)
+  _setInsertionState(n8, null)
   const n0 = _createIf(() => (_ctx.foo), () => {
     const n2 = t0()
     return n2
   })
-  _setInsertionState(n8)
+  _setInsertionState(n8, null, true)
   const n3 = _createIf(() => (_ctx.bar), () => {
     const n5 = t1()
     return n5
index b6107d5a1a1c63067461c43cda989eb22e144065..ae5e9df743a66761aea71b10341f9d223b6b01f1 100644 (file)
@@ -17,8 +17,8 @@ const t0 = _template("<div> <span></span></div>", true)
 
 export function render(_ctx, $props, $emit, $attrs, $slots) {
   const n2 = t0()
-  const n0 = _child(n2)
-  const n1 = _next(n0)
+  const n0 = _child(n2, 0)
+  const n1 = _next(n0, 1)
   _setText(n0, _toDisplayString(_ctx.msg) + " ")
   _setClass(n1, _ctx.clz)
   return n2
@@ -42,7 +42,7 @@ const t0 = _template("<div></div>", true)
 export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
   const n1 = t0()
-  _setInsertionState(n1)
+  _setInsertionState(n1, null, true)
   const n0 = _createComponentWithFallback(_component_Comp, { id: () => (_ctx.foo) }, null, null, true)
   return n1
 }"
@@ -54,7 +54,7 @@ const t0 = _template("<div><div></div></div>", true)
 
 export function render(_ctx) {
   const n1 = t0()
-  const n0 = _child(n1)
+  const n0 = _child(n1, 0)
   _setProp(n0, "id", _ctx.foo)
   return n1
 }"
index c70f49cf05f913b6246e6d64acccef7ed24464db..e7d34070ea33f081a6dc3f19828a06003312ade5 100644 (file)
@@ -352,9 +352,9 @@ export function render(_ctx) {
   const _component_Foo = _resolveComponent("Foo")
   const _component_Bar = _resolveComponent("Bar")
   const n6 = t0()
-  _setInsertionState(n6)
+  _setInsertionState(n6, null)
   const n0 = _createSlot("foo", null)
-  _setInsertionState(n6)
+  _setInsertionState(n6, null, true)
   const n1 = _createIf(() => (true), () => {
     const n3 = _createComponentWithFallback(_component_Foo)
     return n3
index cd77f5e13019180494f6cea1de76b950a6bef567..1cf9aed725522a5bc784bfbc904f3a4c50b854dc 100644 (file)
@@ -1,24 +1,24 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
 exports[`v-text > should convert v-text to setText 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div> </div>", true)
 
 export function render(_ctx, $props, $emit, $attrs, $slots) {
   const n0 = t0()
-  const x0 = _child(n0)
+  const x0 = _txt(n0)
   _renderEffect(() => _setText(x0, _toDisplayString(_ctx.str)))
   return n0
 }"
 `;
 
 exports[`v-text > should raise error and ignore children when v-text is present 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { txt as _txt, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div> </div>", true)
 
 export function render(_ctx) {
   const n0 = t0()
-  const x0 = _child(n0)
+  const x0 = _txt(n0)
   _renderEffect(() => _setText(x0, _toDisplayString(_ctx.test)))
   return n0
 }"
index 2d8ae8c960d2d0bac4222ca2da60f339091bd3bb..9d5226921a4cf2de2800693b0211a74b76e3bfe8 100644 (file)
@@ -56,7 +56,7 @@ describe('compiler: children transform', () => {
         <div>{{ msg }}</div>
       </div>`,
     )
-    expect(code).contains(`const n0 = _nthChild(n1, 2)`)
+    expect(code).contains(`const n0 = _nthChild(n1, 2, 2)`)
     expect(code).toMatchSnapshot()
   })
 
@@ -69,8 +69,8 @@ describe('compiler: children transform', () => {
       </div>`,
     )
     // ensure the insertion anchor is generated before the insertion statement
-    expect(code).toMatch(`const n3 = _next(_child(n4))`)
-    expect(code).toMatch(`_setInsertionState(n4, n3)`)
+    expect(code).toMatch(`const n3 = _next(_child(n4), 1)`)
+    expect(code).toMatch(`_setInsertionState(n4, n3, true)`)
     expect(code).toMatchSnapshot()
   })
 })
index c20ca11614e1af2240669bea073177537a07a4da..66768508f67afcb509311c973a53458235b2bd52 100644 (file)
@@ -583,6 +583,15 @@ describe('compiler: element transform', () => {
     expect(ir.block.effect).lengthOf(0)
   })
 
+  test('checkbox with static indeterminate', () => {
+    const { code } = compileWithElementTransform(
+      `<input type="checkbox" indeterminate/>`,
+    )
+
+    expect(code).toContain('_setProp(n0, "indeterminate", "")')
+    expect(code).toMatchSnapshot()
+  })
+
   test('props + children', () => {
     const { code, ir } = compileWithElementTransform(
       `<div id="foo"><span/></div>`,
index 13ce5477cc1705908d777c3d75ae18089a82c809..804505160248d5bc074f51d4f77ee410bb13048f 100644 (file)
@@ -168,16 +168,24 @@ function genInsertionState(
   operation: InsertionStateTypes,
   context: CodegenContext,
 ): CodeFragment[] {
+  const { parent, anchor, append, last } = operation
   return [
     NEWLINE,
     ...genCall(
       context.helper('setInsertionState'),
-      `n${operation.parent}`,
-      operation.anchor == null
+      `n${parent}`,
+      anchor == null
         ? undefined
-        : operation.anchor === -1 // -1 indicates prepend
+        : anchor === -1 // -1 indicates prepend
           ? `0` // runtime anchor value for prepend
-          : `n${operation.anchor}`,
+          : append // -2 indicates append
+            ? // null or anchor > 0 for append
+              // anchor > 0 is the logical index of append node - used for locate node during hydration
+              anchor === 0
+              ? 'null'
+              : `${anchor}`
+            : `n${anchor}`,
+      last && 'true',
     ),
   ]
 }
index 2b8a9ea0e0488d0966ac31554b3fa43df79ec7c7..45b3703a7f94e92614f8e40bbec130225f206bc9 100644 (file)
@@ -1,5 +1,9 @@
 import type { CodegenContext } from '../generate'
-import { DynamicFlag, type IRDynamicInfo } from '../ir'
+import {
+  DynamicFlag,
+  type IRDynamicInfo,
+  type InsertionStateTypes,
+} from '../ir'
 import { genDirectivesForElement } from './directive'
 import { genOperationWithInsertionState } from './operation'
 import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
@@ -54,10 +58,20 @@ export function genChildren(
 
   let offset = 0
   let prev: [variable: string, elementIndex: number] | undefined
+  let ifBranchCount = 0
+  let prependCount = 0
 
   for (const [index, child] of children.entries()) {
+    if (
+      child.operation &&
+      (child.operation as InsertionStateTypes).anchor === -1
+    ) {
+      prependCount++
+    }
     if (child.flags & DynamicFlag.NON_TEMPLATE) {
       offset--
+    } else if (child.ifBranch) {
+      ifBranchCount++
     }
 
     const id =
@@ -72,7 +86,8 @@ export function genChildren(
       continue
     }
 
-    const elementIndex = Number(index) + offset
+    const elementIndex = index + offset
+    const logicalIndex = elementIndex - ifBranchCount + prependCount
     // p for "placeholder" variables that are meant for possible reuse by
     // other access paths
     const variable = id === undefined ? `p${context.block.tempId++}` : `n${id}`
@@ -80,20 +95,32 @@ export function genChildren(
 
     if (prev) {
       if (elementIndex - prev[1] === 1) {
-        pushBlock(...genCall(helper('next'), prev[0]))
+        pushBlock(...genCall(helper('next'), prev[0], String(logicalIndex)))
       } else {
-        pushBlock(...genCall(helper('nthChild'), from, String(elementIndex)))
+        pushBlock(
+          ...genCall(
+            helper('nthChild'),
+            from,
+            String(elementIndex),
+            String(logicalIndex),
+          ),
+        )
       }
     } else {
       if (elementIndex === 0) {
-        pushBlock(...genCall(helper('child'), from))
+        pushBlock(...genCall(helper('child'), from, String(logicalIndex)))
       } else {
         // check if there's a node that we can reuse from
         let init = genCall(helper('child'), from)
         if (elementIndex === 1) {
-          init = genCall(helper('next'), init)
+          init = genCall(helper('next'), init, String(logicalIndex))
         } else if (elementIndex > 1) {
-          init = genCall(helper('nthChild'), from, String(elementIndex))
+          init = genCall(
+            helper('nthChild'),
+            from,
+            String(elementIndex),
+            String(logicalIndex),
+          )
         }
         pushBlock(...init)
       }
index ea3b041e6f63e6cb424fed0c6e899397d68a62e2..3ad133d9bf861684d68315be8b64c8215823d5e0 100644 (file)
@@ -47,6 +47,6 @@ export function genGetTextChild(
 ): CodeFragment[] {
   return [
     NEWLINE,
-    `const x${oper.parent} = ${context.helper('child')}(n${oper.parent})`,
+    `const x${oper.parent} = ${context.helper('txt')}(n${oper.parent})`,
   ]
 }
index 1018d7baa443cad5c661d97f6449307127f19d19..76ef7c53c49786ea1872aea731d6f5f36210a79d 100644 (file)
@@ -79,6 +79,8 @@ export interface IfIRNode extends BaseIRNode {
   once?: boolean
   parent?: number
   anchor?: number
+  append?: boolean
+  last?: boolean
 }
 
 export interface IRFor {
@@ -98,6 +100,8 @@ export interface ForIRNode extends BaseIRNode, IRFor {
   onlyChild: boolean
   parent?: number
   anchor?: number
+  append?: boolean
+  last?: boolean
 }
 
 export interface SetPropIRNode extends BaseIRNode {
@@ -201,6 +205,8 @@ export interface CreateComponentIRNode extends BaseIRNode {
   dynamic?: SimpleExpressionNode
   parent?: number
   anchor?: number
+  append?: boolean
+  last?: boolean
 }
 
 export interface DeclareOldRefIRNode extends BaseIRNode {
@@ -217,6 +223,8 @@ export interface SlotOutletIRNode extends BaseIRNode {
   forwarded?: boolean
   parent?: number
   anchor?: number
+  append?: boolean
+  last?: boolean
 }
 
 export interface GetTextChildIRNode extends BaseIRNode {
@@ -268,6 +276,7 @@ export interface IRDynamicInfo {
   hasDynamicChild?: boolean
   needsKey?: boolean
   operation?: OperationNode
+  ifBranch?: boolean
 }
 
 export interface IREffect {
index 790cd9d6fb19038110a5d91ca54f4d62cc7c87a6..cdff90edeb2b77d998679434260f723522e8456f 100644 (file)
@@ -8,6 +8,7 @@ import {
   DynamicFlag,
   type IRDynamicInfo,
   IRNodeTypes,
+  type InsertionStateTypes,
   isBlockOperation,
 } from '../ir'
 
@@ -59,17 +60,19 @@ export const transformChildren: NodeTransform = (node, context) => {
 
 function processDynamicChildren(context: TransformContext<ElementNode>) {
   let prevDynamics: IRDynamicInfo[] = []
-  let hasStaticTemplate = false
+  let staticCount = 0
+  let dynamicCount = 0
+  let lastInsertionChild: IRDynamicInfo | undefined
   const children = context.dynamic.children
 
   for (const [index, child] of children.entries()) {
     if (child.flags & DynamicFlag.INSERT) {
-      prevDynamics.push(child)
+      prevDynamics.push((lastInsertionChild = child))
     }
 
     if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
       if (prevDynamics.length) {
-        if (hasStaticTemplate) {
+        if (staticCount) {
           context.childrenTemplate[index - prevDynamics.length] = `<!>`
           prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
           const anchor = (prevDynamics[0].anchor = context.increaseId())
@@ -77,21 +80,33 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
         } else {
           registerInsertion(prevDynamics, context, -1 /* prepend */)
         }
+        dynamicCount += prevDynamics.length
         prevDynamics = []
       }
-      hasStaticTemplate = true
+      staticCount++
     }
   }
 
   if (prevDynamics.length) {
-    registerInsertion(prevDynamics, context)
+    registerInsertion(
+      prevDynamics,
+      context,
+      // the logical index of append child
+      dynamicCount + staticCount,
+      true,
+    )
+  }
+
+  if (lastInsertionChild && lastInsertionChild.operation) {
+    ;(lastInsertionChild.operation! as InsertionStateTypes).last = true
   }
 }
 
 function registerInsertion(
   dynamics: IRDynamicInfo[],
   context: TransformContext,
-  anchor?: number,
+  anchor: number,
+  append?: boolean,
 ) {
   for (const child of dynamics) {
     if (child.template != null) {
@@ -100,12 +115,13 @@ function registerInsertion(
         type: IRNodeTypes.INSERT_NODE,
         elements: dynamics.map(child => child.id!),
         parent: context.reference(),
-        anchor,
+        anchor: append ? undefined : anchor,
       })
     } else if (child.operation && isBlockOperation(child.operation)) {
       // block types
       child.operation.parent = context.reference()
       child.operation.anchor = anchor
+      child.operation.append = append
     }
   }
 }
index dcabe36093813f2e07a995c78cfe60c5fc3f744d..facffadff109422e2997d920e339bad747c7b720 100644 (file)
@@ -198,6 +198,9 @@ function resolveSetupReference(name: string, context: TransformContext) {
         : undefined
 }
 
+// keys cannot be a part of the template and need to be set dynamically
+const dynamicKeys = ['indeterminate']
+
 function transformNativeElement(
   node: PlainElementNode,
   propsResult: PropsResult,
@@ -229,7 +232,12 @@ function transformNativeElement(
   } else {
     for (const prop of propsResult[1]) {
       const { key, values } = prop
-      if (key.isStatic && values.length === 1 && values[0].isStatic) {
+      if (
+        key.isStatic &&
+        values.length === 1 &&
+        values[0].isStatic &&
+        !dynamicKeys.includes(key.content)
+      ) {
         template += ` ${key.content}`
         if (values[0].content) template += `="${values[0].content}"`
       } else {
index 2426fa0215eeee6a0360b22f67c9562494016d5e..531d29b055cebbc449de0d40f037179c09426d37 100644 (file)
@@ -59,6 +59,7 @@ export function processIf(
   } else {
     // check the adjacent v-if
     const siblingIf = getSiblingIf(context, true)
+    context.dynamic.ifBranch = true
 
     const siblings = context.parent && context.parent.dynamic.children
     let lastIfNode
index a6b8fcbe8b56d06f4be81a44058f85d4c1e1023f..caa39c4436b941af80803a81a70c3bffafdfbc37 100644 (file)
@@ -187,6 +187,14 @@ export interface VaporInteropInterface {
   unmount(vnode: VNode, doRemove?: boolean): void
   move(vnode: VNode, container: any, anchor: any): void
   slot(n1: VNode | null, n2: VNode, container: any, anchor: any): void
+  hydrate(
+    vnode: VNode,
+    node: any,
+    container: any,
+    anchor: any,
+    parentComponent: ComponentInternalInstance | null,
+  ): Node
+  hydrateSlot(vnode: VNode, node: any): Node
   activate(
     vnode: VNode,
     container: any,
index 15b3c7512bebb18395ff33ddb5d2c082a6e4d299..38cce8b5a3575c76cf372e53a673ef692becd522 100644 (file)
@@ -5,6 +5,7 @@ import {
   Comment as VComment,
   type VNode,
   type VNodeHook,
+  VaporSlot,
   createTextVNode,
   createVNode,
   invokeVNodeHook,
@@ -36,7 +37,11 @@ import {
   normalizeStyle,
   stringifyStyle,
 } from '@vue/shared'
-import { type RendererInternals, needTransition } from './renderer'
+import {
+  type RendererInternals,
+  getVaporInterface,
+  needTransition,
+} from './renderer'
 import { setRef } from './rendererTemplateRef'
 import {
   type SuspenseBoundary,
@@ -259,6 +264,12 @@ export function createHydrationFunctions(
           )
         }
         break
+      case VaporSlot:
+        nextNode = getVaporInterface(parentComponent, vnode).hydrateSlot(
+          vnode,
+          node,
+        )
+        break
       default:
         if (shapeFlag & ShapeFlags.ELEMENT) {
           if (
@@ -279,10 +290,6 @@ export function createHydrationFunctions(
             )
           }
         } else if (shapeFlag & ShapeFlags.COMPONENT) {
-          if ((vnode.type as ConcreteComponent).__vapor) {
-            throw new Error('Vapor component hydration is not supported yet.')
-          }
-
           // when setting up the render effect, if the initial vnode already
           // has .el set, the component will perform hydration instead of mount
           // on its sub-tree.
@@ -303,15 +310,26 @@ export function createHydrationFunctions(
             nextNode = nextSibling(node)
           }
 
-          mountComponent(
-            vnode,
-            container,
-            null,
-            parentComponent,
-            parentSuspense,
-            getContainerType(container),
-            optimized,
-          )
+          // hydrate vapor component
+          if ((vnode.type as ConcreteComponent).__vapor) {
+            getVaporInterface(parentComponent, vnode).hydrate(
+              vnode,
+              node,
+              container,
+              null,
+              parentComponent,
+            )
+          } else {
+            mountComponent(
+              vnode,
+              container,
+              null,
+              parentComponent,
+              parentSuspense,
+              getContainerType(container),
+              optimized,
+            )
+          }
 
           // #3787
           // if component is async, it may get moved / unmounted before its
@@ -851,35 +869,61 @@ function propHasMismatch(
       mismatchType = MismatchTypes.STYLE
       mismatchKey = 'style'
     }
-  } else if (
-    (el instanceof SVGElement && isKnownSvgAttr(key)) ||
-    (el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key)))
-  ) {
-    if (isBooleanAttr(key)) {
-      actual = el.hasAttribute(key)
-      expected = includeBooleanAttr(clientValue)
-    } else if (clientValue == null) {
-      actual = el.hasAttribute(key)
-      expected = false
-    } else {
-      if (el.hasAttribute(key)) {
-        actual = el.getAttribute(key)
-      } else if (key === 'value' && el.tagName === 'TEXTAREA') {
-        // #10000 textarea.value can't be retrieved by `hasAttribute`
-        actual = (el as HTMLTextAreaElement).value
-      } else {
-        actual = false
-      }
-      expected = isRenderableAttrValue(clientValue)
-        ? String(clientValue)
-        : false
-    }
+  } else if (isValidHtmlOrSvgAttribute(el, key)) {
+    ;({ actual, expected } = getAttributeMismatch(el, key, clientValue))
     if (actual !== expected) {
       mismatchType = MismatchTypes.ATTRIBUTE
       mismatchKey = key
     }
   }
 
+  return warnPropMismatch(el, mismatchKey, mismatchType, actual, expected)
+}
+
+export function getAttributeMismatch(
+  el: Element,
+  key: string,
+  clientValue: any,
+): {
+  actual: string | boolean | null | undefined
+  expected: string | boolean | null | undefined
+} {
+  let actual: string | boolean | null | undefined
+  let expected: string | boolean | null | undefined
+  if (isBooleanAttr(key)) {
+    actual = el.hasAttribute(key)
+    expected = includeBooleanAttr(clientValue)
+  } else if (clientValue == null) {
+    actual = el.hasAttribute(key)
+    expected = false
+  } else {
+    if (el.hasAttribute(key)) {
+      actual = el.getAttribute(key)
+    } else if (key === 'value' && el.tagName === 'TEXTAREA') {
+      // #10000 textarea.value can't be retrieved by `hasAttribute`
+      actual = (el as HTMLTextAreaElement).value
+    } else {
+      actual = false
+    }
+    expected = isRenderableAttrValue(clientValue) ? String(clientValue) : false
+  }
+  return { actual, expected }
+}
+
+export function isValidHtmlOrSvgAttribute(el: Element, key: string): boolean {
+  return (
+    (el instanceof SVGElement && isKnownSvgAttr(key)) ||
+    (el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key)))
+  )
+}
+
+export function warnPropMismatch(
+  el: Element & { $cls?: string },
+  mismatchKey: string | undefined,
+  mismatchType: MismatchTypes | undefined,
+  actual: string | boolean | null | undefined,
+  expected: string | boolean | null | undefined,
+): boolean {
   if (mismatchType != null && !isMismatchAllowed(el, mismatchType)) {
     const format = (v: any) =>
       v === false ? `(not rendered)` : `${mismatchKey}="${v}"`
@@ -902,11 +946,11 @@ function propHasMismatch(
   return false
 }
 
-function toClassSet(str: string): Set<string> {
+export function toClassSet(str: string): Set<string> {
   return new Set(str.trim().split(/\s+/))
 }
 
-function isSetEqual(a: Set<string>, b: Set<string>): boolean {
+export function isSetEqual(a: Set<string>, b: Set<string>): boolean {
   if (a.size !== b.size) {
     return false
   }
@@ -918,7 +962,7 @@ function isSetEqual(a: Set<string>, b: Set<string>): boolean {
   return true
 }
 
-function toStyleMap(str: string): Map<string, string> {
+export function toStyleMap(str: string): Map<string, string> {
   const styleMap: Map<string, string> = new Map()
   for (const item of str.split(';')) {
     let [key, value] = item.split(':')
@@ -931,7 +975,10 @@ function toStyleMap(str: string): Map<string, string> {
   return styleMap
 }
 
-function isMapEqual(a: Map<string, string>, b: Map<string, string>): boolean {
+export function isMapEqual(
+  a: Map<string, string>,
+  b: Map<string, string>,
+): boolean {
   if (a.size !== b.size) {
     return false
   }
@@ -973,7 +1020,7 @@ function resolveCssVars(
 
 const allowMismatchAttr = 'data-allow-mismatch'
 
-enum MismatchTypes {
+export enum MismatchTypes {
   TEXT = 0,
   CHILDREN = 1,
   CLASS = 2,
@@ -989,7 +1036,7 @@ const MismatchTypeString: Record<MismatchTypes, string> = {
   [MismatchTypes.ATTRIBUTE]: 'attribute',
 } as const
 
-function isMismatchAllowed(
+export function isMismatchAllowed(
   el: Element | null,
   allowedType: MismatchTypes,
 ): boolean {
index c77b34135242c96cd07dc2e1bb6215e787d4ad83..d0ae0fe79eebf9a0e487a82d6197cad47a544e1a 100644 (file)
@@ -626,6 +626,20 @@ export { performTransitionEnter, performTransitionLeave } from './renderer'
  * @internal
  */
 export { createInternalObject } from './internalObject'
+/**
+ * @internal
+ */
+export {
+  MismatchTypes,
+  isMismatchAllowed,
+  toClassSet,
+  isSetEqual,
+  warnPropMismatch,
+  toStyleMap,
+  isMapEqual,
+  isValidHtmlOrSvgAttribute,
+  getAttributeMismatch,
+} from './hydration'
 /**
  * @internal
  */
index bd13cb2a40fb86d04de0f0ff749124d05651ef2e..0cf7c351d0eb106deedcdfa45964f2ce68543527 100644 (file)
@@ -108,6 +108,7 @@ export interface Renderer<HostElement = RendererElement> {
 
 export interface HydrationRenderer extends Renderer<Element | ShadowRoot> {
   hydrate: RootHydrateFunction
+  hydrateNode: ReturnType<typeof createHydrationFunctions>[1]
 }
 
 export type ElementNamespace = 'svg' | 'mathml' | undefined
@@ -2606,6 +2607,7 @@ function baseCreateRenderer(
   return {
     render,
     hydrate,
+    hydrateNode,
     internals,
     createApp: createAppAPI(
       mountApp,
index 2ead79760ccc406db1d87d97625c4b0c2248688d..7b3c3b33816b273d9c4ade961d951ab914ebc021 100644 (file)
@@ -319,7 +319,7 @@ export * from './jsx'
 /**
  * @internal
  */
-export { ensureRenderer, normalizeContainer }
+export { ensureRenderer, ensureHydrationRenderer, normalizeContainer }
 /**
  * @internal
  */
index eedbde13e0ec20aff7e68b7fbc2bdd80614b96dd..f185e0e067bf4de1f269f289f026d45a0c5c561a 100644 (file)
@@ -310,6 +310,17 @@ describe('patchProp', () => {
         `Failed setting prop "someProp" on <div>: value foo is invalid.`,
       ).toHaveBeenWarnedLast()
     })
+
+    test('checkbox with indeterminate', () => {
+      const el = document.createElement('input')
+      el.type = 'checkbox'
+      setProp(el, 'indeterminate', true)
+      expect(el.indeterminate).toBe(true)
+      setProp(el, 'indeterminate', false)
+      expect(el.indeterminate).toBe(false)
+      setProp(el, 'indeterminate', '')
+      expect(el.indeterminate).toBe(true)
+    })
   })
 
   describe('setDynamicProp', () => {
index 85de30987a55e22b52dffda3fae163b0412e6fe1..792c55242b16102d858c784ad29c1be619c08226 100644 (file)
@@ -20,8 +20,8 @@ describe('api: template', () => {
 
   test('nthChild', () => {
     const t = template('<div><span><b>nested</b></span><p></p></div>')
-    const root = t()
-    const span = nthChild(root, 0)
+    const root = t() as ParentNode
+    const span = nthChild(root, 0) as ParentNode
     const b = nthChild(span, 0)
     const p = nthChild(root, 1)
     expect(span).toBe(root.firstChild)
@@ -31,7 +31,7 @@ describe('api: template', () => {
 
   test('next', () => {
     const t = template('<div><span></span><b></b><p></p></div>')
-    const root = t()
+    const root = t() as ParentNode
     const span = child(root as ParentNode)
     const b = next(span)
 
index 72d3fe27d648686637c7cb134f1eff219a213b39..213ec275c26ee657abb5fc80bb5731504f5b91b5 100644 (file)
 import { createVaporSSRApp, delegateEvents } from '../src'
-import { nextTick, ref } from '@vue/runtime-dom'
-import { VueServerRenderer, compile, runtimeDom } from './_utils'
+import { nextTick, reactive, ref } from '@vue/runtime-dom'
+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 { isString } from '@vue/shared'
+import type { VaporComponentInstance } from '../src/component'
+
+const formatHtml = (raw: string) => {
+  return raw
+    .replace(/<!--\[/g, '\n<!--[')
+    .replace(/]-->/g, ']-->\n')
+    .replace(/\n{2,}/g, '\n')
+}
+
+const Vue = { ...runtimeDom, ...runtimeVapor }
+
+function compile(
+  sfc: string,
+  data: runtimeDom.Ref<any>,
+  components: Record<string, any> = {},
+  { vapor = true, ssr = false } = {},
+) {
+  if (!sfc.includes(`<script`)) {
+    sfc =
+      `<script vapor>const data = _data; const components = _components;</script>` +
+      sfc
+  }
+  const descriptor = parse(sfc).descriptor
+
+  const script = compileScript(descriptor, {
+    id: 'x',
+    isProd: true,
+    inlineTemplate: true,
+    genDefaultAs: '__sfc__',
+    vapor,
+    templateOptions: {
+      ssr,
+    },
+  })
+
+  const code =
+    script.content
+      .replace(/\bimport {/g, 'const {')
+      .replace(/ as _/g, ': _')
+      .replace(/} from ['"]vue['"]/g, `} = Vue`)
+      .replace(/} from "vue\/server-renderer"/g, '} = VueServerRenderer') +
+    '\nreturn __sfc__'
+
+  return new Function('Vue', 'VueServerRenderer', '_data', '_components', code)(
+    Vue,
+    VueServerRenderer,
+    data,
+    components,
+  )
+}
+
+async function testWithVaporApp(
+  code: string,
+  components?: Record<string, string | { code: string; vapor: boolean }>,
+  data?: any,
+) {
+  return testHydration(code, components, data, {
+    isVaporApp: true,
+    interop: true,
+  })
+}
+
+async function testWithVDOMApp(
+  code: string,
+  components?: Record<string, string | { code: string; vapor: boolean }>,
+  data?: any,
+) {
+  return testHydration(code, components, data, {
+    isVaporApp: false,
+    interop: true,
+  })
+}
+
+function compileVaporComponent(
+  code: string,
+  data: runtimeDom.Ref<any> = ref({}),
+  components?: Record<string, any>,
+  ssr = false,
+) {
+  if (!code.includes(`<script`)) {
+    code = `<template>${code}</template>`
+  }
+  return compile(code, data, components, {
+    vapor: true,
+    ssr,
+  })
+}
+
+async function mountWithHydration(
+  html: string,
+  code: string,
+  data: runtimeDom.Ref<any> = ref({}),
+  components?: Record<string, any>,
+) {
+  const container = document.createElement('div')
+  container.innerHTML = html
+  document.body.appendChild(container)
+
+  const clientComp = compileVaporComponent(code, data, components)
+  const app = createVaporSSRApp(clientComp)
+  app.mount(container)
+
+  return {
+    block: (app._instance! as VaporComponentInstance).block,
+    container,
+  }
+}
 
 async function testHydration(
   code: string,
-  components: Record<string, string> = {},
+  components: Record<string, string | { code: string; vapor: boolean }> = {},
+  data: any = ref('foo'),
+  { isVaporApp = true, interop = false } = {},
 ) {
-  const data = ref('foo')
   const ssrComponents: any = {}
   const clientComponents: any = {}
   for (const key in components) {
-    clientComponents[key] = compile(components[key], data, clientComponents)
-    ssrComponents[key] = compile(components[key], data, ssrComponents, {
+    const comp = components[key]
+    const code = isString(comp) ? comp : comp.code
+    const isVaporComp = isString(comp) || !!comp.vapor
+    clientComponents[key] = compile(code, data, clientComponents, {
+      vapor: isVaporComp,
+      ssr: false,
+    })
+    ssrComponents[key] = compile(code, data, ssrComponents, {
+      vapor: isVaporComp,
       ssr: true,
     })
   }
 
-  const serverComp = compile(code, data, ssrComponents, { ssr: true })
+  const serverComp = compile(code, data, ssrComponents, {
+    vapor: isVaporApp,
+    ssr: true,
+  })
   const html = await VueServerRenderer.renderToString(
     runtimeDom.createSSRApp(serverComp),
   )
@@ -24,8 +146,21 @@ async function testHydration(
   document.body.appendChild(container)
   container.innerHTML = html
 
-  const clientComp = compile(code, data, clientComponents)
-  const app = createVaporSSRApp(clientComp)
+  const clientComp = compile(code, data, clientComponents, {
+    vapor: isVaporApp,
+    ssr: false,
+  })
+  let app
+  if (isVaporApp) {
+    app = createVaporSSRApp(clientComp)
+  } else {
+    app = runtimeDom.createSSRApp(clientComp)
+  }
+
+  if (interop) {
+    app.use(runtimeVapor.vaporInteropPlugin)
+  }
+
   app.mount(container)
   return { data, container }
 }
@@ -35,72 +170,203 @@ const triggerEvent = (type: string, el: Element) => {
   el.dispatchEvent(event)
 }
 
-describe('Vapor Mode hydration', () => {
-  delegateEvents('click')
+delegateEvents('click')
 
-  beforeEach(() => {
-    document.body.innerHTML = ''
-  })
+beforeEach(() => {
+  document.body.innerHTML = ''
+})
 
-  test('root text', async () => {
-    const { data, container } = await testHydration(`
+describe('Vapor Mode hydration', () => {
+  describe('text', () => {
+    test('root text', async () => {
+      const { data, container } = await testHydration(`
       <template>{{ data }}</template>
     `)
-    expect(container.innerHTML).toMatchInlineSnapshot(`"foo"`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"foo"`)
 
-    data.value = 'bar'
-    await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(`"bar"`)
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"bar"`)
+    })
+
+    test('consecutive text nodes', async () => {
+      const { data, container } = await testHydration(`
+      <template>{{ data }}{{ data }}</template>
+    `)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"foofoo"`)
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"barbar"`)
+    })
+
+    test('consecutive text nodes with insertion anchor', async () => {
+      const { data, container } = await testHydration(`
+      <template><span/>{{ data }}{{ data }}<span/></template>
+    `)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span></span>foofoo<span></span><!--]-->
+        "
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span></span>barbar<span></span><!--]-->
+        "
+      `,
+      )
+    })
+
+    test('mixed text nodes', async () => {
+      const { data, container } = await testHydration(`
+      <template>{{ data }}A{{ data }}B{{ data }}</template>
+    `)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"fooAfooBfoo"`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"barAbarBbar"`,
+      )
+    })
+
+    test('mixed text nodes with insertion anchor', async () => {
+      const { data, container } = await testHydration(`
+      <template><span/>{{ data }}A{{ data }}B{{ data }}<span/></template>
+    `)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span></span>fooAfooBfoo<span></span><!--]-->
+        "
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span></span>barAbarBbar<span></span><!--]-->
+        "
+      `,
+      )
+    })
+
+    test('empty text node', async () => {
+      const data = reactive({ txt: '' })
+      const { container } = await testHydration(
+        `<template><div>{{ data.txt }}</div></template>`,
+        undefined,
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div></div>"`,
+      )
+
+      data.txt = 'foo'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div>"`,
+      )
+    })
+
+    test('empty text node in slot', async () => {
+      const data = reactive({ txt: '' })
+      const { container } = await testHydration(
+        `<template><components.Child>{{data.txt}}</components.Child></template>`,
+        {
+          Child: `<template><slot/></template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><!--]-->
+        "
+      `,
+      )
+
+      data.txt = 'foo'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->foo<!--]-->
+        "
+      `,
+      )
+    })
   })
 
-  test('root comment', async () => {
-    const { container } = await testHydration(`
+  describe('element', () => {
+    test('root comment', async () => {
+      const { container } = await testHydration(`
       <template><!----></template>
     `)
-    expect(container.innerHTML).toBe('<!---->')
-    expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
-  })
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"<!---->"`)
+      expect(`mismatch in <div>`).not.toHaveBeenWarned()
+    })
 
-  test('root with mixed element and text', async () => {
-    const { container, data } = await testHydration(`
+    test('root with mixed element and text', async () => {
+      const { container, data } = await testHydration(`
       <template> A<span>{{ data }}</span>{{ data }}</template>
     `)
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<!--[--> A<span>foo</span>foo<!--]-->"`,
-    )
-
-    data.value = 'bar'
-    await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<!--[--> A<span>bar</span>bar<!--]-->"`,
-    )
-  })
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--> A<span>foo</span>foo<!--]-->
+        "
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--> A<span>bar</span>bar<!--]-->
+        "
+      `,
+      )
+    })
 
-  test('empty element', async () => {
-    const { container } = await testHydration(`
+    test('empty element', async () => {
+      const { container } = await testHydration(`
       <template><div/></template>
     `)
-    expect(container.innerHTML).toBe('<div></div>')
-    expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
-  })
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div></div>"`,
+      )
+      expect(`mismatch in <div>`).not.toHaveBeenWarned()
+    })
 
-  test('element with binding and text children', async () => {
-    const { container, data } = await testHydration(`
+    test('element with binding and text children', async () => {
+      const { container, data } = await testHydration(`
       <template><div :class="data">{{ data }}</div></template>
     `)
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div class="foo">foo</div>"`,
-    )
-
-    data.value = 'bar'
-    await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div class="bar">bar</div>"`,
-    )
-  })
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div class="foo">foo</div>"`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div class="bar">bar</div>"`,
+      )
+    })
 
-  test('element with elements children', async () => {
-    const { container } = await testHydration(`
+    test('element with elements children', async () => {
+      const { container } = await testHydration(`
       <template>
         <div>
           <span>{{ data }}</span>
@@ -108,99 +374,140 @@ describe('Vapor Mode hydration', () => {
         </div>
       </template>
     `)
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div><span>foo</span><span class="foo"></span></div>"`,
-    )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span>foo</span><span class="foo"></span></div>"`,
+      )
 
-    // event handler
-    triggerEvent('click', container.querySelector('.foo')!)
+      // event handler
+      triggerEvent('click', container.querySelector('.foo')!)
 
-    await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div><span>bar</span><span class="bar"></span></div>"`,
-    )
-  })
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span>bar</span><span class="bar"></span></div>"`,
+      )
+    })
 
-  test('basic component', async () => {
-    const { container, data } = await testHydration(
-      `
-      <template><div><span></span><components.Child/></div></template>
+    test('element with ref', async () => {
+      const { data, container } = await testHydration(
+        `<template>
+          <div ref="data">hi</div>
+        </template>
       `,
-      { Child: `<template>{{ data }}</template>` },
-    )
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div><span></span>foo</div>"`,
-    )
+        {},
+        ref(null),
+      )
 
-    data.value = 'bar'
-    await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div><span></span>bar</div>"`,
-    )
+      expect(data.value).toBe(container.firstChild)
+    })
   })
 
-  test('fragment component', async () => {
-    const { container, data } = await testHydration(
-      `
+  describe('component', () => {
+    test('basic component', async () => {
+      const { container, data } = await testHydration(
+        `
       <template><div><span></span><components.Child/></div></template>
       `,
-      { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` },
-    )
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div><span></span><!--[--><div>foo</div>-foo-<!--]--></div>"`,
-    )
+        { Child: `<template>{{ data }}</template>` },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span>foo</div>"`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span>bar</div>"`,
+      )
+    })
 
-    data.value = 'bar'
-    await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div><span></span><!--[--><div>bar</div>-bar-<!--]--></div>"`,
-    )
-  })
+    test('fragment component', async () => {
+      const { container, data } = await testHydration(
+        `
+      <template><div><span></span><components.Child/></div></template>
+      `,
+        { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>foo</div>-foo-<!--]-->
+        </div>"
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>bar</div>-bar-<!--]-->
+        </div>"
+      `,
+      )
+    })
 
-  test('fragment component with prepend', async () => {
-    const { container, data } = await testHydration(
-      `
+    test('fragment component with prepend', async () => {
+      const { container, data } = await testHydration(
+        `
       <template><div><components.Child/><span></span></div></template>
       `,
-      { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` },
-    )
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div><!--[--><div>foo</div>-foo-<!--]--><span></span></div>"`,
-    )
-
-    data.value = 'bar'
-    await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div><!--[--><div>bar</div>-bar-<!--]--><span></span></div>"`,
-    )
-  })
+        { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <span></span></div>"
+      `,
+      )
+    })
 
-  test('nested fragment components', async () => {
-    const { container, data } = await testHydration(
-      `
+    test('nested fragment components', async () => {
+      const { container, data } = await testHydration(
+        `
       <template><div><components.Parent/><span></span></div></template>
       `,
-      {
-        Parent: `<template><div/><components.Child/><div/></template>`,
-        Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
-      },
-    )
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div><!--[--><div></div><!--[--><div>foo</div>-foo-<!--]--><div></div><!--]--><span></span></div>"`,
-    )
-
-    data.value = 'bar'
-    await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div><!--[--><div></div><!--[--><div>bar</div>-bar-<!--]--><div></div><!--]--><span></span></div>"`,
-    )
-  })
+        {
+          Parent: `<template><div/><components.Child/><div/></template>`,
+          Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><div></div>
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <div></div><!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><div></div>
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <div></div><!--]-->
+        <span></span></div>"
+      `,
+      )
+    })
 
-  // problem is the <!> placeholder does not exist in SSR output
-  test.todo('component with anchor insertion', async () => {
-    const { container, data } = await testHydration(
-      `
-      <template>
+    test('component with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `<template>
         <div>
           <span/>
           <components.Child/>
@@ -208,1543 +515,3445 @@ describe('Vapor Mode hydration', () => {
         </div>
       </template>
       `,
-      {
-        Child: `<template>{{ data }}</template>`,
-      },
-    )
-    expect(container.innerHTML).toMatchInlineSnapshot()
+        {
+          Child: `<template>{{ data }}</template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span>foo<span></span></div>"`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span>bar<span></span></div>"`,
+      )
+    })
 
-    data.value = 'bar'
-    await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot()
-  })
+    test('nested components with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `
+      <template><components.Parent/></template>
+      `,
+        {
+          Parent: `<template><div><span/><components.Child/><span/></div></template>`,
+          Child: `<template><div>{{ data }}</div></template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><div>foo</div><span></span></div>"`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><div>bar</div><span></span></div>"`,
+      )
+    })
+
+    test('nested components with multi level anchor insertion', async () => {
+      const { container, data } = await testHydration(
+        `
+      <template><div><span></span><components.Parent/><span></span></div></template>
+      `,
+        {
+          Parent: `<template><div><span/><components.Child/><span/></div></template>`,
+          Child: `<template><div>{{ data }}</div></template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><div><span></span><div>foo</div><span></span></div><span></span></div>"`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><div><span></span><div>bar</div><span></span></div><span></span></div>"`,
+      )
+    })
+
+    test('consecutive components with insertion parent', async () => {
+      const data = reactive({ foo: 'foo', bar: 'bar' })
+      const { container } = await testHydration(
+        `<template>
+        <div>
+          <components.Child1/>
+          <components.Child2/>
+        </div>
+      </template>
+      `,
+        {
+          Child1: `<template><span>{{ data.foo }}</span></template>`,
+          Child2: `<template><span>{{ data.bar }}</span></template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span>foo</span><span>bar</span></div>"`,
+      )
+
+      data.foo = 'foo1'
+      data.bar = 'bar1'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span>foo1</span><span>bar1</span></div>"`,
+      )
+    })
+
+    test('nested consecutive components with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `
+      <template><components.Parent/></template>
+      `,
+        {
+          Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`,
+          Child: `<template><div>{{ data }}</div></template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><div>foo</div><div>foo</div><span></span></div>"`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><div>bar</div><div>bar</div><span></span></div>"`,
+      )
+    })
 
-  test.todo('consecutive component with anchor insertion', async () => {
-    const { container, data } = await testHydration(
-      `<template>
+    test('nested consecutive components with multi level anchor insertion', async () => {
+      const { container, data } = await testHydration(
+        `
+      <template><div><span></span><components.Parent/><span></span></div></template>
+      `,
+        {
+          Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`,
+          Child: `<template><div>{{ data }}</div></template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><div><span></span><div>foo</div><div>foo</div><span></span></div><span></span></div>"`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><div><span></span><div>bar</div><div>bar</div><span></span></div><span></span></div>"`,
+      )
+    })
+
+    test('mixed component and element with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `<template>
         <div>
           <span/>
           <components.Child/>
+          <span/>
           <components.Child/>
           <span/>
         </div>
       </template>
       `,
-      {
-        Child: `<template>{{ data }}</template>`,
-      },
-    )
-    expect(container.innerHTML).toMatchInlineSnapshot()
+        {
+          Child: `<template>{{ data }}</template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span>foo<span></span>foo<span></span></div>"`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span>bar<span></span>bar<span></span></div>"`,
+      )
+    })
 
-    data.value = 'bar'
-    await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot()
-  })
+    test('fragment component with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+        <div>
+          <span/>
+          <components.Child/>
+          <span/>
+        </div>
+      </template>
+      `,
+        {
+          Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>foo</div>-foo<!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>bar</div>-bar<!--]-->
+        <span></span></div>"
+      `,
+      )
+    })
 
-  test.todo('if')
+    test('nested fragment component with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `
+      <template><components.Parent/></template>
+      `,
+        {
+          Parent: `<template><div><span/><components.Child/><span/></div></template>`,
+          Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <span></span></div>"
+      `,
+      )
+    })
 
-  test.todo('for')
+    test('nested fragment component with multi level anchor insertion', async () => {
+      const { container, data } = await testHydration(
+        `
+      <template><div><span/><components.Parent/><span/></div></template>
+      `,
+        {
+          Parent: `<template><div><span/><components.Child/><span/></div></template>`,
+          Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span><div><span></span>
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <span></span></div><span></span></div>"
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span><div><span></span>
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <span></span></div><span></span></div>"
+      `,
+      )
+    })
 
-  test.todo('slots')
+    test('consecutive fragment components with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <components.Child/>
+            <components.Child/>
+            <span/>
+          </div>
+        </template>
+      `,
+        {
+          Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>foo</div>-foo<!--]-->
+        <!--[--><div>foo</div>-foo<!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>bar</div>-bar<!--]-->
+        <!--[--><div>bar</div>-bar<!--]-->
+        <span></span></div>"
+      `,
+      )
+    })
 
-  // test('element with ref', () => {
-  //   const el = ref()
-  //   const { vnode, container } = mountWithHydration('<div></div>', () =>
-  //     h('div', { ref: el }),
-  //   )
-  //   expect(vnode.el).toBe(container.firstChild)
-  //   expect(el.value).toBe(vnode.el)
-  // })
+    test('nested consecutive fragment components with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `
+      <template><components.Parent/></template>
+      `,
+        {
+          Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`,
+          Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <span></span></div>"
+      `,
+      )
+    })
 
-  // test('with data-allow-mismatch component when using onServerPrefetch', async () => {
-  //   const Comp = {
-  //     template: `
-  //       <div>Comp2</div>
-  //     `,
-  //   }
-  //   let foo: any
-  //   const App = {
-  //     setup() {
-  //       const flag = ref(true)
-  //       foo = () => {
-  //         flag.value = false
-  //       }
-  //       onServerPrefetch(() => (flag.value = false))
-  //       return { flag }
-  //     },
-  //     components: {
-  //       Comp,
-  //     },
-  //     template: `
-  //       <span data-allow-mismatch>
-  //         <Comp v-if="flag"></Comp>
-  //       </span>
-  //     `,
-  //   }
-  //   // hydrate
-  //   const container = document.createElement('div')
-  //   container.innerHTML = await renderToString(h(App))
-  //   createSSRApp(App).mount(container)
-  //   expect(container.innerHTML).toBe(
-  //     '<span data-allow-mismatch=""><div>Comp2</div></span>',
-  //   )
-  //   foo()
-  //   await nextTick()
-  //   expect(container.innerHTML).toBe(
-  //     '<span data-allow-mismatch=""><!--v-if--></span>',
-  //   )
-  // })
+    test('nested consecutive fragment components with multi level anchor insertion', async () => {
+      const { container, data } = await testHydration(
+        `
+      <template><div><span></span><components.Parent/><span></span></div></template>
+      `,
+        {
+          Parent: `<template><div><span/><components.Child/><components.Child/><span/></div></template>`,
+          Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span><div><span></span>
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <span></span></div><span></span></div>"
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span><div><span></span>
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <span></span></div><span></span></div>"
+      `,
+      )
+    })
 
-  // // compile SSR + client render fn from the same template & hydrate
-  // test('full compiler integration', async () => {
-  //   const mounted: string[] = []
-  //   const log = vi.fn()
-  //   const toggle = ref(true)
-
-  //   const Child = {
-  //     data() {
-  //       return {
-  //         count: 0,
-  //         text: 'hello',
-  //         style: {
-  //           color: 'red',
-  //         },
-  //       }
-  //     },
-  //     mounted() {
-  //       mounted.push('child')
-  //     },
-  //     template: `
-  //     <div>
-  //       <span class="count" :style="style">{{ count }}</span>
-  //       <button class="inc" @click="count++">inc</button>
-  //       <button class="change" @click="style.color = 'green'" >change color</button>
-  //       <button class="emit" @click="$emit('foo')">emit</button>
-  //       <span class="text">{{ text }}</span>
-  //       <input v-model="text">
-  //     </div>
-  //     `,
-  //   }
-
-  //   const App = {
-  //     setup() {
-  //       return { toggle }
-  //     },
-  //     mounted() {
-  //       mounted.push('parent')
-  //     },
-  //     template: `
-  //       <div>
-  //         <span>hello</span>
-  //         <template v-if="toggle">
-  //           <Child @foo="log('child')"/>
-  //           <template v-if="true">
-  //             <button class="parent-click" @click="log('click')">click me</button>
-  //           </template>
-  //         </template>
-  //         <span>hello</span>
-  //       </div>`,
-  //     components: {
-  //       Child,
-  //     },
-  //     methods: {
-  //       log,
-  //     },
-  //   }
-
-  //   const container = document.createElement('div')
-  //   // server render
-  //   container.innerHTML = await renderToString(h(App))
-  //   // hydrate
-  //   createSSRApp(App).mount(container)
-
-  //   // assert interactions
-  //   // 1. parent button click
-  //   triggerEvent('click', container.querySelector('.parent-click')!)
-  //   expect(log).toHaveBeenCalledWith('click')
-
-  //   // 2. child inc click + text interpolation
-  //   const count = container.querySelector('.count') as HTMLElement
-  //   expect(count.textContent).toBe(`0`)
-  //   triggerEvent('click', container.querySelector('.inc')!)
-  //   await nextTick()
-  //   expect(count.textContent).toBe(`1`)
-
-  //   // 3. child color click + style binding
-  //   expect(count.style.color).toBe('red')
-  //   triggerEvent('click', container.querySelector('.change')!)
-  //   await nextTick()
-  //   expect(count.style.color).toBe('green')
-
-  //   // 4. child event emit
-  //   triggerEvent('click', container.querySelector('.emit')!)
-  //   expect(log).toHaveBeenCalledWith('child')
-
-  //   // 5. child v-model
-  //   const text = container.querySelector('.text')!
-  //   const input = container.querySelector('input')!
-  //   expect(text.textContent).toBe('hello')
-  //   input.value = 'bye'
-  //   triggerEvent('input', input)
-  //   await nextTick()
-  //   expect(text.textContent).toBe('bye')
-  // })
+    test('nested consecutive fragment components with root level anchor insertion', async () => {
+      const { container, data } = await testHydration(
+        `
+      <template><div><span></span><components.Parent/><span></span></div></template>
+      `,
+        {
+          Parent: `<template><components.Child/><components.Child/></template>`,
+          Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--]-->
+        <span></span></div>"
+      `,
+      )
+    })
 
-  // test('handle click error in ssr mode', async () => {
-  //   const App = {
-  //     setup() {
-  //       const throwError = () => {
-  //         throw new Error('Sentry Error')
-  //       }
-  //       return { throwError }
-  //     },
-  //     template: `
-  //       <div>
-  //         <button class="parent-click" @click="throwError">click me</button>
-  //       </div>`,
-  //   }
-
-  //   const container = document.createElement('div')
-  //   // server render
-  //   container.innerHTML = await renderToString(h(App))
-  //   // hydrate
-  //   const app = createSSRApp(App)
-  //   const handler = (app.config.errorHandler = vi.fn())
-  //   app.mount(container)
-  //   // assert interactions
-  //   // parent button click
-  //   triggerEvent('click', container.querySelector('.parent-click')!)
-  //   expect(handler).toHaveBeenCalled()
-  // })
+    test('mixed fragment component and element with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+        <div>
+          <span/>
+          <components.Child/>
+          <span/>
+          <components.Child/>
+          <span/>
+        </div>
+      </template>
+      `,
+        {
+          Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>foo</div>-foo<!--]-->
+        <span></span>
+        <!--[--><div>foo</div>-foo<!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>bar</div>-bar<!--]-->
+        <span></span>
+        <!--[--><div>bar</div>-bar<!--]-->
+        <span></span></div>"
+      `,
+      )
+    })
 
-  // test('handle blur error in ssr mode', async () => {
-  //   const App = {
-  //     setup() {
-  //       const throwError = () => {
-  //         throw new Error('Sentry Error')
-  //       }
-  //       return { throwError }
-  //     },
-  //     template: `
-  //       <div>
-  //         <input class="parent-click" @blur="throwError"/>
-  //       </div>`,
-  //   }
-
-  //   const container = document.createElement('div')
-  //   // server render
-  //   container.innerHTML = await renderToString(h(App))
-  //   // hydrate
-  //   const app = createSSRApp(App)
-  //   const handler = (app.config.errorHandler = vi.fn())
-  //   app.mount(container)
-  //   // assert interactions
-  //   // parent blur event
-  //   triggerEvent('blur', container.querySelector('.parent-click')!)
-  //   expect(handler).toHaveBeenCalled()
-  // })
+    test('mixed fragment component and text with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+        <div>
+          <span/>
+          <components.Child/>
+          {{ data }}
+          <components.Child/>
+          <span/>
+        </div>
+      </template>
+      `,
+        {
+          Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>foo</div>-foo<!--]-->
+         foo 
+        <!--[--><div>foo</div>-foo<!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>bar</div>-bar<!--]-->
+         bar 
+        <!--[--><div>bar</div>-bar<!--]-->
+        <span></span></div>"
+      `,
+      )
+    })
+  })
 
-  // test('async component', async () => {
-  //   const spy = vi.fn()
-  //   const Comp = () =>
-  //     h(
-  //       'button',
-  //       {
-  //         onClick: spy,
-  //       },
-  //       'hello!',
-  //     )
-
-  //   let serverResolve: any
-  //   let AsyncComp = defineAsyncComponent(
-  //     () =>
-  //       new Promise(r => {
-  //         serverResolve = r
-  //       }),
-  //   )
+  describe('dynamic component', () => {
+    test('basic dynamic component', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <component :is="components[data]"/>
+        </template>`,
+        {
+          foo: `<template><div>foo</div></template>`,
+          bar: `<template><div>bar</div></template>`,
+        },
+        ref('foo'),
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--dynamic-component-->"`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>bar</div><!--dynamic-component-->"`,
+      )
+    })
 
-  //   const App = {
-  //     render() {
-  //       return ['hello', h(AsyncComp), 'world']
-  //     },
-  //   }
-
-  //   // server render
-  //   const htmlPromise = renderToString(h(App))
-  //   serverResolve(Comp)
-  //   const html = await htmlPromise
-  //   expect(html).toMatchInlineSnapshot(
-  //     `"<!--[-->hello<button>hello!</button>world<!--]-->"`,
-  //   )
+    test('dynamic component with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <component :is="components[data]"/>
+            <span/>
+          </div>
+        </template>`,
+        {
+          foo: `<template><div>foo</div></template>`,
+          bar: `<template><div>bar</div></template>`,
+        },
+        ref('foo'),
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><div>foo</div><!--dynamic-component--><span></span></div>"`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><div>bar</div><!--dynamic-component--><span></span></div>"`,
+      )
+    })
 
-  //   // hydration
-  //   let clientResolve: any
-  //   AsyncComp = defineAsyncComponent(
-  //     () =>
-  //       new Promise(r => {
-  //         clientResolve = r
-  //       }),
-  //   )
+    test('consecutive dynamic components with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <component :is="components[data]"/>
+            <component :is="components[data]"/>
+            <span/>
+          </div>
+        </template>`,
+        {
+          foo: `<template><div>foo</div></template>`,
+          bar: `<template><div>bar</div></template>`,
+        },
+        ref('foo'),
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><div>foo</div><!--dynamic-component--><div>foo</div><!--dynamic-component--><span></span></div>"`,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><div>bar</div><!--dynamic-component--><div>bar</div><!--dynamic-component--><span></span></div>"`,
+      )
+    })
 
-  //   const container = document.createElement('div')
-  //   container.innerHTML = html
-  //   createSSRApp(App).mount(container)
+    test('dynamic component fallback', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+            <component :is="'button'">
+              <span>{{ data }}</span>
+            </component>
+          </template>`,
+        {},
+        ref('foo'),
+      )
+
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<button><span>foo</span></button><!--dynamic-component-->"`,
+      )
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<button><span>bar</span></button><!--dynamic-component-->"`,
+      )
+    })
 
-  //   // hydration not complete yet
-  //   triggerEvent('click', container.querySelector('button')!)
-  //   expect(spy).not.toHaveBeenCalled()
+    test('in ssr slot vnode fallback', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+            <components.Child>
+              <span>{{ data }}</span>
+            </components.Child>
+          </template>`,
+        {
+          Child: `
+          <template>
+            <component :is="'div'">
+              <slot />
+            </component>
+          </template>`,
+        },
+        ref('foo'),
+      )
+
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><span>foo</span><!--]-->
+        </div><!--dynamic-component-->"
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><span>bar</span><!--]-->
+        </div><!--dynamic-component-->"
+      `,
+      )
+    })
+  })
 
-  //   // resolve
-  //   clientResolve(Comp)
-  //   await new Promise(r => setTimeout(r))
+  describe('if', () => {
+    test('basic toggle - true -> false', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div v-if="data">foo</div>
+        </template>`,
+        undefined,
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if-->"`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--if-->"`,
+      )
+    })
 
-  //   // should be hydrated now
-  //   triggerEvent('click', container.querySelector('button')!)
-  //   expect(spy).toHaveBeenCalled()
-  // })
+    test('basic toggle - false -> true', async () => {
+      const data = ref(false)
+      const { container } = await testHydration(
+        `<template>
+          <div v-if="data">foo</div>
+        </template>`,
+        undefined,
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--if-->"`,
+      )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if-->"`,
+      )
+    })
 
-  // test('update async wrapper before resolve', async () => {
-  //   const Comp = {
-  //     render() {
-  //       return h('h1', 'Async component')
-  //     },
-  //   }
-  //   let serverResolve: any
-  //   let AsyncComp = defineAsyncComponent(
-  //     () =>
-  //       new Promise(r => {
-  //         serverResolve = r
-  //       }),
-  //   )
+    test('v-if on insertion parent', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div v-if="data">
+            <components.Child/>
+          </div>
+        </template>`,
+        { Child: `<template>foo</template>` },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if-->"`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--if-->"`,
+      )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if-->"`,
+      )
+    })
 
-  //   const toggle = ref(true)
-  //   const App = {
-  //     setup() {
-  //       onMounted(() => {
-  //         // change state, this makes updateComponent(AsyncComp) execute before
-  //         // the async component is resolved
-  //         toggle.value = false
-  //       })
-
-  //       return () => {
-  //         return [toggle.value ? 'hello' : 'world', h(AsyncComp)]
-  //       }
-  //     },
-  //   }
-
-  //   // server render
-  //   const htmlPromise = renderToString(h(App))
-  //   serverResolve(Comp)
-  //   const html = await htmlPromise
-  //   expect(html).toMatchInlineSnapshot(
-  //     `"<!--[-->hello<h1>Async component</h1><!--]-->"`,
-  //   )
+    test('v-if/else-if/else chain - switch branches', async () => {
+      const data = ref('a')
+      const { container } = await testHydration(
+        `<template>
+          <div v-if="data === 'a'">foo</div>
+          <div v-else-if="data === 'b'">bar</div>
+          <div v-else>baz</div>
+        </template>`,
+        undefined,
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if-->"`,
+      )
+
+      data.value = 'b'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>bar</div><!--if--><!--if-->"`,
+      )
+
+      data.value = 'c'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>baz</div><!--if--><!--if-->"`,
+      )
+
+      data.value = 'a'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if-->"`,
+      )
+    })
 
-  //   // hydration
-  //   let clientResolve: any
-  //   AsyncComp = defineAsyncComponent(
-  //     () =>
-  //       new Promise(r => {
-  //         clientResolve = r
-  //       }),
-  //   )
+    test('nested if', async () => {
+      const data = reactive({ outer: true, inner: true })
+      const { container } = await testHydration(
+        `<template>
+          <div v-if="data.outer">
+            <span>outer</span>
+            <div v-if="data.inner">inner</div>
+          </div>
+        </template>`,
+        undefined,
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span>outer</span><div>inner</div><!--if--></div><!--if-->"`,
+      )
+
+      data.inner = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span>outer</span><!--if--></div><!--if-->"`,
+      )
+
+      data.outer = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--if-->"`,
+      )
+    })
 
-  //   const container = document.createElement('div')
-  //   container.innerHTML = html
-  //   createSSRApp(App).mount(container)
+    test('on component', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <components.Child v-if="data"/>
+        </template>`,
+        { Child: `<template>foo</template>` },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"foo<!--if-->"`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--if-->"`,
+      )
+    })
 
-  //   // resolve
-  //   clientResolve(Comp)
-  //   await new Promise(r => setTimeout(r))
+    test('consecutive if node', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <components.Child v-if="data"/>
+        </template>`,
+        { Child: `<template><div v-if="data">foo</div></template>` },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if--><!--if-->"`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--if-->"`,
+      )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if--><!--if-->"`,
+      )
+    })
 
-  //   // should be hydrated now
-  //   expect(`Hydration node mismatch`).not.toHaveBeenWarned()
-  //   expect(container.innerHTML).toMatchInlineSnapshot(
-  //     `"<!--[-->world<h1>Async component</h1><!--]-->"`,
-  //   )
-  // })
+    test('mixed prepend and insertion anchor', async () => {
+      const data = reactive({
+        show: true,
+        foo: 'foo',
+        bar: 'bar',
+        qux: 'qux',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child/>
+        </template>`,
+        {
+          Child: `<template>
+            <span v-if="data.show">
+              <span v-if="data.show">{{data.foo}}</span>
+              <span v-if="data.show">{{data.bar}}</span>
+              <span>baz</span>
+              <span v-if="data.show">{{data.qux}}</span>
+              <span>quux</span>
+            </span>
+          </template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<span><span>foo</span><!--if--><span>bar</span><!--if--><span>baz</span><span>qux</span><!--if--><span>quux</span></span><!--if-->"`,
+      )
+
+      data.qux = 'qux1'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<span><span>foo</span><!--if--><span>bar</span><!--if--><span>baz</span><span>qux1</span><!--if--><span>quux</span></span><!--if-->"`,
+      )
+
+      data.foo = 'foo1'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<span><span>foo1</span><!--if--><span>bar</span><!--if--><span>baz</span><span>qux1</span><!--if--><span>quux</span></span><!--if-->"`,
+      )
+    })
 
-  // test('hydrate safely when property used by async setup changed before render', async () => {
-  //   const toggle = ref(true)
-
-  //   const AsyncComp = {
-  //     async setup() {
-  //       await new Promise<void>(r => setTimeout(r, 10))
-  //       return () => h('h1', 'Async component')
-  //     },
-  //   }
-
-  //   const AsyncWrapper = {
-  //     render() {
-  //       return h(AsyncComp)
-  //     },
-  //   }
-
-  //   const SiblingComp = {
-  //     setup() {
-  //       toggle.value = false
-  //       return () => h('span')
-  //     },
-  //   }
-
-  //   const App = {
-  //     setup() {
-  //       return () =>
-  //         h(
-  //           Suspense,
-  //           {},
-  //           {
-  //             default: () => [
-  //               h('main', {}, [
-  //                 h(AsyncWrapper, {
-  //                   prop: toggle.value ? 'hello' : 'world',
-  //                 }),
-  //                 h(SiblingComp),
-  //               ]),
-  //             ],
-  //           },
-  //         )
-  //     },
-  //   }
-
-  //   // server render
-  //   const html = await renderToString(h(App))
-
-  //   expect(html).toMatchInlineSnapshot(
-  //     `"<main><h1 prop="hello">Async component</h1><span></span></main>"`,
-  //   )
+    test('v-if/else-if/else chain on component - switch branches', async () => {
+      const data = ref('a')
+      const { container } = await testHydration(
+        `<template>
+          <components.Child1 v-if="data === 'a'"/>
+          <components.Child2 v-else-if="data === 'b'"/>
+          <components.Child3 v-else/>
+        </template>`,
+        {
+          Child1: `<template><span>{{data}} child1</span></template>`,
+          Child2: `<template><span>{{data}} child2</span></template>`,
+          Child3: `<template><span>{{data}} child3</span></template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<span>a child1</span><!--if-->"`,
+      )
+
+      data.value = 'b'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<span>b child2</span><!--if--><!--if-->"`,
+      )
+
+      data.value = 'c'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<span>c child3</span><!--if--><!--if-->"`,
+      )
+
+      data.value = 'a'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<span>a child1</span><!--if-->"`,
+      )
+    })
 
-  //   expect(toggle.value).toBe(false)
+    test('on component with insertion anchor', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <components.Child v-if="data"/>
+            <span/>
+          </div>
+        </template>`,
+        { Child: `<template>foo</template>` },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span>foo<!--if--><span></span></div>"`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><!--if--><span></span></div>"`,
+      )
+    })
 
-  //   // hydration
+    test('consecutive component with insertion parent', async () => {
+      const data = reactive({
+        show: true,
+        foo: 'foo',
+        bar: 'bar',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <div v-if="data.show">
+            <components.Child/>
+            <components.Child2/>
+          </div>
+        </template>`,
+        {
+          Child: `<template><span>{{data.foo}}</span></template>`,
+          Child2: `<template><span>{{data.bar}}</span></template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span>foo</span><span>bar</span></div><!--if-->"`,
+      )
+
+      data.show = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--if-->"`,
+      )
+
+      data.show = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span>foo</span><span>bar</span></div><!--if-->"`,
+      )
+
+      data.foo = 'foo1'
+      data.bar = 'bar1'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span>foo1</span><span>bar1</span></div><!--if-->"`,
+      )
+    })
 
-  //   // reset the value
-  //   toggle.value = true
-  //   expect(toggle.value).toBe(true)
+    test('on fragment component', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <components.Child v-if="data"/>
+          </div>
+        </template>`,
+        {
+          Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><div>true</div>-true-<!--]-->
+        <!--if--></div>"
+      `,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><!--]-->
+        <!--if--></div>"
+      `,
+      )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><!--]-->
+        <div>true</div>-true-<!--if--></div>"
+      `,
+      )
+    })
 
-  //   const container = document.createElement('div')
-  //   container.innerHTML = html
-  //   createSSRApp(App).mount(container)
+    test('on fragment component with insertion anchor', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <components.Child v-if="data"/>
+            <span/>
+          </div>
+        </template>`,
+        {
+          Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>true</div>-true-<!--]-->
+        <!--if--><span></span></div>"
+      `,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><!--]-->
+        <!--if--><span></span></div>"
+      `,
+      )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "<div><span></span>
+        <!--[--><!--]-->
+        <div>true</div>-true-<!--if--><span></span></div>"
+      `)
+    })
 
-  //   await new Promise(r => setTimeout(r, 10))
+    test('consecutive v-if on fragment component with insertion anchor', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <components.Child v-if="data"/>
+            <components.Child v-if="data"/>
+            <span/>
+          </div>
+        </template>`,
+        {
+          Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><div>true</div>-true-<!--]-->
+        <!--if-->
+        <!--[--><div>true</div>-true-<!--]-->
+        <!--if--><span></span></div>"
+      `,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><!--]-->
+        <!--if-->
+        <!--[--><!--]-->
+        <!--if--><span></span></div>"
+      `,
+      )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "<div><span></span>
+        <!--[--><!--]-->
+        <div>true</div>-true-<!--if-->
+        <!--[--><!--]-->
+        <div>true</div>-true-<!--if--><span></span></div>"
+      `)
+    })
 
-  //   expect(toggle.value).toBe(false)
+    test('on dynamic component with insertion anchor', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <component :is="components.Child" v-if="data"/>
+            <span/>
+          </div>
+        </template>`,
+        { Child: `<template>foo</template>` },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span>foo<!--dynamic-component--><!--if--><span></span></div>"`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span><!--if--><span></span></div>"`,
+      )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span></span>foo<!--dynamic-component--><!--if--><span></span></div>"`,
+      )
+    })
+  })
 
-  //   // should be hydrated now
-  //   expect(container.innerHTML).toMatchInlineSnapshot(
-  //     `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
-  //   )
-  // })
+  describe('for', () => {
+    test('basic v-for', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <span v-for="item in data" :key="item">{{ item }}</span>
+        </template>`,
+        undefined,
+        ref(['a', 'b', 'c']),
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        "
+      `,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
+        "
+      `,
+      )
+    })
 
-  // test('hydrate safely when property used by deep nested async setup changed before render', async () => {
-  //   const toggle = ref(true)
-
-  //   const AsyncComp = {
-  //     async setup() {
-  //       await new Promise<void>(r => setTimeout(r, 10))
-  //       return () => h('h1', 'Async component')
-  //     },
-  //   }
-
-  //   const AsyncWrapper = { render: () => h(AsyncComp) }
-  //   const AsyncWrapperWrapper = { render: () => h(AsyncWrapper) }
-
-  //   const SiblingComp = {
-  //     setup() {
-  //       toggle.value = false
-  //       return () => h('span')
-  //     },
-  //   }
-
-  //   const App = {
-  //     setup() {
-  //       return () =>
-  //         h(
-  //           Suspense,
-  //           {},
-  //           {
-  //             default: () => [
-  //               h('main', {}, [
-  //                 h(AsyncWrapperWrapper, {
-  //                   prop: toggle.value ? 'hello' : 'world',
-  //                 }),
-  //                 h(SiblingComp),
-  //               ]),
-  //             ],
-  //           },
-  //         )
-  //     },
-  //   }
-
-  //   // server render
-  //   const html = await renderToString(h(App))
-
-  //   expect(html).toMatchInlineSnapshot(
-  //     `"<main><h1 prop="hello">Async component</h1><span></span></main>"`,
-  //   )
+    test('empty v-for', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <span v-for="item in data" :key="item">{{ item }}</span>
+        </template>`,
+        undefined,
+        ref([]),
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><!--]-->
+        "
+      `,
+      )
+
+      data.value.push('a')
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>a</span><!--]-->
+        "
+      `,
+      )
+    })
 
-  //   expect(toggle.value).toBe(false)
+    test('v-for with insertion parent + sibling component', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <span v-for="item in data" :key="item">{{ item }}</span>
+          </div>
+          <components.Child/>
+        </template>`,
+        {
+          Child: `<template><div>{{data.length}}</div></template>`,
+        },
+        ref(['a', 'b', 'c']),
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div>
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        </div><div>3</div><!--]-->
+        "
+      `,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div>
+        <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
+        </div><div>4</div><!--]-->
+        "
+      `,
+      )
+    })
 
-  //   // hydration
+    test('v-for with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <span v-for="item in data" :key="item">{{ item }}</span>
+            <span/>
+          </div>
+        </template>`,
+        undefined,
+        ref(['a', 'b', 'c']),
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.value.splice(0, 1)
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><span>b</span><span>c</span><span>d</span><!--]-->
+        <span></span></div>"
+      `,
+      )
+    })
 
-  //   // reset the value
-  //   toggle.value = true
-  //   expect(toggle.value).toBe(true)
+    test('consecutive v-for with insertion anchor', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <span v-for="item in data" :key="item">{{ item }}</span>
+            <span v-for="item in data" :key="item">{{ item }}</span>
+            <span/>
+          </div>
+        </template>`,
+        undefined,
+        ref(['a', 'b', 'c']),
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
+        <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.value.splice(0, 2)
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><span>c</span><span>d</span><!--]-->
+        <!--[--><span>c</span><span>d</span><!--]-->
+        <span></span></div>"
+      `,
+      )
+    })
 
-  //   const container = document.createElement('div')
-  //   container.innerHTML = html
-  //   createSSRApp(App).mount(container)
+    test('v-for on component', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <components.Child v-for="item in data" :key="item"/>
+          </div>
+        </template>`,
+        {
+          Child: `<template><div>comp</div></template>`,
+        },
+        ref(['a', 'b', 'c']),
+      )
+
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><div>comp</div><div>comp</div><div>comp</div><!--]-->
+        </div>"
+      `,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><div>comp</div><div>comp</div><div>comp</div><div>comp</div><!--]-->
+        </div>"
+      `,
+      )
+    })
 
-  //   await new Promise(r => setTimeout(r, 10))
+    test('v-for on component with slots', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <components.Child v-for="item in data" :key="item">
+              <span>{{ item }}</span>
+            </components.Child>
+          </div>
+        </template>`,
+        {
+          Child: `<template><slot/></template>`,
+        },
+        ref(['a', 'b', 'c']),
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[-->
+        <!--[--><span>a</span><!--]-->
+        <!--[--><span>b</span><!--]-->
+        <!--[--><span>c</span><!--]-->
+        <!--]-->
+        </div>"
+      `,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[-->
+        <!--[--><span>a</span><!--]-->
+        <!--[--><span>b</span><!--]-->
+        <!--[--><span>c</span><!--]-->
+        <span>d</span><!--slot--><!--]-->
+        </div>"
+      `,
+      )
+    })
 
-  //   expect(toggle.value).toBe(false)
+    test('on fragment component', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <components.Child v-for="item in data" :key="item"/>
+          </div>
+        </template>`,
+        {
+          Child: `<template><div>foo</div>-bar-</template>`,
+        },
+        ref(['a', 'b', 'c']),
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[-->
+        <!--[--><div>foo</div>-bar-<!--]-->
+        <!--[--><div>foo</div>-bar-<!--]-->
+        <!--[--><div>foo</div>-bar-<!--]-->
+        <!--]-->
+        </div>"
+      `,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[-->
+        <!--[--><div>foo</div>-bar-<!--]-->
+        <!--[--><div>foo</div>-bar-<!--]-->
+        <!--[--><div>foo</div>-bar-<!--]-->
+        <div>foo</div>-bar-<!--]-->
+        </div>"
+      `,
+      )
+    })
 
-  //   // should be hydrated now
-  //   expect(container.innerHTML).toMatchInlineSnapshot(
-  //     `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
-  //   )
-  // })
+    test('on component with non-hydration node', async () => {
+      const data = ref({ show: true, msg: 'foo' })
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <components.Child v-for="item in 2" :key="item"/>
+          </div>
+        </template>`,
+        {
+          Child: `<template>
+            <div>
+              <div>
+                <div v-if="data.show">{{ data.msg }}</div>
+              </div>
+              <span>non-hydration node</span>
+            </div>
+          </template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><!--]-->
+        </div>"
+      `,
+      )
+
+      data.value.msg = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--]-->
+        </div>"
+      `,
+      )
+
+      data.value.show = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "<div>
+        <!--[--><div><div><!--if--></div><span>non-hydration node</span></div><div><div><!--if--></div><span>non-hydration node</span></div><!--]-->
+        </div>"
+      `)
+
+      data.value.show = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "<div>
+        <!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--]-->
+        </div>"
+      `)
+    })
 
-  // // #3787
-  // test('unmount async wrapper before load', async () => {
-  //   let resolve: any
-  //   const AsyncComp = defineAsyncComponent(
-  //     () =>
-  //       new Promise(r => {
-  //         resolve = r
-  //       }),
-  //   )
+    test('with non-hydration node', async () => {
+      const data = ref({ show: true, msg: 'foo' })
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <div v-for="item in 2">
+              <div>
+                <div v-if="data.show">{{ data.msg }}</div>
+              </div>
+              <span>non-hydration node</span>
+            </div>
+          </div>
+        </template>`,
+        {},
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><div><div><div>foo</div><!--if--></div><span>non-hydration node</span></div><!--]-->
+        </div>"
+      `,
+      )
+
+      data.value.msg = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--]-->
+        </div>"
+      `,
+      )
+
+      data.value.show = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "<div>
+        <!--[--><div><div><!--if--></div><span>non-hydration node</span></div><div><div><!--if--></div><span>non-hydration node</span></div><!--]-->
+        </div>"
+      `)
+
+      data.value.show = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "<div>
+        <!--[--><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><div><div><div>bar</div><!--if--></div><span>non-hydration node</span></div><!--]-->
+        </div>"
+      `)
+    })
+  })
 
-  //   const show = ref(true)
-  //   const root = document.createElement('div')
-  //   root.innerHTML = '<div><div>async</div></div>'
+  describe('slots', () => {
+    test('basic slot', async () => {
+      const { data, container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span>{{data}}</span>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><slot/></template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>foo</span><!--]-->
+        "
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>bar</span><!--]-->
+        "
+      `,
+      )
+    })
 
-  //   createSSRApp({
-  //     render() {
-  //       return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
-  //     },
-  //   }).mount(root)
+    test('named slot', async () => {
+      const { data, container } = await testHydration(
+        `<template>
+          <components.Child>
+            <template #foo>
+              <span>{{data}}</span>
+            </template>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><slot/><slot name="foo"/></template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><!--]-->
+        <!--[--><span>foo</span><!--]-->
+        <!--]-->
+        "
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><!--]-->
+        <!--[--><span>bar</span><!--]-->
+        <!--]-->
+        "
+      `,
+      )
+    })
 
-  //   show.value = false
-  //   await nextTick()
-  //   expect(root.innerHTML).toBe('<div><div>hi</div></div>')
-  //   resolve({})
-  // })
+    test('named slot with v-if', async () => {
+      const { data, container } = await testHydration(
+        `<template>
+          <components.Child>
+            <template #foo v-if="data">
+              <span>{{data}}</span>
+            </template>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><slot name="foo"/></template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>foo</span><!--]-->
+        "
+      `,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><!--]-->
+        "
+      `,
+      )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "
+        <!--[--><span>true</span><!--]-->
+        "
+      `)
+    })
 
-  // //#12362
-  // test('nested async wrapper', async () => {
-  //   const Toggle = defineAsyncComponent(
-  //     () =>
-  //       new Promise(r => {
-  //         r(
-  //           defineComponent({
-  //             setup(_, { slots }) {
-  //               const show = ref(false)
-  //               onMounted(() => {
-  //                 nextTick(() => {
-  //                   show.value = true
-  //                 })
-  //               })
-  //               return () =>
-  //                 withDirectives(
-  //                   h('div', null, [renderSlot(slots, 'default')]),
-  //                   [[vShow, show.value]],
-  //                 )
-  //             },
-  //           }) as any,
-  //         )
-  //       }),
-  //   )
+    test('named slot with v-if and v-for', async () => {
+      const data = reactive({
+        show: true,
+        items: ['a', 'b', 'c'],
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <template #foo v-if="data.show">
+              <span v-for="item in data.items" :key="item">{{item}}</span>
+            </template>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><slot name="foo"/></template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <!--]-->
+        "
+      `,
+      )
+
+      data.show = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><!--]-->
+        "
+      `,
+      )
+
+      data.show = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><span>a</span><span>b</span><span>c</span><!--for--><!--]-->
+        "
+      `,
+      )
+    })
 
-  //   const Wrapper = defineAsyncComponent(() => {
-  //     return new Promise(r => {
-  //       r(
-  //         defineComponent({
-  //           render(this: any) {
-  //             return renderSlot(this.$slots, 'default')
-  //           },
-  //         }) as any,
-  //       )
-  //     })
-  //   })
-
-  //   const count = ref(0)
-  //   const fn = vi.fn()
-  //   const Child = {
-  //     setup() {
-  //       onMounted(() => {
-  //         fn()
-  //         count.value++
-  //       })
-  //       return () => h('div', count.value)
-  //     },
-  //   }
-
-  //   const App = {
-  //     render() {
-  //       return h(Toggle, null, {
-  //         default: () =>
-  //           h(Wrapper, null, {
-  //             default: () =>
-  //               h(Wrapper, null, {
-  //                 default: () => h(Child),
-  //               }),
-  //           }),
-  //       })
-  //     },
-  //   }
-
-  //   const root = document.createElement('div')
-  //   root.innerHTML = await renderToString(h(App))
-  //   expect(root.innerHTML).toMatchInlineSnapshot(
-  //     `"<div style="display:none;"><!--[--><!--[--><!--[--><div>0</div><!--]--><!--]--><!--]--></div>"`,
-  //   )
+    test('with insertion anchor', async () => {
+      const { data, container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span/>
+            <span>{{data}}</span>
+            <span/>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><slot/></template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span></span><span>foo</span><span></span><!--]-->
+        "
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span></span><span>bar</span><span></span><!--]-->
+        "
+      `,
+      )
+    })
 
-  //   createSSRApp(App).mount(root)
-  //   await nextTick()
-  //   await nextTick()
-  //   expect(root.innerHTML).toMatchInlineSnapshot(
-  //     `"<div style=""><!--[--><!--[--><!--[--><div>1</div><!--]--><!--]--><!--]--></div>"`,
-  //   )
-  //   expect(fn).toBeCalledTimes(1)
-  // })
+    test('with multi level anchor insertion', async () => {
+      const { data, container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span/>
+            <span>{{data}}</span>
+            <span/>
+          </components.Child>
+        </template>`,
+        {
+          Child: `
+          <template>
+            <div/>
+              <div/>
+              <slot/>
+              <div/>
+            </div>
+          </template>`,
+        },
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div></div><div></div>
+        <!--[--><span></span><span>foo</span><span></span><!--]-->
+        <div></div><!--]-->
+        "
+      `,
+      )
+
+      data.value = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div></div><div></div>
+        <!--[--><span></span><span>bar</span><span></span><!--]-->
+        <div></div><!--]-->
+        "
+      `,
+      )
+    })
 
-  // test('unmount async wrapper before load (fragment)', async () => {
-  //   let resolve: any
-  //   const AsyncComp = defineAsyncComponent(
-  //     () =>
-  //       new Promise(r => {
-  //         resolve = r
-  //       }),
-  //   )
+    test('mixed slot and text node', async () => {
+      const data = reactive({
+        text: 'foo',
+        msg: 'hi',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span>{{data.text}}</span>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><div><slot/>{{data.msg}}</div></template>`,
+        },
+        data,
+      )
+
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><span>foo</span><!--]-->
+        hi</div>"
+      `,
+      )
+
+      data.msg = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><span>foo</span><!--]-->
+        bar</div>"
+      `,
+      )
+    })
 
-  //   const show = ref(true)
-  //   const root = document.createElement('div')
-  //   root.innerHTML = '<div><!--[-->async<!--]--></div>'
+    test('mixed root slot and text node', async () => {
+      const data = reactive({
+        text: 'foo',
+        msg: 'hi',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span>{{data.text}}</span>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template>{{data.text}}<slot/>{{data.msg}}</template>`,
+        },
+        data,
+      )
+
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->foo
+        <!--[--><span>foo</span><!--]-->
+        hi<!--]-->
+        "
+      `,
+      )
+
+      data.msg = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->foo
+        <!--[--><span>foo</span><!--]-->
+        bar<!--]-->
+        "
+      `,
+      )
+    })
 
-  //   createSSRApp({
-  //     render() {
-  //       return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
-  //     },
-  //   }).mount(root)
+    test('mixed consecutive slot and element', async () => {
+      const data = reactive({
+        text: 'foo',
+        msg: 'hi',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <template #foo><span>{{data.text}}</span></template>
+            <template #bar><span>bar</span></template>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><div><slot name="foo"/><slot name="bar"/><div>{{data.msg}}</div></div></template>`,
+        },
+        data,
+      )
+
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><span>foo</span><!--]-->
+        <!--[--><span>bar</span><!--]-->
+        <div>hi</div></div>"
+      `,
+      )
+
+      data.msg = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><span>foo</span><!--]-->
+        <!--[--><span>bar</span><!--]-->
+        <div>bar</div></div>"
+      `,
+      )
+    })
 
-  //   show.value = false
-  //   await nextTick()
-  //   expect(root.innerHTML).toBe('<div><div>hi</div></div>')
-  //   resolve({})
-  // })
+    test('mixed slot and element', async () => {
+      const data = reactive({
+        text: 'foo',
+        msg: 'hi',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <span>{{data.text}}</span>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><div><slot/><div>{{data.msg}}</div></div></template>`,
+        },
+        data,
+      )
+
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><span>foo</span><!--]-->
+        <div>hi</div></div>"
+      `,
+      )
+
+      data.msg = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><span>foo</span><!--]-->
+        <div>bar</div></div>"
+      `,
+      )
+    })
 
-  // test('elements with camel-case in svg ', () => {
-  //   const { vnode, container } = mountWithHydration(
-  //     '<animateTransform></animateTransform>',
-  //     () => h('animateTransform'),
-  //   )
-  //   expect(vnode.el).toBe(container.firstChild)
-  //   expect(`Hydration node mismatch`).not.toHaveBeenWarned()
-  // })
+    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(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><div>bar</div>
+        <!--[--><span>foo</span><!--]-->
+        <div>bar</div></div>"
+      `,
+      )
+
+      data.msg2 = 'hello'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><div>hello</div>
+        <!--[--><span>foo</span><!--]-->
+        <div>hello</div></div>"
+      `,
+      )
+    })
 
-  // test('SVG as a mount container', () => {
-  //   const svgContainer = document.createElement('svg')
-  //   svgContainer.innerHTML = '<g></g>'
-  //   const app = createSSRApp({
-  //     render: () => h('g'),
-  //   })
-
-  //   expect(
-  //     (
-  //       app.mount(svgContainer).$.subTree as VNode<Node, Element> & {
-  //         el: Element
-  //       }
-  //     ).el instanceof SVGElement,
+    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(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><div>foo</div> bar<!--]-->
+        <!--[--><span>foo</span><!--]-->
+        <!--[--><div>foo</div> bar<!--]-->
+        </div>"
+      `,
+      )
+
+      data.msg1 = 'hello'
+      data.msg2 = 'vapor'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><div>hello</div> vapor<!--]-->
+        <!--[--><span>hello</span><!--]-->
+        <!--[--><div>hello</div> vapor<!--]-->
+        </div>"
+      `,
+      )
+    })
+
+    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(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div>foo</div><!--if-->
+        <!--[--><span>foo</span><!--]-->
+        <div>foo</div><!--if--><!--]-->
+        "
+      `,
+      )
+
+      data.show = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><!--if-->
+        <!--[--><span>foo</span><!--]-->
+        <!--if--><!--]-->
+        "
+      `,
+      )
+
+      data.show = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "
+        <!--[--><div>foo</div><!--if-->
+        <!--[--><span>foo</span><!--]-->
+        <div>foo</div><!--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(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><div>a</div><div>b</div><div>c</div><!--]-->
+        <!--[--><span>foo</span><!--]-->
+        <!--[--><div>a</div><div>b</div><div>c</div><!--]-->
+        <!--]-->
+        "
+      `,
+      )
+
+      data.items.push('d')
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--]-->
+        <!--[--><span>foo</span><!--]-->
+        <!--[--><div>a</div><div>b</div><div>c</div><div>d</div><!--]-->
+        <!--]-->
+        "
+      `,
+      )
+    })
+
+    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(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><span>foo</span><!--]-->
+        <!--[--><span>bar</span><!--]-->
+        <!--]-->
+        "
+      `,
+      )
+
+      data.msg1 = 'hello'
+      data.msg2 = 'vapor'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><span>hello</span><!--]-->
+        <!--[--><span>vapor</span><!--]-->
+        <!--]-->
+        "
+      `,
+      )
+    })
+
+    test('consecutive slots with insertion anchor', 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(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><span>foo</span><!--]-->
+        <!--[--><span>bar</span><!--]-->
+        <span></span></div>"
+      `,
+      )
+
+      data.msg1 = 'hello'
+      data.msg2 = 'vapor'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[--><span>hello</span><!--]-->
+        <!--[--><span>vapor</span><!--]-->
+        <span></span></div>"
+      `,
+      )
+    })
+
+    test('consecutive slots prepend', async () => {
+      const data = reactive({
+        msg1: 'foo',
+        msg2: 'bar',
+        msg3: 'baz',
+      })
+
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <template #foo>
+              <span>{{data.msg1}}</span>
+            </template>
+            <template #bar>
+              <span>{{data.msg2}}</span>
+            </template>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template>
+            <div>
+              <slot name="foo"/>
+              <slot name="bar"/>
+              <div>{{data.msg3}}</div>
+            </div>
+          </template>`,
+        },
+        data,
+      )
+
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><span>foo</span><!--]-->
+        <!--[--><span>bar</span><!--]-->
+        <div>baz</div></div>"
+      `,
+      )
+
+      data.msg1 = 'hello'
+      data.msg2 = 'vapor'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><span>hello</span><!--]-->
+        <!--[--><span>vapor</span><!--]-->
+        <div>baz</div></div>"
+      `,
+      )
+    })
+
+    test('slot fallback', async () => {
+      const data = reactive({
+        foo: 'foo',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><slot><span>{{data.foo}}</span></slot></template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>foo</span><!--]-->
+        "
+      `,
+      )
+
+      data.foo = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>bar</span><!--]-->
+        "
+      `,
+      )
+    })
+
+    test('forwarded slot', async () => {
+      const data = reactive({
+        foo: 'foo',
+        bar: 'bar',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <components.Parent>
+              <span>{{data.foo}}</span>
+            </components.Parent>
+            <div>{{data.bar}}</div>
+          </div>
+        </template>`,
+        {
+          Parent: `<template><div><components.Child><slot/></components.Child></div></template>`,
+          Child: `<template><div><slot/></div></template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><div><div>
+        <!--[-->
+        <!--[--><span>foo</span><!--]-->
+        <!--]-->
+        </div></div><div>bar</div></div>"
+      `,
+      )
+
+      data.foo = 'foo1'
+      data.bar = 'bar1'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><div><div>
+        <!--[-->
+        <!--[--><span>foo1</span><!--]-->
+        <!--]-->
+        </div></div><div>bar1</div></div>"
+      `,
+      )
+    })
+
+    test('forwarded slot with fallback', async () => {
+      const data = reactive({
+        foo: 'foo',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Parent/>
+        </template>`,
+        {
+          Parent: `<template><components.Child><slot/></components.Child></template>`,
+          Child: `<template><div><slot>{{data.foo}}</slot></div></template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[-->foo<!--]-->
+        </div>"
+      `,
+      )
+
+      data.foo = 'foo1'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[-->foo1<!--]-->
+        </div>"
+      `,
+      )
+    })
+
+    test('forwarded slot with empty content', async () => {
+      const data = reactive({
+        foo: 'foo',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Foo/>
+        </template>`,
+        {
+          Foo: `<template>
+                  <components.Bar>
+                    <template #foo>
+                      <slot name="foo" />
+                    </template>
+                  </components.Bar>
+                </template>`,
+          Bar: `<template>
+                  <components.Baz>
+                    <template #foo>
+                      <slot name="foo" />
+                    </template>
+                  </components.Baz>
+                </template>`,
+          Baz: `<template>
+                  <components.Qux>
+                    <template #foo>
+                      <slot name="foo" />
+                    </template>
+                  </components.Qux>
+                </template>`,
+          Qux: `<template>
+                  <div>
+                    <slot name="foo" />
+                    <div>{{data.foo}}</div>
+                  </div>
+                </template>`,
+        },
+        data,
+      )
+
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><!--]-->
+        <div>foo</div></div>"
+      `,
+      )
+
+      data.foo = 'bar'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[--><!--]-->
+        <div>bar</div></div>"
+      `,
+      )
+    })
+  })
+
+  describe.todo('transition', async () => {
+    test('transition appear', async () => {
+      const { container } = await testHydration(
+        `<template>
+          <transition appear>
+            <div>foo</div>
+          </transition>
+        </template>`,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div style="" class="v-enter-from v-enter-active">foo</div>"`,
+      )
+      expect(`mismatch`).not.toHaveBeenWarned()
+    })
+
+    test('transition appear work with pre-existing class', async () => {
+      const { container } = await testHydration(
+        `<template>
+          <transition appear>
+            <div class="foo">foo</div>
+          </transition>
+        </template>`,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div class="foo v-enter-from v-enter-active" style="">foo</div>"`,
+      )
+      expect(`mismatch`).not.toHaveBeenWarned()
+    })
+
+    test('transition appear work with empty content', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <transition appear>
+            <slot v-if="data"></slot>
+            <span v-else>foo</span>
+          </transition>
+        </template>`,
+        undefined,
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--slot--><!--if-->"`,
+      )
+      expect(`mismatch`).not.toHaveBeenWarned()
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<span class="v-enter-from v-enter-active">foo</span><!--if-->"`,
+      )
+    })
+
+    test('transition appear with v-if', async () => {
+      const data = ref(false)
+      const { container } = await testHydration(
+        `<template>
+          <transition appear>
+            <div v-if="data">foo</div>
+          </transition>
+        </template>`,
+        undefined,
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--if-->"`,
+      )
+      expect(`mismatch`).not.toHaveBeenWarned()
+    })
+
+    test('transition appear with v-show', async () => {
+      const data = ref(false)
+      const { container } = await testHydration(
+        `<template>
+          <transition appear>
+            <div v-show="data">foo</div>
+          </transition>
+        </template>`,
+        undefined,
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div style="display:none;" class="v-enter-from v-enter-active v-leave-from v-leave-active">foo</div>"`,
+      )
+      expect(`mismatch`).not.toHaveBeenWarned()
+    })
+
+    test('transition appear w/ event listener', async () => {
+      const { container } = await testHydration(
+        `<script setup>
+          import { ref } from 'vue'
+          const count = ref(0)
+        </script>
+        <template>
+          <transition appear>
+            <button @click="count++">{{ count }}</button>
+          </transition>
+        </template>`,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<button style="" class="v-enter-from v-enter-active">0</button>"`,
+      )
+
+      triggerEvent('click', container.querySelector('button')!)
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<button style="" class="v-enter-from v-enter-active">1</button>"`,
+      )
+    })
+  })
+
+  describe('force hydrate prop', async () => {
+    test('force hydrate prop with `.prop` modifier', async () => {
+      const { container } = await mountWithHydration(
+        '<input type="checkbox">',
+        `<input type="checkbox" .indeterminate="true"/>`,
+      )
+      expect((container.firstChild! as any).indeterminate).toBe(true)
+    })
+
+    test('force hydrate input v-model with non-string value bindings', async () => {
+      const { container } = await mountWithHydration(
+        '<input type="checkbox" value="true">',
+        `<input type="checkbox" :true-value="true"/>`,
+      )
+      expect((container.firstChild as any)._trueValue).toBe(true)
+    })
+
+    test('force hydrate checkbox with indeterminate', async () => {
+      const { container } = await mountWithHydration(
+        '<input type="checkbox" indeterminate/>',
+        `<input type="checkbox" :indeterminate="true"/>`,
+      )
+      expect((container.firstChild! as any).indeterminate).toBe(true)
+    })
+
+    test('force hydrate select option with non-string value bindings', async () => {
+      const { container } = await mountWithHydration(
+        '<select><option value="true">ok</option></select>',
+        `<select><option :value="true">ok</option></select>`,
+      )
+      expect((container.firstChild!.firstChild as any)._value).toBe(true)
+    })
+
+    test('force hydrate v-bind with .prop modifiers', async () => {
+      const { container } = await mountWithHydration(
+        '<div .foo="true"/>',
+        `<div v-bind="data"/>`,
+        ref({ '.foo': true }),
+      )
+      expect((container.firstChild! as any).foo).toBe(true)
+    })
+
+    // vapor custom element not implemented yet
+    test.todo('force hydrate custom element with dynamic props', () => {})
+  })
+
+  describe.todo('Suspense')
+})
+
+describe('mismatch handling', () => {
+  test('text node', async () => {
+    const foo = ref('bar')
+    const { container } = await mountWithHydration(`foo`, `{{data}}`, foo)
+    expect(container.textContent).toBe('bar')
+    expect(`Hydration text mismatch`).toHaveBeenWarned()
+  })
+
+  test('element text content', async () => {
+    const data = ref({ textContent: 'bar' })
+    const { container } = await mountWithHydration(
+      `<div>foo</div>`,
+      `<div v-bind="data"></div>`,
+      data,
+    )
+    expect(container.innerHTML).toBe('<div>bar</div>')
+    expect(`Hydration text content mismatch`).toHaveBeenWarned()
+  })
+
+  // test('not enough children', () => {
+  //   const { container } = mountWithHydration(`<div></div>`, () =>
+  //     h('div', [h('span', 'foo'), h('span', 'bar')]),
+  //   )
+  //   expect(container.innerHTML).toBe(
+  //     '<div><span>foo</span><span>bar</span></div>',
   //   )
+  //   expect(`Hydration children mismatch`).toHaveBeenWarned()
   // })
-
-  // test('force hydrate prop with `.prop` modifier', () => {
-  //   const { container } = mountWithHydration('<input type="checkbox">', () =>
-  //     h('input', {
-  //       type: 'checkbox',
-  //       '.indeterminate': true,
-  //     }),
+  // test('too many children', () => {
+  //   const { container } = mountWithHydration(
+  //     `<div><span>foo</span><span>bar</span></div>`,
+  //     () => h('div', [h('span', 'foo')]),
   //   )
-  //   expect((container.firstChild! as any).indeterminate).toBe(true)
+  //   expect(container.innerHTML).toBe('<div><span>foo</span></div>')
+  //   expect(`Hydration children mismatch`).toHaveBeenWarned()
   // })
-
-  // test('force hydrate input v-model with non-string value bindings', () => {
+  test('complete mismatch', async () => {
+    const data = ref('span')
+    const { container } = await mountWithHydration(
+      `<div>foo</div>`,
+      `<component :is="data">foo</component>`,
+      data,
+    )
+    expect(container.innerHTML).toBe('<span>foo</span><!--dynamic-component-->')
+    expect(`Hydration node mismatch`).toHaveBeenWarned()
+  })
+  // test('fragment mismatch removal', () => {
   //   const { container } = mountWithHydration(
-  //     '<input type="checkbox" value="true">',
-  //     () =>
-  //       withDirectives(
-  //         createVNode(
-  //           'input',
-  //           { type: 'checkbox', 'true-value': true },
-  //           null,
-  //           PatchFlags.PROPS,
-  //           ['true-value'],
-  //         ),
-  //         [[vModelCheckbox, true]],
-  //       ),
+  //     `<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
+  //     () => h('div', [h('span', 'replaced')]),
   //   )
-  //   expect((container.firstChild as any)._trueValue).toBe(true)
+  //   expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
+  //   expect(`Hydration node mismatch`).toHaveBeenWarned()
   // })
-
-  // test('force hydrate checkbox with indeterminate', () => {
+  // test('fragment not enough children', () => {
   //   const { container } = mountWithHydration(
-  //     '<input type="checkbox" indeterminate>',
-  //     () =>
-  //       createVNode(
-  //         'input',
-  //         { type: 'checkbox', indeterminate: '' },
-  //         null,
-  //         PatchFlags.CACHED,
-  //       ),
+  //     `<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
+  //     () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
   //   )
-  //   expect((container.firstChild as any).indeterminate).toBe(true)
+  //   expect(container.innerHTML).toBe(
+  //     '<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
+  //   )
+  //   expect(`Hydration node mismatch`).toHaveBeenWarned()
   // })
-
-  // test('force hydrate select option with non-string value bindings', () => {
+  // test('fragment too many children', () => {
   //   const { container } = mountWithHydration(
-  //     '<select><option value="true">ok</option></select>',
-  //     () =>
-  //       h('select', [
-  //         // hoisted because bound value is a constant...
-  //         createVNode('option', { value: true }, null, -1 /* HOISTED */),
-  //       ]),
+  //     `<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
+  //     () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
   //   )
-  //   expect((container.firstChild!.firstChild as any)._value).toBe(true)
+  //   expect(container.innerHTML).toBe(
+  //     '<div><!--[--><div>foo</div><!--]--><div>baz</div></div>',
+  //   )
+  //   // fragment ends early and attempts to hydrate the extra <div>bar</div>
+  //   // as 2nd fragment child.
+  //   expect(`Hydration text content mismatch`).toHaveBeenWarned()
+  //   // excessive children removal
+  //   expect(`Hydration children mismatch`).toHaveBeenWarned()
   // })
-
-  // // #7203
-  // test('force hydrate custom element with dynamic props', () => {
-  //   class MyElement extends HTMLElement {
-  //     foo = ''
-  //     constructor() {
-  //       super()
-  //     }
-  //   }
-  //   customElements.define('my-element-7203', MyElement)
-
-  //   const msg = ref('bar')
-  //   const container = document.createElement('div')
-  //   container.innerHTML = '<my-element-7203></my-element-7203>'
-  //   const app = createSSRApp({
-  //     render: () => h('my-element-7203', { foo: msg.value }),
-  //   })
-  //   app.mount(container)
-  //   expect((container.firstChild as any).foo).toBe(msg.value)
+  // test('Teleport target has empty children', () => {
+  //   const teleportContainer = document.createElement('div')
+  //   teleportContainer.id = 'teleport'
+  //   document.body.appendChild(teleportContainer)
+  //   mountWithHydration('<!--teleport start--><!--teleport end-->', () =>
+  //     h(Teleport, { to: '#teleport' }, [h('span', 'value')]),
+  //   )
+  //   expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
+  //   expect(`Hydration children mismatch`).toHaveBeenWarned()
   // })
-
-  // // #5728
-  // test('empty text node in slot', () => {
-  //   const Comp = {
-  //     render(this: any) {
-  //       return renderSlot(this.$slots, 'default', {}, () => [
-  //         createTextVNode(''),
-  //       ])
-  //     },
-  //   }
-  //   const { container, vnode } = mountWithHydration('<!--[--><!--]-->', () =>
-  //     h(Comp),
+  // test('comment mismatch (element)', () => {
+  //   const { container } = mountWithHydration(`<div><span></span></div>`, () =>
+  //     h('div', [createCommentVNode('hi')]),
   //   )
-  //   expect(container.childNodes.length).toBe(3)
-  //   const text = container.childNodes[1]
-  //   expect(text.nodeType).toBe(3)
-  //   expect(vnode.el).toBe(container.childNodes[0])
-  //   // component => slot fragment => text node
-  //   expect((vnode as any).component?.subTree.children[0].el).toBe(text)
+  //   expect(container.innerHTML).toBe('<div><!--hi--></div>')
+  //   expect(`Hydration node mismatch`).toHaveBeenWarned()
   // })
-
-  // // #7215
-  // test('empty text node', () => {
-  //   const Comp = {
-  //     render(this: any) {
-  //       return h('p', [''])
-  //     },
-  //   }
-  //   const { container } = mountWithHydration('<p></p>', () => h(Comp))
-  //   expect(container.childNodes.length).toBe(1)
-  //   const p = container.childNodes[0]
-  //   expect(p.childNodes.length).toBe(1)
-  //   const text = p.childNodes[0]
-  //   expect(text.nodeType).toBe(3)
+  // test('comment mismatch (text)', () => {
+  //   const { container } = mountWithHydration(`<div>foobar</div>`, () =>
+  //     h('div', [createCommentVNode('hi')]),
+  //   )
+  //   expect(container.innerHTML).toBe('<div><!--hi--></div>')
+  //   expect(`Hydration node mismatch`).toHaveBeenWarned()
   // })
+  test('class mismatch', async () => {
+    await mountWithHydration(
+      `<div class="foo bar"></div>`,
+      `<div :class="data"></div>`,
+      ref(['foo', 'bar']),
+    )
 
-  // // #11372
-  // test('object style value tracking in prod', async () => {
-  //   __DEV__ = false
-  //   try {
-  //     const style = reactive({ color: 'red' })
-  //     const Comp = {
-  //       render(this: any) {
-  //         return (
-  //           openBlock(),
-  //           createElementBlock(
-  //             'div',
-  //             {
-  //               style: normalizeStyle(style),
-  //             },
-  //             null,
-  //             4 /* STYLE */,
-  //           )
-  //         )
-  //       },
-  //     }
-  //     const { container } = mountWithHydration(
-  //       `<div style="color: red;"></div>`,
-  //       () => h(Comp),
-  //     )
-  //     style.color = 'green'
-  //     await nextTick()
-  //     expect(container.innerHTML).toBe(`<div style="color: green;"></div>`)
-  //   } finally {
-  //     __DEV__ = true
-  //   }
-  // })
+    await mountWithHydration(
+      `<div class="foo bar"></div>`,
+      `<div :class="data"></div>`,
+      ref({ foo: true, bar: true }),
+    )
 
-  // test('app.unmount()', async () => {
-  //   const container = document.createElement('DIV')
-  //   container.innerHTML = '<button></button>'
-  //   const App = defineComponent({
-  //     setup(_, { expose }) {
-  //       const count = ref(0)
-
-  //       expose({ count })
-
-  //       return () =>
-  //         h('button', {
-  //           onClick: () => count.value++,
-  //         })
-  //     },
-  //   })
-
-  //   const app = createSSRApp(App)
-  //   const vm = app.mount(container)
-  //   await nextTick()
-  //   expect((container as any)._vnode).toBeDefined()
-  //   // @ts-expect-error - expose()'d properties are not available on vm type
-  //   expect(vm.count).toBe(0)
-
-  //   app.unmount()
-  //   expect((container as any)._vnode).toBe(null)
-  // })
+    await mountWithHydration(
+      `<div class="foo bar"></div>`,
+      `<div :class="data"></div>`,
+      ref('foo bar'),
+    )
+
+    // svg classes
+    await mountWithHydration(
+      `<svg class="foo bar"></svg>`,
+      `<svg :class="data"></svg>`,
+      ref('foo bar'),
+    )
+
+    // class with different order
+    await mountWithHydration(
+      `<div class="foo bar"></div>`,
+      `<div :class="data"></div>`,
+      ref('bar foo'),
+    )
+    expect(`Hydration class mismatch`).not.toHaveBeenWarned()
+
+    // single root mismatch
+    const { container: root } = await mountWithHydration(
+      `<div class="foo bar"></div>`,
+      `<div :class="data"></div>`,
+      ref('baz'),
+    )
+    expect(root.innerHTML).toBe('<div class="foo bar baz"></div>')
+    expect(`Hydration class mismatch`).toHaveBeenWarned()
+
+    // multiple root mismatch
+    const { container } = await mountWithHydration(
+      `<div class="foo bar"></div><span/>`,
+      `<div :class="data"></div><span/>`,
+      ref('foo'),
+    )
+    expect(container.innerHTML).toBe('<div class="foo"></div><span></span>')
+    expect(`Hydration class mismatch`).toHaveBeenWarned()
+  })
+
+  test('style mismatch', async () => {
+    await mountWithHydration(
+      `<div style="color:red;"></div>`,
+      `<div :style="data"></div>`,
+      ref({ color: 'red' }),
+    )
+
+    await mountWithHydration(
+      `<div style="color:red;"></div>`,
+      `<div :style="data"></div>`,
+      ref('color:red;'),
+    )
 
-  // // #6637
-  // test('stringified root fragment', () => {
-  //   mountWithHydration(`<!--[--><div></div><!--]-->`, () =>
-  //     createStaticVNode(`<div></div>`, 1),
+    // style with different order
+    await mountWithHydration(
+      `<div style="color:red; font-size: 12px;"></div>`,
+      `<div :style="data"></div>`,
+      ref(`font-size: 12px; color:red;`),
+    )
+
+    expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+
+    // single root mismatch
+    const { container: root } = await mountWithHydration(
+      `<div style="color:red;"></div>`,
+      `<div :style="data"></div>`,
+      ref({ color: 'green' }),
+    )
+    expect(root.innerHTML).toBe('<div style="color: green;"></div>')
+    expect(`Hydration style mismatch`).toHaveBeenWarned()
+
+    // multiple root mismatch
+    const { container } = await mountWithHydration(
+      `<div style="color:red;"></div><span/>`,
+      `<div :style="data"></div><span/>`,
+      ref({ color: 'green' }),
+    )
+    expect(container.innerHTML).toBe(
+      '<div style="color: green;"></div><span></span>',
+    )
+    expect(`Hydration style mismatch`).toHaveBeenWarned()
+  })
+
+  test('style mismatch when no style attribute is present', async () => {
+    await mountWithHydration(
+      `<div></div>`,
+      `<div :style="data"></div>`,
+      ref({ color: 'red' }),
+    )
+    expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
+  })
+
+  test('style mismatch w/ v-show', async () => {
+    await mountWithHydration(
+      `<div style="color:red;display:none"></div>`,
+      `<div v-show="data" style="color: red;"></div>`,
+      ref(false),
+    )
+    expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+
+    // mismatch with single root
+    const { container: root } = await mountWithHydration(
+      `<div style="color:red;"></div>`,
+      `<div v-show="data" style="color: red;"></div>`,
+      ref(false),
+    )
+    expect(root.innerHTML).toBe(
+      '<div style="color: red; display: none;"></div>',
+    )
+    expect(`Hydration style mismatch`).toHaveBeenWarned()
+
+    // mismatch with multiple root
+    const { container } = await mountWithHydration(
+      `<div style="color:red;"></div><span/>`,
+      `<div v-show="data.show" :style="data.style"></div><span/>`,
+      ref({ show: false, style: 'color: red' }),
+    )
+    expect(container.innerHTML).toBe(
+      '<div style="color: red; display: none;"></div><span></span>',
+    )
+    expect(`Hydration style mismatch`).toHaveBeenWarned()
+  })
+
+  test('attr mismatch', async () => {
+    await mountWithHydration(
+      `<div id="foo"></div>`,
+      `<div :id="data"></div>`,
+      ref('foo'),
+    )
+
+    await mountWithHydration(
+      `<div spellcheck></div>`,
+      `<div :spellcheck="data"></div>`,
+      ref(''),
+    )
+
+    await mountWithHydration(
+      `<div></div>`,
+      `<div :id="data"></div>`,
+      ref(undefined),
+    )
+
+    // boolean
+    await mountWithHydration(
+      `<select multiple></div>`,
+      `<select :multiple="data"></select>`,
+      ref(true),
+    )
+
+    await mountWithHydration(
+      `<select multiple></div>`,
+      `<select :multiple="data"></select>`,
+      ref('multiple'),
+    )
+
+    expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+    await mountWithHydration(
+      `<div></div>`,
+      `<div :id="data"></div>`,
+      ref('foo'),
+    )
+    expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(1)
+
+    await mountWithHydration(
+      `<div id="bar"></div>`,
+      `<div :id="data"></div>`,
+      ref('foo'),
+    )
+    expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2)
+  })
+
+  test('attr special case: textarea value', async () => {
+    await mountWithHydration(
+      `<textarea>foo</textarea>`,
+      `<textarea :value="data"></textarea>`,
+      ref('foo'),
+    )
+
+    await mountWithHydration(
+      `<textarea></textarea>`,
+      `<textarea :value="data"></textarea>`,
+      ref(''),
+    )
+    expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+
+    await mountWithHydration(
+      `<textarea>foo</textarea>`,
+      `<textarea :value="data"></textarea>`,
+      ref('bar'),
+    )
+    expect(`Hydration attribute mismatch`).toHaveBeenWarned()
+  })
+
+  test('<textarea> with newlines at the beginning', async () => {
+    await mountWithHydration(
+      `<textarea>\nhello</textarea>`,
+      `<textarea :value="data"></textarea>`,
+      ref('\nhello'),
+    )
+
+    await mountWithHydration(
+      `<textarea>\nhello</textarea>`,
+      `<textarea v-text="data"></textarea>`,
+      ref('\nhello'),
+    )
+
+    await mountWithHydration(
+      `<textarea>\nhello</textarea>`,
+      `<textarea v-bind="data"></textarea>`,
+      ref({ textContent: '\nhello' }),
+    )
+    expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
+  })
+
+  test('<pre> with newlines at the beginning', async () => {
+    await mountWithHydration(`<pre>\n</pre>`, `<pre>{{data}}</pre>`, ref('\n'))
+
+    await mountWithHydration(
+      `<pre>\n</pre>`,
+      `<pre v-text="data"></pre>`,
+      ref('\n'),
+    )
+
+    await mountWithHydration(
+      `<pre>\n</pre>`,
+      `<pre v-bind="data"></pre>`,
+      ref({ textContent: '\n' }),
+    )
+    expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
+  })
+
+  test('boolean attr handling', async () => {
+    await mountWithHydration(
+      `<input />`,
+      `<input :readonly="data" />`,
+      ref(false),
+    )
+
+    await mountWithHydration(
+      `<input readonly />`,
+      `<input :readonly="data" />`,
+      ref(true),
+    )
+
+    await mountWithHydration(
+      `<input readonly="readonly" />`,
+      `<input :readonly="data" />`,
+      ref(true),
+    )
+    expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  })
+
+  test('client value is null or undefined', async () => {
+    await mountWithHydration(
+      `<div></div>`,
+      `<div :draggable="data"></div>`,
+      ref(undefined),
+    )
+    expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+    await mountWithHydration(`<input />`, `<input :type="data" />`, ref(null))
+    expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  })
+
+  test('should not warn against object values', async () => {
+    await mountWithHydration(`<input />`, `<input :from="data" />`, ref({}))
+    expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  })
+
+  test('should not warn on falsy bindings of non-property keys', async () => {
+    await mountWithHydration(
+      `<button></button>`,
+      `<button :href="data"></button>`,
+      ref(undefined),
+    )
+    expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  })
+
+  test('should not warn on non-renderable option values', async () => {
+    await mountWithHydration(
+      `<select><option>hello</option></select>`,
+      `<select><option :value="data">hello</option></select>`,
+      ref(['foo']),
+    )
+    expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  })
+
+  test.todo('should not warn css v-bind', () => {
+    // const container = document.createElement('div')
+    // container.innerHTML = `<div style="--foo:red;color:var(--foo);" />`
+    // const app = createSSRApp({
+    //   setup() {
+    //     useCssVars(() => ({
+    //       foo: 'red',
+    //     }))
+    //     return () => h('div', { style: { color: 'var(--foo)' } })
+    //   },
+    // })
+    // app.mount(container)
+    // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  })
+
+  test.todo(
+    'css vars should only be added to expected on component root dom',
+    () => {
+      // const container = document.createElement('div')
+      // container.innerHTML = `<div style="--foo:red;"><div style="color:var(--foo);" /></div>`
+      // const app = createSSRApp({
+      //   setup() {
+      //     useCssVars(() => ({
+      //       foo: 'red',
+      //     }))
+      //     return () =>
+      //       h('div', null, [h('div', { style: { color: 'var(--foo)' } })])
+      //   },
+      // })
+      // app.mount(container)
+      // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+    },
+  )
+
+  test.todo('css vars support fallthrough', () => {
+    // const container = document.createElement('div')
+    // container.innerHTML = `<div style="padding: 4px;--foo:red;"></div>`
+    // const app = createSSRApp({
+    //   setup() {
+    //     useCssVars(() => ({
+    //       foo: 'red',
+    //     }))
+    //     return () => h(Child)
+    //   },
+    // })
+    // const Child = {
+    //   setup() {
+    //     return () => h('div', { style: 'padding: 4px' })
+    //   },
+    // }
+    // app.mount(container)
+    // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  })
+
+  // vapor directive does not have a created hook
+  test('should not warn for directives that mutate DOM in created', () => {
+    // const container = document.createElement('div')
+    // container.innerHTML = `<div class="test red"></div>`
+    // const vColor: ObjectDirective = {
+    //   created(el, binding) {
+    //     el.classList.add(binding.value)
+    //   },
+    // }
+    // const app = createSSRApp({
+    //   setup() {
+    //     return () =>
+    //       withDirectives(h('div', { class: 'test' }), [[vColor, 'red']])
+    //   },
+    // })
+    // app.mount(container)
+    // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  })
+
+  test.todo('escape css var name', () => {
+    // const container = document.createElement('div')
+    // container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`
+    // const app = createSSRApp({
+    //   setup() {
+    //     useCssVars(() => ({
+    //       'foo.bar': 'red',
+    //     }))
+    //     return () => h(Child)
+    //   },
+    // })
+    // const Child = {
+    //   setup() {
+    //     return () => h('div', { style: 'padding: 4px' })
+    //   },
+    // }
+    // app.mount(container)
+    // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  })
+})
+
+describe('data-allow-mismatch', () => {
+  test('element text content', async () => {
+    const data = ref({ textContent: 'bar' })
+    const { container } = await mountWithHydration(
+      `<div data-allow-mismatch="text">foo</div>`,
+      `<div v-bind="data"></div>`,
+      data,
+    )
+    expect(container.innerHTML).toBe(
+      '<div data-allow-mismatch="text">bar</div>',
+    )
+    expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
+  })
+  // test('not enough children', () => {
+  //   const { container } = mountWithHydration(
+  //     `<div data-allow-mismatch="children"></div>`,
+  //     () => h('div', [h('span', 'foo'), h('span', 'bar')]),
+  //   )
+  //   expect(container.innerHTML).toBe(
+  //     '<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>',
   //   )
-  //   expect(`mismatch`).not.toHaveBeenWarned()
+  //   expect(`Hydration children mismatch`).not.toHaveBeenWarned()
   // })
-
-  // test('transition appear', () => {
-  //   const { vnode, container } = mountWithHydration(
-  //     `<template><div>foo</div></template>`,
-  //     () =>
-  //       h(
-  //         Transition,
-  //         { appear: true },
-  //         {
-  //           default: () => h('div', 'foo'),
-  //         },
-  //       ),
+  // test('too many children', () => {
+  //   const { container } = mountWithHydration(
+  //     `<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
+  //     () => h('div', [h('span', 'foo')]),
   //   )
-  //   expect(container.firstChild).toMatchInlineSnapshot(`
-  //     <div
-  //       class="v-enter-from v-enter-active"
-  //     >
-  //       foo
-  //     </div>
-  //   `)
-  //   expect(vnode.el).toBe(container.firstChild)
-  //   expect(`mismatch`).not.toHaveBeenWarned()
+  //   expect(container.innerHTML).toBe(
+  //     '<div data-allow-mismatch="children"><span>foo</span></div>',
+  //   )
+  //   expect(`Hydration children mismatch`).not.toHaveBeenWarned()
   // })
-
-  // test('transition appear with v-if', () => {
-  //   const show = false
-  //   const { vnode, container } = mountWithHydration(
-  //     `<template><!----></template>`,
-  //     () =>
-  //       h(
-  //         Transition,
-  //         { appear: true },
-  //         {
-  //           default: () => (show ? h('div', 'foo') : createCommentVNode('')),
-  //         },
-  //       ),
+  test('complete mismatch', async () => {
+    const { container } = await mountWithHydration(
+      `<div data-allow-mismatch="children"><div>foo</div></div>`,
+      `<div><component :is="data">foo</component></div>`,
+      ref('span'),
+    )
+    expect(container.innerHTML).toBe(
+      '<div data-allow-mismatch="children"><span>foo</span><!--dynamic-component--></div>',
+    )
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+  })
+  // test('fragment mismatch removal', () => {
+  //   const { container } = mountWithHydration(
+  //     `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
+  //     () => h('div', [h('span', 'replaced')]),
   //   )
-  //   expect(container.firstChild).toMatchInlineSnapshot('<!---->')
-  //   expect(vnode.el).toBe(container.firstChild)
-  //   expect(`mismatch`).not.toHaveBeenWarned()
+  //   expect(container.innerHTML).toBe(
+  //     '<div data-allow-mismatch="children"><span>replaced</span></div>',
+  //   )
+  //   expect(`Hydration node mismatch`).not.toHaveBeenWarned()
   // })
-
-  // test('transition appear with v-show', () => {
-  //   const show = false
-  //   const { vnode, container } = mountWithHydration(
-  //     `<template><div style="display: none;">foo</div></template>`,
-  //     () =>
-  //       h(
-  //         Transition,
-  //         { appear: true },
-  //         {
-  //           default: () =>
-  //             withDirectives(createVNode('div', null, 'foo'), [[vShow, show]]),
-  //         },
-  //       ),
+  // test('fragment not enough children', () => {
+  //   const { container } = mountWithHydration(
+  //     `<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
+  //     () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
+  //   )
+  //   expect(container.innerHTML).toBe(
+  //     '<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
   //   )
-  //   expect(container.firstChild).toMatchInlineSnapshot(`
-  //     <div
-  //       class="v-enter-from v-enter-active"
-  //       style="display: none;"
-  //     >
-  //       foo
-  //     </div>
-  //   `)
-  //   expect((container.firstChild as any)[vShowOriginalDisplay]).toBe('')
-  //   expect(vnode.el).toBe(container.firstChild)
-  //   expect(`mismatch`).not.toHaveBeenWarned()
+  //   expect(`Hydration node mismatch`).not.toHaveBeenWarned()
   // })
-
-  // test('transition appear w/ event listener', async () => {
-  //   const container = document.createElement('div')
-  //   container.innerHTML = `<template><button>0</button></template>`
-  //   createSSRApp({
-  //     data() {
-  //       return {
-  //         count: 0,
-  //       }
-  //     },
-  //     template: `
-  //       <Transition appear>
-  //         <button @click="count++">{{count}}</button>
-  //       </Transition>
-  //     `,
-  //   }).mount(container)
-
-  //   expect(container.firstChild).toMatchInlineSnapshot(`
-  //     <button
-  //       class="v-enter-from v-enter-active"
-  //     >
-  //       0
-  //     </button>
-  //   `)
-
-  //   triggerEvent('click', container.querySelector('button')!)
-  //   await nextTick()
-  //   expect(container.firstChild).toMatchInlineSnapshot(`
-  //     <button
-  //       class="v-enter-from v-enter-active"
-  //     >
-  //       1
-  //     </button>
-  //   `)
+  // test('fragment too many children', () => {
+  //   const { container } = mountWithHydration(
+  //     `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
+  //     () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
+  //   )
+  //   expect(container.innerHTML).toBe(
+  //     '<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>',
+  //   )
+  //   // fragment ends early and attempts to hydrate the extra <div>bar</div>
+  //   // as 2nd fragment child.
+  //   expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
+  //   // excessive children removal
+  //   expect(`Hydration children mismatch`).not.toHaveBeenWarned()
   // })
-
-  // test('Suspense + transition appear', async () => {
-  //   const { vnode, container } = mountWithHydration(
-  //     `<template><div>foo</div></template>`,
-  //     () =>
-  //       h(Suspense, {}, () =>
-  //         h(
-  //           Transition,
-  //           { appear: true },
-  //           {
-  //             default: () => h('div', 'foo'),
-  //           },
-  //         ),
-  //       ),
+  // test('comment mismatch (element)', () => {
+  //   const { container } = mountWithHydration(
+  //     `<div data-allow-mismatch="children"><span></span></div>`,
+  //     () => h('div', [createCommentVNode('hi')]),
   //   )
-
-  //   expect(vnode.el).toBe(container.firstChild)
-  //   // wait for hydration to finish
-  //   await new Promise(r => setTimeout(r))
-
-  //   expect(container.firstChild).toMatchInlineSnapshot(`
-  //     <div
-  //       class="v-enter-from v-enter-active"
-  //     >
-  //       foo
-  //     </div>
-  //   `)
-  //   await nextTick()
-  //   expect(vnode.el).toBe(container.firstChild)
+  //   expect(container.innerHTML).toBe(
+  //     '<div data-allow-mismatch="children"><!--hi--></div>',
+  //   )
+  //   expect(`Hydration node mismatch`).not.toHaveBeenWarned()
   // })
-
-  // // #10607
-  // test('update component stable slot (prod + optimized mode)', async () => {
-  //   __DEV__ = false
-  //   try {
-  //     const container = document.createElement('div')
-  //     container.innerHTML = `<template><div show="false"><!--[--><div><div><!----></div></div><div>0</div><!--]--></div></template>`
-  //     const Comp = {
-  //       render(this: any) {
-  //         return (
-  //           openBlock(),
-  //           createElementBlock('div', null, [
-  //             renderSlot(this.$slots, 'default'),
-  //           ])
-  //         )
-  //       },
-  //     }
-  //     const show = ref(false)
-  //     const clicked = ref(false)
-
-  //     const Wrapper = {
-  //       setup() {
-  //         const items = ref<number[]>([])
-  //         onMounted(() => {
-  //           items.value = [1]
-  //         })
-  //         return () => {
-  //           return (
-  //             openBlock(),
-  //             createBlock(Comp, null, {
-  //               default: withCtx(() => [
-  //                 createElementVNode('div', null, [
-  //                   createElementVNode('div', null, [
-  //                     clicked.value
-  //                       ? (openBlock(),
-  //                         createElementBlock('div', { key: 0 }, 'foo'))
-  //                       : createCommentVNode('v-if', true),
-  //                   ]),
-  //                 ]),
-  //                 createElementVNode(
-  //                   'div',
-  //                   null,
-  //                   items.value.length,
-  //                   1 /* TEXT */,
-  //                 ),
-  //               ]),
-  //               _: 1 /* STABLE */,
-  //             })
-  //           )
-  //         }
-  //       },
-  //     }
-  //     createSSRApp({
-  //       components: { Wrapper },
-  //       data() {
-  //         return { show }
-  //       },
-  //       template: `<Wrapper :show="show"/>`,
-  //     }).mount(container)
-
-  //     await nextTick()
-  //     expect(container.innerHTML).toBe(
-  //       `<div show="false"><!--[--><div><div><!----></div></div><div>1</div><!--]--></div>`,
-  //     )
-
-  //     show.value = true
-  //     await nextTick()
-  //     expect(async () => {
-  //       clicked.value = true
-  //       await nextTick()
-  //     }).not.toThrow("Cannot read properties of null (reading 'insertBefore')")
-
-  //     await nextTick()
-  //     expect(container.innerHTML).toBe(
-  //       `<div show="true"><!--[--><div><div><div>foo</div></div></div><div>1</div><!--]--></div>`,
-  //     )
-  //   } catch (e) {
-  //     throw e
-  //   } finally {
-  //     __DEV__ = true
-  //   }
+  // test('comment mismatch (text)', () => {
+  //   const { container } = mountWithHydration(
+  //     `<div data-allow-mismatch="children">foobar</div>`,
+  //     () => h('div', [createCommentVNode('hi')]),
+  //   )
+  //   expect(container.innerHTML).toBe(
+  //     '<div data-allow-mismatch="children"><!--hi--></div>',
+  //   )
+  //   expect(`Hydration node mismatch`).not.toHaveBeenWarned()
   // })
+  test('class mismatch', async () => {
+    await mountWithHydration(
+      `<div class="foo bar" data-allow-mismatch="class"></div>`,
+      `<div :class="data"></div>`,
+      ref('foo'),
+    )
+    expect(`Hydration class mismatch`).not.toHaveBeenWarned()
+  })
 
-  // describe('mismatch handling', () => {
-  //   test('text node', () => {
-  //     const { container } = mountWithHydration(`foo`, () => 'bar')
-  //     expect(container.textContent).toBe('bar')
-  //     expect(`Hydration text mismatch`).toHaveBeenWarned()
-  //   })
-
-  //   test('element text content', () => {
-  //     const { container } = mountWithHydration(`<div>foo</div>`, () =>
-  //       h('div', 'bar'),
-  //     )
-  //     expect(container.innerHTML).toBe('<div>bar</div>')
-  //     expect(`Hydration text content mismatch`).toHaveBeenWarned()
-  //   })
-
-  //   test('not enough children', () => {
-  //     const { container } = mountWithHydration(`<div></div>`, () =>
-  //       h('div', [h('span', 'foo'), h('span', 'bar')]),
-  //     )
-  //     expect(container.innerHTML).toBe(
-  //       '<div><span>foo</span><span>bar</span></div>',
-  //     )
-  //     expect(`Hydration children mismatch`).toHaveBeenWarned()
-  //   })
-
-  //   test('too many children', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div><span>foo</span><span>bar</span></div>`,
-  //       () => h('div', [h('span', 'foo')]),
-  //     )
-  //     expect(container.innerHTML).toBe('<div><span>foo</span></div>')
-  //     expect(`Hydration children mismatch`).toHaveBeenWarned()
-  //   })
-
-  //   test('complete mismatch', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div><span>foo</span><span>bar</span></div>`,
-  //       () => h('div', [h('div', 'foo'), h('p', 'bar')]),
-  //     )
-  //     expect(container.innerHTML).toBe('<div><div>foo</div><p>bar</p></div>')
-  //     expect(`Hydration node mismatch`).toHaveBeenWarnedTimes(2)
-  //   })
-
-  //   test('fragment mismatch removal', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
-  //       () => h('div', [h('span', 'replaced')]),
-  //     )
-  //     expect(container.innerHTML).toBe('<div><span>replaced</span></div>')
-  //     expect(`Hydration node mismatch`).toHaveBeenWarned()
-  //   })
-
-  //   test('fragment not enough children', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
-  //       () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
-  //     )
-  //     expect(container.innerHTML).toBe(
-  //       '<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
-  //     )
-  //     expect(`Hydration node mismatch`).toHaveBeenWarned()
-  //   })
-
-  //   test('fragment too many children', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
-  //       () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
-  //     )
-  //     expect(container.innerHTML).toBe(
-  //       '<div><!--[--><div>foo</div><!--]--><div>baz</div></div>',
-  //     )
-  //     // fragment ends early and attempts to hydrate the extra <div>bar</div>
-  //     // as 2nd fragment child.
-  //     expect(`Hydration text content mismatch`).toHaveBeenWarned()
-  //     // excessive children removal
-  //     expect(`Hydration children mismatch`).toHaveBeenWarned()
-  //   })
-
-  //   test('Teleport target has empty children', () => {
-  //     const teleportContainer = document.createElement('div')
-  //     teleportContainer.id = 'teleport'
-  //     document.body.appendChild(teleportContainer)
-
-  //     mountWithHydration('<!--teleport start--><!--teleport end-->', () =>
-  //       h(Teleport, { to: '#teleport' }, [h('span', 'value')]),
-  //     )
-  //     expect(teleportContainer.innerHTML).toBe(`<span>value</span>`)
-  //     expect(`Hydration children mismatch`).toHaveBeenWarned()
-  //   })
-
-  //   test('comment mismatch (element)', () => {
-  //     const { container } = mountWithHydration(`<div><span></span></div>`, () =>
-  //       h('div', [createCommentVNode('hi')]),
-  //     )
-  //     expect(container.innerHTML).toBe('<div><!--hi--></div>')
-  //     expect(`Hydration node mismatch`).toHaveBeenWarned()
-  //   })
-
-  //   test('comment mismatch (text)', () => {
-  //     const { container } = mountWithHydration(`<div>foobar</div>`, () =>
-  //       h('div', [createCommentVNode('hi')]),
-  //     )
-  //     expect(container.innerHTML).toBe('<div><!--hi--></div>')
-  //     expect(`Hydration node mismatch`).toHaveBeenWarned()
-  //   })
-
-  //   test('class mismatch', () => {
-  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
-  //       h('div', { class: ['foo', 'bar'] }),
-  //     )
-  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
-  //       h('div', { class: { foo: true, bar: true } }),
-  //     )
-  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
-  //       h('div', { class: 'foo bar' }),
-  //     )
-  //     // SVG classes
-  //     mountWithHydration(`<svg class="foo bar"></svg>`, () =>
-  //       h('svg', { class: 'foo bar' }),
-  //     )
-  //     // class with different order
-  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
-  //       h('div', { class: 'bar foo' }),
-  //     )
-  //     expect(`Hydration class mismatch`).not.toHaveBeenWarned()
-  //     mountWithHydration(`<div class="foo bar"></div>`, () =>
-  //       h('div', { class: 'foo' }),
-  //     )
-  //     expect(`Hydration class mismatch`).toHaveBeenWarned()
-  //   })
-
-  //   test('style mismatch', () => {
-  //     mountWithHydration(`<div style="color:red;"></div>`, () =>
-  //       h('div', { style: { color: 'red' } }),
-  //     )
-  //     mountWithHydration(`<div style="color:red;"></div>`, () =>
-  //       h('div', { style: `color:red;` }),
-  //     )
-  //     mountWithHydration(
-  //       `<div style="color:red; font-size: 12px;"></div>`,
-  //       () => h('div', { style: `font-size: 12px; color:red;` }),
-  //     )
-  //     mountWithHydration(`<div style="color:red;display:none;"></div>`, () =>
-  //       withDirectives(createVNode('div', { style: 'color: red' }, ''), [
-  //         [vShow, false],
-  //       ]),
-  //     )
-  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
-  //     mountWithHydration(`<div style="color:red;"></div>`, () =>
-  //       h('div', { style: { color: 'green' } }),
-  //     )
-  //     expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
-  //   })
-
-  //   test('style mismatch when no style attribute is present', () => {
-  //     mountWithHydration(`<div></div>`, () =>
-  //       h('div', { style: { color: 'red' } }),
-  //     )
-  //     expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
-  //   })
-
-  //   test('style mismatch w/ v-show', () => {
-  //     mountWithHydration(`<div style="color:red;display:none"></div>`, () =>
-  //       withDirectives(createVNode('div', { style: 'color: red' }, ''), [
-  //         [vShow, false],
-  //       ]),
-  //     )
-  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
-  //     mountWithHydration(`<div style="color:red;"></div>`, () =>
-  //       withDirectives(createVNode('div', { style: 'color: red' }, ''), [
-  //         [vShow, false],
-  //       ]),
-  //     )
-  //     expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
-  //   })
-
-  //   test('attr mismatch', () => {
-  //     mountWithHydration(`<div id="foo"></div>`, () => h('div', { id: 'foo' }))
-  //     mountWithHydration(`<div spellcheck></div>`, () =>
-  //       h('div', { spellcheck: '' }),
-  //     )
-  //     mountWithHydration(`<div></div>`, () => h('div', { id: undefined }))
-  //     // boolean
-  //     mountWithHydration(`<select multiple></div>`, () =>
-  //       h('select', { multiple: true }),
-  //     )
-  //     mountWithHydration(`<select multiple></div>`, () =>
-  //       h('select', { multiple: 'multiple' }),
-  //     )
-  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
-
-  //     mountWithHydration(`<div></div>`, () => h('div', { id: 'foo' }))
-  //     expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(1)
-
-  //     mountWithHydration(`<div id="bar"></div>`, () => h('div', { id: 'foo' }))
-  //     expect(`Hydration attribute mismatch`).toHaveBeenWarnedTimes(2)
-  //   })
-
-  //   test('attr special case: textarea value', () => {
-  //     mountWithHydration(`<textarea>foo</textarea>`, () =>
-  //       h('textarea', { value: 'foo' }),
-  //     )
-  //     mountWithHydration(`<textarea></textarea>`, () =>
-  //       h('textarea', { value: '' }),
-  //     )
-  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
-
-  //     mountWithHydration(`<textarea>foo</textarea>`, () =>
-  //       h('textarea', { value: 'bar' }),
-  //     )
-  //     expect(`Hydration attribute mismatch`).toHaveBeenWarned()
-  //   })
-
-  //   // #11873
-  //   test('<textarea> with newlines at the beginning', async () => {
-  //     const render = () => h('textarea', null, '\nhello')
-  //     const html = await renderToString(createSSRApp({ render }))
-  //     mountWithHydration(html, render)
-  //     expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('<pre> with newlines at the beginning', async () => {
-  //     const render = () => h('pre', null, '\n')
-  //     const html = await renderToString(createSSRApp({ render }))
-  //     mountWithHydration(html, render)
-  //     expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('boolean attr handling', () => {
-  //     mountWithHydration(`<input />`, () => h('input', { readonly: false }))
-  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
-
-  //     mountWithHydration(`<input readonly />`, () =>
-  //       h('input', { readonly: true }),
-  //     )
-  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
-
-  //     mountWithHydration(`<input readonly="readonly" />`, () =>
-  //       h('input', { readonly: true }),
-  //     )
-  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('client value is null or undefined', () => {
-  //     mountWithHydration(`<div></div>`, () =>
-  //       h('div', { draggable: undefined }),
-  //     )
-  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
-
-  //     mountWithHydration(`<input />`, () => h('input', { type: null }))
-  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('should not warn against object values', () => {
-  //     mountWithHydration(`<input />`, () => h('input', { from: {} }))
-  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('should not warn on falsy bindings of non-property keys', () => {
-  //     mountWithHydration(`<button />`, () => h('button', { href: undefined }))
-  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('should not warn on non-renderable option values', () => {
-  //     mountWithHydration(`<select><option>hello</option></select>`, () =>
-  //       h('select', [h('option', { value: ['foo'] }, 'hello')]),
-  //     )
-  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('should not warn css v-bind', () => {
-  //     const container = document.createElement('div')
-  //     container.innerHTML = `<div style="--foo:red;color:var(--foo);" />`
-  //     const app = createSSRApp({
-  //       setup() {
-  //         useCssVars(() => ({
-  //           foo: 'red',
-  //         }))
-  //         return () => h('div', { style: { color: 'var(--foo)' } })
-  //       },
-  //     })
-  //     app.mount(container)
-  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   // #10317 - test case from #10325
-  //   test('css vars should only be added to expected on component root dom', () => {
-  //     const container = document.createElement('div')
-  //     container.innerHTML = `<div style="--foo:red;"><div style="color:var(--foo);" /></div>`
-  //     const app = createSSRApp({
-  //       setup() {
-  //         useCssVars(() => ({
-  //           foo: 'red',
-  //         }))
-  //         return () =>
-  //           h('div', null, [h('div', { style: { color: 'var(--foo)' } })])
-  //       },
-  //     })
-  //     app.mount(container)
-  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   // #11188
-  //   test('css vars support fallthrough', () => {
-  //     const container = document.createElement('div')
-  //     container.innerHTML = `<div style="padding: 4px;--foo:red;"></div>`
-  //     const app = createSSRApp({
-  //       setup() {
-  //         useCssVars(() => ({
-  //           foo: 'red',
-  //         }))
-  //         return () => h(Child)
-  //       },
-  //     })
-  //     const Child = {
-  //       setup() {
-  //         return () => h('div', { style: 'padding: 4px' })
-  //       },
-  //     }
-  //     app.mount(container)
-  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   // #11189
-  //   test('should not warn for directives that mutate DOM in created', () => {
-  //     const container = document.createElement('div')
-  //     container.innerHTML = `<div class="test red"></div>`
-  //     const vColor: ObjectDirective = {
-  //       created(el, binding) {
-  //         el.classList.add(binding.value)
-  //       },
-  //     }
-  //     const app = createSSRApp({
-  //       setup() {
-  //         return () =>
-  //           withDirectives(h('div', { class: 'test' }), [[vColor, 'red']])
-  //       },
-  //     })
-  //     app.mount(container)
-  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('escape css var name', () => {
-  //     const container = document.createElement('div')
-  //     container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`
-  //     const app = createSSRApp({
-  //       setup() {
-  //         useCssVars(() => ({
-  //           'foo.bar': 'red',
-  //         }))
-  //         return () => h(Child)
-  //       },
-  //     })
-  //     const Child = {
-  //       setup() {
-  //         return () => h('div', { style: 'padding: 4px' })
-  //       },
-  //     }
-  //     app.mount(container)
-  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
-  //   })
-  // })
+  test('style mismatch', async () => {
+    await mountWithHydration(
+      `<div style="color:red;" data-allow-mismatch="style"></div>`,
+      `<div :style="data"></div>`,
+      ref({ color: 'green' }),
+    )
+    expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+  })
 
-  // describe('data-allow-mismatch', () => {
-  //   test('element text content', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div data-allow-mismatch="text">foo</div>`,
-  //       () => h('div', 'bar'),
-  //     )
-  //     expect(container.innerHTML).toBe(
-  //       '<div data-allow-mismatch="text">bar</div>',
-  //     )
-  //     expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('not enough children', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div data-allow-mismatch="children"></div>`,
-  //       () => h('div', [h('span', 'foo'), h('span', 'bar')]),
-  //     )
-  //     expect(container.innerHTML).toBe(
-  //       '<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>',
-  //     )
-  //     expect(`Hydration children mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('too many children', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
-  //       () => h('div', [h('span', 'foo')]),
-  //     )
-  //     expect(container.innerHTML).toBe(
-  //       '<div data-allow-mismatch="children"><span>foo</span></div>',
-  //     )
-  //     expect(`Hydration children mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('complete mismatch', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div data-allow-mismatch="children"><span>foo</span><span>bar</span></div>`,
-  //       () => h('div', [h('div', 'foo'), h('p', 'bar')]),
-  //     )
-  //     expect(container.innerHTML).toBe(
-  //       '<div data-allow-mismatch="children"><div>foo</div><p>bar</p></div>',
-  //     )
-  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('fragment mismatch removal', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--></div>`,
-  //       () => h('div', [h('span', 'replaced')]),
-  //     )
-  //     expect(container.innerHTML).toBe(
-  //       '<div data-allow-mismatch="children"><span>replaced</span></div>',
-  //     )
-  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('fragment not enough children', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>`,
-  //       () => h('div', [[h('div', 'foo'), h('div', 'bar')], h('div', 'baz')]),
-  //     )
-  //     expect(container.innerHTML).toBe(
-  //       '<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>',
-  //     )
-  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('fragment too many children', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div data-allow-mismatch="children"><!--[--><div>foo</div><div>bar</div><!--]--><div>baz</div></div>`,
-  //       () => h('div', [[h('div', 'foo')], h('div', 'baz')]),
-  //     )
-  //     expect(container.innerHTML).toBe(
-  //       '<div data-allow-mismatch="children"><!--[--><div>foo</div><!--]--><div>baz</div></div>',
-  //     )
-  //     // fragment ends early and attempts to hydrate the extra <div>bar</div>
-  //     // as 2nd fragment child.
-  //     expect(`Hydration text content mismatch`).not.toHaveBeenWarned()
-  //     // excessive children removal
-  //     expect(`Hydration children mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('comment mismatch (element)', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div data-allow-mismatch="children"><span></span></div>`,
-  //       () => h('div', [createCommentVNode('hi')]),
-  //     )
-  //     expect(container.innerHTML).toBe(
-  //       '<div data-allow-mismatch="children"><!--hi--></div>',
-  //     )
-  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('comment mismatch (text)', () => {
-  //     const { container } = mountWithHydration(
-  //       `<div data-allow-mismatch="children">foobar</div>`,
-  //       () => h('div', [createCommentVNode('hi')]),
-  //     )
-  //     expect(container.innerHTML).toBe(
-  //       '<div data-allow-mismatch="children"><!--hi--></div>',
-  //     )
-  //     expect(`Hydration node mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('class mismatch', () => {
-  //     mountWithHydration(
-  //       `<div class="foo bar" data-allow-mismatch="class"></div>`,
-  //       () => h('div', { class: 'foo' }),
-  //     )
-  //     expect(`Hydration class mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('style mismatch', () => {
-  //     mountWithHydration(
-  //       `<div style="color:red;" data-allow-mismatch="style"></div>`,
-  //       () => h('div', { style: { color: 'green' } }),
-  //     )
-  //     expect(`Hydration style mismatch`).not.toHaveBeenWarned()
-  //   })
-
-  //   test('attr mismatch', () => {
-  //     mountWithHydration(`<div data-allow-mismatch="attribute"></div>`, () =>
-  //       h('div', { id: 'foo' }),
-  //     )
-  //     mountWithHydration(
-  //       `<div id="bar" data-allow-mismatch="attribute"></div>`,
-  //       () => h('div', { id: 'foo' }),
-  //     )
-  //     expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
-  //   })
-  // })
+  test('attr mismatch', async () => {
+    await mountWithHydration(
+      `<div data-allow-mismatch="attribute"></div>`,
+      `<div :id="data"></div>`,
+      ref('foo'),
+    )
+
+    await mountWithHydration(
+      `<div id="bar" data-allow-mismatch="attribute"></div>`,
+      `<div :id="data"></div>`,
+      ref('foo'),
+    )
+
+    expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
+  })
+})
+
+describe('VDOM interop', () => {
+  test('basic render vapor component', async () => {
+    const data = ref(true)
+    const { container } = await testWithVDOMApp(
+      `<script setup>const data = _data; const components = _components;</script>
+      <template>
+        <components.VaporChild/>
+      </template>`,
+      {
+        VaporChild: {
+          code: `<template>{{ data }}</template>`,
+          vapor: true,
+        },
+      },
+      data,
+    )
+
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"true"`)
+
+    data.value = false
+    await nextTick()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"false"`)
+  })
+
+  test('nested components (VDOM -> Vapor -> VDOM)', async () => {
+    const data = ref(true)
+    const { container } = await testWithVDOMApp(
+      `<script setup>const data = _data; const components = _components;</script>
+      <template>
+        <components.VaporChild/>
+      </template>`,
+      {
+        VaporChild: {
+          code: `<template><components.VdomChild/></template>`,
+          vapor: true,
+        },
+        VdomChild: {
+          code: `<script setup>const data = _data;</script>
+            <template>{{ data }}</template>`,
+          vapor: false,
+        },
+      },
+      data,
+    )
+
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"true"`)
+
+    data.value = false
+    await nextTick()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"false"`)
+  })
+
+  test('nested components (VDOM -> Vapor -> VDOM (with slot fallback))', async () => {
+    const data = ref(true)
+    const { container } = await testWithVDOMApp(
+      `<script setup>const data = _data; const components = _components;</script>
+      <template>
+        <components.VaporChild/>
+      </template>`,
+      {
+        VaporChild: {
+          code: `<template><components.VdomChild/></template>`,
+          vapor: true,
+        },
+        VdomChild: {
+          code: `<script setup>const data = _data;</script>
+            <template><slot><span>{{data}}</span></slot></template>`,
+          vapor: false,
+        },
+      },
+      data,
+    )
+
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><span>true</span><!--]-->
+      "
+    `,
+    )
+
+    data.value = false
+    await nextTick()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><span>false</span><!--]-->
+      "
+    `,
+    )
+  })
+
+  test('nested components (VDOM -> Vapor(with slot content) -> VDOM)', async () => {
+    const data = ref(true)
+    const { container } = await testWithVDOMApp(
+      `<script setup>const data = _data; const components = _components;</script>
+          <template>
+            <components.VaporChild/>
+          </template>`,
+      {
+        VaporChild: {
+          code: `<template>
+            <components.VdomChild>
+              <template #default>
+                <span>{{data}} vapor fallback</span>
+              </template>
+            </components.VdomChild>
+          </template>`,
+          vapor: true,
+        },
+        VdomChild: {
+          code: `<script setup>const data = _data;</script>
+            <template><slot><span>vdom fallback</span></slot></template>`,
+          vapor: false,
+        },
+      },
+      data,
+    )
+
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><span>true vapor fallback</span><!--]-->
+      "
+    `,
+    )
+
+    data.value = false
+    await nextTick()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><span>false vapor fallback</span><!--]-->
+      "
+    `,
+    )
+  })
+
+  test('nested components (VDOM -> Vapor(with slot content) -> Vapor)', async () => {
+    const data = ref(true)
+    const { container } = await testWithVDOMApp(
+      `<script setup>const data = _data; const components = _components;</script>
+          <template>
+            <components.VaporChild/>
+          </template>`,
+      {
+        VaporChild: {
+          code: `<template>
+            <components.VaporChild2>
+              <template #default>
+                <span>{{data}} vapor fallback</span>
+              </template>
+            </components.VaporChild2>
+          </template>`,
+          vapor: true,
+        },
+        VaporChild2: {
+          code: `<template><slot><span>vapor fallback2</span></slot></template>`,
+          vapor: true,
+        },
+      },
+      data,
+    )
+
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><span>true vapor fallback</span><!--]-->
+      "
+    `,
+    )
+
+    data.value = false
+    await nextTick()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><span>false vapor fallback</span><!--]-->
+      "
+    `,
+    )
+  })
+
+  test('vapor slot render vdom component', async () => {
+    const data = ref(true)
+    const { container } = await testWithVDOMApp(
+      `<script setup>const data = _data; const components = _components;</script>
+      <template>
+        <components.VaporChild>
+          <components.VdomChild/>
+        </components.VaporChild>
+      </template>`,
+      {
+        VaporChild: {
+          code: `<template><div><slot/></div></template>`,
+          vapor: true,
+        },
+        VdomChild: {
+          code: `<script setup>const data = _data;</script>
+            <template>{{ data }}</template>`,
+          vapor: false,
+        },
+      },
+      data,
+    )
+
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "<div>
+      <!--[-->true<!--]-->
+      </div>"
+    `,
+    )
+
+    data.value = false
+    await nextTick()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "<div>
+      <!--[-->false<!--]-->
+      </div>"
+    `,
+    )
+  })
 
-  test.todo('Teleport')
-  test.todo('Suspense')
+  test('vapor slot render vdom component (render function)', async () => {
+    const data = ref(true)
+    const { container } = await testWithVaporApp(
+      `<script setup>
+        import { h } from 'vue'
+        const data = _data; const components = _components;
+        const VdomChild = {
+          setup() {
+            return () => h('div', null, [h('div', [String(data.value)])])
+          }
+        }
+      </script>
+      <template>
+        <components.VaporChild>
+          <VdomChild/>
+        </components.VaporChild>
+      </template>`,
+      {
+        VaporChild: {
+          code: `<template><div><slot/></div></template>`,
+          vapor: true,
+        },
+      },
+      data,
+    )
+
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "<div>
+      <!--[--><div><div>true</div></div><!--]-->
+      </div>"
+    `,
+    )
+
+    expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+
+    data.value = false
+    await nextTick()
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "<div>
+      <!--[--><div><div>false</div></div><!--]-->
+      </div>"
+    `,
+    )
+  })
 })
index 1dc00dbb3d19389bb431a20279ca13017caf3029..daef0479d814d42f8bb3a77a07e5df392380ef00 100644 (file)
@@ -7,9 +7,10 @@ import type { RawSlots } from './componentSlots'
 import {
   insertionAnchor,
   insertionParent,
+  isLastInsertion,
   resetInsertionState,
 } from './insertionState'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
+import { advanceHydrationNode, isHydrating } from './dom/hydration'
 import { DynamicFragment, type VaporFragment } from './fragment'
 
 export function createDynamicComponent(
@@ -20,15 +21,13 @@ export function createDynamicComponent(
 ): VaporFragment {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
-  if (isHydrating) {
-    locateHydrationNode()
-  } else {
-    resetInsertionState()
-  }
+  const _isLastInsertion = isLastInsertion
+  if (!isHydrating) resetInsertionState()
 
-  const frag = __DEV__
-    ? new DynamicFragment('dynamic-component')
-    : new DynamicFragment()
+  const frag =
+    isHydrating || __DEV__
+      ? new DynamicFragment('dynamic-component')
+      : new DynamicFragment()
 
   renderEffect(() => {
     const value = getter()
@@ -47,9 +46,12 @@ export function createDynamicComponent(
     )
   })
 
-  if (!isHydrating && _insertionParent) {
-    insert(frag, _insertionParent, _insertionAnchor)
+  if (!isHydrating) {
+    if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
+  } else {
+    if (_isLastInsertion) {
+      advanceHydrationNode(_insertionParent!)
+    }
   }
-
   return frag
 }
index 25126315e19d6ee5587e7e4f4ad10b821bcacade..5c4e598b9109801f7826718fa8cee4a3adb282c8 100644 (file)
@@ -12,19 +12,31 @@ import {
   watch,
 } from '@vue/reactivity'
 import { isArray, isObject, isString } from '@vue/shared'
-import { createComment, createTextNode } from './dom/node'
-import { type Block, insert, remove } from './block'
+import {
+  createComment,
+  createTextNode,
+  updateLastLogicalChild,
+} from './dom/node'
+import { type Block, findBlockNode, insert, remove } from './block'
 import { warn } from '@vue/runtime-dom'
 import { currentInstance, isVaporComponent } from './component'
 import type { DynamicSlot } from './componentSlots'
 import { renderEffect } from './renderEffect'
 import { VaporVForFlags } from '../../shared/src/vaporFlags'
+import {
+  advanceHydrationNode,
+  currentHydrationNode,
+  isComment,
+  isHydrating,
+  locateHydrationNode,
+  setCurrentHydrationNode,
+} from './dom/hydration'
 import { applyTransitionHooks } from './components/Transition'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
 import { ForFragment, VaporFragment } from './fragment'
 import {
   insertionAnchor,
   insertionParent,
+  isLastInsertion,
   resetInsertionState,
 } from './insertionState'
 
@@ -80,6 +92,7 @@ export const createFor = (
 ): ForFragment => {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
+  const _isLastInsertion = isLastInsertion
   if (isHydrating) {
     locateHydrationNode()
   } else {
@@ -92,8 +105,11 @@ export const createFor = (
   let parent: ParentNode | undefined | null
   // createSelector only
   let currentKey: any
-  // TODO handle this in hydration
-  const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
+  let parentAnchor: Node
+  if (!isHydrating) {
+    parentAnchor = __DEV__ ? createComment('for') : createTextNode()
+  }
+
   const frag = new ForFragment(oldBlocks)
   const instance = currentInstance!
   const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE)
@@ -119,7 +135,29 @@ export const createFor = (
     if (!isMounted) {
       isMounted = true
       for (let i = 0; i < newLength; i++) {
-        mount(source, i)
+        const nodes = mount(source, i).nodes
+        if (isHydrating) {
+          setCurrentHydrationNode(findBlockNode(nodes!).nextNode)
+        }
+      }
+
+      if (isHydrating) {
+        parentAnchor =
+          newLength === 0
+            ? currentHydrationNode!.nextSibling!
+            : currentHydrationNode!
+        if (
+          __DEV__ &&
+          (!parentAnchor || (parentAnchor && !isComment(parentAnchor, ']')))
+        ) {
+          throw new Error(
+            `v-for fragment anchor node was not found. this is likely a Vue internal bug.`,
+          )
+        }
+
+        if (_insertionParent) {
+          updateLastLogicalChild(_insertionParent!, parentAnchor)
+        }
       }
     } else {
       parent = parent || parentAnchor!.parentNode
@@ -448,8 +486,10 @@ export const createFor = (
     renderEffect(renderList)
   }
 
-  if (!isHydrating && _insertionParent) {
-    insert(frag, _insertionParent, _insertionAnchor)
+  if (!isHydrating) {
+    if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
+  } else {
+    advanceHydrationNode(_isLastInsertion ? _insertionParent! : parentAnchor!)
   }
 
   return frag
index 37f6077b0f5517755f61400026bf82f64d4c2052..b9764f7d1a53c87699378a15998ff3128d16de94 100644 (file)
@@ -1,8 +1,9 @@
 import { type Block, type BlockFn, insert } from './block'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
+import { advanceHydrationNode, isHydrating } from './dom/hydration'
 import {
   insertionAnchor,
   insertionParent,
+  isLastInsertion,
   resetInsertionState,
 } from './insertionState'
 import { renderEffect } from './renderEffect'
@@ -16,22 +17,24 @@ export function createIf(
 ): Block {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
-  if (isHydrating) {
-    locateHydrationNode()
-  } else {
-    resetInsertionState()
-  }
+  const _isLastInsertion = isLastInsertion
+  if (!isHydrating) resetInsertionState()
 
   let frag: Block
   if (once) {
     frag = condition() ? b1() : b2 ? b2() : []
   } else {
-    frag = __DEV__ ? new DynamicFragment('if') : new DynamicFragment()
+    frag =
+      isHydrating || __DEV__ ? new DynamicFragment('if') : new DynamicFragment()
     renderEffect(() => (frag as DynamicFragment).update(condition() ? b1 : b2))
   }
 
-  if (!isHydrating && _insertionParent) {
-    insert(frag, _insertionParent, _insertionAnchor)
+  if (!isHydrating) {
+    if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
+  } else {
+    if (_isLastInsertion) {
+      advanceHydrationNode(_insertionParent!)
+    }
   }
 
   return frag
index 3543f7c18b9f7b8c84a9f4047516ea9cdafce4c0..c4c2f0e188a2f6693cedf78a48523f290aa138c3 100644 (file)
@@ -5,13 +5,8 @@ import {
   mountComponent,
   unmountComponent,
 } from './component'
-import { isHydrating } from './dom/hydration'
-import {
-  type DynamicFragment,
-  type VaporFragment,
-  isFragment,
-} from './fragment'
-import { TeleportFragment } from './components/Teleport'
+import { _child } from './dom/node'
+import { isComment, isHydrating } from './dom/hydration'
 import {
   type TransitionHooks,
   type TransitionProps,
@@ -19,11 +14,12 @@ import {
   performTransitionEnter,
   performTransitionLeave,
 } from '@vue/runtime-dom'
-
-export interface TransitionOptions {
-  $key?: any
-  $transition?: VaporTransitionHooks
-}
+import {
+  type DynamicFragment,
+  type VaporFragment,
+  isFragment,
+} from './fragment'
+import { TeleportFragment } from './components/Teleport'
 
 export interface VaporTransitionHooks extends TransitionHooks {
   state: TransitionState
@@ -34,13 +30,17 @@ export interface VaporTransitionHooks extends TransitionHooks {
   disabled?: boolean
 }
 
+export interface TransitionOptions {
+  $key?: any
+  $transition?: VaporTransitionHooks
+}
+
 export type TransitionBlock =
   | (Node & TransitionOptions)
   | (VaporFragment & TransitionOptions)
   | (DynamicFragment & TransitionOptions)
 
 export type Block = TransitionBlock | VaporComponentInstance | Block[]
-
 export type BlockFn = (...args: any[]) => Block
 
 export function isBlock(val: NonNullable<unknown>): val is Block {
@@ -67,11 +67,11 @@ export function isValidBlock(block: Block): boolean {
 
 export function insert(
   block: Block,
-  parent: ParentNode & { $anchor?: Node | null },
+  parent: ParentNode & { $fc?: Node | null },
   anchor: Node | null | 0 = null, // 0 means prepend
   parentSuspense?: any, // TODO Suspense
 ): void {
-  anchor = anchor === 0 ? parent.$anchor || parent.firstChild : anchor
+  anchor = anchor === 0 ? parent.$fc || _child(parent) : anchor
   if (block instanceof Node) {
     if (!isHydrating) {
       // only apply transition on Element nodes
@@ -107,7 +107,6 @@ export function insert(
     }
     // fragment
     if (block.insert) {
-      // TODO handle hydration for vdom interop
       block.insert(parent, anchor, (block as TransitionBlock).$transition)
     } else {
       insert(block.nodes, parent, anchor, parentSuspense)
@@ -179,3 +178,45 @@ export function normalizeBlock(block: Block): Node[] {
   }
   return nodes
 }
+
+export function findBlockNode(block: Block): {
+  parentNode: Node | null
+  nextNode: Node | null
+} {
+  let { parentNode, nextSibling: nextNode } = findLastChild(block)!
+
+  // if nodes render as a fragment and the current nextNode is fragment
+  // end anchor, need to move to the next node
+  if (nextNode && isComment(nextNode, ']') && isFragmentBlock(block)) {
+    nextNode = nextNode.nextSibling
+  }
+
+  return {
+    parentNode,
+    nextNode,
+  }
+}
+
+function findLastChild(node: Block): Node | undefined | null {
+  if (node && node instanceof Node) {
+    return node
+  } else if (isArray(node)) {
+    return findLastChild(node[node.length - 1])
+  } else if (isVaporComponent(node)) {
+    return findLastChild(node.block!)
+  } else {
+    if (node.anchor) return node.anchor
+    return findLastChild(node.nodes!)
+  }
+}
+
+export function isFragmentBlock(block: Block): boolean {
+  if (isArray(block)) {
+    return true
+  } else if (isVaporComponent(block)) {
+    return isFragmentBlock(block.block!)
+  } else if (isFragment(block)) {
+    return isFragmentBlock(block.nodes)
+  }
+  return false
+}
index d4c5ce006e40bd93aac30d968bf9730e3040bf07..e1e4cffe16e0e244fcf664132da5968ec4ecbe7e 100644 (file)
@@ -49,7 +49,6 @@ import {
   getPropsProxyHandlers,
   hasFallthroughAttrs,
   normalizePropsOptions,
-  resolveDynamicProps,
   setupPropsValidation,
 } from './componentProps'
 import { type RenderEffect, renderEffect } from './renderEffect'
@@ -64,13 +63,22 @@ import {
   getSlot,
 } from './componentSlots'
 import { hmrReload, hmrRerender } from './hmr'
+import {
+  adoptTemplate,
+  advanceHydrationNode,
+  currentHydrationNode,
+  isHydrating,
+  locateHydrationNode,
+  locateNextNode,
+  setCurrentHydrationNode,
+} from './dom/hydration'
 import { createElement } from './dom/node'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
 import { type TeleportFragment, isVaporTeleport } from './components/Teleport'
 import type { KeepAliveInstance } from './components/KeepAlive'
 import {
   insertionAnchor,
   insertionParent,
+  isLastInsertion,
   resetInsertionState,
 } from './insertionState'
 import { DynamicFragment } from './fragment'
@@ -156,6 +164,7 @@ export function createComponent(
 ): VaporComponentInstance {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
+  const _isLastInsertion = isLastInsertion
   if (isHydrating) {
     locateHydrationNode()
   } else {
@@ -200,8 +209,14 @@ export function createComponent(
       rawProps,
       rawSlots,
     )
-    if (!isHydrating && _insertionParent) {
-      insert(frag, _insertionParent, _insertionAnchor)
+
+    if (!isHydrating) {
+      if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
+    } else {
+      frag.hydrate()
+      if (_isLastInsertion) {
+        advanceHydrationNode(_insertionParent!)
+      }
     }
     return frag
   }
@@ -309,10 +324,14 @@ export function createComponent(
 
   onScopeDispose(() => unmountComponent(instance), true)
 
-  if (!isHydrating && _insertionParent) {
+  if (_insertionParent) {
     mountComponent(instance, _insertionParent, _insertionAnchor)
   }
 
+  if (isHydrating && _isLastInsertion) {
+    advanceHydrationNode(_insertionParent!)
+  }
+
   return instance
 }
 
@@ -538,34 +557,44 @@ export function createComponentWithFallback(
     return createComponent(comp, rawProps, rawSlots, isSingleRoot, appContext)
   }
 
-  const el = createElement(comp)
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
+  const _isLastInsertion = isLastInsertion
   if (isHydrating) {
     locateHydrationNode()
   } else {
     resetInsertionState()
   }
 
+  const el = isHydrating
+    ? (adoptTemplate(currentHydrationNode!, `<${comp}/>`) as HTMLElement)
+    : createElement(comp)
+
   // mark single root
   ;(el as any).$root = isSingleRoot
 
-  if (rawProps) {
-    renderEffect(() => {
-      setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)])
-    })
-  }
-
   if (rawSlots) {
+    let nextNode: Node | null = null
+    if (isHydrating) {
+      nextNode = locateNextNode(el)
+      setCurrentHydrationNode(el.firstChild)
+    }
     if (rawSlots.$) {
       // TODO dynamic slot fragment
     } else {
       insert(getSlot(rawSlots as RawSlots, 'default')!(), el)
     }
+    if (isHydrating) {
+      setCurrentHydrationNode(nextNode)
+    }
   }
 
-  if (!isHydrating && _insertionParent) {
-    insert(el, _insertionParent, _insertionAnchor)
+  if (!isHydrating) {
+    if (_insertionParent) insert(el, _insertionParent, _insertionAnchor)
+  } else {
+    if (_isLastInsertion) {
+      advanceHydrationNode(_insertionParent!)
+    }
   }
 
   return el
index 49b577ec3dc035473ed00042bab13fe54793df91..aa0651658c057fdce6c02bbdd0fce7c95d8982b5 100644 (file)
@@ -7,10 +7,15 @@ import { renderEffect } from './renderEffect'
 import {
   insertionAnchor,
   insertionParent,
+  isLastInsertion,
   resetInsertionState,
 } from './insertionState'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
-import { DynamicFragment } from './fragment'
+import {
+  advanceHydrationNode,
+  isHydrating,
+  locateHydrationNode,
+} from './dom/hydration'
+import { DynamicFragment, type VaporFragment } from './fragment'
 
 export type RawSlots = Record<string, VaporSlot> & {
   $?: DynamicSlotSource[]
@@ -110,11 +115,8 @@ export function createSlot(
 ): Block {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
-  if (isHydrating) {
-    locateHydrationNode()
-  } else {
-    resetInsertionState()
-  }
+  const _isLastInsertion = isLastInsertion
+  if (!isHydrating) resetInsertionState()
 
   const instance = i || (currentInstance as VaporComponentInstance)
   const rawSlots = instance.rawSlots
@@ -123,8 +125,8 @@ export function createSlot(
     : EMPTY_OBJ
 
   let fragment: DynamicFragment
-
   if (isRef(rawSlots._)) {
+    if (isHydrating) locateHydrationNode()
     fragment = instance.appContext.vapor!.vdomSlot(
       rawSlots._,
       name,
@@ -133,7 +135,10 @@ export function createSlot(
       fallback,
     )
   } else {
-    fragment = __DEV__ ? new DynamicFragment('slot') : new DynamicFragment()
+    fragment =
+      isHydrating || __DEV__
+        ? new DynamicFragment('slot')
+        : new DynamicFragment()
     const isDynamicName = isFunction(name)
     const renderSlot = () => {
       const slot = getSlot(rawSlots, isFunction(name) ? name() : name)
@@ -155,8 +160,15 @@ export function createSlot(
     }
   }
 
-  if (!isHydrating && _insertionParent) {
-    insert(fragment, _insertionParent, _insertionAnchor)
+  if (!isHydrating) {
+    if (_insertionParent) insert(fragment, _insertionParent, _insertionAnchor)
+  } else {
+    if (fragment.insert) {
+      ;(fragment as VaporFragment).hydrate!()
+    }
+    if (_isLastInsertion) {
+      advanceHydrationNode(_insertionParent!)
+    }
   }
 
   return fragment
index 68888b0dde55f6e7f4969af2ce8026b3fb64aa97..079a6449e8e7664007ea523d9b4c6361c28982a1 100644 (file)
@@ -1,13 +1,16 @@
 import {
+  MismatchTypes,
   type VShowElement,
   vShowHidden,
   vShowOriginalDisplay,
   warn,
+  warnPropMismatch,
 } from '@vue/runtime-dom'
 import { renderEffect } from '../renderEffect'
 import { isVaporComponent } from '../component'
 import type { Block, TransitionBlock } from '../block'
 import { isArray } from '@vue/shared'
+import { isHydrating, logMismatchError } from '../dom/hydration'
 import { DynamicFragment, VaporFragment } from '../fragment'
 
 export function applyVShow(target: Block, source: () => any): void {
@@ -58,24 +61,43 @@ function setDisplay(target: Block, value: unknown): void {
       el[vShowOriginalDisplay] =
         el.style.display === 'none' ? '' : el.style.display
     }
-    if ($transition) {
-      if (value) {
-        $transition.beforeEnter(target)
-        el.style.display = el[vShowOriginalDisplay]!
-        $transition.enter(target)
-      } else {
-        // during initial render, the element is not yet inserted into the
-        // DOM, and it is hidden, no need to trigger transition
-        if (target.isConnected) {
-          $transition.leave(target, () => {
-            el.style.display = 'none'
-          })
+    if (
+      (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
+      isHydrating
+    ) {
+      if (!value && el.style.display !== 'none') {
+        warnPropMismatch(
+          el,
+          'style',
+          MismatchTypes.STYLE,
+          `display: ${el.style.display}`,
+          'display: none',
+        )
+        logMismatchError()
+
+        el.style.display = 'none'
+        el[vShowOriginalDisplay] = ''
+      }
+    } else {
+      if ($transition) {
+        if (value) {
+          $transition.beforeEnter(target)
+          el.style.display = el[vShowOriginalDisplay]!
+          $transition.enter(target)
         } else {
-          el.style.display = 'none'
+          // during initial render, the element is not yet inserted into the
+          // DOM, and it is hidden, no need to trigger transition
+          if (target.isConnected) {
+            $transition.leave(target, () => {
+              el.style.display = 'none'
+            })
+          } else {
+            el.style.display = 'none'
+          }
         }
+      } else {
+        el.style.display = value ? el[vShowOriginalDisplay]! : 'none'
       }
-    } else {
-      el.style.display = value ? el[vShowOriginalDisplay]! : 'none'
     }
     el[vShowHidden] = !value
   } else if (__DEV__) {
index d34d9db7da58c4e2b2aaa9d5a417168ec316ab18..5ad6f1e3070a6b6355376e448c2f7ca9f9037514 100644 (file)
-import { warn } from '@vue/runtime-dom'
+import { MismatchTypes, isMismatchAllowed, warn } from '@vue/runtime-dom'
 import {
+  type ChildItem,
   insertionAnchor,
   insertionParent,
   resetInsertionState,
   setInsertionState,
 } from '../insertionState'
-import { child, next } from './node'
+import {
+  _child,
+  _next,
+  createElement,
+  createTextNode,
+  disableHydrationNodeLookup,
+  enableHydrationNodeLookup,
+  locateChildByLogicalIndex,
+  parentNode,
+} from './node'
+import { remove } from '../block'
 
+const isHydratingStack = [] as boolean[]
 export let isHydrating = false
 export let currentHydrationNode: Node | null = null
 
-export function setCurrentHydrationNode(node: Node | null): void {
-  currentHydrationNode = node
+function pushIsHydrating(value: boolean): void {
+  isHydratingStack.push((isHydrating = value))
+}
+
+function popIsHydrating(): void {
+  isHydratingStack.pop()
+  isHydrating = isHydratingStack[isHydratingStack.length - 1] || false
+}
+
+export function runWithoutHydration(fn: () => any): any {
+  try {
+    pushIsHydrating(false)
+    return fn()
+  } finally {
+    popIsHydrating()
+  }
 }
 
 let isOptimized = false
 
-export function withHydration(container: ParentNode, fn: () => void): void {
-  adoptTemplate = adoptTemplateImpl
-  locateHydrationNode = locateHydrationNodeImpl
+function performHydration<T>(
+  fn: () => T,
+  setup: () => void,
+  cleanup: () => void,
+): T {
   if (!isOptimized) {
+    adoptTemplate = adoptTemplateImpl
+    locateHydrationNode = locateHydrationNodeImpl
     // optimize anchor cache lookup
-    ;(Comment.prototype as any).$fs = undefined
+    ;(Comment.prototype as any).$fe = undefined
+    ;(Node.prototype as any).$pns = undefined
+    ;(Node.prototype as any).$idx = undefined
+    ;(Node.prototype as any).$llc = undefined
+    ;(Node.prototype as any).$lpn = undefined
+    ;(Node.prototype as any).$lan = undefined
+    ;(Node.prototype as any).$lin = undefined
+    ;(Node.prototype as any).$curIdx = undefined
+
     isOptimized = true
   }
-  isHydrating = true
-  setInsertionState(container, 0)
+  enableHydrationNodeLookup()
+  pushIsHydrating(true)
+  setup()
   const res = fn()
-  resetInsertionState()
+  cleanup()
   currentHydrationNode = null
-  isHydrating = false
+  popIsHydrating()
+  if (!isHydrating) disableHydrationNodeLookup()
   return res
 }
 
+export function withHydration(container: ParentNode, fn: () => void): void {
+  const setup = () => setInsertionState(container)
+  const cleanup = () => resetInsertionState()
+  return performHydration(fn, setup, cleanup)
+}
+
+export function hydrateNode(node: Node, fn: () => void): void {
+  const setup = () => (currentHydrationNode = node)
+  const cleanup = () => {}
+  return performHydration(fn, setup, cleanup)
+}
+
 export let adoptTemplate: (node: Node, template: string) => Node | null
 export let locateHydrationNode: () => void
 
 type Anchor = Comment & {
-  // cached matching fragment start to avoid repeated traversal
+  // cached matching fragment end to avoid repeated traversal
   // on nested fragments
-  $fs?: Anchor
+  $fe?: Anchor
 }
 
-const isComment = (node: Node, data: string): node is Anchor =>
+export const isComment = (node: Node, data: string): node is Anchor =>
   node.nodeType === 8 && (node as Comment).data === data
 
+export function setCurrentHydrationNode(node: Node | null): void {
+  currentHydrationNode = node
+}
+
+function locateNextSiblingOfParent(n: Node): Node | null {
+  if (!n.parentNode) return null
+  return n.parentNode.nextSibling || locateNextSiblingOfParent(n.parentNode)
+}
+
+export function advanceHydrationNode(
+  node: Node & { $pns?: Node | null },
+): void {
+  // if no next sibling, find the next node in the parent chain
+  const ret =
+    node.nextSibling ||
+    // pns is short for "parent next sibling"
+    node.$pns ||
+    (node.$pns = locateNextSiblingOfParent(node))
+  if (ret) setCurrentHydrationNode(ret)
+}
+
 /**
  * Locate the first non-fragment-comment node and locate the next node
  * while handling potential fragments.
  */
 function adoptTemplateImpl(node: Node, template: string): Node | null {
   if (!(template[0] === '<' && template[1] === '!')) {
-    while (node.nodeType === 8) node = next(node)
-  }
+    while (node.nodeType === 8) {
+      node = node.nextSibling!
 
-  if (__DEV__) {
-    const type = node.nodeType
-    if (
-      (type === 8 && !template.startsWith('<!')) ||
-      (type === 1 &&
-        !template.startsWith(`<` + (node as Element).tagName.toLowerCase())) ||
-      (type === 3 &&
-        template.trim() &&
-        !template.startsWith((node as Text).data))
-    ) {
-      // TODO recover and provide more info
-      warn(`adopted: `, node)
-      warn(`template: ${template}`)
-      warn('hydration mismatch!')
+      // empty text node in slot
+      if (
+        template.trim() === '' &&
+        isComment(node, ']') &&
+        isComment(node.previousSibling!, '[')
+      ) {
+        node.before((node = createTextNode()))
+        break
+      }
     }
   }
 
-  currentHydrationNode = next(node)
+  const type = node.nodeType
+  if (
+    // comment node
+    (type === 8 && !template.startsWith('<!')) ||
+    // element node
+    (type === 1 &&
+      !template.startsWith(`<` + (node as Element).tagName.toLowerCase()))
+  ) {
+    node = handleMismatch(node, template)
+  }
+
+  currentHydrationNode = node.nextSibling
   return node
 }
 
-function locateHydrationNodeImpl() {
+export function locateNextNode(node: Node): Node | null {
+  return isComment(node, '[')
+    ? _next(locateEndAnchor(node)!)
+    : isComment(node, 'teleport start')
+      ? _next(locateEndAnchor(node, 'teleport start', 'teleport end')!)
+      : _next(node)
+}
+
+function locateHydrationNodeImpl(): void {
   let node: Node | null
+  if (insertionAnchor !== undefined) {
+    const { $lpn: lastPrepend, $lan: lastAppend, firstChild } = insertionParent!
+    // prepend
+    if (insertionAnchor === 0) {
+      node = insertionParent!.$lpn = lastPrepend
+        ? locateNextNode(lastPrepend)
+        : firstChild
+    }
+    // insert
+    else if (insertionAnchor instanceof Node) {
+      const { $lin: lastInsertedNode } = insertionAnchor as ChildItem
+      node = (insertionAnchor as ChildItem).$lin = lastInsertedNode
+        ? locateNextNode(lastInsertedNode)
+        : insertionAnchor
+    }
+    // append
+    else {
+      node = insertionParent!.$lan = lastAppend
+        ? locateNextNode(lastAppend)
+        : insertionAnchor === null
+          ? firstChild
+          : locateChildByLogicalIndex(insertionParent!, insertionAnchor)!
+    }
 
-  // prepend / firstChild
-  if (insertionAnchor === 0) {
-    node = child(insertionParent!)
+    insertionParent!.$llc = node
+    ;(node as ChildItem).$idx = insertionParent!.$curIdx =
+      insertionParent!.$curIdx === undefined ? 0 : insertionParent!.$curIdx + 1
   } else {
-    node = insertionAnchor
-      ? insertionAnchor.previousSibling
-      : insertionParent
-        ? insertionParent.lastChild
-        : currentHydrationNode
-
-    if (node && isComment(node, ']')) {
-      // fragment backward search
-      if (node.$fs) {
-        // already cached matching fragment start
-        node = node.$fs
-      } else {
-        let cur: Node | null = node
-        let curFragEnd = node
-        let fragDepth = 0
-        node = null
-        while (cur) {
-          cur = cur.previousSibling
-          if (cur) {
-            if (isComment(cur, '[')) {
-              curFragEnd.$fs = cur
-              if (!fragDepth) {
-                node = cur
-                break
-              } else {
-                fragDepth--
-              }
-            } else if (isComment(cur, ']')) {
-              curFragEnd = cur
-              fragDepth++
-            }
-          }
-        }
-      }
+    node = currentHydrationNode
+    if (insertionParent && (!node || node.parentNode !== insertionParent)) {
+      node = insertionParent.firstChild
     }
   }
 
   if (__DEV__ && !node) {
-    // TODO more info
-    warn('Hydration mismatch in ', insertionParent)
+    throw new Error(
+      `No current hydration node was found.\n` +
+        `this is likely a Vue internal bug.`,
+    )
   }
 
   resetInsertionState()
   currentHydrationNode = node
 }
+
+export function locateEndAnchor(
+  node: Anchor,
+  open = '[',
+  close = ']',
+): Node | null {
+  // already cached matching end
+  if (node.$fe) {
+    return node.$fe
+  }
+
+  const stack: Anchor[] = [node]
+  while ((node = node.nextSibling as Anchor) && stack.length > 0) {
+    if (node.nodeType === 8) {
+      if (node.data === open) {
+        stack.push(node)
+      } else if (node.data === close) {
+        const matchingOpen = stack.pop()!
+        matchingOpen.$fe = node
+        if (stack.length === 0) return node
+      }
+    }
+  }
+
+  return null
+}
+export function locateFragmentEndAnchor(label: string = ']'): Comment | null {
+  let node = currentHydrationNode!
+  while (node) {
+    if (isComment(node, label)) return node
+    node = node.nextSibling!
+  }
+  return null
+}
+
+function handleMismatch(node: Node, template: string): Node {
+  if (!isMismatchAllowed(node.parentElement!, MismatchTypes.CHILDREN)) {
+    ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
+      warn(
+        `Hydration node mismatch:\n- rendered on server:`,
+        node,
+        node.nodeType === 3
+          ? `(text)`
+          : isComment(node, '[[')
+            ? `(start of block node)`
+            : ``,
+        `\n- expected on client:`,
+        template,
+      )
+    logMismatchError()
+  }
+
+  // fragment
+  if (isComment(node, '[')) {
+    removeFragmentNodes(node)
+  }
+
+  const next = _next(node)
+  const container = parentNode(node)!
+  remove(node, container)
+
+  // fast path for text nodes
+  if (template[0] !== '<') {
+    return container.insertBefore(createTextNode(template), next)
+  }
+
+  // element node
+  const t = createElement('template') as HTMLTemplateElement
+  t.innerHTML = template
+  const newNode = _child(t.content).cloneNode(true) as Element
+  newNode.innerHTML = (node as Element).innerHTML
+  Array.from((node as Element).attributes).forEach(attr => {
+    newNode.setAttribute(attr.name, attr.value)
+  })
+  container.insertBefore(newNode, next)
+  return newNode
+}
+
+let hasLoggedMismatchError = false
+export const logMismatchError = (): void => {
+  if (__TEST__ || hasLoggedMismatchError) {
+    return
+  }
+  // this error should show up in production
+  console.error('Hydration completed but contains mismatches.')
+  hasLoggedMismatchError = true
+}
+
+export function removeFragmentNodes(node: Node, endAnchor?: Node): void {
+  const end = endAnchor || locateEndAnchor(node as Anchor)
+  while (true) {
+    const next = _next(node)
+    if (next && next !== end) {
+      remove(next, parentNode(node)!)
+    } else {
+      break
+    }
+  }
+}
index 26cb66c462cf6634b1b3f54c1b20881cfc557350..4144aa7a081a592735210783f890848742baa7d5 100644 (file)
-/*! #__NO_SIDE_EFFECTS__ */
+import type { ChildItem, InsertionParent } from '../insertionState'
+import { isComment, locateEndAnchor } from './hydration'
+
+/* @__NO_SIDE_EFFECTS__ */
 export function createElement(tagName: string): HTMLElement {
   return document.createElement(tagName)
 }
 
-/*! #__NO_SIDE_EFFECTS__ */
+/* @__NO_SIDE_EFFECTS__ */
 export function createTextNode(value = ''): Text {
   return document.createTextNode(value)
 }
 
-/*! #__NO_SIDE_EFFECTS__ */
+/* @__NO_SIDE_EFFECTS__ */
 export function createComment(data: string): Comment {
   return document.createComment(data)
 }
 
-/*! #__NO_SIDE_EFFECTS__ */
+/* @__NO_SIDE_EFFECTS__ */
 export function querySelector(selectors: string): Element | null {
   return document.querySelector(selectors)
 }
 
-/*! #__NO_SIDE_EFFECTS__ */
-export function child(node: ParentNode): Node {
+/*! @__NO_SIDE_EFFECTS__ */
+export function parentNode(node: Node): ParentNode | null {
+  return node.parentNode
+}
+
+/* @__NO_SIDE_EFFECTS__ */
+const _txt: typeof _child = _child
+
+/**
+ * Hydration-specific version of `txt`.
+ */
+/* @__NO_SIDE_EFFECTS__ */
+const __txt = (node: ParentNode): Node => {
+  let n = node.firstChild!
+
+  // since SSR doesn't generate blank text nodes,
+  // manually insert a text node as the first child
+  if (!n) {
+    return node.appendChild(createTextNode())
+  }
+
+  return n
+}
+
+/* @__NO_SIDE_EFFECTS__ */
+export function _child(node: InsertionParent): Node {
   return node.firstChild!
 }
 
-/*! #__NO_SIDE_EFFECTS__ */
-export function nthChild(node: Node, i: number): Node {
+/**
+ * Hydration-specific version of `child`.
+ */
+/* @__NO_SIDE_EFFECTS__ */
+export function __child(node: ParentNode, logicalIndex: number = 0): Node {
+  return locateChildByLogicalIndex(node as InsertionParent, logicalIndex)!
+}
+
+/* @__NO_SIDE_EFFECTS__ */
+export function _nthChild(node: InsertionParent, i: number): Node {
   return node.childNodes[i]
 }
 
-/*! #__NO_SIDE_EFFECTS__ */
-export function next(node: Node): Node {
+/**
+ * Hydration-specific version of `nthChild`.
+ */
+/* @__NO_SIDE_EFFECTS__ */
+export function __nthChild(node: Node, logicalIndex: number): Node {
+  return locateChildByLogicalIndex(node as InsertionParent, logicalIndex)!
+}
+
+/* @__NO_SIDE_EFFECTS__ */
+export function _next(node: Node): Node {
   return node.nextSibling!
 }
+
+/**
+ * Hydration-specific version of `next`.
+ */
+/* @__NO_SIDE_EFFECTS__ */
+export function __next(node: Node, logicalIndex: number): Node {
+  return locateChildByLogicalIndex(
+    node.parentNode! as InsertionParent,
+    logicalIndex,
+  )!
+}
+
+type DelegatedFunction<T extends (...args: any[]) => any> = T & {
+  impl: T
+}
+
+/* @__NO_SIDE_EFFECTS__ */
+export const txt: DelegatedFunction<typeof _txt> = (...args) => {
+  return txt.impl(...args)
+}
+txt.impl = _txt
+
+/* @__NO_SIDE_EFFECTS__ */
+export const child: DelegatedFunction<typeof _child> = (...args) => {
+  return child.impl(...args)
+}
+child.impl = _child
+
+/* @__NO_SIDE_EFFECTS__ */
+export const next: DelegatedFunction<typeof _next> = (...args) => {
+  return next.impl(...args)
+}
+next.impl = _next
+
+/* @__NO_SIDE_EFFECTS__ */
+export const nthChild: DelegatedFunction<typeof _nthChild> = (...args) => {
+  return nthChild.impl(...args)
+}
+nthChild.impl = _nthChild
+
+/**
+ * Enables hydration-specific node lookup behavior.
+ *
+ * Temporarily switches the implementations of the exported
+ * `txt`, `child`, `next`, and `nthChild` functions to their hydration-specific
+ * versions (`__txt`, `__child`, `__next`, `__nthChild`). This allows traversal
+ * logic to correctly handle SSR comment anchors during hydration.
+ */
+export function enableHydrationNodeLookup(): void {
+  txt.impl = __txt
+  child.impl = __child as typeof _child
+  next.impl = __next as typeof _next
+  nthChild.impl = __nthChild as any as typeof _nthChild
+}
+
+export function disableHydrationNodeLookup(): void {
+  txt.impl = _txt
+  child.impl = _child
+  next.impl = _next
+  nthChild.impl = _nthChild
+}
+
+export function locateChildByLogicalIndex(
+  parent: InsertionParent,
+  logicalIndex: number,
+): Node | null {
+  let child = (parent.$llc || parent.firstChild) as ChildItem
+  let fromIndex = child.$idx || 0
+
+  while (child) {
+    if (fromIndex === logicalIndex) {
+      child.$idx = logicalIndex
+      return (parent.$llc = child)
+    }
+
+    child = (
+      isComment(child, '[')
+        ? // fragment start: jump to the node after the matching end anchor
+          locateEndAnchor(child)!.nextSibling
+        : child.nextSibling
+    ) as ChildItem
+
+    fromIndex++
+  }
+
+  return null
+}
+
+// use fragment end anchor as the logical child to avoid locateEndAnchor calls
+// in locateChildByLogicalIndex
+export function updateLastLogicalChild(
+  parent: InsertionParent,
+  child: Node,
+): void {
+  if (!isComment(child, ']')) return
+  ;(child as any as ChildItem).$idx = parent.$curIdx || 0
+  parent.$llc = child
+}
index 2d60cf6b283ae11095dbd5dd9c1ab6f2b13a68fc..9cd0f2149148188f0d33b72a8674d4365c184c0d 100644 (file)
@@ -1,28 +1,41 @@
 import {
   type NormalizedStyle,
   canSetValueDirectly,
+  includeBooleanAttr,
   isArray,
   isOn,
   isString,
   normalizeClass,
   normalizeStyle,
   parseStringStyle,
+  stringifyStyle,
   toDisplayString,
 } from '@vue/shared'
 import { on } from './event'
 import {
+  MismatchTypes,
   currentInstance,
+  getAttributeMismatch,
+  isMapEqual,
+  isMismatchAllowed,
+  isSetEqual,
+  isValidHtmlOrSvgAttribute,
   mergeProps,
   patchStyle,
   shouldSetAsProp,
+  toClassSet,
+  toStyleMap,
   unsafeToTrustedHTML,
+  vShowHidden,
   warn,
+  warnPropMismatch,
 } from '@vue/runtime-dom'
 import {
   type VaporComponentInstance,
   isApplyingFallthroughProps,
   isVaporComponent,
 } from '../component'
+import { isHydrating, logMismatchError } from './hydration'
 import type { Block } from '../block'
 
 type TargetElement = Element & {
@@ -61,6 +74,15 @@ export function setAttr(el: any, key: string, value: any): void {
     ;(el as any)._falseValue = value
   }
 
+  if (
+    (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
+    isHydrating &&
+    !attributeHasMismatch(el, key, value)
+  ) {
+    el[`$${key}`] = value
+    return
+  }
+
   if (value !== el[`$${key}`]) {
     el[`$${key}`] = value
     if (value != null) {
@@ -71,11 +93,26 @@ export function setAttr(el: any, key: string, value: any): void {
   }
 }
 
-export function setDOMProp(el: any, key: string, value: any): void {
+export function setDOMProp(
+  el: any,
+  key: string,
+  value: any,
+  forceHydrate: boolean = false,
+): void {
   if (!isApplyingFallthroughProps && el.$root && hasFallthroughKey(key)) {
     return
   }
 
+  if (
+    (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
+    isHydrating &&
+    !attributeHasMismatch(el, key, value) &&
+    !shouldForceHydrate(el, key) &&
+    !forceHydrate
+  ) {
+    return
+  }
+
   const prev = el[key]
   if (value === prev) {
     return
@@ -84,7 +121,9 @@ export function setDOMProp(el: any, key: string, value: any): void {
   let needRemove = false
   if (value === '' || value == null) {
     const type = typeof prev
-    if (value == null && type === 'string') {
+    if (type === 'boolean') {
+      value = includeBooleanAttr(value)
+    } else if (value == null && type === 'string') {
       // e.g. <div :id="null">
       value = ''
       needRemove = true
@@ -116,15 +155,38 @@ export function setDOMProp(el: any, key: string, value: any): void {
 export function setClass(el: TargetElement, value: any): void {
   if (el.$root) {
     setClassIncremental(el, value)
-  } else if ((value = normalizeClass(value)) !== el.$cls) {
-    el.className = el.$cls = value
+  } else {
+    value = normalizeClass(value)
+    if (
+      (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
+      isHydrating &&
+      !classHasMismatch(el, value, false)
+    ) {
+      el.$cls = value
+      return
+    }
+
+    if (value !== el.$cls) {
+      el.className = el.$cls = value
+    }
   }
 }
 
 function setClassIncremental(el: any, value: any): void {
   const cacheKey = `$clsi${isApplyingFallthroughProps ? '$' : ''}`
+  const normalizedValue = normalizeClass(value)
+
+  if (
+    (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
+    isHydrating &&
+    !classHasMismatch(el, normalizedValue, true)
+  ) {
+    el[cacheKey] = normalizedValue
+    return
+  }
+
   const prev = el[cacheKey]
-  if ((value = el[cacheKey] = normalizeClass(value)) !== prev) {
+  if ((value = el[cacheKey] = normalizedValue) !== prev) {
     const nextList = value.split(/\s+/)
     if (value) {
       el.classList.add(...nextList)
@@ -141,23 +203,43 @@ export function setStyle(el: TargetElement, value: any): void {
   if (el.$root) {
     setStyleIncremental(el, value)
   } else {
-    const prev = el.$sty
-    value = el.$sty = normalizeStyle(value)
-    patchStyle(el, prev, value)
+    const normalizedValue = normalizeStyle(value)
+    if (
+      (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
+      isHydrating &&
+      !styleHasMismatch(el, value, normalizedValue, false)
+    ) {
+      el.$sty = normalizedValue
+      return
+    }
+
+    patchStyle(el, el.$sty, (el.$sty = normalizedValue))
   }
 }
 
 function setStyleIncremental(el: any, value: any): NormalizedStyle | undefined {
   const cacheKey = `$styi${isApplyingFallthroughProps ? '$' : ''}`
-  const prev = el[cacheKey]
-  value = el[cacheKey] = isString(value)
+  const normalizedValue = isString(value)
     ? parseStringStyle(value)
     : (normalizeStyle(value) as NormalizedStyle | undefined)
-  patchStyle(el, prev, value)
-  return value
+
+  if (
+    (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
+    isHydrating &&
+    !styleHasMismatch(el, value, normalizedValue, true)
+  ) {
+    el[cacheKey] = normalizedValue
+    return
+  }
+
+  patchStyle(el, el[cacheKey], (el[cacheKey] = normalizedValue))
 }
 
-export function setValue(el: TargetElement, value: any): void {
+export function setValue(
+  el: TargetElement,
+  value: any,
+  forceHydrate: boolean = false,
+): void {
   if (!isApplyingFallthroughProps && el.$root && hasFallthroughKey('value')) {
     return
   }
@@ -165,6 +247,17 @@ export function setValue(el: TargetElement, value: any): void {
   // store value as _value as well since
   // non-string values will be stringified.
   el._value = value
+
+  if (
+    (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
+    isHydrating &&
+    !attributeHasMismatch(el, 'value', getClientText(el, value)) &&
+    !shouldForceHydrate(el, 'value') &&
+    !forceHydrate
+  ) {
+    return
+  }
+
   // #4956: <option> value will fallback to its text content so we need to
   // compare against its attribute value instead.
   const oldValue = el.tagName === 'OPTION' ? el.getAttribute('value') : el.value
@@ -183,6 +276,23 @@ export function setValue(el: TargetElement, value: any): void {
  * `toDisplayString`
  */
 export function setText(el: Text & { $txt?: string }, value: string): void {
+  if (isHydrating) {
+    const clientText = getClientText(el.parentNode!, value)
+    if (el.nodeValue == clientText) {
+      el.$txt = clientText
+      return
+    }
+
+    ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
+      warn(
+        `Hydration text mismatch in`,
+        el.parentNode,
+        `\n  - rendered on server: ${JSON.stringify((el as Text).data)}` +
+          `\n  - expected on client: ${JSON.stringify(value)}`,
+      )
+    logMismatchError()
+  }
+
   if (el.$txt !== value) {
     el.nodeValue = el.$txt = value
   }
@@ -195,7 +305,27 @@ export function setElementText(
   el: Node & { $txt?: string },
   value: unknown,
 ): void {
-  if (el.$txt !== (value = toDisplayString(value))) {
+  value = toDisplayString(value)
+  if (isHydrating) {
+    let clientText = getClientText(el, value as string)
+    if (el.textContent === clientText) {
+      el.$txt = clientText
+      return
+    }
+
+    if (!isMismatchAllowed(el as Element, MismatchTypes.TEXT)) {
+      ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
+        warn(
+          `Hydration text content mismatch on`,
+          el,
+          `\n  - rendered on server: ${el.textContent}` +
+            `\n  - expected on client: ${clientText}`,
+        )
+      logMismatchError()
+    }
+  }
+
+  if (el.$txt !== value) {
     el.textContent = el.$txt = value as string
   }
 }
@@ -303,6 +433,7 @@ export function setDynamicProp(
 ): void {
   // TODO
   const isSVG = false
+  let forceHydrate = false
   if (key === 'class') {
     setClass(el, value)
   } else if (key === 'style') {
@@ -310,7 +441,8 @@ export function setDynamicProp(
   } else if (isOn(key)) {
     on(el, key[2].toLowerCase() + key.slice(3), value, { effect: true })
   } else if (
-    key[0] === '.'
+    // force hydrate v-bind with .prop modifiers
+    (forceHydrate = key[0] === '.')
       ? ((key = key.slice(1)), true)
       : key[0] === '^'
         ? ((key = key.slice(1)), false)
@@ -321,12 +453,11 @@ export function setDynamicProp(
     } else if (key === 'textContent') {
       setElementText(el, value)
     } else if (key === 'value' && canSetValueDirectly(el.tagName)) {
-      setValue(el, value)
+      setValue(el, value, forceHydrate)
     } else {
-      setDOMProp(el, key, value)
+      setDOMProp(el, key, value, forceHydrate)
     }
   } else {
-    // TODO special case for <input v-model type="checkbox">
     setAttr(el, key, value)
   }
   return value
@@ -343,8 +474,7 @@ export function optimizePropertyLookup(): void {
   const proto = Element.prototype as any
   proto.$transition = undefined
   proto.$key = undefined
-  proto.$evtclick = undefined
-  proto.$anchor = proto.$evtclick = undefined
+  proto.$fc = proto.$evtclick = undefined
   proto.$root = false
   proto.$html =
     proto.$txt =
@@ -353,3 +483,104 @@ export function optimizePropertyLookup(): void {
     (Text.prototype as any).$txt =
       ''
 }
+
+function classHasMismatch(
+  el: TargetElement | any,
+  expected: string,
+  isIncremental: boolean,
+): boolean {
+  const actual = el.getAttribute('class')
+  const actualClassSet = toClassSet(actual || '')
+  const expectedClassSet = toClassSet(expected)
+
+  let hasMismatch: boolean = false
+  if (isIncremental) {
+    if (expected) {
+      hasMismatch = Array.from(expectedClassSet).some(
+        cls => !actualClassSet.has(cls),
+      )
+    }
+  } else {
+    hasMismatch = !isSetEqual(actualClassSet, expectedClassSet)
+  }
+
+  if (hasMismatch) {
+    warnPropMismatch(el, 'class', MismatchTypes.CLASS, actual, expected)
+    logMismatchError()
+    return true
+  }
+
+  return false
+}
+
+function styleHasMismatch(
+  el: TargetElement | any,
+  value: any,
+  normalizedValue: string | NormalizedStyle | undefined,
+  isIncremental: boolean,
+): boolean {
+  const actual = el.getAttribute('style')
+  const actualStyleMap = toStyleMap(actual || '')
+  const expected = isString(value) ? value : stringifyStyle(normalizedValue)
+  const expectedStyleMap = toStyleMap(expected)
+
+  // If `v-show=false`, `display: 'none'` should be added to expected
+  if (el[vShowHidden]) {
+    expectedStyleMap.set('display', 'none')
+  }
+
+  // TODO: handle css vars
+
+  let hasMismatch: boolean = false
+  if (isIncremental) {
+    if (expected) {
+      // check if the expected styles are present in the actual styles
+      hasMismatch = Array.from(expectedStyleMap.entries()).some(
+        ([key, val]) => actualStyleMap.get(key) !== val,
+      )
+    }
+  } else {
+    hasMismatch = !isMapEqual(actualStyleMap, expectedStyleMap)
+  }
+
+  if (hasMismatch) {
+    warnPropMismatch(el, 'style', MismatchTypes.STYLE, actual, expected)
+    logMismatchError()
+    return true
+  }
+
+  return false
+}
+
+function attributeHasMismatch(el: any, key: string, value: any): boolean {
+  if (isValidHtmlOrSvgAttribute(el, key)) {
+    const { actual, expected } = getAttributeMismatch(el, key, value)
+    if (actual !== expected) {
+      warnPropMismatch(el, key, MismatchTypes.ATTRIBUTE, actual, expected)
+      logMismatchError()
+      return true
+    }
+  }
+  return false
+}
+
+function getClientText(el: Node, value: string): string {
+  if (
+    value[0] === '\n' &&
+    ((el as Element).tagName === 'PRE' ||
+      (el as Element).tagName === 'TEXTAREA')
+  ) {
+    value = value.slice(1)
+  }
+  return value
+}
+
+function shouldForceHydrate(el: Element, key: string): boolean {
+  const { tagName } = el
+  return (
+    ((tagName === 'INPUT' || tagName === 'OPTION') &&
+      (key.endsWith('value') || key === 'indeterminate')) ||
+    // force hydrate custom element dynamic props
+    tagName.includes('-')
+  )
+}
index 7bfbca4e52b862a303f3f0ead9af091053201ba2..5db46476f71415da96139410e2b65257a8fe3773 100644 (file)
@@ -1,5 +1,5 @@
-import { child, createElement, createTextNode } from './node'
 import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration'
+import { _child, createElement, createTextNode } from './node'
 
 let t: HTMLTemplateElement
 
@@ -8,12 +8,13 @@ export function template(html: string, root?: boolean) {
   let node: Node
   return (): Node & { $root?: true } => {
     if (isHydrating) {
-      if (__DEV__ && !currentHydrationNode) {
-        // TODO this should not happen
-        throw new Error('No current hydration node')
-      }
-      return adoptTemplate(currentHydrationNode!, html)!
+      // do not cache the adopted node in node because it contains child nodes
+      // this avoids duplicate rendering of children
+      const adopted = adoptTemplate(currentHydrationNode!, html)!
+      if (root) (adopted as any).$root = true
+      return adopted
     }
+
     // fast path for text nodes
     if (html[0] !== '<') {
       return createTextNode(html)
@@ -21,7 +22,7 @@ export function template(html: string, root?: boolean) {
     if (!node) {
       t = t || createElement('template')
       t.innerHTML = html
-      node = child(t.content)
+      node = _child(t.content)
     }
     const ret = node.cloneNode(true)
     if (root) (ret as any).$root = true
index 3ac1e09bf80159a748f7fcaa542ec7fc4878b3af..07f1243e4e5076acf4b4cba41af9c5d9f8eb9440 100644 (file)
@@ -5,15 +5,18 @@ import {
   type BlockFn,
   type TransitionOptions,
   type VaporTransitionHooks,
+  findBlockNode,
   insert,
   isValidBlock,
   remove,
 } from './block'
 import {
+  type GenericComponentInstance,
   type TransitionHooks,
   type VNode,
   currentInstance,
   isKeepAlive,
+  queuePostFlushCb,
 } from '@vue/runtime-dom'
 import type { VaporComponentInstance } from './component'
 import type { NodeRef } from './apiTemplateRef'
@@ -22,28 +25,36 @@ import {
   applyTransitionHooks,
   applyTransitionLeaveHooks,
 } from './components/Transition'
+import {
+  currentHydrationNode,
+  isComment,
+  isHydrating,
+  locateFragmentEndAnchor,
+  locateHydrationNode,
+} from './dom/hydration'
 
 export class VaporFragment<T extends Block = Block>
   implements TransitionOptions
 {
+  $key?: any
+  $transition?: VaporTransitionHooks | undefined
   nodes: T
   vnode?: VNode | null = null
   anchor?: Node
-  setRef?: (
-    instance: VaporComponentInstance,
-    ref: NodeRef,
-    refFor: boolean,
-    refKey: string | undefined,
-  ) => void
   fallback?: BlockFn
-  $key?: any
-  $transition?: VaporTransitionHooks | undefined
   insert?: (
     parent: ParentNode,
     anchor: Node | null,
     transitionHooks?: TransitionHooks,
   ) => void
   remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void
+  hydrate?: (...args: any[]) => void
+  setRef?: (
+    instance: VaporComponentInstance,
+    ref: NodeRef,
+    refFor: boolean,
+    refKey: string | undefined,
+  ) => void
 
   constructor(nodes: T) {
     this.nodes = nodes
@@ -57,42 +68,35 @@ export class ForFragment extends VaporFragment<Block[]> {
 }
 
 export class DynamicFragment extends VaporFragment {
-  anchor: Node
+  anchor!: Node
   scope: EffectScope | undefined
   current?: BlockFn
+  fallback?: BlockFn
+  anchorLabel?: string
 
   constructor(anchorLabel?: string) {
     super([])
-    this.anchor =
-      __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
+    if (isHydrating) {
+      this.anchorLabel = anchorLabel
+      locateHydrationNode()
+    } else {
+      this.anchor =
+        __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
+    }
   }
 
   update(render?: BlockFn, key: any = render): void {
     if (key === this.current) {
+      if (isHydrating) this.hydrate(true)
       return
     }
     this.current = key
 
     const prevSub = setActiveSub()
-    const parent = this.anchor.parentNode
+    const parent = isHydrating ? null : this.anchor.parentNode
     const transition = this.$transition
-    const renderBranch = () => {
-      if (render) {
-        this.scope = new EffectScope()
-        this.nodes = this.scope.run(render) || []
-        if (isKeepAlive(instance)) {
-          ;(instance as KeepAliveInstance).process(this.nodes)
-        }
-        if (transition) {
-          this.$transition = applyTransitionHooks(this.nodes, transition)
-        }
-        if (parent) insert(this.nodes, parent, this.anchor)
-      } else {
-        this.scope = undefined
-        this.nodes = []
-      }
-    }
     const instance = currentInstance!
+
     // teardown previous branch
     if (this.scope) {
       if (isKeepAlive(instance)) {
@@ -102,7 +106,9 @@ export class DynamicFragment extends VaporFragment {
       }
       const mode = transition && transition.mode
       if (mode) {
-        applyTransitionLeaveHooks(this.nodes, transition, renderBranch)
+        applyTransitionLeaveHooks(this.nodes, transition, () =>
+          this.render(render, instance, transition, parent),
+        )
         parent && remove(this.nodes, parent)
         if (mode === 'out-in') {
           setActiveSub(prevSub)
@@ -113,7 +119,7 @@ export class DynamicFragment extends VaporFragment {
       }
     }
 
-    renderBranch()
+    this.render(render, instance, transition, parent)
 
     if (this.fallback) {
       // set fallback for nested fragments
@@ -139,6 +145,84 @@ export class DynamicFragment extends VaporFragment {
     }
 
     setActiveSub(prevSub)
+
+    if (isHydrating) this.hydrate()
+  }
+
+  private render(
+    render: BlockFn | undefined,
+    instance: GenericComponentInstance,
+    transition: VaporTransitionHooks | undefined,
+    parent: ParentNode | null,
+  ) {
+    if (render) {
+      this.scope = new EffectScope()
+      this.nodes = this.scope.run(render) || []
+      if (isKeepAlive(instance)) {
+        ;(instance as KeepAliveInstance).process(this.nodes)
+      }
+      if (transition) {
+        this.$transition = applyTransitionHooks(this.nodes, transition)
+      }
+      if (parent) insert(this.nodes, parent, this.anchor)
+    } else {
+      this.scope = undefined
+      this.nodes = []
+    }
+  }
+
+  hydrate = (isEmpty = false): void => {
+    // avoid repeated hydration during fallback rendering
+    if (this.anchor) return
+
+    if (this.anchorLabel === 'if') {
+      // reuse the empty comment node as the anchor for empty if
+      // e.g. `<div v-if="false"></div>` -> `<!---->`
+      if (isEmpty) {
+        this.anchor = locateFragmentEndAnchor('')!
+        if (__DEV__ && !this.anchor) {
+          throw new Error(
+            'Failed to locate if anchor. this is likely a Vue internal bug.',
+          )
+        } else {
+          if (__DEV__) {
+            ;(this.anchor as Comment).data = this.anchorLabel
+          }
+          return
+        }
+      }
+    } else if (this.anchorLabel === 'slot') {
+      // reuse the empty comment node for empty slot
+      // e.g. `<slot v-if="false"></slot>`
+      if (isEmpty && isComment(currentHydrationNode!, '')) {
+        this.anchor = currentHydrationNode!
+        if (__DEV__) {
+          ;(this.anchor as Comment).data = this.anchorLabel!
+        }
+        return
+      }
+
+      // reuse the vdom fragment end anchor
+      this.anchor = locateFragmentEndAnchor()!
+      if (__DEV__ && !this.anchor) {
+        throw new Error(
+          'Failed to locate slot anchor. this is likely a Vue internal bug.',
+        )
+      } else {
+        return
+      }
+    }
+
+    const { parentNode, nextNode } = findBlockNode(this.nodes)!
+    // create an anchor
+    queuePostFlushCb(() => {
+      parentNode!.insertBefore(
+        (this.anchor = __DEV__
+          ? createComment(this.anchorLabel!)
+          : createTextNode()),
+        nextNode,
+      )
+    })
   }
 }
 
index f98c7477baab8285d74cd8d10e17e7f73cad939a..3e76b6955d33d9da78277e905e46131c283c5860 100644 (file)
@@ -18,7 +18,7 @@ export {
 export { renderEffect } from './renderEffect'
 export { createSlot, forwardedSlotCreator } from './componentSlots'
 export { template } from './dom/template'
-export { createTextNode, child, nthChild, next } from './dom/node'
+export { createTextNode, child, nthChild, next, txt } from './dom/node'
 export {
   setText,
   setBlockText,
index 8c66843bd93cdd067fbe0cda6b1d98d23ba75562..993dc65b0d2f380579f100914598210b2a27ac68 100644 (file)
@@ -1,5 +1,31 @@
-export let insertionParent: ParentNode | undefined
-export let insertionAnchor: Node | 0 | undefined
+import { isHydrating } from './dom/hydration'
+export type ChildItem = ChildNode & {
+  // logical index, used during hydration to locate the node
+  $idx: number
+  // last inserted node
+  $lin?: Node | null
+}
+
+export type InsertionParent = ParentNode & {
+  // cache the first child for potential consecutive prepends
+  $fc?: Node | null
+
+  // last located logical child
+  $llc?: Node | null
+  // last prepend node
+  $lpn?: Node | null
+  // last append node
+  $lan?: Node | null
+  // the logical index of current hydration node
+  $curIdx?: number
+}
+export let insertionParent: InsertionParent | undefined
+export let insertionAnchor: Node | 0 | undefined | null
+
+// indicates whether the insertion is the last one in the parent.
+// if true, means no more nodes need to be hydrated after this insertion,
+// advancing current hydration node to parent nextSibling
+export let isLastInsertion: boolean | undefined
 
 /**
  * This function is called before a block type that requires insertion
@@ -7,21 +33,30 @@ export let insertionAnchor: Node | 0 | undefined
  * insertion on client-side render, and used for node adoption during hydration.
  */
 export function setInsertionState(
-  parent: ParentNode & { $anchor?: Node | null },
-  anchor?: Node | 0,
+  parent: ParentNode & { $fc?: Node | null },
+  anchor?: Node | 0 | null | number,
+  last?: boolean,
 ): void {
-  // When setInsertionState(n3, 0) is called consecutively, the first prepend operation
-  // uses parent.firstChild as the anchor. However, after insertion, parent.firstChild
-  // changes and cannot serve as the anchor for subsequent prepends. Therefore, we cache
-  // the original parent.firstChild on the first call for subsequent prepend operations.
-  if (anchor === 0 && !parent.$anchor) {
-    parent.$anchor = parent.firstChild
-  }
-
   insertionParent = parent
-  insertionAnchor = anchor
+  isLastInsertion = last
+
+  if (anchor !== undefined) {
+    if (isHydrating) {
+      insertionAnchor = anchor as Node
+    } else {
+      // special handling append anchor value to null
+      insertionAnchor =
+        typeof anchor === 'number' && anchor > 0 ? null : (anchor as Node)
+
+      if (anchor === 0 && !parent.$fc) {
+        parent.$fc = parent.firstChild
+      }
+    }
+  } else {
+    insertionAnchor = undefined
+  }
 }
 
 export function resetInsertionState(): void {
-  insertionParent = insertionAnchor = undefined
+  insertionParent = insertionAnchor = isLastInsertion = undefined
 }
index 3d6fba608dae121d0aaf27efff72f6673af9984c..85a7bc6ebb97eaa4aff3d3d281bb4b048c78e26b 100644 (file)
@@ -2,6 +2,8 @@ import {
   type App,
   type ComponentInternalInstance,
   type ConcreteComponent,
+  Fragment,
+  type HydrationRenderer,
   type KeepAliveContext,
   MoveType,
   type Plugin,
@@ -17,13 +19,16 @@ import {
   createInternalObject,
   createVNode,
   currentInstance,
+  ensureHydrationRenderer,
   ensureRenderer,
   ensureVaporSlotFallback,
   isEmitListener,
   isKeepAlive,
+  isRef,
   isVNode,
   normalizeRef,
   onScopeDispose,
+  queuePostFlushCb,
   renderSlot,
   setTransitionHooks as setVNodeTransitionHooks,
   shallowReactive,
@@ -39,6 +44,7 @@ import {
   type VaporComponent,
   VaporComponentInstance,
   createComponent,
+  isVaporComponent,
   mountComponent,
   unmountComponent,
 } from './component'
@@ -54,8 +60,17 @@ import {
 import { type RawProps, rawPropsProxyHandlers } from './componentProps'
 import type { RawSlots, VaporSlot } from './componentSlots'
 import { renderEffect } from './renderEffect'
-import { createTextNode } from './dom/node'
+import { _next, createTextNode } from './dom/node'
 import { optimizePropertyLookup } from './dom/prop'
+import {
+  advanceHydrationNode,
+  currentHydrationNode,
+  isComment,
+  isHydrating,
+  locateHydrationNode,
+  setCurrentHydrationNode,
+  hydrateNode as vaporHydrateNode,
+} from './dom/hydration'
 import { VaporFragment, isFragment, setFragmentFallback } from './fragment'
 import type { NodeRef } from './apiTemplateRef'
 import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition'
@@ -73,8 +88,13 @@ const vaporInteropImpl: Omit<
   'vdomMount' | 'vdomUnmount' | 'vdomSlot'
 > = {
   mount(vnode, container, anchor, parentComponent) {
-    const selfAnchor = (vnode.el = vnode.anchor = createTextNode())
-    container.insertBefore(selfAnchor, anchor)
+    let selfAnchor = (vnode.el = vnode.anchor = createTextNode())
+    if (isHydrating) {
+      // avoid vdom hydration children mismatch by the selfAnchor, delay its insertion
+      queuePostFlushCb(() => container.insertBefore(selfAnchor, anchor))
+    } else {
+      container.insertBefore(selfAnchor, anchor)
+    }
     const prev = currentInstance
     simpleSetCurrentInstance(parentComponent)
 
@@ -103,6 +123,8 @@ const vaporInteropImpl: Omit<
       {
         _: slotsRef, // pass the slots ref
       } as any as RawSlots,
+      undefined,
+      (parentComponent ? parentComponent.appContext : vnode.appContext) as any,
     ))
     instance.rawPropsRef = propsRef
     instance.rawSlotsRef = slotsRef
@@ -173,6 +195,28 @@ const vaporInteropImpl: Omit<
     insert(vnode.anchor as any, container, anchor)
   },
 
+  hydrate(vnode, node, container, anchor, parentComponent) {
+    vaporHydrateNode(node, () =>
+      this.mount(vnode, container, anchor, parentComponent),
+    )
+    return _next(node)
+  },
+  hydrateSlot(vnode, node) {
+    const { slot } = vnode.vs!
+    const propsRef = (vnode.vs!.ref = shallowRef(vnode.props))
+    vaporHydrateNode(node, () => {
+      vnode.vb = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
+      vnode.anchor = vnode.el = currentHydrationNode!
+
+      if (__DEV__ && !vnode.anchor) {
+        throw new Error(
+          `Failed to locate slot anchor. this is likely a Vue internal bug.`,
+        )
+      }
+    })
+    return _next(vnode.anchor as Node)
+  },
+
   setTransitionHooks(component, hooks) {
     setVaporTransitionHooks(component as any, hooks as VaporTransitionHooks)
   },
@@ -219,6 +263,8 @@ const vaporSlotsProxyHandler: ProxyHandler<any> = {
   },
 }
 
+let vdomHydrateNode: HydrationRenderer['hydrateNode'] | undefined
+
 /**
  * Mount vdom component in vapor
  */
@@ -277,7 +323,15 @@ function createVDOMComponent(
     internals.umt(vnode.component!, null, !!parentNode)
   }
 
+  frag.hydrate = () => {
+    hydrateVNode(vnode, parentInstance as any)
+    onScopeDispose(unmount, true)
+    isMounted = true
+    frag.nodes = vnode.el as any
+  }
+
   frag.insert = (parentNode, anchor, transition) => {
+    if (isHydrating) return
     if (vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
       vdomActivate(
         vnode,
@@ -364,27 +418,62 @@ function renderVDOMSlot(
 
   frag.fallback = fallback
   frag.insert = (parentNode, anchor) => {
+    if (isHydrating) return
+
     if (!isMounted) {
-      renderEffect(() => {
-        let vnode: VNode | undefined
-        let isValidSlot = false
-        // only render slot if rawSlots is defined and slot nodes are not empty
-        // otherwise, render fallback
-        if (slotsRef.value) {
-          vnode = renderSlot(
-            slotsRef.value,
-            isFunction(name) ? name() : name,
-            props,
-          )
+      render(parentNode, anchor)
+      isMounted = true
+    } else {
+      // move
+      internals.m(
+        oldVNode!,
+        parentNode,
+        anchor,
+        MoveType.REORDER,
+        parentComponent as any,
+      )
+    }
 
-          let children = vnode.children as any[]
-          // handle forwarded vapor slot without its own fallback
-          // use the fallback provided by the slot outlet
-          ensureVaporSlotFallback(children, fallback as any)
-          isValidSlot = children.length > 0
-        }
+    frag.remove = parentNode => {
+      if (fallbackNodes) {
+        remove(fallbackNodes, parentNode)
+      } else if (oldVNode) {
+        internals.um(oldVNode, parentComponent as any, null)
+      }
+    }
+  }
+
+  const render = (parentNode?: ParentNode, anchor?: Node | null) => {
+    renderEffect(() => {
+      let vnode: VNode | undefined
+      let isValidSlot = false
+      // only render slot if rawSlots is defined and slot nodes are not empty
+      // otherwise, render fallback
+      if (slotsRef.value) {
+        vnode = renderSlot(
+          slotsRef.value,
+          isFunction(name) ? name() : name,
+          props,
+        )
+
+        let children = vnode.children as any[]
+        // handle forwarded vapor slot without its own fallback
+        // use the fallback provided by the slot outlet
+        ensureVaporSlotFallback(children, fallback as any)
+        isValidSlot = children.length > 0
+      }
 
-        if (isValidSlot) {
+      if (isValidSlot) {
+        if (isHydrating) {
+          // if slot content is a vnode, hydrate it
+          // otherwise, it's a vapor Block that was already hydrated during
+          // renderSlot
+          if (isVNode(vnode)) {
+            hydrateVNode(vnode!, parentComponent as any)
+            oldVNode = vnode
+            frag.nodes = vnode.el as any
+          }
+        } else {
           if (fallbackNodes) {
             remove(fallbackNodes, parentNode)
             fallbackNodes = undefined
@@ -392,49 +481,43 @@ function renderVDOMSlot(
           internals.p(
             oldVNode,
             vnode!,
-            parentNode,
+            parentNode!,
             anchor,
             parentComponent as any,
           )
           oldVNode = vnode!
-        } else {
-          // for forwarded slot without its own fallback, use the fallback
-          // provided by the slot outlet.
-          // re-fetch `frag.fallback` as it may have been updated at `createSlot`
-          fallback = frag.fallback
-          if (fallback && !fallbackNodes) {
+          frag.nodes = vnode!.el as any
+        }
+      } else {
+        // for forwarded slot without its own fallback, use the fallback
+        // provided by the slot outlet.
+        // re-fetch `frag.fallback` as it may have been updated at `createSlot`
+        fallback = frag.fallback
+        if (fallback && !fallbackNodes) {
+          fallbackNodes = fallback(internals, parentComponent)
+          if (isHydrating) {
+            // hydrate fallback
+            if (isVNode(fallbackNodes)) {
+              hydrateVNode(fallbackNodes, parentComponent as any)
+              frag.nodes = fallbackNodes.el as any
+            }
+          } else {
             // mount fallback
             if (oldVNode) {
               internals.um(oldVNode, parentComponent as any, null, true)
             }
-            insert(
-              (fallbackNodes = fallback(internals, parentComponent)),
-              parentNode,
-              anchor,
-            )
+            insert(fallbackNodes, parentNode!, anchor)
+            frag.nodes = fallbackNodes as any
           }
-          oldVNode = null
         }
-      })
-      isMounted = true
-    } else {
-      // move
-      internals.m(
-        oldVNode!,
-        parentNode,
-        anchor,
-        MoveType.REORDER,
-        parentComponent as any,
-      )
-    }
-
-    frag.remove = parentNode => {
-      if (fallbackNodes) {
-        remove(fallbackNodes, parentNode)
-      } else if (oldVNode) {
-        internals.um(oldVNode, parentComponent as any, null)
+        oldVNode = null
       }
-    }
+    })
+  }
+
+  frag.hydrate = () => {
+    render()
+    isMounted = true
   }
 
   return frag
@@ -454,6 +537,41 @@ export const vaporInteropPlugin: Plugin = app => {
   }) satisfies App['mount']
 }
 
+function hydrateVNode(
+  vnode: VNode,
+  parentComponent: ComponentInternalInstance | null,
+) {
+  locateHydrationNode()
+
+  // skip fragment start anchor
+  let node = currentHydrationNode!
+  while (
+    isComment(node, '[') &&
+    // vnode is not a fragment
+    vnode.type !== Fragment &&
+    // not inside vdom slot
+    !(
+      isVaporComponent(parentComponent) &&
+      isRef((parentComponent as VaporComponentInstance).rawSlots._)
+    )
+  ) {
+    node = node.nextSibling!
+  }
+  if (currentHydrationNode !== node) setCurrentHydrationNode(node)
+
+  if (!vdomHydrateNode) vdomHydrateNode = ensureHydrationRenderer().hydrateNode!
+  const nextNode = vdomHydrateNode(
+    currentHydrationNode!,
+    vnode,
+    parentComponent,
+    null,
+    null,
+    false,
+  )
+  if (nextNode) setCurrentHydrationNode(nextNode)
+  else advanceHydrationNode(node)
+}
+
 const createFallback =
   (fallback: () => any) =>
   (
index 0679c82168bea003edaee2a15bb3383afdf95168..ccca72bd1b6c7d4cec934f1e4e4be9cd1822cb77 100644 (file)
@@ -30,7 +30,7 @@ describe('ssr: dynamic component', () => {
         }),
       ),
     ).toBe(
-      `<div><!--[--><div style=\"display:none;\"><!--[-->hi<!--]--></div><!--]--></div>`,
+      `<div><!--[--><div style="display:none;"><!--[-->hi<!--]--></div><!--]--></div>`,
     )
   })
 
index 40d3bbaff498208e4c3d35f0dcd6548bd74ed5c2..e497b98d186e31cbfd6646a2e97fddca2a38e18d 100644 (file)
@@ -40,19 +40,19 @@ importers:
         version: 7.28.2
       '@rollup/plugin-alias':
         specifier: ^5.1.1
-        version: 5.1.1(rollup@4.50.1)
+        version: 5.1.1(rollup@4.52.5)
       '@rollup/plugin-commonjs':
         specifier: ^28.0.6
-        version: 28.0.6(rollup@4.50.1)
+        version: 28.0.6(rollup@4.52.5)
       '@rollup/plugin-json':
         specifier: ^6.1.0
-        version: 6.1.0(rollup@4.50.1)
+        version: 6.1.0(rollup@4.52.5)
       '@rollup/plugin-node-resolve':
         specifier: ^16.0.1
-        version: 16.0.1(rollup@4.50.1)
+        version: 16.0.1(rollup@4.52.5)
       '@rollup/plugin-replace':
         specifier: 5.0.4
-        version: 5.0.4(rollup@4.50.1)
+        version: 5.0.4(rollup@4.52.5)
       '@swc/core':
         specifier: ^1.13.3
         version: 1.13.3
@@ -141,17 +141,17 @@ importers:
         specifier: ^6.0.1
         version: 6.0.1
       rollup:
-        specifier: 4.50.1
-        version: 4.50.1
+        specifier: ^4.52.5
+        version: 4.52.5
       rollup-plugin-dts:
         specifier: ^6.2.3
-        version: 6.2.3(rollup@4.50.1)(typescript@5.6.3)
+        version: 6.2.3(rollup@4.52.5)(typescript@5.6.3)
       rollup-plugin-esbuild:
         specifier: ^6.2.1
-        version: 6.2.1(esbuild@0.25.9)(rollup@4.50.1)
+        version: 6.2.1(esbuild@0.25.9)(rollup@4.52.5)
       rollup-plugin-polyfill-node:
         specifier: ^0.13.0
-        version: 0.13.0(rollup@4.50.1)
+        version: 0.13.0(rollup@4.52.5)
       semver:
         specifier: ^7.7.2
         version: 7.7.2
@@ -246,7 +246,7 @@ importers:
         version: 0.4.1(@types/node@22.17.2)(sass@1.90.0)(vite@6.3.5(@types/node@22.17.2)(sass@1.90.0)(yaml@2.8.1))
       vite-plugin-inspect:
         specifier: ^0.8.7
-        version: 0.8.9(rollup@4.50.1)(vite@6.3.5(@types/node@22.17.2)(sass@1.90.0)(yaml@2.8.1))
+        version: 0.8.9(rollup@4.52.5)(vite@6.3.5(@types/node@22.17.2)(sass@1.90.0)(yaml@2.8.1))
 
   packages-private/sfc-playground:
     dependencies:
@@ -1200,119 +1200,124 @@ packages:
       rollup:
         optional: true
 
-  '@rollup/rollup-android-arm-eabi@4.50.1':
-    resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==}
+  '@rollup/rollup-android-arm-eabi@4.52.5':
+    resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==}
     cpu: [arm]
     os: [android]
 
-  '@rollup/rollup-android-arm64@4.50.1':
-    resolution: {integrity: sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==}
+  '@rollup/rollup-android-arm64@4.52.5':
+    resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==}
     cpu: [arm64]
     os: [android]
 
-  '@rollup/rollup-darwin-arm64@4.50.1':
-    resolution: {integrity: sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==}
+  '@rollup/rollup-darwin-arm64@4.52.5':
+    resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==}
     cpu: [arm64]
     os: [darwin]
 
-  '@rollup/rollup-darwin-x64@4.50.1':
-    resolution: {integrity: sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==}
+  '@rollup/rollup-darwin-x64@4.52.5':
+    resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==}
     cpu: [x64]
     os: [darwin]
 
-  '@rollup/rollup-freebsd-arm64@4.50.1':
-    resolution: {integrity: sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==}
+  '@rollup/rollup-freebsd-arm64@4.52.5':
+    resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==}
     cpu: [arm64]
     os: [freebsd]
 
-  '@rollup/rollup-freebsd-x64@4.50.1':
-    resolution: {integrity: sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==}
+  '@rollup/rollup-freebsd-x64@4.52.5':
+    resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==}
     cpu: [x64]
     os: [freebsd]
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.50.1':
-    resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==}
+  '@rollup/rollup-linux-arm-gnueabihf@4.52.5':
+    resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==}
     cpu: [arm]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-arm-musleabihf@4.50.1':
-    resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==}
+  '@rollup/rollup-linux-arm-musleabihf@4.52.5':
+    resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==}
     cpu: [arm]
     os: [linux]
     libc: [musl]
 
-  '@rollup/rollup-linux-arm64-gnu@4.50.1':
-    resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==}
+  '@rollup/rollup-linux-arm64-gnu@4.52.5':
+    resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==}
     cpu: [arm64]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-arm64-musl@4.50.1':
-    resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==}
+  '@rollup/rollup-linux-arm64-musl@4.52.5':
+    resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==}
     cpu: [arm64]
     os: [linux]
     libc: [musl]
 
-  '@rollup/rollup-linux-loongarch64-gnu@4.50.1':
-    resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==}
+  '@rollup/rollup-linux-loong64-gnu@4.52.5':
+    resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==}
     cpu: [loong64]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-ppc64-gnu@4.50.1':
-    resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==}
+  '@rollup/rollup-linux-ppc64-gnu@4.52.5':
+    resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==}
     cpu: [ppc64]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-riscv64-gnu@4.50.1':
-    resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==}
+  '@rollup/rollup-linux-riscv64-gnu@4.52.5':
+    resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==}
     cpu: [riscv64]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-riscv64-musl@4.50.1':
-    resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==}
+  '@rollup/rollup-linux-riscv64-musl@4.52.5':
+    resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==}
     cpu: [riscv64]
     os: [linux]
     libc: [musl]
 
-  '@rollup/rollup-linux-s390x-gnu@4.50.1':
-    resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==}
+  '@rollup/rollup-linux-s390x-gnu@4.52.5':
+    resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==}
     cpu: [s390x]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-x64-gnu@4.50.1':
-    resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==}
+  '@rollup/rollup-linux-x64-gnu@4.52.5':
+    resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==}
     cpu: [x64]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-x64-musl@4.50.1':
-    resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==}
+  '@rollup/rollup-linux-x64-musl@4.52.5':
+    resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==}
     cpu: [x64]
     os: [linux]
     libc: [musl]
 
-  '@rollup/rollup-openharmony-arm64@4.50.1':
-    resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==}
+  '@rollup/rollup-openharmony-arm64@4.52.5':
+    resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==}
     cpu: [arm64]
     os: [openharmony]
 
-  '@rollup/rollup-win32-arm64-msvc@4.50.1':
-    resolution: {integrity: sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==}
+  '@rollup/rollup-win32-arm64-msvc@4.52.5':
+    resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==}
     cpu: [arm64]
     os: [win32]
 
-  '@rollup/rollup-win32-ia32-msvc@4.50.1':
-    resolution: {integrity: sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==}
+  '@rollup/rollup-win32-ia32-msvc@4.52.5':
+    resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==}
     cpu: [ia32]
     os: [win32]
 
-  '@rollup/rollup-win32-x64-msvc@4.50.1':
-    resolution: {integrity: sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==}
+  '@rollup/rollup-win32-x64-gnu@4.52.5':
+    resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==}
+    cpu: [x64]
+    os: [win32]
+
+  '@rollup/rollup-win32-x64-msvc@4.52.5':
+    resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==}
     cpu: [x64]
     os: [win32]
 
@@ -3344,8 +3349,8 @@ packages:
     peerDependencies:
       rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0
 
-  rollup@4.50.1:
-    resolution: {integrity: sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==}
+  rollup@4.52.5:
+    resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
 
@@ -4380,13 +4385,13 @@ snapshots:
 
   '@rolldown/pluginutils@1.0.0-beta.29': {}
 
-  '@rollup/plugin-alias@5.1.1(rollup@4.50.1)':
+  '@rollup/plugin-alias@5.1.1(rollup@4.52.5)':
     optionalDependencies:
-      rollup: 4.50.1
+      rollup: 4.52.5
 
-  '@rollup/plugin-commonjs@28.0.6(rollup@4.50.1)':
+  '@rollup/plugin-commonjs@28.0.6(rollup@4.52.5)':
     dependencies:
-      '@rollup/pluginutils': 5.2.0(rollup@4.50.1)
+      '@rollup/pluginutils': 5.2.0(rollup@4.52.5)
       commondir: 1.0.1
       estree-walker: 2.0.2
       fdir: 6.5.0(picomatch@4.0.3)
@@ -4394,108 +4399,111 @@ snapshots:
       magic-string: 0.30.17
       picomatch: 4.0.3
     optionalDependencies:
-      rollup: 4.50.1
+      rollup: 4.52.5
 
-  '@rollup/plugin-inject@5.0.5(rollup@4.50.1)':
+  '@rollup/plugin-inject@5.0.5(rollup@4.52.5)':
     dependencies:
-      '@rollup/pluginutils': 5.2.0(rollup@4.50.1)
+      '@rollup/pluginutils': 5.2.0(rollup@4.52.5)
       estree-walker: 2.0.2
       magic-string: 0.30.17
     optionalDependencies:
-      rollup: 4.50.1
+      rollup: 4.52.5
 
-  '@rollup/plugin-json@6.1.0(rollup@4.50.1)':
+  '@rollup/plugin-json@6.1.0(rollup@4.52.5)':
     dependencies:
-      '@rollup/pluginutils': 5.2.0(rollup@4.50.1)
+      '@rollup/pluginutils': 5.2.0(rollup@4.52.5)
     optionalDependencies:
-      rollup: 4.50.1
+      rollup: 4.52.5
 
-  '@rollup/plugin-node-resolve@16.0.1(rollup@4.50.1)':
+  '@rollup/plugin-node-resolve@16.0.1(rollup@4.52.5)':
     dependencies:
-      '@rollup/pluginutils': 5.2.0(rollup@4.50.1)
+      '@rollup/pluginutils': 5.2.0(rollup@4.52.5)
       '@types/resolve': 1.20.2
       deepmerge: 4.3.1
       is-module: 1.0.0
       resolve: 1.22.10
     optionalDependencies:
-      rollup: 4.50.1
+      rollup: 4.52.5
 
-  '@rollup/plugin-replace@5.0.4(rollup@4.50.1)':
+  '@rollup/plugin-replace@5.0.4(rollup@4.52.5)':
     dependencies:
-      '@rollup/pluginutils': 5.2.0(rollup@4.50.1)
+      '@rollup/pluginutils': 5.2.0(rollup@4.52.5)
       magic-string: 0.30.17
     optionalDependencies:
-      rollup: 4.50.1
+      rollup: 4.52.5
 
-  '@rollup/pluginutils@5.2.0(rollup@4.50.1)':
+  '@rollup/pluginutils@5.2.0(rollup@4.52.5)':
     dependencies:
       '@types/estree': 1.0.8
       estree-walker: 2.0.2
       picomatch: 4.0.3
     optionalDependencies:
-      rollup: 4.50.1
+      rollup: 4.52.5
+
+  '@rollup/rollup-android-arm-eabi@4.52.5':
+    optional: true
 
-  '@rollup/rollup-android-arm-eabi@4.50.1':
+  '@rollup/rollup-android-arm64@4.52.5':
     optional: true
 
-  '@rollup/rollup-android-arm64@4.50.1':
+  '@rollup/rollup-darwin-arm64@4.52.5':
     optional: true
 
-  '@rollup/rollup-darwin-arm64@4.50.1':
+  '@rollup/rollup-darwin-x64@4.52.5':
     optional: true
 
-  '@rollup/rollup-darwin-x64@4.50.1':
+  '@rollup/rollup-freebsd-arm64@4.52.5':
     optional: true
 
-  '@rollup/rollup-freebsd-arm64@4.50.1':
+  '@rollup/rollup-freebsd-x64@4.52.5':
     optional: true
 
-  '@rollup/rollup-freebsd-x64@4.50.1':
+  '@rollup/rollup-linux-arm-gnueabihf@4.52.5':
     optional: true
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.50.1':
+  '@rollup/rollup-linux-arm-musleabihf@4.52.5':
     optional: true
 
-  '@rollup/rollup-linux-arm-musleabihf@4.50.1':
+  '@rollup/rollup-linux-arm64-gnu@4.52.5':
     optional: true
 
-  '@rollup/rollup-linux-arm64-gnu@4.50.1':
+  '@rollup/rollup-linux-arm64-musl@4.52.5':
     optional: true
 
-  '@rollup/rollup-linux-arm64-musl@4.50.1':
+  '@rollup/rollup-linux-loong64-gnu@4.52.5':
     optional: true
 
-  '@rollup/rollup-linux-loongarch64-gnu@4.50.1':
+  '@rollup/rollup-linux-ppc64-gnu@4.52.5':
     optional: true
 
-  '@rollup/rollup-linux-ppc64-gnu@4.50.1':
+  '@rollup/rollup-linux-riscv64-gnu@4.52.5':
     optional: true
 
-  '@rollup/rollup-linux-riscv64-gnu@4.50.1':
+  '@rollup/rollup-linux-riscv64-musl@4.52.5':
     optional: true
 
-  '@rollup/rollup-linux-riscv64-musl@4.50.1':
+  '@rollup/rollup-linux-s390x-gnu@4.52.5':
     optional: true
 
-  '@rollup/rollup-linux-s390x-gnu@4.50.1':
+  '@rollup/rollup-linux-x64-gnu@4.52.5':
     optional: true
 
-  '@rollup/rollup-linux-x64-gnu@4.50.1':
+  '@rollup/rollup-linux-x64-musl@4.52.5':
     optional: true
 
-  '@rollup/rollup-linux-x64-musl@4.50.1':
+  '@rollup/rollup-openharmony-arm64@4.52.5':
     optional: true
 
-  '@rollup/rollup-openharmony-arm64@4.50.1':
+  '@rollup/rollup-win32-arm64-msvc@4.52.5':
     optional: true
 
-  '@rollup/rollup-win32-arm64-msvc@4.50.1':
+  '@rollup/rollup-win32-ia32-msvc@4.52.5':
     optional: true
 
-  '@rollup/rollup-win32-ia32-msvc@4.50.1':
+  '@rollup/rollup-win32-x64-gnu@4.52.5':
     optional: true
 
-  '@rollup/rollup-win32-x64-msvc@4.50.1':
+  '@rollup/rollup-win32-x64-msvc@4.52.5':
     optional: true
 
   '@swc/core-darwin-arm64@1.13.3':
@@ -6620,55 +6628,56 @@ snapshots:
       glob: 11.0.3
       package-json-from-dist: 1.0.1
 
-  rollup-plugin-dts@6.2.3(rollup@4.50.1)(typescript@5.6.3):
+  rollup-plugin-dts@6.2.3(rollup@4.52.5)(typescript@5.6.3):
     dependencies:
       magic-string: 0.30.17
-      rollup: 4.50.1
+      rollup: 4.52.5
       typescript: 5.6.3
     optionalDependencies:
       '@babel/code-frame': 7.27.1
 
-  rollup-plugin-esbuild@6.2.1(esbuild@0.25.9)(rollup@4.50.1):
+  rollup-plugin-esbuild@6.2.1(esbuild@0.25.9)(rollup@4.52.5):
     dependencies:
       debug: 4.4.1
       es-module-lexer: 1.7.0
       esbuild: 0.25.9
       get-tsconfig: 4.10.1
-      rollup: 4.50.1
+      rollup: 4.52.5
       unplugin-utils: 0.2.5
     transitivePeerDependencies:
       - supports-color
 
-  rollup-plugin-polyfill-node@0.13.0(rollup@4.50.1):
+  rollup-plugin-polyfill-node@0.13.0(rollup@4.52.5):
     dependencies:
-      '@rollup/plugin-inject': 5.0.5(rollup@4.50.1)
-      rollup: 4.50.1
+      '@rollup/plugin-inject': 5.0.5(rollup@4.52.5)
+      rollup: 4.52.5
 
-  rollup@4.50.1:
+  rollup@4.52.5:
     dependencies:
       '@types/estree': 1.0.8
     optionalDependencies:
-      '@rollup/rollup-android-arm-eabi': 4.50.1
-      '@rollup/rollup-android-arm64': 4.50.1
-      '@rollup/rollup-darwin-arm64': 4.50.1
-      '@rollup/rollup-darwin-x64': 4.50.1
-      '@rollup/rollup-freebsd-arm64': 4.50.1
-      '@rollup/rollup-freebsd-x64': 4.50.1
-      '@rollup/rollup-linux-arm-gnueabihf': 4.50.1
-      '@rollup/rollup-linux-arm-musleabihf': 4.50.1
-      '@rollup/rollup-linux-arm64-gnu': 4.50.1
-      '@rollup/rollup-linux-arm64-musl': 4.50.1
-      '@rollup/rollup-linux-loongarch64-gnu': 4.50.1
-      '@rollup/rollup-linux-ppc64-gnu': 4.50.1
-      '@rollup/rollup-linux-riscv64-gnu': 4.50.1
-      '@rollup/rollup-linux-riscv64-musl': 4.50.1
-      '@rollup/rollup-linux-s390x-gnu': 4.50.1
-      '@rollup/rollup-linux-x64-gnu': 4.50.1
-      '@rollup/rollup-linux-x64-musl': 4.50.1
-      '@rollup/rollup-openharmony-arm64': 4.50.1
-      '@rollup/rollup-win32-arm64-msvc': 4.50.1
-      '@rollup/rollup-win32-ia32-msvc': 4.50.1
-      '@rollup/rollup-win32-x64-msvc': 4.50.1
+      '@rollup/rollup-android-arm-eabi': 4.52.5
+      '@rollup/rollup-android-arm64': 4.52.5
+      '@rollup/rollup-darwin-arm64': 4.52.5
+      '@rollup/rollup-darwin-x64': 4.52.5
+      '@rollup/rollup-freebsd-arm64': 4.52.5
+      '@rollup/rollup-freebsd-x64': 4.52.5
+      '@rollup/rollup-linux-arm-gnueabihf': 4.52.5
+      '@rollup/rollup-linux-arm-musleabihf': 4.52.5
+      '@rollup/rollup-linux-arm64-gnu': 4.52.5
+      '@rollup/rollup-linux-arm64-musl': 4.52.5
+      '@rollup/rollup-linux-loong64-gnu': 4.52.5
+      '@rollup/rollup-linux-ppc64-gnu': 4.52.5
+      '@rollup/rollup-linux-riscv64-gnu': 4.52.5
+      '@rollup/rollup-linux-riscv64-musl': 4.52.5
+      '@rollup/rollup-linux-s390x-gnu': 4.52.5
+      '@rollup/rollup-linux-x64-gnu': 4.52.5
+      '@rollup/rollup-linux-x64-musl': 4.52.5
+      '@rollup/rollup-openharmony-arm64': 4.52.5
+      '@rollup/rollup-win32-arm64-msvc': 4.52.5
+      '@rollup/rollup-win32-ia32-msvc': 4.52.5
+      '@rollup/rollup-win32-x64-gnu': 4.52.5
+      '@rollup/rollup-win32-x64-msvc': 4.52.5
       fsevents: 2.3.3
 
   rrweb-cssom@0.8.0: {}
@@ -7081,10 +7090,10 @@ snapshots:
       - tsx
       - yaml
 
-  vite-plugin-inspect@0.8.9(rollup@4.50.1)(vite@6.3.5(@types/node@22.17.2)(sass@1.90.0)(yaml@2.8.1)):
+  vite-plugin-inspect@0.8.9(rollup@4.52.5)(vite@6.3.5(@types/node@22.17.2)(sass@1.90.0)(yaml@2.8.1)):
     dependencies:
       '@antfu/utils': 0.7.10
-      '@rollup/pluginutils': 5.2.0(rollup@4.50.1)
+      '@rollup/pluginutils': 5.2.0(rollup@4.52.5)
       debug: 4.4.1
       error-stack-parser-es: 0.1.5
       fs-extra: 11.3.1
@@ -7101,7 +7110,7 @@ snapshots:
     dependencies:
       esbuild: 0.21.5
       postcss: 8.5.6
-      rollup: 4.50.1
+      rollup: 4.52.5
     optionalDependencies:
       '@types/node': 22.17.2
       fsevents: 2.3.3
@@ -7113,7 +7122,7 @@ snapshots:
       fdir: 6.5.0(picomatch@4.0.3)
       picomatch: 4.0.3
       postcss: 8.5.6
-      rollup: 4.50.1
+      rollup: 4.52.5
       tinyglobby: 0.2.14
     optionalDependencies:
       '@types/node': 22.17.2