]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor: vapor hydration
authordaiwei <daiwei521@126.com>
Mon, 11 Aug 2025 03:49:09 +0000 (11:49 +0800)
committerdaiwei <daiwei521@126.com>
Mon, 11 Aug 2025 09:58:53 +0000 (17:58 +0800)
44 files changed:
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/options.ts
packages/compiler-core/src/transform.ts
packages/compiler-sfc/src/compileTemplate.ts
packages/compiler-ssr/__tests__/ssrComponent.spec.ts
packages/compiler-ssr/__tests__/ssrElement.spec.ts
packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts
packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts
packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts
packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts
packages/compiler-ssr/__tests__/ssrVFor.spec.ts
packages/compiler-ssr/__tests__/ssrVIf.spec.ts
packages/compiler-ssr/__tests__/ssrVModel.spec.ts
packages/compiler-ssr/__tests__/ssrVaporAnchors.spec.ts [new file with mode: 0644]
packages/compiler-ssr/__tests__/utils.ts
packages/compiler-ssr/src/ssrCodegenTransform.ts
packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts
packages/compiler-ssr/src/transforms/ssrVFor.ts
packages/compiler-ssr/src/transforms/ssrVIf.ts
packages/compiler-vapor/__tests__/__snapshots__/compile.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/src/generate.ts
packages/compiler-vapor/src/generators/operation.ts
packages/compiler-vapor/src/generators/template.ts
packages/compiler-vapor/src/transform.ts
packages/compiler-vapor/src/transforms/transformChildren.ts
packages/runtime-core/__tests__/hydration.spec.ts
packages/runtime-core/src/hydration.ts
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/dom/hydration.ts
packages/runtime-vapor/src/dom/node.ts
packages/runtime-vapor/src/insertionState.ts
packages/server-renderer/__tests__/render.spec.ts
packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts
packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts
packages/server-renderer/__tests__/ssrScopeId.spec.ts
packages/server-renderer/__tests__/ssrSlot.spec.ts
packages/server-renderer/src/helpers/ssrRenderSlot.ts
packages/shared/src/domAnchors.ts

index bae13372a98be3d205d15cc1c045edd0b0220197..dfcc1bef3c35d0f07ed9a4048d7abe61367878ad 100644 (file)
@@ -163,6 +163,7 @@ export interface ComponentNode extends BaseElementNode {
     | MemoExpression // when cached by v-memo
     | undefined
   ssrCodegenNode?: CallExpression
+  anchor?: string
 }
 
 export interface SlotOutletNode extends BaseElementNode {
@@ -172,6 +173,7 @@ export interface SlotOutletNode extends BaseElementNode {
     | CacheExpression // when cached by v-once
     | undefined
   ssrCodegenNode?: CallExpression
+  anchor?: string
 }
 
 export interface TemplateNode extends BaseElementNode {
@@ -287,6 +289,7 @@ export interface IfNode extends Node {
   type: NodeTypes.IF
   branches: IfBranchNode[]
   codegenNode?: IfConditionalExpression | CacheExpression // <div v-if v-once>
+  anchor?: string
 }
 
 export interface IfBranchNode extends Node {
@@ -306,6 +309,7 @@ export interface ForNode extends Node {
   parseResult: ForParseResult
   children: TemplateChildNode[]
   codegenNode?: ForCodegenNode
+  anchor?: string
 }
 
 export interface ForParseResult {
index 99020bcf1ae1f93eda6ab6a7f458cd539293b701..0b5dcca8f70a980d751394c07608a12d42793929 100644 (file)
@@ -167,6 +167,7 @@ function createCodegenContext(
     ssr = false,
     isTS = false,
     inSSR = false,
+    vapor = false,
   }: CodegenOptions,
 ): CodegenContext {
   const context: CodegenContext = {
@@ -182,6 +183,7 @@ function createCodegenContext(
     ssr,
     isTS,
     inSSR,
+    vapor,
     source: ast.source,
     code: ``,
     column: 1,
index 9983071609eaad3d1337543438771eed7a8d7c61..03a32e0113cf20a41d3bcfebaebc2ce935e72c8f 100644 (file)
@@ -220,6 +220,11 @@ interface SharedTransformCodegenOptions {
    * @default 'template.vue.html'
    */
   filename?: string
+
+  /**
+   * Indicates vapor component
+   */
+  vapor?: boolean
 }
 
 export interface TransformOptions
index 10121fb5d5cb1fcfdb36e04d2371fea430bcd0ec..11077df6252b7f4a976020285777f013cec50b93 100644 (file)
@@ -146,6 +146,7 @@ export function createTransformContext(
     slotted = true,
     ssr = false,
     inSSR = false,
+    vapor = false,
     ssrCssVars = ``,
     bindingMetadata = EMPTY_OBJ,
     inline = false,
@@ -173,6 +174,7 @@ export function createTransformContext(
     slotted,
     ssr,
     inSSR,
+    vapor,
     ssrCssVars,
     bindingMetadata,
     inline,
index 29d1853d2d6233836ade13f34040de58ccf47fb8..1d832388fe1a3fc3a286eab69294842edb906cdc 100644 (file)
@@ -253,6 +253,7 @@ function doCompileTemplate({
       slotted,
       sourceMap: true,
       ...compilerOptions,
+      vapor,
       hmr: !isProd,
       nodeTransforms: nodeTransforms.concat(
         compilerOptions.nodeTransforms || [],
index fb2fff86574a1cf4d75ab74c560c59652706714d..2fde4560ec43e4e81dfc0848e4ac5aa4c4d14ccb 100644 (file)
@@ -39,7 +39,6 @@ describe('ssr: components', () => {
 
         return function ssrRender(_ctx, _push, _parent, _attrs) {
           _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent("foo"), _mergeProps({ prop: "b" }, _attrs), null), _parent)
-          _push(\`<!--dynamic-component-->\`)
         }"
       `)
 
@@ -50,7 +49,6 @@ describe('ssr: components', () => {
 
         return function ssrRender(_ctx, _push, _parent, _attrs) {
           _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.foo), _mergeProps({ prop: "b" }, _attrs), null), _parent)
-          _push(\`<!--dynamic-component-->\`)
         }"
       `)
   })
@@ -246,8 +244,7 @@ describe('ssr: components', () => {
                   _ssrRenderList(list, (i) => {
                     _push(\`<span\${_scopeId}></span>\`)
                   })
-                  _push(\`<!--]--><!--for--></div>\`)
-                  _push(\`<!--if-->\`)
+                  _push(\`<!--]--></div>\`)
                 } else {
                   _push(\`<!---->\`)
                 }
@@ -270,8 +267,7 @@ describe('ssr: components', () => {
                   _ssrRenderList(_ctx.list, (i) => {
                     _push(\`<span\${_scopeId}></span>\`)
                   })
-                  _push(\`<!--]--><!--for--></div>\`)
-                  _push(\`<!--if-->\`)
+                  _push(\`<!--]--></div>\`)
                 } else {
                   _push(\`<!---->\`)
                 }
@@ -365,7 +361,6 @@ describe('ssr: components', () => {
                   _push(\`\`)
                   if (false) {
                     _push(\`<div\${_scopeId}></div>\`)
-                    _push(\`<!--if-->\`)
                   } else {
                     _push(\`<!---->\`)
                   }
index 57e4f022c034b137a391d7f7f70c42e1e823b2ce..f1d509acfb011a8b48acb2fd1191cca0a7c8cda1 100644 (file)
@@ -396,50 +396,4 @@ describe('ssr: element', () => {
       `)
     })
   })
-
-  describe('dynamic anchor', () => {
-    test('two consecutive components', () => {
-      expect(
-        getCompiledString(`
-        <div>
-          <div/>
-          <Comp1/>
-          <Comp2/>
-          <div/>
-        </div>
-        `),
-      ).toMatchInlineSnapshot(`
-        "\`<div><div></div><!--[[-->\`)
-          _push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
-          _push(\`<!--]]--><!--[[-->\`)
-          _push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
-          _push(\`<!--]]--><div></div></div>\`"
-      `)
-    })
-
-    test('multiple consecutive components', () => {
-      expect(
-        getCompiledString(`
-        <div>
-          <div/>
-          <Comp1/>
-          <Comp2/>
-          <Comp3/>
-          <Comp4/>
-          <div/>
-        </div>
-        `),
-      ).toMatchInlineSnapshot(`
-        "\`<div><div></div><!--[[-->\`)
-          _push(_ssrRenderComponent(_component_Comp1, null, null, _parent))
-          _push(\`<!--]]--><!--[[-->\`)
-          _push(_ssrRenderComponent(_component_Comp2, null, null, _parent))
-          _push(\`<!--]]--><!--[[-->\`)
-          _push(_ssrRenderComponent(_component_Comp3, null, null, _parent))
-          _push(\`<!--]]--><!--[[-->\`)
-          _push(_ssrRenderComponent(_component_Comp4, null, null, _parent))
-          _push(\`<!--]]--><div></div></div>\`"
-      `)
-    })
-  })
 })
index 712c09d0946041d48abef1709d5e3f1d8591aefa..7b3d1962c3e0947f30acd2d960870d69bbd4a460 100644 (file)
@@ -29,7 +29,6 @@ describe('ssr: attrs fallthrough', () => {
         _push(\`<!--[-->\`)
         if (true) {
           _push(\`<div></div>\`)
-          _push(\`<!--if-->\`)
         } else {
           _push(\`<!---->\`)
         }
index e8e7dfb253d764f751bd0947165cf436a43024ed..9e70dac0bdca3cc1f535def98297eddd0a1692e2 100644 (file)
@@ -70,14 +70,12 @@ describe('ssr: inject <style vars>', () => {
         const _cssVars = { style: { color: _ctx.color }}
         if (_ctx.ok) {
           _push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
-          _push(\`<!--if-->\`)
         } else {
           _push(\`<!--[--><div\${
             _ssrRenderAttrs(_cssVars)
           }></div><div\${
             _ssrRenderAttrs(_cssVars)
           }></div><!--]-->\`)
-          _push(\`<!--if-->\`)
         }
       }"
     `)
index 4d73dfe082744420a973c8961867aefd6f4f4068..86863cfb85f07d85d2b647fc2118d6e75f0d877a 100644 (file)
@@ -153,7 +153,6 @@ describe('ssr: <slot>', () => {
       return function ssrRender(_ctx, _push, _parent, _attrs) {
         if (true) {
           _ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true)
-          _push(\`<!--if-->\`)
         } else {
           _push(\`<!---->\`)
         }
index 73d4331a7d7e6e6a5b6a60521ac24c71b3faf20b..82122e621c7b5890a9160172f51bba692086b270 100644 (file)
@@ -15,7 +15,7 @@ describe('transition-group', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<div></div>\`)
         })
-        _push(\`<!--for--><!--]-->\`)
+        _push(\`<!--]-->\`)
       }"
     `)
   })
@@ -33,7 +33,7 @@ describe('transition-group', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<div></div>\`)
         })
-        _push(\`<!--for--></ul>\`)
+        _push(\`</ul>\`)
       }"
     `)
   })
@@ -52,10 +52,8 @@ describe('transition-group', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<div></div>\`)
         })
-        _push(\`<!--for-->\`)
         if (false) {
           _push(\`<div></div>\`)
-          _push(\`<!--if-->\`)
         }
         _push(\`</ul>\`)
       }"
@@ -76,7 +74,7 @@ describe('transition-group', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<div></div>\`)
         })
-        _push(\`<!--for--></ul>\`)
+        _push(\`</ul>\`)
       }"
     `)
   })
@@ -98,7 +96,7 @@ describe('transition-group', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<div></div>\`)
         })
-        _push(\`<!--for--></\${_ctx.someTag}>\`)
+        _push(\`</\${_ctx.someTag}>\`)
       }"
     `)
   })
@@ -120,14 +118,11 @@ describe('transition-group', () => {
         _ssrRenderList(10, (i) => {
           _push(\`<div></div>\`)
         })
-        _push(\`<!--for-->\`)
         _ssrRenderList(10, (i) => {
           _push(\`<div></div>\`)
         })
-        _push(\`<!--for-->\`)
         if (_ctx.ok) {
           _push(\`<div>ok</div>\`)
-          _push(\`<!--if-->\`)
         }
         _push(\`<!--]-->\`)
       }"
index dad426de04ceb5590db2cbe5317c5e9eb86e5f99..0d957265120986165b35cf80b9247fa1b3f23ac1 100644 (file)
@@ -10,7 +10,7 @@ describe('ssr: v-for', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<div></div>\`)
         })
-        _push(\`<!--]--><!--for-->\`)
+        _push(\`<!--]-->\`)
       }"
     `)
   })
@@ -25,7 +25,7 @@ describe('ssr: v-for', () => {
           _ssrRenderList(_ctx.list, (i) => {
             _push(\`<div>foo<span>bar</span></div>\`)
           })
-          _push(\`<!--]--><!--for-->\`)
+          _push(\`<!--]-->\`)
         }"
       `)
   })
@@ -51,9 +51,9 @@ describe('ssr: v-for', () => {
               _ssrInterpolate(j)
             }</div>\`)
           })
-          _push(\`<!--]--><!--for--></div>\`)
+          _push(\`<!--]--></div>\`)
         })
-        _push(\`<!--]--><!--for-->\`)
+        _push(\`<!--]-->\`)
       }"
     `)
   })
@@ -68,7 +68,7 @@ describe('ssr: v-for', () => {
           _ssrRenderList(_ctx.list, (i) => {
             _push(\`<!--[-->\${_ssrInterpolate(i)}<!--]-->\`)
           })
-          _push(\`<!--]--><!--for-->\`)
+          _push(\`<!--]-->\`)
         }"
       `)
   })
@@ -85,7 +85,7 @@ describe('ssr: v-for', () => {
         _ssrRenderList(_ctx.list, (i) => {
           _push(\`<span>\${_ssrInterpolate(i)}</span>\`)
         })
-        _push(\`<!--]--><!--for-->\`)
+        _push(\`<!--]-->\`)
       }"
     `)
   })
@@ -107,7 +107,7 @@ describe('ssr: v-for', () => {
             _ssrInterpolate(i + 1)
           }</span><!--]-->\`)
         })
-        _push(\`<!--]--><!--for-->\`)
+        _push(\`<!--]-->\`)
       }"
     `)
   })
@@ -127,7 +127,7 @@ describe('ssr: v-for', () => {
         _ssrRenderList(_ctx.list, ({ foo }, index) => {
           _push(\`<div>\${_ssrInterpolate(foo + _ctx.bar + index)}</div>\`)
         })
-        _push(\`<!--]--><!--for-->\`)
+        _push(\`<!--]-->\`)
       }"
     `)
   })
index 485d52352a373fdabf9929bbd04b7dab8931f2c9..7867a4b5fb50724117a0ac01ffec5353d54fbac9 100644 (file)
@@ -8,7 +8,6 @@ describe('ssr: v-if', () => {
       return function ssrRender(_ctx, _push, _parent, _attrs) {
         if (_ctx.foo) {
           _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
-          _push(\`<!--if-->\`)
         } else {
           _push(\`<!---->\`)
         }
@@ -24,7 +23,6 @@ describe('ssr: v-if', () => {
         return function ssrRender(_ctx, _push, _parent, _attrs) {
           if (_ctx.foo) {
             _push(\`<div\${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>\`)
-            _push(\`<!--if-->\`)
           } else {
             _push(\`<!---->\`)
           }
@@ -40,10 +38,8 @@ describe('ssr: v-if', () => {
         return function ssrRender(_ctx, _push, _parent, _attrs) {
           if (_ctx.foo) {
             _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
-            _push(\`<!--if-->\`)
           } else {
             _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
-            _push(\`<!--if-->\`)
           }
         }"
       `)
@@ -57,10 +53,8 @@ describe('ssr: v-if', () => {
         return function ssrRender(_ctx, _push, _parent, _attrs) {
           if (_ctx.foo) {
             _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
-            _push(\`<!--if-->\`)
           } else if (_ctx.bar) {
             _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
-            _push(\`<!--if-->\`)
           } else {
             _push(\`<!---->\`)
           }
@@ -76,13 +70,10 @@ describe('ssr: v-if', () => {
         return function ssrRender(_ctx, _push, _parent, _attrs) {
           if (_ctx.foo) {
             _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
-            _push(\`<!--if-->\`)
           } else if (_ctx.bar) {
             _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
-            _push(\`<!--if-->\`)
           } else {
             _push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`)
-            _push(\`<!--if-->\`)
           }
         }"
       `)
@@ -95,7 +86,6 @@ describe('ssr: v-if', () => {
         return function ssrRender(_ctx, _push, _parent, _attrs) {
           if (_ctx.foo) {
             _push(\`<!--[-->hello<!--]-->\`)
-            _push(\`<!--if-->\`)
           } else {
             _push(\`<!---->\`)
           }
@@ -112,7 +102,6 @@ describe('ssr: v-if', () => {
         return function ssrRender(_ctx, _push, _parent, _attrs) {
           if (_ctx.foo) {
             _push(\`<div\${_ssrRenderAttrs(_attrs)}>hi</div>\`)
-            _push(\`<!--if-->\`)
           } else {
             _push(\`<!---->\`)
           }
@@ -129,7 +118,6 @@ describe('ssr: v-if', () => {
       return function ssrRender(_ctx, _push, _parent, _attrs) {
         if (_ctx.foo) {
           _push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
-          _push(\`<!--if-->\`)
         } else {
           _push(\`<!---->\`)
         }
@@ -149,8 +137,7 @@ describe('ssr: v-if', () => {
           _ssrRenderList(_ctx.list, (i) => {
             _push(\`<div></div>\`)
           })
-          _push(\`<!--]--><!--for-->\`)
-          _push(\`<!--if-->\`)
+          _push(\`<!--]-->\`)
         } else {
           _push(\`<!---->\`)
         }
@@ -169,10 +156,8 @@ describe('ssr: v-if', () => {
       return function ssrRender(_ctx, _push, _parent, _attrs) {
         if (_ctx.foo) {
           _push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
-          _push(\`<!--if-->\`)
         } else {
           _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
-          _push(\`<!--if-->\`)
         }
       }"
     `)
index f7b7a3241b22042423b22155a3b749523a9259b7..8a439dbf4b5cc6063d0a74d7f1859539b1d8082a 100644 (file)
@@ -70,7 +70,7 @@ describe('ssr: v-model', () => {
               : _ssrLooseEqual(_ctx.model, i))) ? " selected" : ""
           }></option>\`)
         })
-        _push(\`<!--]--><!--for--></select></div>\`)
+        _push(\`<!--]--></select></div>\`)
       }"
     `)
 
@@ -91,7 +91,6 @@ describe('ssr: v-model', () => {
               ? _ssrLooseContain(_ctx.model, _ctx.i)
               : _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : ""
           }></option>\`)
-          _push(\`<!--if-->\`)
         } else {
           _push(\`<!---->\`)
         }
@@ -191,7 +190,7 @@ describe('ssr: v-model', () => {
             _ssrInterpolate(item)
           }</option>\`)
         })
-        _push(\`<!--]--><!--for--></optgroup></select></div>\`)
+        _push(\`<!--]--></optgroup></select></div>\`)
       }"
     `)
 
@@ -217,7 +216,6 @@ describe('ssr: v-model', () => {
           }>\${
             _ssrInterpolate(_ctx.item)
           }</option>\`)
-          _push(\`<!--if-->\`)
         } else {
           _push(\`<!---->\`)
         }
@@ -252,8 +250,7 @@ describe('ssr: v-model', () => {
               _ssrInterpolate(item)
             }</option>\`)
           })
-          _push(\`<!--]--><!--for-->\`)
-          _push(\`<!--if-->\`)
+          _push(\`<!--]-->\`)
         } else {
           _push(\`<!---->\`)
         }
@@ -287,13 +284,12 @@ describe('ssr: v-model', () => {
             }>\${
               _ssrInterpolate(item)
             }</option>\`)
-            _push(\`<!--if-->\`)
           } else {
             _push(\`<!---->\`)
           }
           _push(\`<!--]-->\`)
         })
-        _push(\`<!--]--><!--for--></optgroup></select></div>\`)
+        _push(\`<!--]--></optgroup></select></div>\`)
       }"
     `)
   })
diff --git a/packages/compiler-ssr/__tests__/ssrVaporAnchors.spec.ts b/packages/compiler-ssr/__tests__/ssrVaporAnchors.spec.ts
new file mode 100644 (file)
index 0000000..9fd5fed
--- /dev/null
@@ -0,0 +1,322 @@
+// import {
+//   BLOCK_APPEND_ANCHOR_LABEL,
+//   BLOCK_INSERTION_ANCHOR_LABEL,
+//   BLOCK_PREPEND_ANCHOR_LABEL,
+// } from '@vue/shared'
+import { getCompiledString } from './utils'
+
+describe('insertion anchors', () => {
+  describe('prepend', () => {
+    test('prepend anchor with component', () => {
+      expect(
+        getCompiledString('<div><Comp/><Comp/><span/></div>', { vapor: true }),
+      ).toMatchInlineSnapshot(`
+        "\`<div><!--[p-->\`)
+          _push(_ssrRenderComponent(_component_Comp, null, null, _parent))
+          _push(\`<!--p]--><!--[p-->\`)
+          _push(_ssrRenderComponent(_component_Comp, null, null, _parent))
+          _push(\`<!--p]--><span></span></div>\`"
+      `)
+    })
+
+    test('prepend anchor with component in ssr slot vnode fallback', () => {
+      expect(
+        getCompiledString(
+          `<component :is="'div'">
+            <div><Comp/><Comp/><span/></div>
+          </component>`,
+          { vapor: true },
+        ),
+      ).toMatchInlineSnapshot(`
+        "\`<!--[a-->\`)
+          _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent('div'), null, {
+            default: _withCtx((_, _push, _parent, _scopeId) => {
+              if (_push) {
+                _push(\`<div\${_scopeId}><!--[p-->\`)
+                _push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
+                _push(\`<!--p]--><!--[p-->\`)
+                _push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
+                _push(\`<!--p]--><span\${_scopeId}></span></div>\`)
+              } else {
+                return [
+                  _createVNode("div", null, [
+                    _createCommentVNode("[p"),
+                    _createVNode(_component_Comp),
+                    _createCommentVNode("p]"),
+                    _createCommentVNode("[p"),
+                    _createVNode(_component_Comp),
+                    _createCommentVNode("p]"),
+                    _createVNode("span")
+                  ])
+                ]
+              }
+            }),
+            _: 1 /* STABLE */
+          }), _parent)
+          _push(\`<!--dynamic-component--><!--a]-->\`"
+      `)
+    })
+
+    test('prepend anchor with slot', () => {
+      expect(
+        getCompiledString('<div><slot name="foo"/><slot/><span/></div>', {
+          vapor: true,
+        }),
+      ).toMatchInlineSnapshot(`
+        "\`<div><!--[p-->\`)
+          _ssrRenderSlot(_ctx.$slots, "foo", {}, null, _push, _parent)
+          _push(\`<!--slot--><!--p]--><!--[p-->\`)
+          _ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent)
+          _push(\`<!--slot--><!--p]--><span></span></div>\`"
+      `)
+    })
+
+    test('prepend anchor with slot in ssr slot vnode fallback', () => {
+      expect(
+        getCompiledString(
+          `<component :is="'div'">
+            <div><slot name="foo"/><slot/><span/></div>
+          </component>`,
+          { vapor: true },
+        ),
+      ).toMatchInlineSnapshot(`
+        "\`<!--[a-->\`)
+          _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent('div'), null, {
+            default: _withCtx((_, _push, _parent, _scopeId) => {
+              if (_push) {
+                _push(\`<div\${_scopeId}><!--[p-->\`)
+                _ssrRenderSlot(_ctx.$slots, "foo", {}, null, _push, _parent, _scopeId)
+                _push(\`<!--slot--><!--p]--><!--[p-->\`)
+                _ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent, _scopeId)
+                _push(\`<!--slot--><!--p]--><span\${_scopeId}></span></div>\`)
+              } else {
+                return [
+                  _createVNode("div", null, [
+                    _createCommentVNode("[p"),
+                    _renderSlot(_ctx.$slots, "foo"),
+                    _createCommentVNode("p]"),
+                    _createCommentVNode("[p"),
+                    _renderSlot(_ctx.$slots, "default"),
+                    _createCommentVNode("p]"),
+                    _createVNode("span")
+                  ])
+                ]
+              }
+            }),
+            _: 3 /* FORWARDED */
+          }), _parent)
+          _push(\`<!--dynamic-component--><!--a]-->\`"
+      `)
+    })
+
+    test('prepend anchor with v-if', () => {
+      expect(
+        getCompiledString('<div><span v-if="foo"/><span/></div>', {
+          vapor: true,
+        }),
+      ).toMatchInlineSnapshot(`
+        "\`<div><!--[p-->\`)
+          if (_ctx.foo) {
+            _push(\`<span></span>\`)
+            _push(\`<!--if-->\`)
+          } else {
+            _push(\`<!---->\`)
+          }
+          _push(\`<!--p]--><span></span></div>\`"
+      `)
+    })
+
+    test('prepend anchor with v-if in ssr slot vnode fallback', () => {
+      expect(
+        getCompiledString(
+          `<component :is="'div'">
+            <div><span v-if="foo"/><span/></div>
+          </component>`,
+          { vapor: true },
+        ),
+      ).toMatchInlineSnapshot(`
+        "\`<!--[a-->\`)
+          _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent('div'), null, {
+            default: _withCtx((_, _push, _parent, _scopeId) => {
+              if (_push) {
+                _push(\`<div\${_scopeId}><!--[p-->\`)
+                if (_ctx.foo) {
+                  _push(\`<span\${_scopeId}></span>\`)
+                  _push(\`<!--if-->\`)
+                } else {
+                  _push(\`<!---->\`)
+                }
+                _push(\`<!--p]--><span\${_scopeId}></span></div>\`)
+              } else {
+                return [
+                  _createVNode("div", null, [
+                    _createCommentVNode("[p"),
+                    (_ctx.foo)
+                      ? (_openBlock(), _createBlock("span", { key: 0 }))
+                      : _createCommentVNode("v-if", true),
+                    _createCommentVNode("p]"),
+                    _createVNode("span")
+                  ])
+                ]
+              }
+            }),
+            _: 1 /* STABLE */
+          }), _parent)
+          _push(\`<!--dynamic-component--><!--a]-->\`"
+      `)
+    })
+
+    test('prepend anchor with v-for', () => {
+      expect(
+        getCompiledString('<div><span v-for="item in items"/><span/></div>', {
+          vapor: true,
+        }),
+      ).toMatchInlineSnapshot(`
+        "\`<div><!--[p--><!--[-->\`)
+          _ssrRenderList(_ctx.items, (item) => {
+            _push(\`<span></span>\`)
+          })
+          _push(\`<!--]--><!--for--><!--p]--><span></span></div>\`"
+      `)
+    })
+
+    test('prepend anchor with v-for in ssr slot vnode fallback', () => {
+      expect(
+        getCompiledString(
+          `<component :is="'div'">
+            <div><span v-for="item in items"/><span/></div>
+          </component>`,
+          { vapor: true },
+        ),
+      ).toMatchInlineSnapshot(`
+        "\`<!--[a-->\`)
+          _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent('div'), null, {
+            default: _withCtx((_, _push, _parent, _scopeId) => {
+              if (_push) {
+                _push(\`<div\${_scopeId}><!--[p--><!--[-->\`)
+                _ssrRenderList(_ctx.items, (item) => {
+                  _push(\`<span\${_scopeId}></span>\`)
+                })
+                _push(\`<!--]--><!--for--><!--p]--><span\${_scopeId}></span></div>\`)
+              } else {
+                return [
+                  _createVNode("div", null, [
+                    _createCommentVNode("[p"),
+                    (_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.items, (item) => {
+                      return (_openBlock(), _createBlock("span"))
+                    }), 256 /* UNKEYED_FRAGMENT */)),
+                    _createCommentVNode("p]"),
+                    _createVNode("span")
+                  ])
+                ]
+              }
+            }),
+            _: 1 /* STABLE */
+          }), _parent)
+          _push(\`<!--dynamic-component--><!--a]-->\`"
+      `)
+    })
+  })
+
+  // TODO add more tests
+  describe('insertion anchor', () => {
+    test('insertion anchor with component', () => {
+      expect(
+        getCompiledString('<div><span/><Comp/><span/></div>', { vapor: true }),
+      ).toMatchInlineSnapshot(`
+        "\`<div><span></span><!--[i-->\`)
+          _push(_ssrRenderComponent(_component_Comp, null, null, _parent))
+          _push(\`<!--i]--><span></span></div>\`"
+      `)
+    })
+  })
+
+  // TODO add more tests
+  describe('append', () => {
+    test('append anchor', () => {
+      expect(
+        getCompiledString('<div><span/><Comp/><Comp/></div>', { vapor: true }),
+      ).toMatchInlineSnapshot(`
+        "\`<div><span></span><!--[a-->\`)
+          _push(_ssrRenderComponent(_component_Comp, null, null, _parent))
+          _push(\`<!--a]--><!--[a-->\`)
+          _push(_ssrRenderComponent(_component_Comp, null, null, _parent))
+          _push(\`<!--a]--></div>\`"
+      `)
+    })
+  })
+
+  test('mixed anchors', () => {
+    expect(
+      getCompiledString('<div><Comp/><span/><Comp/><span/><Comp/></div>', {
+        vapor: true,
+      }),
+    ).toMatchInlineSnapshot(`
+      "\`<div><!--[p-->\`)
+        _push(_ssrRenderComponent(_component_Comp, null, null, _parent))
+        _push(\`<!--p]--><span></span><!--[i-->\`)
+        _push(_ssrRenderComponent(_component_Comp, null, null, _parent))
+        _push(\`<!--i]--><span></span><!--[a-->\`)
+        _push(_ssrRenderComponent(_component_Comp, null, null, _parent))
+        _push(\`<!--a]--></div>\`"
+    `)
+  })
+
+  test('mixed anchors in ssr slot vnode fallback', () => {
+    expect(
+      getCompiledString(
+        `<component :is="'div'"><Comp/><span/><Comp/><span/><Comp/></component>`,
+        {
+          vapor: true,
+        },
+      ),
+    ).toMatchInlineSnapshot(`
+      "\`<!--[a-->\`)
+        _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent('div'), null, {
+          default: _withCtx((_, _push, _parent, _scopeId) => {
+            if (_push) {
+              _push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
+              _push(\`<span\${_scopeId}></span>\`)
+              _push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
+              _push(\`<span\${_scopeId}></span>\`)
+              _push(_ssrRenderComponent(_component_Comp, null, null, _parent, _scopeId))
+            } else {
+              return [
+                _createCommentVNode("[p"),
+                _createVNode(_component_Comp),
+                _createCommentVNode("p]"),
+                _createVNode("span"),
+                _createCommentVNode("[i"),
+                _createVNode(_component_Comp),
+                _createCommentVNode("i]"),
+                _createVNode("span"),
+                _createCommentVNode("[a"),
+                _createVNode(_component_Comp),
+                _createCommentVNode("a]")
+              ]
+            }
+          }),
+          _: 1 /* STABLE */
+        }), _parent)
+        _push(\`<!--dynamic-component--><!--a]-->\`"
+    `)
+  })
+})
+
+describe.todo('block anchors', () => {
+  test('if', () => {})
+
+  test('if in ssr slot vnode fallback', () => {})
+
+  test('for', () => {})
+
+  test('for in ssr slot vnode fallback', () => {})
+
+  test('slot', () => {})
+
+  test('slot in ssr slot vnode fallback', () => {})
+
+  test('dynamic component', () => {})
+
+  test('dynamic in ssr slot vnode fallback', () => {})
+})
index c656517cdb87d0b28874ccf9f0c29a5c0555dfeb..29f193daee6f43ebeb6c8cbfdcc56c6a1d448479 100644 (file)
@@ -1,10 +1,14 @@
+import type { CompilerOptions } from '@vue/compiler-core'
 import { compile } from '../src'
 
-export function getCompiledString(src: string): string {
+export function getCompiledString(
+  src: string,
+  options?: CompilerOptions,
+): string {
   // Wrap src template in a root div so that it doesn't get injected
   // fallthrough attr. This results in less noise in generated snapshots
   // but also means this util can only be used for non-root cases.
-  const { code } = compile(`<div>${src}</div>`)
+  const { code } = compile(`<div>${src}</div>`, options)
   const match = code.match(
     /_push\(\`<div\${\s*_ssrRenderAttrs\(_attrs\)\s*}>([^]*)<\/div>\`\)/,
   )
index 2221e6c948eb209ed8583a80a0aaeb59b08066a0..30ec8050ed80280ef6452771e16490d9968c823e 100644 (file)
@@ -22,8 +22,9 @@ import {
   processExpression,
 } from '@vue/compiler-dom'
 import {
-  BLOCK_END_ANCHOR_LABEL,
-  BLOCK_START_ANCHOR_LABEL,
+  BLOCK_APPEND_ANCHOR_LABEL,
+  BLOCK_INSERTION_ANCHOR_LABEL,
+  BLOCK_PREPEND_ANCHOR_LABEL,
   escapeHtml,
   isString,
 } from '@vue/shared'
@@ -163,33 +164,23 @@ export function processChildren(
   asFragment = false,
   disableNestedFragments = false,
   disableComment = false,
-  asBlock = false,
 ): void {
-  if (asBlock) {
-    context.pushStringPart(`<!--${BLOCK_START_ANCHOR_LABEL}-->`)
-  }
   if (asFragment) {
     context.pushStringPart(`<!--[-->`)
   }
 
   const { children, type, tagType } = parent as PlainElementNode
-  const inElement =
-    type === NodeTypes.ELEMENT && tagType === ElementTypes.ELEMENT
-  if (inElement) processChildrenBlockInfo(children)
+
+  if (
+    context.options.vapor &&
+    type === NodeTypes.ELEMENT &&
+    tagType === ElementTypes.ELEMENT
+  ) {
+    processBlockNodeAnchor(children)
+  }
 
   for (let i = 0; i < children.length; i++) {
     const child = children[i]
-    if (inElement && shouldProcessChildAsBlock(parent, child)) {
-      processChildren(
-        { children: [child] },
-        context,
-        asFragment,
-        disableNestedFragments,
-        disableComment,
-        true,
-      )
-      continue
-    }
     switch (child.type) {
       case NodeTypes.ELEMENT:
         switch (child.tagType) {
@@ -197,14 +188,14 @@ export function processChildren(
             ssrProcessElement(child, context)
             break
           case ElementTypes.COMPONENT:
-            if (inElement)
-              context.pushStringPart(`<!--${BLOCK_START_ANCHOR_LABEL}-->`)
+            if (child.anchor) context.pushStringPart(`<!--[${child.anchor}-->`)
             ssrProcessComponent(child, context, parent)
-            if (inElement)
-              context.pushStringPart(`<!--${BLOCK_END_ANCHOR_LABEL}-->`)
+            if (child.anchor) context.pushStringPart(`<!--${child.anchor}]-->`)
             break
           case ElementTypes.SLOT:
+            if (child.anchor) context.pushStringPart(`<!--[${child.anchor}-->`)
             ssrProcessSlotOutlet(child, context)
+            if (child.anchor) context.pushStringPart(`<!--${child.anchor}]-->`)
             break
           case ElementTypes.TEMPLATE:
             // TODO
@@ -239,10 +230,14 @@ export function processChildren(
         )
         break
       case NodeTypes.IF:
+        if (child.anchor) context.pushStringPart(`<!--[${child.anchor}-->`)
         ssrProcessIf(child, context, disableNestedFragments, disableComment)
+        if (child.anchor) context.pushStringPart(`<!--${child.anchor}]-->`)
         break
       case NodeTypes.FOR:
+        if (child.anchor) context.pushStringPart(`<!--[${child.anchor}-->`)
         ssrProcessFor(child, context, disableNestedFragments)
+        if (child.anchor) context.pushStringPart(`<!--${child.anchor}]-->`)
         break
       case NodeTypes.IF_BRANCH:
         // no-op - handled by ssrProcessIf
@@ -267,9 +262,6 @@ export function processChildren(
   if (asFragment) {
     context.pushStringPart(`<!--]-->`)
   }
-  if (asBlock) {
-    context.pushStringPart(`<!--${BLOCK_END_ANCHOR_LABEL}-->`)
-  }
 }
 
 export function processChildrenAsStatement(
@@ -283,117 +275,69 @@ export function processChildrenAsStatement(
   return createBlockStatement(childContext.body)
 }
 
-const isStaticChildNode = (c: TemplateChildNode): boolean =>
-  (c.type === NodeTypes.ELEMENT && c.tagType !== ElementTypes.COMPONENT) ||
-  c.type === NodeTypes.TEXT ||
-  c.type === NodeTypes.COMMENT
-
-interface BlockInfo {
-  hasStaticPrevious: boolean
-  hasStaticNext: boolean
-  prevBlockCount: number
-  nextBlockCount: number
-}
-
-function processChildrenBlockInfo(
-  children: (TemplateChildNode & { _ssrBlockInfo?: BlockInfo })[],
-): void {
-  const filteredChildren = children.filter(
-    child => !(child.type === NodeTypes.TEXT && !child.content.trim()),
-  )
-
-  for (let i = 0; i < filteredChildren.length; i++) {
-    const child = filteredChildren[i]
-    if (
-      isStaticChildNode(child) ||
-      // fragment has it's own anchor, which can be used to distinguish the boundary
-      isFragmentChild(child)
-    ) {
-      continue
+export function processBlockNodeAnchor(children: TemplateChildNode[]): void {
+  let prevBlocks: (TemplateChildNode & { anchor?: string })[] = []
+  let hasStaticNode = false
+  for (const child of children) {
+    if (isBlockNode(child)) {
+      prevBlocks.push(child)
     }
-    child._ssrBlockInfo = {
-      hasStaticPrevious: false,
-      hasStaticNext: false,
-      prevBlockCount: 0,
-      nextBlockCount: 0,
-    }
-
-    const info = child._ssrBlockInfo
 
-    // Calculate the previous static and block node counts
-    let foundStaticPrev = false
-    let blockCountPrev = 0
-    for (let j = i - 1; j >= 0; j--) {
-      const prevChild = filteredChildren[j]
-      if (isStaticChildNode(prevChild)) {
-        foundStaticPrev = true
-        break
-      }
-      // if the previous child has block info, use it
-      else if (prevChild._ssrBlockInfo) {
-        foundStaticPrev = prevChild._ssrBlockInfo.hasStaticPrevious
-        blockCountPrev = prevChild._ssrBlockInfo.prevBlockCount + 1
-        break
-      }
-      blockCountPrev++
-    }
-    info.hasStaticPrevious = foundStaticPrev
-    info.prevBlockCount = blockCountPrev
-
-    // Calculate the number of static and block nodes afterwards
-    let foundStaticNext = false
-    let blockCountNext = 0
-    for (let j = i + 1; j < filteredChildren.length; j++) {
-      const nextChild = filteredChildren[j]
-      if (isStaticChildNode(nextChild)) {
-        foundStaticNext = true
-        break
-      }
-      // if the next child has block info, use it
-      else if (nextChild._ssrBlockInfo) {
-        foundStaticNext = nextChild._ssrBlockInfo.hasStaticNext
-        blockCountNext = nextChild._ssrBlockInfo.nextBlockCount + 1
-        break
+    if (isStaticNode(child)) {
+      if (prevBlocks.length) {
+        if (hasStaticNode) {
+          // insertion anchor
+          prevBlocks.forEach(
+            child => (child.anchor = BLOCK_INSERTION_ANCHOR_LABEL),
+          )
+        } else {
+          // prepend
+          prevBlocks.forEach(
+            child => (child.anchor = BLOCK_PREPEND_ANCHOR_LABEL),
+          )
+        }
+        prevBlocks = []
       }
-      blockCountNext++
+      hasStaticNode = true
     }
-    info.hasStaticNext = foundStaticNext
-    info.nextBlockCount = blockCountNext
   }
-}
-
-function shouldProcessChildAsBlock(
-  parent: { tag?: string; children: TemplateChildNode[] },
-  node: TemplateChildNode & { _ssrBlockInfo?: BlockInfo },
-): boolean {
-  // must be inside a parent element
-  if (!parent.tag) return false
 
-  // must has block info
-  const { _ssrBlockInfo: info } = node
-  if (!info) return false
-
-  const { hasStaticPrevious, hasStaticNext, prevBlockCount, nextBlockCount } =
-    info
-
-  // must have static nodes on both sides
-  if (!hasStaticPrevious || !hasStaticNext) return false
-
-  const blockNodeCount = 1 + prevBlockCount + nextBlockCount
-
-  // For two consecutive block nodes, mark the second one as block
-  if (blockNodeCount === 2) {
-    return prevBlockCount > 0
-  }
-  // For three or more block nodes, mark the middle nodes as block
-  else if (blockNodeCount >= 3) {
-    return prevBlockCount > 0 && nextBlockCount > 0
+  if (prevBlocks.length) {
+    // append anchor
+    prevBlocks.forEach(child => (child.anchor = BLOCK_APPEND_ANCHOR_LABEL))
   }
+}
 
-  return false
+function isBlockNode(child: TemplateChildNode): boolean {
+  return (
+    child.type === NodeTypes.IF ||
+    child.type === NodeTypes.FOR ||
+    (child.type === NodeTypes.ELEMENT &&
+      (child.tagType === ElementTypes.COMPONENT ||
+        child.tagType === ElementTypes.SLOT ||
+        child.props.some(
+          p =>
+            p.name === 'if' ||
+            p.name === 'else-if' ||
+            p.name === 'else' ||
+            p.name === 'for',
+        )))
+  )
 }
 
-function isFragmentChild(child: TemplateChildNode): boolean {
-  const { type } = child
-  return type === NodeTypes.IF || type === NodeTypes.FOR
+function isStaticNode(child: TemplateChildNode): boolean {
+  return (
+    child.type === NodeTypes.TEXT ||
+    child.type === NodeTypes.INTERPOLATION ||
+    child.type === NodeTypes.COMMENT ||
+    (child.type === NodeTypes.ELEMENT &&
+      child.tagType === ElementTypes.ELEMENT &&
+      !child.props.some(
+        p =>
+          p.name === 'if' ||
+          p.name === 'else-if' ||
+          p.name === 'else' ||
+          p.name === 'for',
+      ))
+  )
 }
index 419c2e4e49cab88279712b7ac2136638298841b9..5af6041480de8b57e8d96a9b00c1e5c6bac61136 100644 (file)
@@ -43,6 +43,7 @@ import {
 import { SSR_RENDER_COMPONENT, SSR_RENDER_VNODE } from '../runtimeHelpers'
 import {
   type SSRTransformContext,
+  processBlockNodeAnchor,
   processChildren,
   processChildrenAsStatement,
 } from '../ssrCodegenTransform'
@@ -271,8 +272,11 @@ export function ssrProcessComponent(
       // dynamic component (`resolveDynamicComponent` call)
       // the codegen node is a `renderVNode` call
       context.pushStatement(node.ssrCodegenNode)
-      // anchor for dynamic component for vapor hydration
-      context.pushStringPart(`<!--${DYNAMIC_COMPONENT_ANCHOR_LABEL}-->`)
+
+      // anchor for vapor dynamic component
+      if (context.options.vapor) {
+        context.pushStringPart(`<!--${DYNAMIC_COMPONENT_ANCHOR_LABEL}-->`)
+      }
     }
   }
 }
@@ -339,6 +343,11 @@ function createVNodeSlotBranch(
     loc: locStub,
     codegenNode: undefined,
   }
+
+  if (parentContext.vapor) {
+    injectVaporInsertionAnchors(children)
+  }
+
   subTransform(wrapperNode, subOptions, parentContext)
   return createReturnStatement(children)
 }
@@ -380,6 +389,71 @@ function subTransform(
   // - hoists are not enabled for the client branch here
 }
 
+function injectVaporInsertionAnchors(children: TemplateChildNode[]) {
+  processBlockNodeAnchor(children)
+  for (let i = 0; i < children.length; i++) {
+    const child = children[i]
+    switch (child.type) {
+      case NodeTypes.ELEMENT:
+        switch (child.tagType) {
+          case ElementTypes.COMPONENT:
+          case ElementTypes.SLOT:
+            if (child.anchor) {
+              children.splice(i, 0, {
+                type: NodeTypes.COMMENT,
+                content: `[${child.anchor}`,
+                loc: locStub,
+              })
+              children.splice(i + 2, 0, {
+                type: NodeTypes.COMMENT,
+                content: `${child.anchor}]`,
+                loc: locStub,
+              })
+              i += 2
+            }
+            break
+          default: {
+            const { props } = child
+            if (
+              props.some(
+                p =>
+                  p.name === 'if' ||
+                  p.name === 'else-if' ||
+                  p.name === 'else' ||
+                  p.name === 'for',
+              )
+            ) {
+              // @ts-expect-error
+              if (child.anchor) {
+                children.splice(i, 0, {
+                  type: NodeTypes.COMMENT,
+                  // @ts-expect-error
+                  content: `[${child.anchor}`,
+                  loc: locStub,
+                })
+                children.splice(i + 2, 0, {
+                  type: NodeTypes.COMMENT,
+                  // @ts-expect-error
+                  content: `${child.anchor}]`,
+                  loc: locStub,
+                })
+              }
+              i += 2
+              break
+            }
+          }
+        }
+    }
+
+    if (
+      child.type === NodeTypes.ELEMENT &&
+      child.tagType === ElementTypes.ELEMENT
+    ) {
+      injectVaporInsertionAnchors(child.children)
+    }
+  }
+}
+
 function clone(v: any): any {
   if (isArray(v)) {
     return v.map(clone)
index aae7a31e8b8b01bfec066d31785553b26cb43fb9..5c72c8a2a629b5c866eef55a36faa37d819fe0aa 100644 (file)
@@ -16,6 +16,7 @@ import {
   type SSRTransformContext,
   processChildrenAsStatement,
 } from '../ssrCodegenTransform'
+import { SLOT_ANCHOR_LABEL } from '@vue/shared'
 
 export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
   if (isSlotOutlet(node)) {
@@ -93,4 +94,9 @@ export function ssrProcessSlotOutlet(
   }
 
   context.pushStatement(node.ssrCodegenNode!)
+
+  // anchor for vapor slot
+  if (context.options.vapor) {
+    context.pushStringPart(`<!--${SLOT_ANCHOR_LABEL}-->`)
+  }
 }
index 251b1fef5c9505b5a2907d2c2984fc086a6193cd..f7878d4f80719627283950faa27a11c0f95b1e2f 100644 (file)
@@ -50,6 +50,9 @@ export function ssrProcessFor(
   if (!disableNestedFragments) {
     context.pushStringPart(`<!--]-->`)
   }
-  // v-for anchor for vapor hydration
-  context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`)
+
+  // anchor for vapor v-for fragment
+  if (context.options.vapor) {
+    context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`)
+  }
 }
index 4e7f5ca1b1c10a11f4b18b89fed0d1464e442cf7..1470850c701e9fe501e929ca47dc382525316870 100644 (file)
@@ -81,10 +81,12 @@ function processIfBranch(
     needFragmentWrapper,
   )
 
-  // v-if/v-else-if/v-else anchor for vapor hydration
-  statement.body.push(
-    createCallExpression(`_push`, [`\`<!--${IF_ANCHOR_LABEL}-->\``]),
-  )
+  // anchor for vapor v-if/v-else-if
+  if (context.options.vapor) {
+    statement.body.push(
+      createCallExpression(`_push`, [`\`<!--${IF_ANCHOR_LABEL}-->\``]),
+    )
+  }
 
   return statement
 }
index a6b13b9de58700e195174d34498579970d3f6539..103e174e99328f4142c192e808c280f5c5e81167 100644 (file)
@@ -38,7 +38,7 @@ export function render(_ctx) {
     "default": () => {
       const n0 = _createIf(() => (true), () => {
         const n3 = t0()
-        _setInsertionState(n3)
+        _setInsertionState(n3, null)
         const n2 = _createComponentWithFallback(_component_Bar)
         _withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]])
         return n3
@@ -157,7 +157,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
   const _component_Comp = _resolveComponent("Comp")
   const n0 = t0()
   const n3 = t1()
-  const n2 = _child(n3, 1)
+  const n2 = _child(n3)
   _setInsertionState(n3, 0)
   const n1 = _createComponentWithFallback(_component_Comp)
   _renderEffect(() => {
@@ -220,9 +220,9 @@ export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
   const n3 = t0()
   const n1 = _child(n3)
-  _setInsertionState(n1)
+  _setInsertionState(n1, null)
   const n0 = _createSlot("default", null)
-  _setInsertionState(n3, null, 1)
+  _setInsertionState(n3, null)
   const n2 = _createComponentWithFallback(_component_Comp)
   return n3
 }"
index 09329d2e9cd84323ddd7d7303866f0e1e8e2cba1..e7745851e5aab15a342940aebefc81a7a45459d6 100644 (file)
@@ -87,7 +87,7 @@ 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)
     const n2 = _createFor(() => (_for_item0.value), (_for_item1) => {
       const n4 = t0()
       const x4 = _child(n4, -1)
index f718fe9394002bc97e12728e7e682ad7c7f2228d..12b7acd93721378f4b7317a11e5f67c8702368a1 100644 (file)
@@ -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)
   const n3 = _createIf(() => (_ctx.bar), () => {
     const n5 = t1()
     return n5
index b6107d5a1a1c63067461c43cda989eb22e144065..4ca745ef0f575bcfefe521764cfea16fb0ddea7b 100644 (file)
@@ -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)
   const n0 = _createComponentWithFallback(_component_Comp, { id: () => (_ctx.foo) }, null, null, true)
   return n1
 }"
index a13da5abf9d316adcd3c6fc10c76447f8ae51df2..9b11f074c40d009751f819b13be603e3b2df7ebc 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)
   const n1 = _createIf(() => (true), () => {
     const n3 = _createComponentWithFallback(_component_Foo)
     return n3
index 337f27799622fc92e8f6c8f8d93bb4bc931382ea..8250966617f42748b4dfaefd368b8b2eca5b0c9d 100644 (file)
@@ -39,8 +39,6 @@ export class CodegenContext {
 
   seenInlineHandlerNames: Record<string, number> = Object.create(null)
 
-  seenChildIndexes: Map<number, number> = new Map()
-
   block: BlockIRNode
   withId<T>(
     fn: () => T,
@@ -88,6 +86,7 @@ export class CodegenContext {
       isTS: false,
       inSSR: false,
       inline: false,
+      vapor: false,
       bindingMetadata: {},
       expressionPlugins: [],
     }
index 2069c9f6a6673f1ee5a3aab32a22159d7065d675..bc78032380f92f350744df1962e91dbce76ccde5 100644 (file)
@@ -168,33 +168,19 @@ function genInsertionState(
   operation: InsertionStateTypes,
   context: CodegenContext,
 ): CodeFragment[] {
-  const { seenChildIndexes } = context
-  const { parent, childIndex, anchor } = operation
-  const insertionAnchor =
-    anchor == null
-      ? undefined
-      : anchor === -1 // -1 indicates prepend
-        ? `0` // runtime anchor value for prepend
-        : `n${anchor}`
-
-  // the index of next block node, used to locate node during hydration
-  // only passed when anchor is null and childIndex > 0
-  let index: number | undefined
-  if (anchor == null && childIndex) {
-    const existingOffset = seenChildIndexes.get(parent!)
-    seenChildIndexes.set(
-      parent!,
-      (index = existingOffset ? existingOffset + 1 : childIndex),
-    )
-  }
-
+  const { parent, anchor } = operation
   return [
     NEWLINE,
     ...genCall(
       context.helper('setInsertionState'),
       `n${parent}`,
-      insertionAnchor,
-      index ? `${index}` : undefined,
+      anchor == null
+        ? undefined
+        : anchor === -1 // -1 indicates prepend
+          ? `0` // runtime anchor value for prepend
+          : anchor === -2 // -2 indicates append
+            ? `null` // runtime anchor value for append
+            : `n${anchor}`,
     ),
   ]
 }
index d7d29d5d155ad2166e0d030611262502260e3f65..f710c8869fb3c973871639b9708ed78ebc49f79d 100644 (file)
@@ -53,11 +53,9 @@ export function genChildren(
   const { children } = dynamic
 
   let offset = 0
-  let ifBranchCount = 0
   let prev: [variable: string, elementIndex: number] | undefined
 
   for (const [index, child] of children.entries()) {
-    if (child.isIfBranch) ifBranchCount++
     if (child.flags & DynamicFlag.NON_TEMPLATE) {
       offset--
     }
@@ -87,29 +85,11 @@ export function genChildren(
         pushBlock(...genCall(helper('nthChild'), from, String(elementIndex)))
       }
     } else {
-      // child index is used to find the child during hydration.
-      // if offset is not 0, we need to specify the offset to skip the dynamic
-      // children and get the correct child.
-      const asAnchor =
-        id !== undefined && children.some(child => child.anchor === id)
-      let childIndex =
-        offset === 0
-          ? undefined
-          : // if the current node is used as insertionAnchor, subtract 1 here
-            // this ensures that insertionAnchor points to the current node itself
-            // rather than its next sibling, since insertionAnchor is used as the
-            // hydration node
-            `${
-              (asAnchor ? index - 1 : index) -
-              // treat v-if/v-else/v-else-if as a single node
-              ifBranchCount
-            }`
-
       if (elementIndex === 0) {
-        pushBlock(...genCall(helper('child'), from, childIndex))
+        pushBlock(...genCall(helper('child'), from))
       } else {
         // check if there's a node that we can reuse from
-        let init = genCall(helper('child'), from, childIndex)
+        let init = genCall(helper('child'), from)
         if (elementIndex === 1) {
           init = genCall(helper('next'), init)
         } else if (elementIndex > 1) {
index b79152f37b6dd88089ab5df225dc2413f5be1f3d..b705f8f4db1eee5605c26160007a2a6dcfc8c3a5 100644 (file)
@@ -69,7 +69,7 @@ export class TransformContext<T extends AllNode = AllNode> {
 
   block: BlockIRNode = this.ir.block
   options: Required<
-    Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
+    Omit<TransformOptions, 'vapor' | 'filename' | keyof CompilerCompatOptions>
   >
 
   template: string = ''
index 71e31718909dd46bb18ce0a5a23ffb0d671c166a..e4dcd4d9f37efef42eb0535bd80e349da8e008a3 100644 (file)
@@ -70,30 +70,12 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
     if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
       if (prevDynamics.length) {
         if (hasStaticTemplate) {
-          // each dynamic child gets its own placeholder node.
-          // this makes it easier to locate the corresponding node during hydration.
-          for (let i = 0; i < prevDynamics.length; i++) {
-            const idx = index - prevDynamics.length + i
-            context.childrenTemplate[idx] = `<!>`
-            const dynamicChild = prevDynamics[i]
-            dynamicChild.flags -= DynamicFlag.NON_TEMPLATE
-            const anchor = (dynamicChild.anchor = context.increaseId())
-            if (
-              dynamicChild.operation &&
-              isBlockOperation(dynamicChild.operation)
-            ) {
-              // block types
-              dynamicChild.operation.parent = context.reference()
-              dynamicChild.operation.anchor = anchor
-            }
-          }
+          context.childrenTemplate[index - prevDynamics.length] = `<!>`
+          prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
+          const anchor = (prevDynamics[0].anchor = context.increaseId())
+          registerInsertion(prevDynamics, context, anchor)
         } else {
-          registerInsertion(
-            prevDynamics,
-            context,
-            -1 /* prepend */,
-            getChildIndex(children, prevDynamics[0]),
-          )
+          registerInsertion(prevDynamics, context, -1 /* prepend */)
         }
         prevDynamics = []
       }
@@ -102,12 +84,7 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
   }
 
   if (prevDynamics.length) {
-    registerInsertion(
-      prevDynamics,
-      context,
-      undefined,
-      getChildIndex(children, prevDynamics[0]),
-    )
+    registerInsertion(prevDynamics, context, -2 /* append */)
   }
 }
 
@@ -115,7 +92,6 @@ function registerInsertion(
   dynamics: IRDynamicInfo[],
   context: TransformContext,
   anchor?: number,
-  childIndex?: number,
 ) {
   for (const child of dynamics) {
     if (child.template != null) {
@@ -124,27 +100,12 @@ function registerInsertion(
         type: IRNodeTypes.INSERT_NODE,
         elements: dynamics.map(child => child.id!),
         parent: context.reference(),
-        anchor,
+        anchor: anchor === -2 ? undefined : anchor,
       })
     } else if (child.operation && isBlockOperation(child.operation)) {
       // block types
       child.operation.parent = context.reference()
       child.operation.anchor = anchor
-      child.operation.childIndex = childIndex
     }
   }
 }
-
-function getChildIndex(
-  children: IRDynamicInfo[],
-  child: IRDynamicInfo,
-): number {
-  let index = 0
-  for (const c of children) {
-    // treat v-if/v-else/v-else-if as a single node
-    if (c.isIfBranch) continue
-    if (c === child) break
-    index++
-  }
-  return index
-}
index 418d37cc8a66df459213342fca6227ef22bf1c8f..4a9e0fac2b6e8f1f69215d47219556d5bcbb7749 100644 (file)
@@ -601,14 +601,14 @@ describe('SSR hydration', () => {
     const ctx: SSRContext = {}
     container.innerHTML = await renderToString(h(App), ctx)
     expect(container.innerHTML).toBe(
-      '<div><!--teleport start--><!--teleport end--><!--if--></div>',
+      '<div><!--teleport start--><!--teleport end--></div>',
     )
     teleportContainer.innerHTML = ctx.teleports!['#target']
 
     // hydrate
     createSSRApp(App).mount(container)
     expect(container.innerHTML).toBe(
-      '<div><!--teleport start--><!--teleport end--><!--if--></div>',
+      '<div><!--teleport start--><!--teleport end--></div>',
     )
     expect(teleportContainer.innerHTML).toBe(
       '<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
@@ -617,7 +617,7 @@ describe('SSR hydration', () => {
 
     toggle.value = false
     await nextTick()
-    expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
+    expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
     expect(teleportContainer.innerHTML).toBe('')
   })
 
@@ -660,21 +660,21 @@ describe('SSR hydration', () => {
     // server render
     container.innerHTML = await renderToString(h(App))
     expect(container.innerHTML).toBe(
-      '<div><!--teleport start--><!--teleport end--><!--if--></div>',
+      '<div><!--teleport start--><!--teleport end--></div>',
     )
     expect(teleportContainer.innerHTML).toBe('')
 
     // hydrate
     createSSRApp(App).mount(container)
     expect(container.innerHTML).toBe(
-      '<div><!--teleport start--><!--teleport end--><!--if--></div>',
+      '<div><!--teleport start--><!--teleport end--></div>',
     )
     expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
     expect(`Hydration children mismatch`).toHaveBeenWarned()
 
     toggle.value = false
     await nextTick()
-    expect(container.innerHTML).toBe('<div><div>Comp2</div><!--if--></div>')
+    expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
     expect(teleportContainer.innerHTML).toBe('')
   })
 
@@ -713,7 +713,7 @@ describe('SSR hydration', () => {
     // server render
     container.innerHTML = await renderToString(h(App))
     expect(container.innerHTML).toBe(
-      '<div><!--[[--><!--teleport start--><!--teleport end--><!--]]--></div>',
+      '<div><!--teleport start--><!--teleport end--></div>',
     )
     expect(teleportContainer1.innerHTML).toBe('')
     expect(teleportContainer2.innerHTML).toBe('')
@@ -721,7 +721,7 @@ describe('SSR hydration', () => {
     // hydrate
     createSSRApp(App).mount(container)
     expect(container.innerHTML).toBe(
-      '<div><!--[[--><!--teleport start--><!--teleport end--><!--]]--></div>',
+      '<div><!--teleport start--><!--teleport end--></div>',
     )
     expect(teleportContainer1.innerHTML).toBe('<span>Teleported</span>')
     expect(teleportContainer2.innerHTML).toBe('')
@@ -1005,7 +1005,7 @@ describe('SSR hydration', () => {
     // server render
     container.innerHTML = await renderToString(h(App))
     expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div><!--[[--><span>1</span><!--]]--><!--[[--><span>2</span><!--]]--></div>"`,
+      `"<div><span>1</span><span>2</span></div>"`,
     )
     // reset asyncDeps from ssr
     asyncDeps.length = 0
@@ -1869,36 +1869,6 @@ describe('SSR hydration', () => {
     }
   })
 
-  describe('dynamic anchor', () => {
-    test('two consecutive components', () => {
-      const Comp = {
-        render() {
-          return createTextVNode('foo')
-        },
-      }
-      const { vnode, container } = mountWithHydration(
-        `<div><span></span>foo<!--[[-->foo<!--]]--><span></span></div>`,
-        () => h('div', null, [h('span'), h(Comp), h(Comp), h('span')]),
-      )
-      expect(vnode.el).toBe(container.firstChild)
-      expect(`Hydration children mismatch`).not.toHaveBeenWarned()
-    })
-
-    test('multiple consecutive components', () => {
-      const Comp = {
-        render() {
-          return createTextVNode('foo')
-        },
-      }
-      const { vnode, container } = mountWithHydration(
-        `<div><span></span>foo<!--[[-->foo<!--]]-->foo<span></span></div>`,
-        () => h('div', null, [h('span'), h(Comp), h(Comp), h(Comp), h('span')]),
-      )
-      expect(vnode.el).toBe(container.firstChild)
-      expect(`Hydration children mismatch`).not.toHaveBeenWarned()
-    })
-  })
-
   test('hmr reload child wrapped in KeepAlive', async () => {
     const id = 'child-reload'
     const Child = {
@@ -1923,14 +1893,14 @@ describe('SSR hydration', () => {
     const root = document.createElement('div')
     root.innerHTML = await renderToString(h(App))
     createSSRApp(App).mount(root)
-    expect(root.innerHTML).toBe('<div><!--[[--><div>foo</div><!--]]--></div>')
+    expect(root.innerHTML).toBe('<div><div>foo</div></div>')
 
     reload(id, {
       __hmrId: id,
       template: `<div>bar</div>`,
     })
     await nextTick()
-    expect(root.innerHTML).toBe('<div><!--[[--><div>bar</div><!--]]--></div>')
+    expect(root.innerHTML).toBe('<div><div>bar</div></div>')
   })
 
   test('hmr root reload', async () => {
index 364a1e832091069e8b51d8a2fec1cc41dd9eb778..ae660abf1546e268df440aa164d743da6d63f028 100644 (file)
@@ -32,7 +32,6 @@ import {
   isRenderableAttrValue,
   isReservedProp,
   isString,
-  isVaporAnchor,
   normalizeClass,
   normalizeCssVarValue,
   normalizeStyle,
@@ -118,7 +117,7 @@ export function createHydrationFunctions(
     o: {
       patchProp,
       createText,
-      nextSibling: next,
+      nextSibling,
       parentNode,
       remove,
       insert,
@@ -126,15 +125,6 @@ export function createHydrationFunctions(
     },
   } = rendererInternals
 
-  function nextSibling(node: Node) {
-    let n = next(node)
-    // skip vapor mode specific anchors
-    if (n && isVaporAnchor(n)) {
-      n = next(n)
-    }
-    return n
-  }
-
   const hydrate: RootHydrateFunction = (vnode, container) => {
     if (!container.hasChildNodes()) {
       ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
@@ -161,10 +151,6 @@ export function createHydrationFunctions(
     slotScopeIds: string[] | null,
     optimized = false,
   ): Node | null => {
-    // skip vapor mode specific anchors
-    if (isVaporAnchor(node)) {
-      node = nextSibling(node)!
-    }
     optimized = optimized || !!vnode.dynamicChildren
     const isFragmentStart = isComment(node) && node.data === '['
     const onMismatch = () =>
@@ -486,7 +472,7 @@ export function createHydrationFunctions(
 
           // The SSRed DOM contains more nodes than it should. Remove them.
           const cur = next
-          next = nextSibling(next)
+          next = next.nextSibling
           remove(cur)
         }
       } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
@@ -592,7 +578,7 @@ export function createHydrationFunctions(
       }
     }
 
-    return nextSibling(el)
+    return el.nextSibling
   }
 
   const hydrateChildren = (
index 7b471ab7ae640d2a78af71e0d3fb620e4ea09bd6..ecfd1a8977abd148edb880b11a6ad03de7ee70c1 100644 (file)
@@ -4,13 +4,14 @@ import { compileScript, parse } from '@vue/compiler-sfc'
 import * as runtimeVapor from '../src'
 import * as runtimeDom from '@vue/runtime-dom'
 import * as VueServerRenderer from '@vue/server-renderer'
-import {
-  DYNAMIC_COMPONENT_ANCHOR_LABEL,
-  FOR_ANCHOR_LABEL,
-  IF_ANCHOR_LABEL,
-  SLOT_ANCHOR_LABEL,
-  isString,
-} from '@vue/shared'
+import { isString } from '@vue/shared'
+
+const formatHtml = (raw: string) => {
+  return raw
+    .replace(/<!--\[/g, '\n<!--[')
+    .replace(/]-->/g, ']-->\n')
+    .replace(/\n{2,}/g, '\n')
+}
 
 const Vue = { ...runtimeDom, ...runtimeVapor }
 
@@ -124,36 +125,44 @@ describe('Vapor Mode hydration', () => {
       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"`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"bar"`)
     })
 
     test('consecutive text nodes', async () => {
       const { data, container } = await testHydration(`
       <template>{{ data }}{{ data }}</template>
     `)
-      expect(container.innerHTML).toMatchInlineSnapshot(`"foofoo"`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"foofoo"`)
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(`"barbar"`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"barbar"`)
     })
 
-    test('consecutive text nodes with anchor insertion', async () => {
+    test('consecutive text nodes with insertion anchor', async () => {
       const { data, container } = await testHydration(`
       <template><span/>{{ data }}{{ data }}<span/></template>
     `)
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<!--[--><span></span>foofoo<span></span><!--]-->"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span></span>foofoo<span></span><!--]-->
+        "
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<!--[--><span></span>barbar<span></span><!--]-->"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span></span>barbar<span></span><!--]-->
+        "
+      `,
       )
     })
 
@@ -161,25 +170,37 @@ describe('Vapor Mode hydration', () => {
       const { data, container } = await testHydration(`
       <template>{{ data }}A{{ data }}B{{ data }}</template>
     `)
-      expect(container.innerHTML).toMatchInlineSnapshot(`"fooAfooBfoo"`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"fooAfooBfoo"`,
+      )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(`"barAbarBbar"`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"barAbarBbar"`,
+      )
     })
 
-    test('mixed text nodes with anchor insertion', async () => {
+    test('mixed text nodes with insertion anchor', async () => {
       const { data, container } = await testHydration(`
       <template><span/>{{ data }}A{{ data }}B{{ data }}<span/></template>
     `)
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<!--[--><span></span>fooAfooBfoo<span></span><!--]-->"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span></span>fooAfooBfoo<span></span><!--]-->
+        "
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<!--[--><span></span>barAbarBbar<span></span><!--]-->"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span></span>barAbarBbar<span></span><!--]-->
+        "
+      `,
       )
     })
 
@@ -190,11 +211,15 @@ describe('Vapor Mode hydration', () => {
         undefined,
         data,
       )
-      expect(container.innerHTML).toMatchInlineSnapshot(`"<div> </div>"`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div> </div>"`,
+      )
 
       data.txt = 'foo'
       await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(`"<div>foo</div>"`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div>"`,
+      )
     })
 
     test('empty text node in slot', async () => {
@@ -206,14 +231,22 @@ describe('Vapor Mode hydration', () => {
         },
         data,
       )
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<!--[--> <!--]--><!--slot-->"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--> <!--]-->
+        <!--slot-->"
+      `,
       )
 
       data.txt = 'foo'
       await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<!--[-->foo<!--]--><!--slot-->"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->foo<!--]-->
+        <!--slot-->"
+      `,
       )
     })
   })
@@ -223,7 +256,7 @@ describe('Vapor Mode hydration', () => {
       const { container } = await testHydration(`
       <template><!----></template>
     `)
-      expect(container.innerHTML).toBe('<!---->')
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"<!---->"`)
       expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
     })
 
@@ -231,14 +264,22 @@ describe('Vapor Mode hydration', () => {
       const { container, data } = await testHydration(`
       <template> A<span>{{ data }}</span>{{ data }}</template>
     `)
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<!--[--> A<span>foo</span>foo<!--]-->"`,
+      expect(formatHtml(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>bar</span>bar<!--]-->
+        "
+      `,
       )
     })
 
@@ -246,7 +287,9 @@ describe('Vapor Mode hydration', () => {
       const { container } = await testHydration(`
       <template><div/></template>
     `)
-      expect(container.innerHTML).toBe('<div></div>')
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div></div>"`,
+      )
       expect(`Hydration children mismatch in <div>`).not.toHaveBeenWarned()
     })
 
@@ -254,13 +297,13 @@ describe('Vapor Mode hydration', () => {
       const { container, data } = await testHydration(`
       <template><div :class="data">{{ data }}</div></template>
     `)
-      expect(container.innerHTML).toMatchInlineSnapshot(
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
         `"<div class="foo">foo</div>"`,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
         `"<div class="bar">bar</div>"`,
       )
     })
@@ -274,7 +317,7 @@ describe('Vapor Mode hydration', () => {
         </div>
       </template>
     `)
-      expect(container.innerHTML).toMatchInlineSnapshot(
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
         `"<div><span>foo</span><span class="foo"></span></div>"`,
       )
 
@@ -282,7 +325,7 @@ describe('Vapor Mode hydration', () => {
       triggerEvent('click', container.querySelector('.foo')!)
 
       await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
         `"<div><span>bar</span><span class="bar"></span></div>"`,
       )
     })
@@ -309,14 +352,22 @@ describe('Vapor Mode hydration', () => {
       `,
         { Child: `<template>{{ data }}</template>` },
       )
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span></span><!--[[-->foo<!--]]--></div>"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[a-->foo<!--a]-->
+        </div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span></span><!--[[-->bar<!--]]--></div>"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[a-->bar<!--a]-->
+        </div>"
+      `,
       )
     })
 
@@ -327,14 +378,26 @@ describe('Vapor Mode hydration', () => {
       `,
         { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` },
       )
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span></span><!--[[--><!--[--><div>foo</div>-foo-<!--]--><!--]]--></div>"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[a-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--a]-->
+        </div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span></span><!--[[--><!--[--><div>bar</div>-bar-<!--]--><!--]]--></div>"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[a-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--a]-->
+        </div>"
+      `,
       )
     })
 
@@ -345,14 +408,26 @@ describe('Vapor Mode hydration', () => {
       `,
         { Child: `<template><div>{{ data }}</div>-{{ data }}-</template>` },
       )
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><!--[[--><!--[--><div>foo</div>-foo-<!--]--><!--]]--><span></span></div>"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--p]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><!--[[--><!--[--><div>bar</div>-bar-<!--]--><!--]]--><span></span></div>"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--p]-->
+        <span></span></div>"
+      `,
       )
     })
 
@@ -366,32 +441,34 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[[--><!--[-->` +
-          `<div></div>` +
-          `<!--[--><div>foo</div>-foo-<!--]-->` +
-          `<div></div>` +
-          `<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><div></div>
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <div></div><!--]-->
+        <!--p]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[[--><!--[-->` +
-          `<div></div>` +
-          `<!--[--><div>bar</div>-bar-<!--]-->` +
-          `<div></div>` +
-          `<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><div></div>
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <div></div><!--]-->
+        <!--p]-->
+        <span></span></div>"
+      `,
       )
     })
 
-    test('component with anchor insertion', async () => {
+    test('component with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `<template>
         <div>
@@ -405,18 +482,26 @@ describe('Vapor Mode hydration', () => {
           Child: `<template>{{ data }}</template>`,
         },
       )
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span></span><!--[[-->foo<!--]]--><span></span></div>"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->foo<!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toMatchInlineSnapshot(
-        `"<div><span></span><!--[[-->bar<!--]]--><span></span></div>"`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->bar<!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
-    test('nested components with anchor insertion', async () => {
+    test('nested components with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `
       <template><components.Parent/></template>
@@ -426,14 +511,22 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div></template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div><span></span><!--[[--><div>foo</div><!--]]--><span></span></div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div>foo</div><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div><span></span><!--[[--><div>bar</div><!--]]--><span></span></div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div>bar</div><!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
@@ -447,34 +540,30 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div></template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>` +
-          `<span></span>` +
-          `<!--[[--><div>foo</div><!--]]-->` +
-          `<span></span>` +
-          `</div><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div><span></span>
+        <!--[i--><div>foo</div><!--i]-->
+        <span></span></div><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>` +
-          `<span></span>` +
-          `<!--[[--><div>bar</div><!--]]-->` +
-          `<span></span>` +
-          `</div><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div><span></span>
+        <!--[i--><div>bar</div><!--i]-->
+        <span></span></div><!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
-    test('consecutive components with anchor insertion', async () => {
+    test('consecutive components with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `<template>
         <div>
@@ -489,24 +578,24 @@ describe('Vapor Mode hydration', () => {
           Child: `<template>{{ data }}</template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[-->foo<!--]]-->` +
-          `<!--[[-->foo<!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->foo<!--i]-->
+        <!--[i-->foo<!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[-->bar<!--]]-->` +
-          `<!--[[-->bar<!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->bar<!--i]-->
+        <!--[i-->bar<!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
@@ -526,19 +615,29 @@ describe('Vapor Mode hydration', () => {
         },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<div><!--[[--><span>foo</span><!--]]--><!--[[--><span>bar</span><!--]]--></div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a--><span>foo</span><!--a]-->
+        <!--[a--><span>bar</span><!--a]-->
+        </div>"
+      `,
       )
 
       data.foo = 'foo1'
       data.bar = 'bar1'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div><!--[[--><span>foo1</span><!--]]--><!--[[--><span>bar1</span><!--]]--></div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a--><span>foo1</span><!--a]-->
+        <!--[a--><span>bar1</span><!--a]-->
+        </div>"
+      `,
       )
     })
 
-    test('nested consecutive components with anchor insertion', async () => {
+    test('nested consecutive components with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `
       <template><components.Parent/></template>
@@ -548,24 +647,24 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div></template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>foo</div><!--]]-->` +
-          `<!--[[--><div>foo</div><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div>foo</div><!--i]-->
+        <!--[i--><div>foo</div><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>bar</div><!--]]-->` +
-          `<!--[[--><div>bar</div><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div>bar</div><!--i]-->
+        <!--[i--><div>bar</div><!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
@@ -579,36 +678,32 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div></template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>` +
-          `<span></span>` +
-          `<!--[[--><div>foo</div><!--]]-->` +
-          `<!--[[--><div>foo</div><!--]]-->` +
-          `<span></span>` +
-          `</div><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div><span></span>
+        <!--[i--><div>foo</div><!--i]-->
+        <!--[i--><div>foo</div><!--i]-->
+        <span></span></div><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>` +
-          `<span></span>` +
-          `<!--[[--><div>bar</div><!--]]-->` +
-          `<!--[[--><div>bar</div><!--]]-->` +
-          `<span></span>` +
-          `</div><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div><span></span>
+        <!--[i--><div>bar</div><!--i]-->
+        <!--[i--><div>bar</div><!--i]-->
+        <span></span></div><!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
-    test('mixed component and element with anchor insertion', async () => {
+    test('mixed component and element with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `<template>
         <div>
@@ -624,30 +719,30 @@ describe('Vapor Mode hydration', () => {
           Child: `<template>{{ data }}</template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[-->foo<!--]]-->` +
-          `<span></span>` +
-          `<!--[[-->foo<!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->foo<!--i]-->
+        <span></span>
+        <!--[i-->foo<!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[-->bar<!--]]-->` +
-          `<span></span>` +
-          `<!--[[-->bar<!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->bar<!--i]-->
+        <span></span>
+        <!--[i-->bar<!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
-    test('mixed component and text with anchor insertion', async () => {
+    test('mixed component and text with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `<template>
         <div>
@@ -663,30 +758,30 @@ describe('Vapor Mode hydration', () => {
           Child: `<template>{{ data }}</template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[-->foo<!--]]-->` +
-          ` <!--[[--> foo <!--]]--> ` +
-          `<!--[[-->foo<!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->foo<!--i]-->
+         foo 
+        <!--[i-->foo<!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[-->bar<!--]]-->` +
-          ` <!--[[--> bar <!--]]--> ` +
-          `<!--[[-->bar<!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->bar<!--i]-->
+         bar 
+        <!--[i-->bar<!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
-    test('fragment component with anchor insertion', async () => {
+    test('fragment component with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `<template>
         <div>
@@ -700,26 +795,30 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>foo</div>-foo<!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>bar</div>-bar<!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
-    test('nested fragment component with anchor insertion', async () => {
+    test('nested fragment component with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `
       <template><components.Parent/></template>
@@ -729,22 +828,26 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>foo</div>-foo-<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>bar</div>-bar-<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
@@ -758,34 +861,34 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>foo</div>-foo-<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div><span></span>
+        <!--[i-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--i]-->
+        <span></span></div><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>bar</div>-bar-<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div><span></span>
+        <!--[i-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--i]-->
+        <span></span></div><!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
-    test('consecutive fragment components with anchor insertion', async () => {
+    test('consecutive fragment components with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `<template>
           <div>
@@ -800,28 +903,36 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]-->` +
-          `<!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>foo</div>-foo<!--]-->
+        <!--i]-->
+        <!--[i-->
+        <!--[--><div>foo</div>-foo<!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]-->` +
-          `<!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>bar</div>-bar<!--]-->
+        <!--i]-->
+        <!--[i-->
+        <!--[--><div>bar</div>-bar<!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
-    test('nested consecutive fragment components with anchor insertion', async () => {
+    test('nested consecutive fragment components with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `
       <template><components.Parent/></template>
@@ -831,24 +942,32 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>foo</div>-foo-<!--]--><!--]]-->` +
-          `<!--[[--><!--[--><div>foo</div>-foo-<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--i]-->
+        <!--[i-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>bar</div>-bar-<!--]--><!--]]-->` +
-          `<!--[[--><!--[--><div>bar</div>-bar-<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--i]-->
+        <!--[i-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
@@ -862,32 +981,36 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>foo</div>-foo-<!--]--><!--]]-->` +
-          `<!--[[--><!--[--><div>foo</div>-foo-<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div><span></span>
+        <!--[i-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--i]-->
+        <!--[i-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--i]-->
+        <span></span></div><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>bar</div>-bar-<!--]--><!--]]-->` +
-          `<!--[[--><!--[--><div>bar</div>-bar-<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div><span></span>
+        <!--[i-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--i]-->
+        <!--[i-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--i]-->
+        <span></span></div><!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
@@ -901,32 +1024,36 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[-->` +
-          `<!--[--><div>foo</div>-foo-<!--]-->` +
-          `<!--[--><div>foo</div>-foo-<!--]-->` +
-          `<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--[--><div>foo</div>-foo-<!--]-->
+        <!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[-->` +
-          `<!--[--><div>bar</div>-bar-<!--]-->` +
-          `<!--[--><div>bar</div>-bar-<!--]-->` +
-          `<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--[--><div>bar</div>-bar-<!--]-->
+        <!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
-    test('mixed fragment component and element with anchor insertion', async () => {
+    test('mixed fragment component and element with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `<template>
         <div>
@@ -942,30 +1069,38 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]-->` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>foo</div>-foo<!--]-->
+        <!--i]-->
+        <span></span>
+        <!--[i-->
+        <!--[--><div>foo</div>-foo<!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]-->` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>bar</div>-bar<!--]-->
+        <!--i]-->
+        <span></span>
+        <!--[i-->
+        <!--[--><div>bar</div>-bar<!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
-    test('mixed fragment component and text with anchor insertion', async () => {
+    test('mixed fragment component and text with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `<template>
         <div>
@@ -981,33 +1116,39 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><div>{{ data }}</div>-{{ data }}</template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]-->` +
-          ` <!--[[--> foo <!--]]--> ` +
-          `<!--[[--><!--[--><div>foo</div>-foo<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>foo</div>-foo<!--]-->
+        <!--i]-->
+         foo 
+        <!--[i-->
+        <!--[--><div>foo</div>-foo<!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]-->` +
-          ` <!--[[--> bar <!--]]--> ` +
-          `<!--[[--><!--[--><div>bar</div>-bar<!--]--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>bar</div>-bar<!--]-->
+        <!--i]-->
+         bar 
+        <!--[i-->
+        <!--[--><div>bar</div>-bar<!--]-->
+        <!--i]-->
+        <span></span></div>"
+      `,
       )
     })
   })
 
   describe('dynamic component', () => {
-    const anchorLabel = DYNAMIC_COMPONENT_ANCHOR_LABEL
-
     test('basic dynamic component', async () => {
       const { container, data } = await testHydration(
         `<template>
@@ -1019,14 +1160,18 @@ describe('Vapor Mode hydration', () => {
         },
         ref('foo'),
       )
-      expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--dynamic-component-->"`,
+      )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(`<div>bar</div><!--${anchorLabel}-->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>bar</div><!--dynamic-component-->"`,
+      )
     })
 
-    test('dynamic component with anchor insertion', async () => {
+    test('dynamic component with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `<template>
           <div>
@@ -1041,26 +1186,26 @@ describe('Vapor Mode hydration', () => {
         },
         ref('foo'),
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>foo</div><!--${anchorLabel}--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div>foo</div><!--dynamic-component--><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>bar</div><!--${anchorLabel}--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div>bar</div><!--dynamic-component--><!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
-    test('consecutive dynamic components with anchor insertion', async () => {
+    test('consecutive dynamic components with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `<template>
           <div>
@@ -1076,24 +1221,24 @@ describe('Vapor Mode hydration', () => {
         },
         ref('foo'),
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>foo</div><!--${anchorLabel}--><!--]]-->` +
-          `<!--[[--><div>foo</div><!--${anchorLabel}--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div>foo</div><!--dynamic-component--><!--i]-->
+        <!--[i--><div>foo</div><!--dynamic-component--><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[[--><div>bar</div><!--${anchorLabel}--><!--]]-->` +
-          `<!--[[--><div>bar</div><!--${anchorLabel}--><!--]]-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><div>bar</div><!--dynamic-component--><!--i]-->
+        <!--[i--><div>bar</div><!--dynamic-component--><!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
@@ -1108,20 +1253,20 @@ describe('Vapor Mode hydration', () => {
         ref('foo'),
       )
 
-      expect(container.innerHTML).toBe(
-        `<button><span>foo</span></button><!--${anchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<button><span>foo</span></button><!--dynamic-component-->"`,
       )
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<button><span>bar</span></button><!--${anchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<button><span>bar</span></button><!--dynamic-component-->"`,
       )
     })
+
+    test.todo('with ssr slot vnode fallback', () => {})
   })
 
   describe('if', () => {
-    const anchorLabel = IF_ANCHOR_LABEL
-
     test('basic toggle - true -> false', async () => {
       const data = ref(true)
       const { container } = await testHydration(
@@ -1131,11 +1276,15 @@ describe('Vapor Mode hydration', () => {
         undefined,
         data,
       )
-      expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if-->"`,
+      )
 
       data.value = false
       await nextTick()
-      expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--if-->"`,
+      )
     })
 
     test('basic toggle - false -> true', async () => {
@@ -1149,11 +1298,13 @@ describe('Vapor Mode hydration', () => {
       )
       // v-if="false" is rendered as <!----> in the server-rendered HTML
       // it reused as anchor, so the anchor label is empty
-      expect(container.innerHTML).toBe(`<!---->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"<!---->"`)
 
       data.value = true
       await nextTick()
-      expect(container.innerHTML).toBe(`<div>foo</div><!---->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!---->"`,
+      )
     })
 
     test('v-if on insertion parent', async () => {
@@ -1167,17 +1318,25 @@ describe('Vapor Mode hydration', () => {
         { Child: `<template>foo</template>` },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<div><!--[[-->foo<!--]]--></div><!--${anchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a-->foo<!--a]-->
+        </div><!--if-->"
+      `,
       )
 
       data.value = false
       await nextTick()
-      expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--if-->"`,
+      )
 
       data.value = true
       await nextTick()
-      expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if-->"`,
+      )
     })
 
     test('v-if/else-if/else chain - switch branches', async () => {
@@ -1191,57 +1350,27 @@ describe('Vapor Mode hydration', () => {
         undefined,
         data,
       )
-      expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if-->"`,
+      )
 
       data.value = 'b'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>bar</div><!--${anchorLabel}--><!--${anchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>bar</div><!--if--><!--if-->"`,
       )
 
       data.value = 'c'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>baz</div><!--${anchorLabel}--><!--${anchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>baz</div><!--if--><!--if-->"`,
       )
 
       data.value = 'a'
       await nextTick()
-      expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
-    })
-
-    test('v-if/else-if/else chain - switch branches (PROD)', async () => {
-      try {
-        __DEV__ = false
-        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(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
-
-        data.value = 'b'
-        await nextTick()
-        // In PROD, the anchor of v-else-if (DynamicFragment) is an empty text node,
-        // so it won't be rendered
-        expect(container.innerHTML).toBe(`<div>bar</div><!--${anchorLabel}-->`)
-
-        data.value = 'c'
-        await nextTick()
-        // same as above
-        expect(container.innerHTML).toBe(`<div>baz</div><!--${anchorLabel}-->`)
-
-        data.value = 'a'
-        await nextTick()
-        expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
-      } finally {
-        __DEV__ = true
-      }
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if-->"`,
+      )
     })
 
     test('nested if', async () => {
@@ -1256,25 +1385,29 @@ describe('Vapor Mode hydration', () => {
         undefined,
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span>outer</span>` +
-          `<div>inner</div><!--${anchorLabel}-->` +
-          `</div><!--${anchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span>outer</span>
+        <!--[a--><div>inner</div><!--if--><!--a]-->
+        </div><!--if-->"
+      `,
       )
 
       data.inner = false
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span>outer</span>` +
-          `<!--${anchorLabel}-->` +
-          `</div><!--${anchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span>outer</span>
+        <!--[a--><!--if--><!--a]-->
+        </div><!--if-->"
+      `,
       )
 
       data.outer = false
       await nextTick()
-      expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--if-->"`,
+      )
     })
 
     test('on component', async () => {
@@ -1286,11 +1419,15 @@ describe('Vapor Mode hydration', () => {
         { Child: `<template>foo</template>` },
         data,
       )
-      expect(container.innerHTML).toBe(`foo<!--${anchorLabel}-->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"foo<!--if-->"`,
+      )
 
       data.value = false
       await nextTick()
-      expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--if-->"`,
+      )
     })
 
     test('consecutive if node', async () => {
@@ -1302,15 +1439,21 @@ describe('Vapor Mode hydration', () => {
         { Child: `<template><div v-if="data">foo</div></template>` },
         data,
       )
-      expect(container.innerHTML).toBe(`<div>foo</div><!--if--><!--if-->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if--><!--if-->"`,
+      )
 
       data.value = false
       await nextTick()
-      expect(container.innerHTML).toBe(`<!--if-->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<!--if-->"`,
+      )
 
       data.value = true
       await nextTick()
-      expect(container.innerHTML).toBe(`<div>foo</div><!--if--><!--if-->`)
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div>foo</div><!--if--><!--if-->"`,
+      )
     })
 
     test('mixed prepend and insertion anchor', async () => {
@@ -1337,38 +1480,41 @@ describe('Vapor Mode hydration', () => {
         },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<span>` +
-          `<span>foo</span><!--if-->` +
-          `<span>bar</span><!--if-->` +
-          `<span>baz</span>` +
-          `<span>qux</span><!--if-->` +
-          `<span>quux</span>` +
-          `</span><!--if-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<span>
+        <!--[p--><span>foo</span><!--if--><!--p]-->
+        <!--[p--><span>bar</span><!--if--><!--p]-->
+        <span>baz</span>
+        <!--[i--><span>qux</span><!--if--><!--i]-->
+        <span>quux</span></span><!--if-->"
+      `,
       )
 
       data.qux = 'qux1'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<span>` +
-          `<span>foo</span><!--if-->` +
-          `<span>bar</span><!--if-->` +
-          `<span>baz</span>` +
-          `<span>qux1</span><!--if-->` +
-          `<span>quux</span>` +
-          `</span><!--if-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<span>
+        <!--[p--><span>foo</span><!--if--><!--p]-->
+        <!--[p--><span>bar</span><!--if--><!--p]-->
+        <span>baz</span>
+        <!--[i--><span>qux1</span><!--if--><!--i]-->
+        <span>quux</span></span><!--if-->"
+      `,
       )
 
       data.foo = 'foo1'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<span>` +
-          `<span>foo1</span><!--if-->` +
-          `<span>bar</span><!--if-->` +
-          `<span>baz</span>` +
-          `<span>qux1</span><!--if-->` +
-          `<span>quux</span>` +
-          `</span><!--if-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<span>
+        <!--[p--><span>foo1</span><!--if--><!--p]-->
+        <!--[p--><span>bar</span><!--if--><!--p]-->
+        <span>baz</span>
+        <!--[i--><span>qux1</span><!--if--><!--i]-->
+        <span>quux</span></span><!--if-->"
+      `,
       )
     })
 
@@ -1387,76 +1533,30 @@ describe('Vapor Mode hydration', () => {
         },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<span>a child1</span><!--${anchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<span>a child1</span><!--if-->"`,
       )
 
       data.value = 'b'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<span>b child2</span><!--${anchorLabel}--><!--${anchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<span>b child2</span><!--if--><!--if-->"`,
       )
 
       data.value = 'c'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<span>c child3</span><!--${anchorLabel}--><!--${anchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<span>c child3</span><!--if--><!--if-->"`,
       )
 
       data.value = 'a'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<span>a child1</span><!--${anchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<span>a child1</span><!--if-->"`,
       )
     })
 
-    test('v-if/else-if/else chain on component - switch branches (PROD)', async () => {
-      try {
-        __DEV__ = false
-        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(container.innerHTML).toBe(
-          `<span>a child1</span><!--${anchorLabel}-->`,
-        )
-
-        data.value = 'b'
-        await nextTick()
-        // In PROD, the anchor of v-else-if (DynamicFragment) is an empty text node,
-        // so it won't be rendered
-        expect(container.innerHTML).toBe(
-          `<span>b child2</span><!--${anchorLabel}-->`,
-        )
-
-        data.value = 'c'
-        await nextTick()
-        // same as above
-        expect(container.innerHTML).toBe(
-          `<span>c child3</span><!--${anchorLabel}-->`,
-        )
-
-        data.value = 'a'
-        await nextTick()
-        expect(container.innerHTML).toBe(
-          `<span>a child1</span><!--${anchorLabel}-->`,
-        )
-      } finally {
-        __DEV__ = true
-      }
-    })
-
-    test('on component with anchor insertion', async () => {
+    test('on component with insertion anchor', async () => {
       const data = ref(true)
       const { container } = await testHydration(
         `<template>
@@ -1469,22 +1569,22 @@ describe('Vapor Mode hydration', () => {
         { Child: `<template>foo</template>` },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `foo<!--${anchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->foo<!--if--><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = false
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--${anchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><!--if--><!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
@@ -1507,20 +1607,36 @@ describe('Vapor Mode hydration', () => {
         },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[[--><span>foo</span><!--]]-->` +
-          `<!--[[--><span>bar</span><!--]]-->` +
-          `</div>` +
-          `<!--${anchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a--><span>foo</span><!--a]-->
+        <!--[a--><span>bar</span><!--a]-->
+        </div><!--if-->"
+      `,
       )
 
       data.show = false
       await nextTick()
-      expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+      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-->"`,
+      )
     })
 
-    test('consecutive v-if on component with anchor insertion', async () => {
+    test('consecutive v-if on component with insertion anchor', async () => {
       const data = ref(true)
       const { container } = await testHydration(
         `<template>
@@ -1534,25 +1650,34 @@ describe('Vapor Mode hydration', () => {
         { Child: `<template>foo</template>` },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `foo<!--${anchorLabel}-->` +
-          `foo<!--${anchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->foo<!--if--><!--i]-->
+        <!--[i-->foo<!--if--><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = false
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--${anchorLabel}-->` +
-          `<!--${anchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><!--if--><!--i]-->
+        <!--[i--><!--if--><!--i]-->
+        <span></span></div>"
+      `,
       )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "<div><span></span>
+        <!--[i-->foo<!--if--><!--i]-->
+        <!--[i-->foo<!--if--><!--i]-->
+        <span></span></div>"
+      `)
     })
 
     test('on fragment component', async () => {
@@ -1568,21 +1693,40 @@ describe('Vapor Mode hydration', () => {
         },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[--><div>true</div>-true-<!--]-->` +
-          `<!--if-->` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a-->
+        <!--[--><div>true</div>-true-<!--]-->
+        <!--if--><!--a]-->
+        </div>"
+      `,
       )
 
       data.value = false
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div><!--[--><!--]--><!--${anchorLabel}--></div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a-->
+        <!--[--><!--]-->
+        <!--if--><!--a]-->
+        </div>"
+      `,
       )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "<div>
+        <!--[a-->
+        <!--[--><!--]-->
+        <div>true</div>-true-<!--if--><!--a]-->
+        </div>"
+      `)
     })
 
-    test('on fragment component with anchor insertion', async () => {
+    test('on fragment component with insertion anchor', async () => {
       const data = ref(true)
       const { container } = await testHydration(
         `<template>
@@ -1597,28 +1741,40 @@ describe('Vapor Mode hydration', () => {
         },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[--><div>true</div>-true-<!--]-->` +
-          `<!--if-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>true</div>-true-<!--]-->
+        <!--if--><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = false
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[--><!--]-->` +
-          `<!--${anchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><!--]-->
+        <!--if--><!--i]-->
+        <span></span></div>"
+      `,
       )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><!--]-->
+        <div>true</div>-true-<!--if--><!--i]-->
+        <span></span></div>"
+      `)
     })
 
-    test('consecutive v-if on fragment component with anchor insertion', async () => {
+    test('consecutive v-if on fragment component with insertion anchor', async () => {
       const data = ref(true)
       const { container } = await testHydration(
         `<template>
@@ -1634,29 +1790,49 @@ describe('Vapor Mode hydration', () => {
         },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[--><div>true</div>-true-<!--]--><!--${anchorLabel}-->` +
-          `<!--[--><div>true</div>-true-<!--]--><!--${anchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><div>true</div>-true-<!--]-->
+        <!--if--><!--i]-->
+        <!--[i-->
+        <!--[--><div>true</div>-true-<!--]-->
+        <!--if--><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = false
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[--><!--]--><!--${anchorLabel}-->` +
-          `<!--[--><!--]--><!--${anchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><!--]-->
+        <!--if--><!--i]-->
+        <!--[i-->
+        <!--[--><!--]-->
+        <!--if--><!--i]-->
+        <span></span></div>"
+      `,
       )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><!--]-->
+        <div>true</div>-true-<!--if--><!--i]-->
+        <!--[i-->
+        <!--[--><!--]-->
+        <div>true</div>-true-<!--if--><!--i]-->
+        <span></span></div>"
+      `)
     })
 
-    test('on dynamic component with anchor insertion', async () => {
-      const dynamicComponentAnchorLabel = DYNAMIC_COMPONENT_ANCHOR_LABEL
+    test('on dynamic component with insertion anchor', async () => {
       const data = ref(true)
       const { container } = await testHydration(
         `<template>
@@ -1669,30 +1845,35 @@ describe('Vapor Mode hydration', () => {
         { Child: `<template>foo</template>` },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `foo<!--${dynamicComponentAnchorLabel}--><!--${anchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->foo<!--dynamic-component--><!--if--><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value = false
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--${anchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i--><!--if--><!--i]-->
+        <span></span></div>"
+      `,
       )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "<div><span></span>
+        <!--[i-->foo<!--dynamic-component--><!--if--><!--i]-->
+        <span></span></div>"
+      `)
     })
   })
 
   describe('for', () => {
-    const forAnchorLabel = FOR_ANCHOR_LABEL
-    const slotAnchorLabel = SLOT_ANCHOR_LABEL
-
     test('basic v-for', async () => {
       const { container, data } = await testHydration(
         `<template>
@@ -1703,63 +1884,30 @@ describe('Vapor Mode hydration', () => {
         undefined,
         ref(['a', 'b', 'c']),
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[-->` +
-          `<span>a</span>` +
-          `<span>b</span>` +
-          `<span>c</span>` +
-          `<!--]--><!--${forAnchorLabel}-->` +
-          `</div>`,
-      )
-
-      data.value.push('d')
-      await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[-->` +
-          `<span>a</span>` +
-          `<span>b</span>` +
-          `<span>c</span>` +
-          `<!--]-->` +
-          `<span>d</span>` +
-          `<!--${forAnchorLabel}-->` +
-          `</div>`,
-      )
-    })
-
-    test('v-for with text node', async () => {
-      const { container, data } = await testHydration(
-        `<template>
-          <div>
-            <span v-for="item in data" :key="item">{{ item }}</span>
-          </div>
-        </template>`,
-        undefined,
-        ref(['a', 'b', 'c']),
-      )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[-->` +
-          `<span>a</span><span>b</span><span>c</span>` +
-          `<!--]--><!--${forAnchorLabel}-->` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a-->
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <!--for--><!--a]-->
+        </div>"
+      `,
       )
 
       data.value.push('d')
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[-->` +
-          `<span>a</span><span>b</span><span>c</span>` +
-          `<!--]-->` +
-          `<span>d</span>` +
-          `<!--${forAnchorLabel}-->` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a-->
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <span>d</span><!--for--><!--a]-->
+        </div>"
+      `,
       )
     })
 
-    test('v-for with anchor insertion', async () => {
+    test('v-for with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `<template>
           <div>
@@ -1771,51 +1919,42 @@ describe('Vapor Mode hydration', () => {
         undefined,
         ref(['a', 'b', 'c']),
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[-->` +
-          `<span>a</span>` +
-          `<span>b</span>` +
-          `<span>c</span>` +
-          `<!--]--><!--${forAnchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <!--for--><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value.push('d')
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[-->` +
-          `<span>a</span>` +
-          `<span>b</span>` +
-          `<span>c</span>` +
-          `<!--]-->` +
-          `<span>d</span>` +
-          `<!--${forAnchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <span>d</span><!--for--><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value.splice(0, 1)
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[-->` +
-          `<span>b</span>` +
-          `<span>c</span>` +
-          `<!--]-->` +
-          `<span>d</span>` +
-          `<!--${forAnchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><span>b</span><span>c</span><!--]-->
+        <span>d</span><!--for--><!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
-    test('consecutive v-for with anchor insertion', async () => {
+    test('consecutive v-for with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `<template>
           <div>
@@ -1828,63 +1967,47 @@ describe('Vapor Mode hydration', () => {
         undefined,
         ref(['a', 'b', 'c']),
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[-->` +
-          `<span>a</span>` +
-          `<span>b</span>` +
-          `<span>c</span>` +
-          `<!--]--><!--${forAnchorLabel}-->` +
-          `<!--[-->` +
-          `<span>a</span>` +
-          `<span>b</span>` +
-          `<span>c</span>` +
-          `<!--]--><!--${forAnchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <!--for--><!--i]-->
+        <!--[i-->
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <!--for--><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value.push('d')
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[-->` +
-          `<span>a</span>` +
-          `<span>b</span>` +
-          `<span>c</span>` +
-          `<!--]-->` +
-          `<span>d</span>` +
-          `<!--${forAnchorLabel}-->` +
-          `<!--[-->` +
-          `<span>a</span>` +
-          `<span>b</span>` +
-          `<span>c</span>` +
-          `<!--]-->` +
-          `<span>d</span>` +
-          `<!--${forAnchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <span>d</span><!--for--><!--i]-->
+        <!--[i-->
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <span>d</span><!--for--><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.value.splice(0, 2)
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[-->` +
-          `<span>c</span>` +
-          `<!--]-->` +
-          `<span>d</span>` +
-          `<!--${forAnchorLabel}-->` +
-          `<!--[-->` +
-          `<span>c</span>` +
-          `<!--]-->` +
-          `<span>d</span>` +
-          `<!--${forAnchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><span>c</span><!--]-->
+        <span>d</span><!--for--><!--i]-->
+        <!--[i-->
+        <!--[--><span>c</span><!--]-->
+        <span>d</span><!--for--><!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
@@ -1901,28 +2024,26 @@ describe('Vapor Mode hydration', () => {
         ref(['a', 'b', 'c']),
       )
 
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[-->` +
-          `<div>comp</div>` +
-          `<div>comp</div>` +
-          `<div>comp</div>` +
-          `<!--]--><!--${forAnchorLabel}-->` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a-->
+        <!--[--><div>comp</div><div>comp</div><div>comp</div><!--]-->
+        <!--for--><!--a]-->
+        </div>"
+      `,
       )
 
       data.value.push('d')
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[-->` +
-          `<div>comp</div>` +
-          `<div>comp</div>` +
-          `<div>comp</div>` +
-          `<!--]-->` +
-          `<div>comp</div>` +
-          `<!--${forAnchorLabel}-->` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a-->
+        <!--[--><div>comp</div><div>comp</div><div>comp</div><!--]-->
+        <div>comp</div><!--for--><!--a]-->
+        </div>"
+      `,
       )
     })
 
@@ -1940,28 +2061,38 @@ describe('Vapor Mode hydration', () => {
         },
         ref(['a', 'b', 'c']),
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[-->` +
-          `<!--[--><span>a</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><span>b</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><span>c</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--]--><!--${forAnchorLabel}-->` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a-->
+        <!--[-->
+        <!--[--><span>a</span><!--]-->
+        <!--slot-->
+        <!--[--><span>b</span><!--]-->
+        <!--slot-->
+        <!--[--><span>c</span><!--]-->
+        <!--slot--><!--]-->
+        <!--for--><!--a]-->
+        </div>"
+      `,
       )
 
       data.value.push('d')
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[-->` +
-          `<!--[--><span>a</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><span>b</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><span>c</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--]-->` +
-          `<span>d</span><!--${slotAnchorLabel}-->` +
-          `<!--${forAnchorLabel}-->` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a-->
+        <!--[-->
+        <!--[--><span>a</span><!--]-->
+        <!--slot-->
+        <!--[--><span>b</span><!--]-->
+        <!--slot-->
+        <!--[--><span>c</span><!--]-->
+        <!--slot--><!--]-->
+        <span>d</span><!--slot--><!--for--><!--a]-->
+        </div>"
+      `,
       )
     })
 
@@ -1977,35 +2108,39 @@ describe('Vapor Mode hydration', () => {
         },
         ref(['a', 'b', 'c']),
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[-->` +
-          `<!--[--><div>foo</div>-bar-<!--]-->` +
-          `<!--[--><div>foo</div>-bar-<!--]-->` +
-          `<!--[--><div>foo</div>-bar-<!--]-->` +
-          `<!--]--><!--${forAnchorLabel}-->` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a-->
+        <!--[-->
+        <!--[--><div>foo</div>-bar-<!--]-->
+        <!--[--><div>foo</div>-bar-<!--]-->
+        <!--[--><div>foo</div>-bar-<!--]-->
+        <!--]-->
+        <!--for--><!--a]-->
+        </div>"
+      `,
       )
 
       data.value.push('d')
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[-->` +
-          `<!--[--><div>foo</div>-bar-<!--]-->` +
-          `<!--[--><div>foo</div>-bar-<!--]-->` +
-          `<!--[--><div>foo</div>-bar-<!--]-->` +
-          `<!--]-->` +
-          `<div>foo</div>-bar-` +
-          `<!--${forAnchorLabel}-->` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a-->
+        <!--[-->
+        <!--[--><div>foo</div>-bar-<!--]-->
+        <!--[--><div>foo</div>-bar-<!--]-->
+        <!--[--><div>foo</div>-bar-<!--]-->
+        <!--]-->
+        <div>foo</div>-bar-<!--for--><!--a]-->
+        </div>"
+      `,
       )
     })
   })
 
   describe('slots', () => {
-    const slotAnchorLabel = SLOT_ANCHOR_LABEL
-    const forAnchorLabel = FOR_ANCHOR_LABEL
     test('basic slot', async () => {
       const { data, container } = await testHydration(
         `<template>
@@ -2017,14 +2152,22 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><slot/></template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>foo</span><!--]-->
+        <!--slot-->"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>bar</span><!--]-->
+        <!--slot-->"
+      `,
       )
     })
 
@@ -2038,17 +2181,33 @@ describe('Vapor Mode hydration', () => {
           </components.Child>
         </template>`,
         {
-          Child: `<template><slot name="foo"/></template>`,
+          Child: `<template><slot/><slot name="foo"/></template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><!--]-->
+        <!--slot-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--]-->
+        "
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><!--]-->
+        <!--slot-->
+        <!--[--><span>bar</span><!--]-->
+        <!--slot--><!--]-->
+        "
+      `,
       )
     })
 
@@ -2065,15 +2224,31 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><slot name="foo"/></template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>foo</span><!--]-->
+        <!--slot-->"
+      `,
       )
 
       data.value = false
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<!--[--><!--]--><!--${slotAnchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><!--]-->
+        <!--slot-->"
+      `,
       )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "
+        <!--[--><!--]-->
+        <span>true</span><!--slot-->"
+      `)
     })
 
     test('named slot with v-if and v-for', async () => {
@@ -2094,21 +2269,42 @@ describe('Vapor Mode hydration', () => {
         },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<!--[-->` +
-          `<!--[--><span>a</span><span>b</span><span>c</span><!--]--><!--${forAnchorLabel}-->` +
-          `<!--]-->` +
-          `<!--${slotAnchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <!--for--><!--]-->
+        <!--slot-->"
+      `,
       )
 
       data.show = false
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<!--[--><!--[--><!--]--><!--]--><!--${slotAnchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><!--]-->
+        <!--]-->
+        <!--slot-->"
+      `,
+      )
+
+      data.show = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><!--]-->
+        <!--]-->
+        <span>a</span><span>b</span><span>c</span><!--for--><!--slot-->"
+      `,
       )
     })
 
-    test('with anchor insertion', async () => {
+    test('with insertion anchor', async () => {
       const { data, container } = await testHydration(
         `<template>
           <components.Child>
@@ -2121,24 +2317,22 @@ describe('Vapor Mode hydration', () => {
           Child: `<template><slot/></template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<!--[-->` +
-          `<span></span>` +
-          `<span>foo</span>` +
-          `<span></span>` +
-          `<!--]-->` +
-          `<!--${slotAnchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span></span><span>foo</span><span></span><!--]-->
+        <!--slot-->"
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<!--[-->` +
-          `<span></span>` +
-          `<span>bar</span>` +
-          `<span></span>` +
-          `<!--]-->` +
-          `<!--${slotAnchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span></span><span>bar</span><span></span><!--]-->
+        <!--slot-->"
+      `,
       )
     })
 
@@ -2162,34 +2356,26 @@ describe('Vapor Mode hydration', () => {
           </template>`,
         },
       )
-      expect(container.innerHTML).toBe(
-        `<!--[-->` +
-          `<div></div>` +
-          `<div></div>` +
-          `<!--[-->` +
-          `<span></span>` +
-          `<span>foo</span>` +
-          `<span></span>` +
-          `<!--]-->` +
-          `<!--${slotAnchorLabel}-->` +
-          `<div></div>` +
-          `<!--]-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div></div><div></div>
+        <!--[--><span></span><span>foo</span><span></span><!--]-->
+        <!--slot--><div></div><!--]-->
+        "
+      `,
       )
 
       data.value = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<!--[-->` +
-          `<div></div>` +
-          `<div></div>` +
-          `<!--[-->` +
-          `<span></span>` +
-          `<span>bar</span>` +
-          `<span></span>` +
-          `<!--]-->` +
-          `<!--${slotAnchorLabel}-->` +
-          `<div></div>` +
-          `<!--]-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div></div><div></div>
+        <!--[--><span></span><span>bar</span><span></span><!--]-->
+        <!--slot--><div></div><!--]-->
+        "
+      `,
       )
     })
 
@@ -2210,20 +2396,26 @@ describe('Vapor Mode hydration', () => {
         data,
       )
 
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `hi` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--p]-->
+        hi</div>"
+      `,
       )
 
       data.msg = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `bar` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--p]-->
+        bar</div>"
+      `,
       )
     })
 
@@ -2244,22 +2436,26 @@ describe('Vapor Mode hydration', () => {
         data,
       )
 
-      expect(container.innerHTML).toBe(
-        `<!--[-->` +
-          `foo` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `hi` +
-          `<!--]-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->foo
+        <!--[--><span>foo</span><!--]-->
+        <!--slot-->hi<!--]-->
+        "
+      `,
       )
 
       data.msg = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<!--[-->` +
-          `foo` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `bar` +
-          `<!--]-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->foo
+        <!--[--><span>foo</span><!--]-->
+        <!--slot-->bar<!--]-->
+        "
+      `,
       )
     })
 
@@ -2281,22 +2477,32 @@ describe('Vapor Mode hydration', () => {
         data,
       )
 
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<div>hi</div>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--p]-->
+        <!--[p-->
+        <!--[--><span>bar</span><!--]-->
+        <!--slot--><!--p]-->
+        <div>hi</div></div>"
+      `,
       )
 
       data.msg = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<div>bar</div>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--p]-->
+        <!--[p-->
+        <!--[--><span>bar</span><!--]-->
+        <!--slot--><!--p]-->
+        <div>bar</div></div>"
+      `,
       )
     })
 
@@ -2317,20 +2523,26 @@ describe('Vapor Mode hydration', () => {
         data,
       )
 
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<div>hi</div>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--p]-->
+        <div>hi</div></div>"
+      `,
       )
 
       data.msg = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<div>bar</div>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--p]-->
+        <div>bar</div></div>"
+      `,
       )
     })
 
@@ -2361,22 +2573,30 @@ describe('Vapor Mode hydration', () => {
         },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[[--><div>bar</div><!--]]-->` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[[--><div>bar</div><!--]]-->` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a--><div>bar</div><!--a]-->
+        <!--[a-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--a]-->
+        <!--[a--><div>bar</div><!--a]-->
+        </div>"
+      `,
       )
 
       data.msg2 = 'hello'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[[--><div>hello</div><!--]]-->` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[[--><div>hello</div><!--]]-->` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a--><div>hello</div><!--a]-->
+        <!--[a-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--a]-->
+        <!--[a--><div>hello</div><!--a]-->
+        </div>"
+      `,
       )
     })
 
@@ -2407,23 +2627,39 @@ describe('Vapor Mode hydration', () => {
         },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[[--><!--[--><div>foo</div> bar<!--]--><!--]]-->` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[[--><!--[--><div>foo</div> bar<!--]--><!--]]-->` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a-->
+        <!--[--><div>foo</div> bar<!--]-->
+        <!--a]-->
+        <!--[a-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--a]-->
+        <!--[a-->
+        <!--[--><div>foo</div> bar<!--]-->
+        <!--a]-->
+        </div>"
+      `,
       )
 
       data.msg1 = 'hello'
       data.msg2 = 'vapor'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[[--><!--[--><div>hello</div> vapor<!--]--><!--]]-->` +
-          `<!--[--><span>hello</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[[--><!--[--><div>hello</div> vapor<!--]--><!--]]-->` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[a-->
+        <!--[--><div>hello</div> vapor<!--]-->
+        <!--a]-->
+        <!--[a-->
+        <!--[--><span>hello</span><!--]-->
+        <!--slot--><!--a]-->
+        <!--[a-->
+        <!--[--><div>hello</div> vapor<!--]-->
+        <!--a]-->
+        </div>"
+      `,
       )
     })
 
@@ -2449,23 +2685,37 @@ describe('Vapor Mode hydration', () => {
         data,
       )
 
-      expect(container.innerHTML).toBe(
-        `<!--[-->` +
-          `<div>foo</div><!--if-->` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<div>foo</div><!--if-->` +
-          `<!--]-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div>foo</div><!--if-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><div>foo</div><!--if--><!--]-->
+        "
+      `,
       )
 
       data.show = false
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<!--[-->` +
-          `<!--if-->` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--if-->` +
-          `<!--]-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><!--if-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--if--><!--]-->
+        "
+      `,
       )
+
+      data.show = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`
+        "
+        <!--[--><div>foo</div><!--if-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><div>foo</div><!--if--><!--]-->
+        "
+      `)
     })
 
     test('mixed slot and v-for', async () => {
@@ -2490,22 +2740,34 @@ describe('Vapor Mode hydration', () => {
         data,
       )
 
-      expect(container.innerHTML).toBe(
-        `<!--[-->` +
-          `<!--[--><div>a</div><div>b</div><div>c</div><!--]--><!--${forAnchorLabel}-->` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><div>a</div><div>b</div><div>c</div><!--]--><!--${forAnchorLabel}-->` +
-          `<!--]-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><div>a</div><div>b</div><div>c</div><!--]-->
+        <!--for-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot-->
+        <!--[--><div>a</div><div>b</div><div>c</div><!--]-->
+        <!--for--><!--]-->
+        "
+      `,
       )
 
       data.items.push('d')
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<!--[-->` +
-          `<!--[--><div>a</div><div>b</div><div>c</div><!--]--><div>d</div><!--${forAnchorLabel}-->` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><div>a</div><div>b</div><div>c</div><!--]--><div>d</div><!--${forAnchorLabel}-->` +
-          `<!--]-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><div>a</div><div>b</div><div>c</div><!--]-->
+        <div>d</div><!--for-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot-->
+        <!--[--><div>a</div><div>b</div><div>c</div><!--]-->
+        <div>d</div><!--for--><!--]-->
+        "
+      `,
       )
     })
 
@@ -2530,25 +2792,35 @@ describe('Vapor Mode hydration', () => {
         data,
       )
 
-      expect(container.innerHTML).toBe(
-        `<!--[-->` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--]-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot-->
+        <!--[--><span>bar</span><!--]-->
+        <!--slot--><!--]-->
+        "
+      `,
       )
 
       data.msg1 = 'hello'
       data.msg2 = 'vapor'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<!--[-->` +
-          `<!--[--><span>hello</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><span>vapor</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--]-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[-->
+        <!--[--><span>hello</span><!--]-->
+        <!--slot-->
+        <!--[--><span>vapor</span><!--]-->
+        <!--slot--><!--]-->
+        "
+      `,
       )
     })
 
-    test('consecutive slots with anchor insertion', async () => {
+    test('consecutive slots with insertion anchor', async () => {
       const data = reactive({
         msg1: 'foo',
         msg2: 'bar',
@@ -2576,25 +2848,33 @@ describe('Vapor Mode hydration', () => {
         data,
       )
 
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--i]-->
+        <!--[i-->
+        <!--[--><span>bar</span><!--]-->
+        <!--slot--><!--i]-->
+        <span></span></div>"
+      `,
       )
 
       data.msg1 = 'hello'
       data.msg2 = 'vapor'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<span></span>` +
-          `<!--[--><span>hello</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><span>vapor</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<span></span>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div><span></span>
+        <!--[i-->
+        <!--[--><span>hello</span><!--]-->
+        <!--slot--><!--i]-->
+        <!--[i-->
+        <!--[--><span>vapor</span><!--]-->
+        <!--slot--><!--i]-->
+        <span></span></div>"
+      `,
       )
     })
 
@@ -2628,23 +2908,33 @@ describe('Vapor Mode hydration', () => {
         data,
       )
 
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<div>baz</div>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--p]-->
+        <!--[p-->
+        <!--[--><span>bar</span><!--]-->
+        <!--slot--><!--p]-->
+        <div>baz</div></div>"
+      `,
       )
 
       data.msg1 = 'hello'
       data.msg2 = 'vapor'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[--><span>hello</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<!--[--><span>vapor</span><!--]--><!--${slotAnchorLabel}-->` +
-          `<div>baz</div>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><span>hello</span><!--]-->
+        <!--slot--><!--p]-->
+        <!--[p-->
+        <!--[--><span>vapor</span><!--]-->
+        <!--slot--><!--p]-->
+        <div>baz</div></div>"
+      `,
       )
     })
 
@@ -2662,14 +2952,22 @@ describe('Vapor Mode hydration', () => {
         },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>foo</span><!--]-->
+        <!--slot-->"
+      `,
       )
 
       data.foo = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>bar</span><!--]-->
+        <!--slot-->"
+      `,
       )
     })
 
@@ -2693,35 +2991,39 @@ describe('Vapor Mode hydration', () => {
         },
         data,
       )
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[[--><div>` +
-          `<!--[[--><div>` +
-          `<!--[-->` +
-          `<!--[--><span>foo</span><!--]--><!--slot-->` +
-          `<!--]-->` +
-          `<!--slot-->` +
-          `</div><!--]]-->` +
-          `</div><!--]]-->` +
-          `<div>bar</div>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p--><div>
+        <!--[a--><div>
+        <!--[a-->
+        <!--[-->
+        <!--[--><span>foo</span><!--]-->
+        <!--slot--><!--]-->
+        <!--slot--><!--a]-->
+        </div><!--a]-->
+        </div><!--p]-->
+        <div>bar</div></div>"
+      `,
       )
 
       data.foo = 'foo1'
       data.bar = 'bar1'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div>` +
-          `<!--[[--><div>` +
-          `<!--[[--><div>` +
-          `<!--[-->` +
-          `<!--[--><span>foo1</span><!--]--><!--slot-->` +
-          `<!--]-->` +
-          `<!--slot-->` +
-          `</div><!--]]-->` +
-          `</div><!--]]-->` +
-          `<div>bar1</div>` +
-          `</div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p--><div>
+        <!--[a--><div>
+        <!--[a-->
+        <!--[-->
+        <!--[--><span>foo1</span><!--]-->
+        <!--slot--><!--]-->
+        <!--slot--><!--a]-->
+        </div><!--a]-->
+        </div><!--p]-->
+        <div>bar1</div></div>"
+      `,
       )
     })
 
@@ -2765,1523 +3067,82 @@ describe('Vapor Mode hydration', () => {
         data,
       )
 
-      expect(container.innerHTML).toBe(
-        `<div><!--[--><!--]--><!--slot--><!--slot--><!--slot--><!--slot--><div>foo</div></div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><!--]-->
+        <!--slot--><!--slot--><!--slot--><!--slot--><!--p]-->
+        <div>foo</div></div>"
+      `,
       )
 
       data.foo = 'bar'
       await nextTick()
-      expect(container.innerHTML).toBe(
-        `<div><!--[--><!--]--><!--slot--><!--slot--><!--slot--><!--slot--><div>bar</div></div>`,
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "<div>
+        <!--[p-->
+        <!--[--><!--]-->
+        <!--slot--><!--slot--><!--slot--><!--slot--><!--p]-->
+        <div>bar</div></div>"
+      `,
       )
     })
   })
 
   describe.todo('transition', async () => {
     test('transition appear', async () => {})
+
     test('transition appear with v-if', async () => {})
+
     test('transition appear with v-show', async () => {})
+
     test('transition appear w/ event listener', async () => {})
   })
 
-  describe.todo('async component')
+  describe.todo('async component', async () => {
+    test('async component', async () => {})
+
+    test('update async wrapper before resolve', async () => {})
+
+    test('hydrate safely when property used by async setup changed before render', async () => {})
+
+    test('unmount async wrapper before load', async () => {})
+
+    test('nested async wrapper', async () => {})
+
+    test('unmount async wrapper before load (fragment)', async () => {})
+  })
+
+  describe.todo('force hydrate prop', async () => {
+    test.todo('force hydrate prop with `.prop` modifier', () => {})
+
+    test.todo(
+      'force hydrate input v-model with non-string value bindings',
+      () => {},
+    )
+
+    test.todo('force hydrate checkbox with indeterminate', () => {})
+
+    test.todo(
+      'force hydrate select option with non-string value bindings',
+      () => {},
+    )
+
+    test.todo('force hydrate custom element with dynamic props', () => {})
+  })
 
   describe.todo('data-allow-mismatch')
 
-  // 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>',
-  //   )
-  // })
-
-  // // 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('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('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('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
-  //       }),
-  //   )
-
-  //   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<!--]-->"`,
-  //   )
-
-  //   // hydration
-  //   let clientResolve: any
-  //   AsyncComp = defineAsyncComponent(
-  //     () =>
-  //       new Promise(r => {
-  //         clientResolve = r
-  //       }),
-  //   )
-
-  //   const container = document.createElement('div')
-  //   container.innerHTML = html
-  //   createSSRApp(App).mount(container)
-
-  //   // hydration not complete yet
-  //   triggerEvent('click', container.querySelector('button')!)
-  //   expect(spy).not.toHaveBeenCalled()
-
-  //   // resolve
-  //   clientResolve(Comp)
-  //   await new Promise(r => setTimeout(r))
-
-  //   // should be hydrated now
-  //   triggerEvent('click', container.querySelector('button')!)
-  //   expect(spy).toHaveBeenCalled()
-  // })
-
-  // 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
-  //       }),
-  //   )
-
-  //   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><!--]-->"`,
-  //   )
-
-  //   // hydration
-  //   let clientResolve: any
-  //   AsyncComp = defineAsyncComponent(
-  //     () =>
-  //       new Promise(r => {
-  //         clientResolve = r
-  //       }),
-  //   )
-
-  //   const container = document.createElement('div')
-  //   container.innerHTML = html
-  //   createSSRApp(App).mount(container)
-
-  //   // resolve
-  //   clientResolve(Comp)
-  //   await new Promise(r => setTimeout(r))
-
-  //   // should be hydrated now
-  //   expect(`Hydration node mismatch`).not.toHaveBeenWarned()
-  //   expect(container.innerHTML).toMatchInlineSnapshot(
-  //     `"<!--[-->world<h1>Async component</h1><!--]-->"`,
-  //   )
-  // })
-
-  // 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>"`,
-  //   )
-
-  //   expect(toggle.value).toBe(false)
-
-  //   // hydration
-
-  //   // reset the value
-  //   toggle.value = true
-  //   expect(toggle.value).toBe(true)
-
-  //   const container = document.createElement('div')
-  //   container.innerHTML = html
-  //   createSSRApp(App).mount(container)
-
-  //   await new Promise(r => setTimeout(r, 10))
-
-  //   expect(toggle.value).toBe(false)
-
-  //   // should be hydrated now
-  //   expect(container.innerHTML).toMatchInlineSnapshot(
-  //     `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
-  //   )
-  // })
-
-  // 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>"`,
-  //   )
-
-  //   expect(toggle.value).toBe(false)
-
-  //   // hydration
-
-  //   // reset the value
-  //   toggle.value = true
-  //   expect(toggle.value).toBe(true)
-
-  //   const container = document.createElement('div')
-  //   container.innerHTML = html
-  //   createSSRApp(App).mount(container)
-
-  //   await new Promise(r => setTimeout(r, 10))
-
-  //   expect(toggle.value).toBe(false)
-
-  //   // should be hydrated now
-  //   expect(container.innerHTML).toMatchInlineSnapshot(
-  //     `"<main><h1 prop="world">Async component</h1><span></span></main>"`,
-  //   )
-  // })
-
-  // // #3787
-  // test('unmount async wrapper before load', async () => {
-  //   let resolve: any
-  //   const AsyncComp = defineAsyncComponent(
-  //     () =>
-  //       new Promise(r => {
-  //         resolve = r
-  //       }),
-  //   )
-
-  //   const show = ref(true)
-  //   const root = document.createElement('div')
-  //   root.innerHTML = '<div><div>async</div></div>'
-
-  //   createSSRApp({
-  //     render() {
-  //       return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
-  //     },
-  //   }).mount(root)
-
-  //   show.value = false
-  //   await nextTick()
-  //   expect(root.innerHTML).toBe('<div><div>hi</div></div>')
-  //   resolve({})
-  // })
-
-  // //#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,
-  //         )
-  //       }),
-  //   )
-
-  //   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>"`,
-  //   )
-
-  //   createSSRApp(App).mount(root)
-  //   await nextTick()
-  //   await nextTick()
-  //   expect(root.innerHTML).toMatchInlineSnapshot(
-  //     `"<div style=""><!--[--><!--[--><!--[--><div>1</div><!--]--><!--]--><!--]--></div>"`,
-  //   )
-  //   expect(fn).toBeCalledTimes(1)
-  // })
-
-  // test('unmount async wrapper before load (fragment)', async () => {
-  //   let resolve: any
-  //   const AsyncComp = defineAsyncComponent(
-  //     () =>
-  //       new Promise(r => {
-  //         resolve = r
-  //       }),
-  //   )
-
-  //   const show = ref(true)
-  //   const root = document.createElement('div')
-  //   root.innerHTML = '<div><!--[-->async<!--]--></div>'
-
-  //   createSSRApp({
-  //     render() {
-  //       return h('div', [show.value ? h(AsyncComp) : h('div', 'hi')])
-  //     },
-  //   }).mount(root)
-
-  //   show.value = false
-  //   await nextTick()
-  //   expect(root.innerHTML).toBe('<div><div>hi</div></div>')
-  //   resolve({})
-  // })
-
-  // 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('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('force hydrate prop with `.prop` modifier', () => {
-  //   const { container } = mountWithHydration('<input type="checkbox">', () =>
-  //     h('input', {
-  //       type: 'checkbox',
-  //       '.indeterminate': true,
-  //     }),
-  //   )
-  //   expect((container.firstChild! as any).indeterminate).toBe(true)
-  // })
-
-  // test('force hydrate input v-model with non-string value bindings', () => {
-  //   const { container } = mountWithHydration(
-  //     '<input type="checkbox" value="true">',
-  //     () =>
-  //       withDirectives(
-  //         createVNode(
-  //           'input',
-  //           { type: 'checkbox', 'true-value': true },
-  //           null,
-  //           PatchFlags.PROPS,
-  //           ['true-value'],
-  //         ),
-  //         [[vModelCheckbox, true]],
-  //       ),
-  //   )
-  //   expect((container.firstChild as any)._trueValue).toBe(true)
-  // })
-
-  // test('force hydrate checkbox with indeterminate', () => {
-  //   const { container } = mountWithHydration(
-  //     '<input type="checkbox" indeterminate>',
-  //     () =>
-  //       createVNode(
-  //         'input',
-  //         { type: 'checkbox', indeterminate: '' },
-  //         null,
-  //         PatchFlags.CACHED,
-  //       ),
-  //   )
-  //   expect((container.firstChild as any).indeterminate).toBe(true)
-  // })
-
-  // test('force hydrate select option with non-string value bindings', () => {
-  //   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 */),
-  //       ]),
-  //   )
-  //   expect((container.firstChild!.firstChild as any)._value).toBe(true)
-  // })
-
-  // // #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)
-  // })
-
-  // // #5728
-  // test('empty text node in slot', () => {
-  //   const Comp = {
-  //     render(this: any) {
-  //       return renderSlot(this.$slots, 'default', {}, () => [
-  //         createTextVNode(''),
-  //       ])
-  //     },
-  //   }
-  //   const { container, vnode } = mountWithHydration('<!--[--><!--]-->', () =>
-  //     h(Comp),
-  //   )
-  //   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)
-  // })
-
-  // // #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)
-  // })
-
-  // // #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
-  //   }
-  // })
-
-  // 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)
-  // })
-
-  // // #6637
-  // test('stringified root fragment', () => {
-  //   mountWithHydration(`<!--[--><div></div><!--]-->`, () =>
-  //     createStaticVNode(`<div></div>`, 1),
-  //   )
-  //   expect(`mismatch`).not.toHaveBeenWarned()
-  // })
-
-  // test('transition appear', () => {
-  //   const { vnode, container } = mountWithHydration(
-  //     `<template><div>foo</div></template>`,
-  //     () =>
-  //       h(
-  //         Transition,
-  //         { appear: true },
-  //         {
-  //           default: () => h('div', '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()
-  // })
-
-  // 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('')),
-  //         },
-  //       ),
-  //   )
-  //   expect(container.firstChild).toMatchInlineSnapshot('<!---->')
-  //   expect(vnode.el).toBe(container.firstChild)
-  //   expect(`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]]),
-  //         },
-  //       ),
-  //   )
-  //   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()
-  // })
-
-  // 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('Suspense + transition appear', async () => {
-  //   const { vnode, container } = mountWithHydration(
-  //     `<template><div>foo</div></template>`,
-  //     () =>
-  //       h(Suspense, {}, () =>
-  //         h(
-  //           Transition,
-  //           { appear: true },
-  //           {
-  //             default: () => h('div', 'foo'),
-  //           },
-  //         ),
-  //       ),
-  //   )
-
-  //   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)
-  // })
-
-  // // #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
-  //   }
-  // })
-
-  // 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()
-  //   })
-  // })
-
-  // 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.todo('Teleport')
-  test.todo('Suspense')
+  describe.todo('mismatch handling')
+
+  describe.todo('Teleport')
+
+  describe.todo('Suspense')
 })
 
-describe('VDOM hydration interop', () => {
+describe('VDOM interop', () => {
   test('basic vapor component', async () => {
     const data = ref(true)
     const { container } = await testHydrationInterop(
@@ -4298,11 +3159,11 @@ describe('VDOM hydration interop', () => {
       data,
     )
 
-    expect(container.innerHTML).toMatchInlineSnapshot(`"true"`)
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"true"`)
 
     data.value = false
     await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(`"false"`)
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"false"`)
   })
 
   test('nested components (VDOM -> Vapor -> VDOM)', async () => {
@@ -4326,11 +3187,11 @@ describe('VDOM hydration interop', () => {
       data,
     )
 
-    expect(container.innerHTML).toMatchInlineSnapshot(`"true"`)
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"true"`)
 
     data.value = false
     await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(`"false"`)
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(`"false"`)
   })
 
   test('nested components (VDOM -> Vapor -> VDOM (with slot fallback))', async () => {
@@ -4354,14 +3215,22 @@ describe('VDOM hydration interop', () => {
       data,
     )
 
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<!--[--><span>true</span><!--]--><!--slot-->"`,
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><span>true</span><!--]-->
+      "
+    `,
     )
 
     data.value = false
     await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<!--[--><span>false</span><!--]--><!--slot-->"`,
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><span>false</span><!--]-->
+      "
+    `,
     )
   })
 
@@ -4392,14 +3261,22 @@ describe('VDOM hydration interop', () => {
       data,
     )
 
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<!--[--><span>true vapor fallback</span><!--]--><!--slot-->"`,
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><span>true vapor fallback</span><!--]-->
+      "
+    `,
     )
 
     data.value = false
     await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<!--[--><span>false vapor fallback</span><!--]--><!--slot-->"`,
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "
+      <!--[--><span>false vapor fallback</span><!--]-->
+      "
+    `,
     )
   })
 
@@ -4426,14 +3303,26 @@ describe('VDOM hydration interop', () => {
       data,
     )
 
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div><!--[-->true<!--]--><!--slot--></div>"`,
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "<div>
+      <!--[a-->
+      <!--[-->true<!--]-->
+      <!--slot--><!--a]-->
+      </div>"
+    `,
     )
 
     data.value = false
     await nextTick()
-    expect(container.innerHTML).toMatchInlineSnapshot(
-      `"<div><!--[-->false<!--]--><!--slot--></div>"`,
+    expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+      `
+      "<div>
+      <!--[a-->
+      <!--[-->false<!--]-->
+      <!--slot--><!--a]-->
+      </div>"
+    `,
     )
   })
 })
index 03efcb4786d7b51ec6a095095fae15cf3964f198..0f5adf9f92d713a346d5dbadcaac5bb92f637a75 100644 (file)
@@ -1,19 +1,25 @@
 import { warn } from '@vue/runtime-dom'
 import {
   insertionAnchor,
-  insertionChildIndex,
   insertionParent,
   resetInsertionState,
   setInsertionState,
 } from '../insertionState'
 import {
-  __next,
-  __nthChild,
+  _child,
+  _next,
   createTextNode,
   disableHydrationNodeLookup,
   enableHydrationNodeLookup,
 } from './node'
-import { BLOCK_END_ANCHOR_LABEL, isVaporAnchor } from '@vue/shared'
+import {
+  BLOCK_APPEND_ANCHOR_LABEL,
+  BLOCK_INSERTION_ANCHOR_LABEL,
+  BLOCK_PREPEND_ANCHOR_LABEL,
+  isVaporAnchor,
+} from '@vue/shared'
+
+const isHydratingStack = [] as boolean[]
 
 export let isHydrating = false
 export let currentHydrationNode: Node | null = null
@@ -24,21 +30,13 @@ export function setCurrentHydrationNode(node: Node | null): void {
 
 function findParentSibling(n: Node): Node | null {
   if (!n.parentNode) return null
-  let next = n.parentNode.nextSibling
-  while (next && isComment(next, BLOCK_END_ANCHOR_LABEL)) {
-    next = next.nextElementSibling
-  }
-  return next ? next : findParentSibling(n.parentNode)
+  return n.parentNode.nextSibling || findParentSibling(n.parentNode)
 }
 
 export function advanceHydrationNode(node: Node & { $ps?: Node | null }): void {
-  let next = node.nextSibling
-  while (next && isComment(next, BLOCK_END_ANCHOR_LABEL)) {
-    next = next.nextSibling
-  }
-
   // if no next sibling, find the next node in the parent chain
-  const ret = next || node.$ps || (node.$ps = findParentSibling(node))
+  const ret =
+    node.nextSibling || node.$ps || (node.$ps = findParentSibling(node))
   if (ret) setCurrentHydrationNode(ret)
 }
 
@@ -54,22 +52,26 @@ function performHydration<T>(
     locateHydrationNode = locateHydrationNodeImpl
     // optimize anchor cache lookup
     ;(Comment.prototype as any).$fe = undefined
-    ;(Node.prototype as any).$np = undefined
+    ;(Node.prototype as any).$pa = undefined
+    ;(Node.prototype as any).$ia = undefined
+    ;(Node.prototype as any).$aa = undefined
     isOptimized = true
   }
   enableHydrationNodeLookup()
-  isHydrating = true
+  isHydratingStack.push((isHydrating = true))
   setup()
   const res = fn()
   cleanup()
   currentHydrationNode = null
-  isHydrating = false
+  isHydratingStack.pop()
+  isHydrating = isHydratingStack[isHydratingStack.length - 1] || false
   disableHydrationNodeLookup()
   return res
 }
 
 export function withHydration(container: ParentNode, fn: () => void): void {
-  const setup = () => setInsertionState(container, 0)
+  // @ts-expect-error
+  const setup = () => setInsertionState(container, -1)
   const cleanup = () => resetInsertionState()
   return performHydration(fn, setup, cleanup)
 }
@@ -136,20 +138,32 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
 
 function locateHydrationNodeImpl(): void {
   let node: Node | null
-  // prepend / firstChild
-  if (insertionAnchor === 0) {
-    const n = insertionParent!.$np || 0
-    node = __nthChild(insertionParent!, n)
-    insertionParent!.$np = n + 1
+  // @ts-expect-error
+  if (insertionAnchor === -1) {
+    // firstChild
+    node = _child(insertionParent!)!
+  } else if (insertionAnchor === 0) {
+    // prepend
+    node = insertionParent!.$pa = locateHydrationNodeByAnchor(
+      insertionParent!.$pa || _child(insertionParent!),
+      BLOCK_PREPEND_ANCHOR_LABEL,
+    )!
   } else if (insertionAnchor) {
-    // `insertionAnchor` is a Node, it is the DOM node to hydrate
-    // Template:   `...<span/><!----><span/>...`// `insertionAnchor` is the placeholder
-    // SSR Output: `...<span/>Content<span/>...`// `insertionAnchor` is the actual node
-    node = insertionAnchor
+    // insertion anchor
+    node = insertionParent!.$ia = locateHydrationNodeByAnchor(
+      insertionParent!.$ia || _child(insertionParent!),
+      BLOCK_INSERTION_ANCHOR_LABEL,
+    )!
+  } else if (insertionAnchor === null) {
+    // append anchor
+    node = insertionParent!.$aa = locateHydrationNodeByAnchor(
+      insertionParent!.$aa || _child(insertionParent!),
+      BLOCK_APPEND_ANCHOR_LABEL,
+    )!
   } else {
     node = currentHydrationNode
     if (insertionParent && (!node || node.parentNode !== insertionParent)) {
-      node = __nthChild(insertionParent, insertionChildIndex || 0)
+      node = _child(insertionParent)
     }
   }
 
@@ -213,3 +227,20 @@ export function locateVaporFragmentAnchor(
 export function isEmptyTextNode(node: Node): node is Text {
   return node.nodeType === 3 && !(node as Text).data.trim()
 }
+
+function locateHydrationNodeByAnchor(
+  node: Node,
+  anchorLabel: string,
+): Node | null {
+  while (node) {
+    if (isComment(node, `[${anchorLabel}`)) return node.nextSibling
+    node = node.nextSibling!
+  }
+
+  if (__DEV__) {
+    throw new Error(
+      `Could not locate hydration node with anchor label: ${anchorLabel}`,
+    )
+  }
+  return null
+}
index 1ad467ac14bda0abf3fbfa44ae759dc52ab96f43..6c85d70feca975ba4f974924c12c76af6c8c307a 100644 (file)
@@ -1,7 +1,7 @@
 import { isComment, isNonHydrationNode, locateEndAnchor } from './hydration'
 import {
-  BLOCK_END_ANCHOR_LABEL,
-  BLOCK_START_ANCHOR_LABEL,
+  BLOCK_INSERTION_ANCHOR_LABEL,
+  BLOCK_PREPEND_ANCHOR_LABEL,
   isVaporAnchor,
 } from '@vue/shared'
 
@@ -25,6 +25,29 @@ export function querySelector(selectors: string): Element | null {
   return document.querySelector(selectors)
 }
 
+function skipBlockNodes(node: Node): Node {
+  while (node) {
+    if (isComment(node, `[${BLOCK_PREPEND_ANCHOR_LABEL}`)) {
+      node = locateEndAnchor(
+        node,
+        `[${BLOCK_PREPEND_ANCHOR_LABEL}`,
+        `${BLOCK_PREPEND_ANCHOR_LABEL}]`,
+      )!
+      continue
+    } else if (isComment(node, `[${BLOCK_INSERTION_ANCHOR_LABEL}`)) {
+      node = locateEndAnchor(
+        node,
+        `[${BLOCK_INSERTION_ANCHOR_LABEL}`,
+        `${BLOCK_INSERTION_ANCHOR_LABEL}]`,
+      )!
+      continue
+    }
+
+    break
+  }
+  return node
+}
+
 /*! #__NO_SIDE_EFFECTS__ */
 export function _child(node: ParentNode): Node {
   return node.firstChild!
@@ -60,16 +83,19 @@ export function _child(node: ParentNode): Node {
  */
 /*! #__NO_SIDE_EFFECTS__ */
 export function __child(node: ParentNode, offset?: number): Node {
+  let n = node.firstChild!
+
   // when offset is -1, it means we need to get the text node of this element
   // since server-side rendering doesn't generate whitespace placeholder text nodes,
   // if firstChild is null, manually insert a text node and return it
-  if (offset === -1 && !node.firstChild) {
+  if (offset === -1 && !n) {
     node.textContent = ' '
     return node.firstChild!
   }
 
-  let n = offset ? __nthChild(node, offset) : node.firstChild!
   while (n && (isComment(n, '[') || isVaporAnchor(n))) {
+    // skip block node
+    n = skipBlockNodes(n) as ChildNode
     if (isComment(n, '[')) {
       n = locateEndAnchor(n)!.nextSibling!
     } else {
@@ -90,7 +116,7 @@ export function _nthChild(node: Node, i: number): Node {
  */
 /*! #__NO_SIDE_EFFECTS__ */
 export function __nthChild(node: Node, i: number): Node {
-  let n = node.firstChild!
+  let n = __child(node as ParentNode)
   for (let start = 0; start < i; start++) {
     n = __next(n) as ChildNode
   }
@@ -105,7 +131,7 @@ export function _next(node: Node): Node {
 /**
  * Hydration-specific version of `next`.
  *
- * SSR comment anchors (fragments `<!--[-->...<!--]-->`, block `<!--[[-->...<!--]]-->`)
+ * SSR comment anchors (fragments `<!--[-->...<!--]-->`, block nodes `<!--[x-->...<!--x]-->`)
  * disrupt standard `node.nextSibling` traversal during hydration. `_next` might
  * return a comment node or an internal node of a fragment instead of skipping
  * the entire fragment block.
@@ -144,20 +170,13 @@ export function _next(node: Node): Node {
  */
 /*! #__NO_SIDE_EFFECTS__ */
 export function __next(node: Node): Node {
-  // process block node (<!--[[-->...<!--]]-->) as a single node
-  if (isComment(node, BLOCK_START_ANCHOR_LABEL)) {
-    node = locateEndAnchor(
-      node,
-      BLOCK_START_ANCHOR_LABEL,
-      BLOCK_END_ANCHOR_LABEL,
-    )!
-  }
-
   // process fragment (<!--[-->...<!--]-->) as a single node
-  else if (isComment(node, '[')) {
+  if (isComment(node, '[')) {
     node = locateEndAnchor(node)!
   }
 
+  node = skipBlockNodes(node)
+
   let n = node.nextSibling!
   while (n && isNonHydrationNode(n)) {
     n = n.nextSibling!
index 292ffb43c42d8cbe43d103da24e9ce9f69e23af3..006b4196c26f7db1eeea750cb276ac9dd8727a7f 100644 (file)
@@ -1,14 +1,11 @@
 export let insertionParent:
   | (ParentNode & {
-      // number of prepends - hydration only
-      // consecutive prepends need to skip nodes that were prepended earlier
-      // each prepend increases the value of $prepend
-      $np?: number
+      $pa?: Node
+      $ia?: Node
+      $aa?: Node
     })
   | undefined
-export let insertionAnchor: Node | 0 | undefined
-
-export let insertionChildIndex: number | undefined
+export let insertionAnchor: Node | 0 | undefined | null
 
 /**
  * This function is called before a block type that requires insertion
@@ -17,14 +14,12 @@ export let insertionChildIndex: number | undefined
  */
 export function setInsertionState(
   parent: ParentNode,
-  anchor?: Node | 0,
-  offset?: number,
+  anchor?: Node | 0 | null,
 ): void {
   insertionParent = parent
   insertionAnchor = anchor
-  insertionChildIndex = offset
 }
 
 export function resetInsertionState(): void {
-  insertionParent = insertionAnchor = insertionChildIndex = undefined
+  insertionParent = insertionAnchor = undefined
 }
index 03b7402ff922448ddf63032eaf764a8115790819..d0a5223b2ff635b2df39b187f8f4438b01bfff38 100644 (file)
@@ -446,7 +446,7 @@ function testRender(type: string, render: typeof renderToString) {
         ).toBe(
           `<div>parent<div class="child">` +
             `<!--[--><span>from slot</span><!--]-->` +
-            `<!--slot--></div></div>`,
+            `</div></div>`,
         )
 
         // test fallback
@@ -461,7 +461,7 @@ function testRender(type: string, render: typeof renderToString) {
             }),
           ),
         ).toBe(
-          `<div>parent<div class="child"><!--[-->fallback<!--]--><!--slot--></div></div>`,
+          `<div>parent<div class="child"><!--[-->fallback<!--]--></div></div>`,
         )
       })
 
@@ -507,7 +507,7 @@ function testRender(type: string, render: typeof renderToString) {
         ).toBe(
           `<div>parent<div class="child">` +
             `<!--[--><span>from slot</span><!--]-->` +
-            `<!--slot--></div></div>`,
+            `</div></div>`,
         )
       })
 
@@ -525,7 +525,7 @@ function testRender(type: string, render: typeof renderToString) {
         expect(await render(app)).toBe(
           `<div>parent<div class="child">` +
             `<!--[--><span>from slot</span><!--]-->` +
-            `<!--slot--></div></div>`,
+            `</div></div>`,
         )
       })
 
@@ -572,7 +572,7 @@ function testRender(type: string, render: typeof renderToString) {
         })
 
         expect(await render(app)).toBe(
-          `<div><!--[--><!--[-->hello<!--]--><!--slot--><!--]--><!--slot--></div>`,
+          `<div><!--[--><!--[-->hello<!--]--><!--]--></div>`,
         )
       })
 
@@ -593,7 +593,7 @@ function testRender(type: string, render: typeof renderToString) {
 
         expect(await render(app)).toBe(
           // should only have a single fragment
-          `<div><!--[--><!--]--><!--slot--></div>`,
+          `<div><!--[--><!--]--></div>`,
         )
       })
 
@@ -614,7 +614,7 @@ function testRender(type: string, render: typeof renderToString) {
 
         expect(await render(app)).toBe(
           // should only have a single fragment
-          `<div><!--[-->fallback<!--]--><!--slot--></div>`,
+          `<div><!--[-->fallback<!--]--></div>`,
         )
       })
     })
index 70939bb4c8d4dbfa61ebb1ac0d29fc24fa164721..e8cfa75e77c58e03435bf9452a4110bbb42199fd 100644 (file)
@@ -25,10 +25,10 @@ describe('ssr: attr fallthrough', () => {
       template: `<child :ok="ok" class="bar"/>`,
     }
     expect(await renderToString(createApp(Parent, { ok: true }))).toBe(
-      `<div class="foo bar"></div><!--if-->`,
+      `<div class="foo bar"></div>`,
     )
     expect(await renderToString(createApp(Parent, { ok: false }))).toBe(
-      `<span class="bar"></span><!--if-->`,
+      `<span class="bar"></span>`,
     )
   })
 
index 181720c5b364968cee58e5e3348af06bb01adfcb..ccca72bd1b6c7d4cec934f1e4e4be9cd1822cb77 100644 (file)
@@ -14,9 +14,7 @@ describe('ssr: dynamic component', () => {
           template: `<component :is="'one'"><span>slot</span></component>`,
         }),
       ),
-    ).toBe(
-      `<div><!--[--><span>slot</span><!--]--><!--slot--></div><!--dynamic-component-->`,
-    )
+    ).toBe(`<div><!--[--><span>slot</span><!--]--></div>`)
   })
 
   test('resolved to component with v-show', async () => {
@@ -32,7 +30,7 @@ describe('ssr: dynamic component', () => {
         }),
       ),
     ).toBe(
-      `<div><!--[--><div style="display:none;"><!--[-->hi<!--]--></div><!--dynamic-component--><!--]--></div><!--dynamic-component-->`,
+      `<div><!--[--><div style="display:none;"><!--[-->hi<!--]--></div><!--]--></div>`,
     )
   })
 
@@ -43,7 +41,7 @@ describe('ssr: dynamic component', () => {
           template: `<component :is="'p'"><span>slot</span></component>`,
         }),
       ),
-    ).toBe(`<p><span>slot</span></p><!--dynamic-component-->`)
+    ).toBe(`<p><span>slot</span></p>`)
   })
 
   test('resolve to component vnode', async () => {
@@ -62,9 +60,7 @@ describe('ssr: dynamic component', () => {
           template: `<component :is="vnode"><span>slot</span></component>`,
         }),
       ),
-    ).toBe(
-      `<div>test<!--[--><span>slot</span><!--]--><!--slot--></div><!--dynamic-component-->`,
-    )
+    ).toBe(`<div>test<!--[--><span>slot</span><!--]--></div>`)
   })
 
   test('resolve to element vnode', async () => {
@@ -79,6 +75,6 @@ describe('ssr: dynamic component', () => {
           template: `<component :is="vnode"><span>slot</span></component>`,
         }),
       ),
-    ).toBe(`<div id="test"><span>slot</span></div><!--dynamic-component-->`)
+    ).toBe(`<div id="test"><span>slot</span></div>`)
   })
 })
index c4135e498b78e73084668970da612641e6f6316d..4ceb865fb50faefdbb8cf9c674fa36ee887c9f43 100644 (file)
@@ -68,9 +68,7 @@ describe('ssr: scopedId runtime behavior', () => {
     }
 
     const result = await renderToString(createApp(Comp))
-    expect(result).toBe(
-      `<!--[--><div parent wrapper-s child></div><!--]--><!--slot-->`,
-    )
+    expect(result).toBe(`<!--[--><div parent wrapper-s child></div><!--]-->`)
   })
 
   // #2892
@@ -152,8 +150,8 @@ describe('ssr: scopedId runtime behavior', () => {
     const result = await renderToString(createApp(Root))
     expect(result).toBe(
       `<div class="wrapper" root slotted wrapper>` +
-        `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` +
-        `<!--slot--></div>`,
+        `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
+        `</div>`,
     )
   })
 
@@ -267,8 +265,8 @@ describe('ssr: scopedId runtime behavior', () => {
     const result = await renderToString(createApp(Root))
     expect(result).toBe(
       `<div class="wrapper" root slotted wrapper>` +
-        `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--slot--><!--]-->` +
-        `<!--slot--></div>`,
+        `<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
+        `</div>`,
     )
   })
 })
index e8435225481ac12831da2d8084b624481ecf5f81..4cc7fd97ef2cd51609e85682a7a84535ee486d1a 100644 (file)
@@ -16,7 +16,7 @@ describe('ssr: slot', () => {
           template: `<one>hello</one>`,
         }),
       ),
-    ).toBe(`<div><!--[-->hello<!--]--><!--slot--></div>`)
+    ).toBe(`<div><!--[-->hello<!--]--></div>`)
   })
 
   test('element slot', async () => {
@@ -27,7 +27,7 @@ describe('ssr: slot', () => {
           template: `<one><div>hi</div></one>`,
         }),
       ),
-    ).toBe(`<div><!--[--><div>hi</div><!--]--><!--slot--></div>`)
+    ).toBe(`<div><!--[--><div>hi</div><!--]--></div>`)
   })
 
   test('empty slot', async () => {
@@ -42,7 +42,7 @@ describe('ssr: slot', () => {
           template: `<one><template v-if="false"/></one>`,
         }),
       ),
-    ).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
+    ).toBe(`<div><!--[--><!--]--></div>`)
   })
 
   test('empty slot (manual comments)', async () => {
@@ -57,7 +57,7 @@ describe('ssr: slot', () => {
           template: `<one><!--hello--></one>`,
         }),
       ),
-    ).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
+    ).toBe(`<div><!--[--><!--]--></div>`)
   })
 
   test('empty slot (multi-line comments)', async () => {
@@ -72,7 +72,7 @@ describe('ssr: slot', () => {
           template: `<one><!--he\nllo--></one>`,
         }),
       ),
-    ).toBe(`<div><!--[--><!--]--><!--slot--></div>`)
+    ).toBe(`<div><!--[--><!--]--></div>`)
   })
 
   test('multiple elements', async () => {
@@ -83,7 +83,7 @@ describe('ssr: slot', () => {
           template: `<one><div>one</div><div>two</div></one>`,
         }),
       ),
-    ).toBe(`<div><!--[--><div>one</div><div>two</div><!--]--><!--slot--></div>`)
+    ).toBe(`<div><!--[--><div>one</div><div>two</div><!--]--></div>`)
   })
 
   test('fragment slot (template v-if)', async () => {
@@ -94,9 +94,7 @@ describe('ssr: slot', () => {
           template: `<one><template v-if="true">hello</template></one>`,
         }),
       ),
-    ).toBe(
-      `<div><!--[--><!--[-->hello<!--]--><!--if--><!--]--><!--slot--></div>`,
-    )
+    ).toBe(`<div><!--[--><!--[-->hello<!--]--><!--]--></div>`)
   })
 
   test('fragment slot (template v-if + multiple elements)', async () => {
@@ -108,7 +106,7 @@ describe('ssr: slot', () => {
         }),
       ),
     ).toBe(
-      `<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--if--><!--]--><!--slot--></div>`,
+      `<div><!--[--><!--[--><div>one</div><div>two</div><!--]--><!--]--></div>`,
     )
   })
 
@@ -137,7 +135,7 @@ describe('ssr: slot', () => {
           template: `<one><div v-if="true">foo</div></one>`,
         }),
       ),
-    ).toBe(`<div>foo</div><!--if-->`)
+    ).toBe(`<div>foo</div>`)
   })
 
   // #9933
@@ -185,7 +183,7 @@ describe('ssr: slot', () => {
           `,
         }),
       ),
-    ).toBe(`<div><!--[--> new header <!--]--><!--slot--></div>`)
+    ).toBe(`<div><!--[--> new header <!--]--></div>`)
   })
 
   // #11326
@@ -204,9 +202,7 @@ describe('ssr: slot', () => {
           template: `<ButtonComp><Wrap><div v-if="false">hello</div></Wrap></ButtonComp>`,
         }),
       ),
-    ).toBe(
-      `<button><!--[--><div><!--[--><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`,
-    )
+    ).toBe(`<button><!--[--><div><!--[--><!--]--></div><!--]--></button>`)
 
     expect(
       await renderToString(
@@ -223,7 +219,7 @@ describe('ssr: slot', () => {
         }),
       ),
     ).toBe(
-      `<button><!--[--><div><!--[--><div>hello</div><!--]--><!--slot--></div><!--]--></button><!--dynamic-component-->`,
+      `<button><!--[--><div><!--[--><div>hello</div><!--]--></div><!--]--></button>`,
     )
 
     expect(
@@ -237,6 +233,6 @@ describe('ssr: slot', () => {
           template: `<ButtonComp><template v-if="false">hello</template></ButtonComp>`,
         }),
       ),
-    ).toBe(`<button><!--[--><!--]--></button><!--dynamic-component-->`)
+    ).toBe(`<button><!--[--><!--]--></button>`)
   })
 })
index 0733c8233907c5e6883358b5f377a0a3d265b293..19aa4ce63b76c1a1118fd13a4dc32c6bae35533f 100644 (file)
@@ -5,7 +5,7 @@ import {
   type SSRBufferItem,
   renderVNodeChildren,
 } from '../render'
-import { SLOT_ANCHOR_LABEL, isArray } from '@vue/shared'
+import { isArray } from '@vue/shared'
 
 const { ensureValidVNode } = ssrUtils
 
@@ -37,7 +37,7 @@ export function ssrRenderSlot(
     parentComponent,
     slotScopeId,
   )
-  push(`<!--]--><!--${SLOT_ANCHOR_LABEL}-->`)
+  push(`<!--]-->`)
 }
 
 export function ssrRenderSlotInner(
@@ -104,7 +104,7 @@ export function ssrRenderSlotInner(
         if (
           transition &&
           slotBuffer[0] === '<!--[-->' &&
-          (slotBuffer[end - 1] as string).startsWith('<!--]-->')
+          slotBuffer[end - 1] === '<!--]-->'
         ) {
           start++
           end--
index d4c634c7abacf98b68334caa2423881fbc8ed891..fc8bc095c086edf762b61040ffdcb26db8af9e76 100644 (file)
@@ -1,17 +1,24 @@
-export const BLOCK_START_ANCHOR_LABEL = '[['
-export const BLOCK_END_ANCHOR_LABEL = ']]'
-
+export const BLOCK_INSERTION_ANCHOR_LABEL = 'i'
+export const BLOCK_APPEND_ANCHOR_LABEL = 'a'
+export const BLOCK_PREPEND_ANCHOR_LABEL = 'p'
 export const IF_ANCHOR_LABEL: string = 'if'
 export const ELSE_IF_ANCHOR_LABEL: string = 'else-if'
 export const DYNAMIC_COMPONENT_ANCHOR_LABEL: string = 'dynamic-component'
 export const FOR_ANCHOR_LABEL: string = 'for'
 export const SLOT_ANCHOR_LABEL: string = 'slot'
 
-export function isBlockAnchor(node: Node): node is Comment {
+export function isInsertionAnchor(node: Node): node is Comment {
   if (node.nodeType !== 8) return false
 
   const data = (node as Comment).data
-  return data === BLOCK_START_ANCHOR_LABEL || data === BLOCK_END_ANCHOR_LABEL
+  return (
+    data === `[${BLOCK_INSERTION_ANCHOR_LABEL}` ||
+    data === `${BLOCK_INSERTION_ANCHOR_LABEL}]` ||
+    data === `[${BLOCK_APPEND_ANCHOR_LABEL}` ||
+    data === `${BLOCK_APPEND_ANCHOR_LABEL}]` ||
+    data === `[${BLOCK_PREPEND_ANCHOR_LABEL}` ||
+    data === `${BLOCK_PREPEND_ANCHOR_LABEL}]`
+  )
 }
 
 export function isVaporFragmentAnchor(node: Node): node is Comment {
@@ -27,5 +34,5 @@ export function isVaporFragmentAnchor(node: Node): node is Comment {
 }
 
 export function isVaporAnchor(node: Node): node is Comment {
-  return isBlockAnchor(node) || isVaporFragmentAnchor(node)
+  return isVaporFragmentAnchor(node) || isInsertionAnchor(node)
 }