Browse Source

refactor(hydration): simplify hydration with compiler-calculated logicalIndex (#14340)

edison 3 months ago
parent
commit
021de406ee
23 changed files with 1203 additions and 120 deletions
  1. 6 6
      packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
  2. 1 1
      packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap
  3. 480 0
      packages/compiler-vapor/__tests__/transforms/__snapshots__/logicalIndex.spec.ts.snap
  4. 1 1
      packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap
  5. 1 1
      packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
  6. 2 2
      packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap
  7. 2 2
      packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
  8. 7 7
      packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
  9. 427 0
      packages/compiler-vapor/__tests__/transforms/logicalIndex.spec.ts
  10. 1 1
      packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts
  11. 5 7
      packages/compiler-vapor/src/generators/operation.ts
  12. 8 18
      packages/compiler-vapor/src/generators/template.ts
  13. 7 0
      packages/compiler-vapor/src/ir/index.ts
  14. 12 0
      packages/compiler-vapor/src/transforms/transformChildren.ts
  15. 1 1
      packages/runtime-vapor/__tests__/componentSlots.spec.ts
  16. 1 1
      packages/runtime-vapor/__tests__/components/Teleport.spec.ts
  17. 3 3
      packages/runtime-vapor/__tests__/customElement.spec.ts
  18. 200 0
      packages/runtime-vapor/__tests__/hydration.spec.ts
  19. 1 1
      packages/runtime-vapor/__tests__/scopeId.spec.ts
  20. 9 7
      packages/runtime-vapor/src/apiCreateFor.ts
  21. 7 35
      packages/runtime-vapor/src/dom/hydration.ts
  22. 8 11
      packages/runtime-vapor/src/dom/node.ts
  23. 13 15
      packages/runtime-vapor/src/insertionState.ts

+ 6 - 6
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap

@@ -38,7 +38,7 @@ export function render(_ctx) {
     "default": _withVaporCtx(() => {
       const n0 = _createIf(() => (true), () => {
         const n3 = t0()
-        _setInsertionState(n3, null, true)
+        _setInsertionState(n3, null, 0, true)
         const n2 = _createComponentWithFallback(_component_Bar)
         _withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]])
         return n3
@@ -158,7 +158,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
   const n0 = t0()
   const n3 = t1()
   const n2 = _child(n3, 1)
-  _setInsertionState(n3, 0, true)
+  _setInsertionState(n3, 0, 0, true)
   const n1 = _createComponentWithFallback(_component_Comp)
   _renderEffect(() => {
     _setProp(n3, "id", _ctx.foo)
@@ -224,9 +224,9 @@ export function render(_ctx) {
   const n7 = _nthChild(n6, 3, 3)
   const p0 = _next(n7, 4)
   const n4 = _child(p0)
-  _setInsertionState(n6, n5)
+  _setInsertionState(n6, n5, 1)
   const n0 = _createComponentWithFallback(_component_Comp)
-  _setInsertionState(n6, n7, true)
+  _setInsertionState(n6, n7, 3, true)
   const n1 = _createIf(() => (true), () => {
     const n3 = t0()
     return n3
@@ -244,9 +244,9 @@ export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
   const n3 = t0()
   const n1 = _child(n3)
-  _setInsertionState(n1, null, true)
+  _setInsertionState(n1, null, 0, true)
   const n0 = _createSlot("default", null)
-  _setInsertionState(n3, 1, true)
+  _setInsertionState(n3, null, 1, true)
   const n2 = _createComponentWithFallback(_component_Comp)
   return n3
 }"

+ 1 - 1
packages/compiler-vapor/__tests__/transforms/__snapshots__/TransformTransition.spec.ts.snap

@@ -49,7 +49,7 @@ export function render(_ctx) {
         return n5
       }, () => {
         const n14 = t2()
-        _setInsertionState(n14, 0, true)
+        _setInsertionState(n14, 0, 0, true)
         const n9 = _createIf(() => (_ctx.c), () => {
           const n11 = t1()
           return n11

+ 480 - 0
packages/compiler-vapor/__tests__/transforms/__snapshots__/logicalIndex.spec.ts.snap

@@ -0,0 +1,480 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: logicalIndex > child/nthChild/next with logicalIndex > child with logicalIndex when prepend exists and insert anchor needed 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, child as _child, next as _next, template as _template } from 'vue';
+const t0 = _template("<div><div></div><!><span>", true)
+
+export function render(_ctx) {
+  const _component_Comp1 = _resolveComponent("Comp1")
+  const _component_Comp2 = _resolveComponent("Comp2")
+  const n2 = t0()
+  const n3 = _next(_child(n2), 2)
+  _setInsertionState(n2, 0, 0)
+  const n0 = _createComponentWithFallback(_component_Comp1)
+  _setInsertionState(n2, n3, 2, true)
+  const n1 = _createComponentWithFallback(_component_Comp2)
+  return n2
+}"
+`;
+
+exports[`compiler: logicalIndex > child/nthChild/next with logicalIndex > multiple prepends affect logicalIndex 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, child as _child, next as _next, template as _template } from 'vue';
+const t0 = _template("<div><div></div><!><span>", true)
+
+export function render(_ctx) {
+  const _component_Comp1 = _resolveComponent("Comp1")
+  const _component_Comp2 = _resolveComponent("Comp2")
+  const _component_Comp3 = _resolveComponent("Comp3")
+  const n3 = t0()
+  const n4 = _next(_child(n3), 3)
+  _setInsertionState(n3, 0, 0)
+  const n0 = _createComponentWithFallback(_component_Comp1)
+  _setInsertionState(n3, 0, 1)
+  const n1 = _createComponentWithFallback(_component_Comp2)
+  _setInsertionState(n3, n4, 3, true)
+  const n2 = _createComponentWithFallback(_component_Comp3)
+  return n3
+}"
+`;
+
+exports[`compiler: logicalIndex > child/nthChild/next with logicalIndex > next with logicalIndex for insert anchor 1`] = `
+"import { resolveComponent as _resolveComponent, child as _child, next as _next, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>")
+const t1 = _template("<div><div></div><!><div></div>", true)
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n5 = t1()
+  const n4 = _next(_child(n5), 1)
+  _setInsertionState(n5, n4, 1)
+  const n0 = _createComponentWithFallback(_component_Comp)
+  _setInsertionState(n5, null, 3, true)
+  const n1 = _createIf(() => (true), () => {
+    const n3 = t0()
+    return n3
+  }, null, true)
+  return n5
+}"
+`;
+
+exports[`compiler: logicalIndex > child/nthChild/next with logicalIndex > nthChild with logicalIndex 1`] = `
+"import { resolveComponent as _resolveComponent, child as _child, next as _next, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, nthChild as _nthChild, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>")
+const t1 = _template("<div><div></div><!><div></div><!><div><button>", true)
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n5 = t1()
+  const n4 = _next(_child(n5), 1)
+  const n6 = _nthChild(n5, 3, 3)
+  _setInsertionState(n5, n4, 1)
+  const n0 = _createComponentWithFallback(_component_Comp)
+  _setInsertionState(n5, n6, 3, true)
+  const n1 = _createIf(() => (true), () => {
+    const n3 = t0()
+    return n3
+  }, null, true)
+  return n5
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > append scenarios > multiple consecutive append 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("<div><span>A", true)
+
+export function render(_ctx) {
+  const _component_Comp1 = _resolveComponent("Comp1")
+  const _component_Comp2 = _resolveComponent("Comp2")
+  const n2 = t0()
+  _setInsertionState(n2, null, 1)
+  const n0 = _createComponentWithFallback(_component_Comp1)
+  _setInsertionState(n2, null, 2, true)
+  const n1 = _createComponentWithFallback(_component_Comp2)
+  return n2
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > append scenarios > only component (append with logicalIndex 0) 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("<div>", true)
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n1 = t0()
+  _setInsertionState(n1, null, 0, true)
+  const n0 = _createComponentWithFallback(_component_Comp)
+  return n1
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > append scenarios > single component append 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("<div><span>A", true)
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n1 = t0()
+  _setInsertionState(n1, null, 1, true)
+  const n0 = _createComponentWithFallback(_component_Comp)
+  return n1
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > insert scenarios > multiple consecutive insert in middle 1`] = `
+"import { resolveComponent as _resolveComponent, child as _child, next as _next, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("<div><span>A</span><!><p>B", true)
+
+export function render(_ctx) {
+  const _component_Comp1 = _resolveComponent("Comp1")
+  const _component_Comp2 = _resolveComponent("Comp2")
+  const n3 = t0()
+  const n2 = _next(_child(n3), 1)
+  _setInsertionState(n3, n2, 1)
+  const n0 = _createComponentWithFallback(_component_Comp1)
+  _setInsertionState(n3, n2, 2, true)
+  const n1 = _createComponentWithFallback(_component_Comp2)
+  return n3
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > insert scenarios > single component insert in middle 1`] = `
+"import { resolveComponent as _resolveComponent, child as _child, next as _next, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("<div><span>A</span><!><p>B", true)
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = t0()
+  const n1 = _next(_child(n2), 1)
+  _setInsertionState(n2, n1, 1, true)
+  const n0 = _createComponentWithFallback(_component_Comp)
+  return n2
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > mixed scenarios > prepend + append 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("<div><span>A", true)
+
+export function render(_ctx) {
+  const _component_Comp1 = _resolveComponent("Comp1")
+  const _component_Comp2 = _resolveComponent("Comp2")
+  const n2 = t0()
+  _setInsertionState(n2, 0, 0)
+  const n0 = _createComponentWithFallback(_component_Comp1)
+  _setInsertionState(n2, null, 2, true)
+  const n1 = _createComponentWithFallback(_component_Comp2)
+  return n2
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > mixed scenarios > prepend + insert + append 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, child as _child, next as _next, template as _template } from 'vue';
+const t0 = _template("<div><span>A</span><!><p>B", true)
+
+export function render(_ctx) {
+  const _component_Comp1 = _resolveComponent("Comp1")
+  const _component_Comp2 = _resolveComponent("Comp2")
+  const _component_Comp3 = _resolveComponent("Comp3")
+  const n3 = t0()
+  const n4 = _next(_child(n3), 2)
+  _setInsertionState(n3, 0, 0)
+  const n0 = _createComponentWithFallback(_component_Comp1)
+  _setInsertionState(n3, n4, 2)
+  const n1 = _createComponentWithFallback(_component_Comp2)
+  _setInsertionState(n3, null, 4, true)
+  const n2 = _createComponentWithFallback(_component_Comp3)
+  return n3
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > prepend scenarios > multiple consecutive prepend 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("<div><span>A", true)
+
+export function render(_ctx) {
+  const _component_Comp1 = _resolveComponent("Comp1")
+  const _component_Comp2 = _resolveComponent("Comp2")
+  const n2 = t0()
+  _setInsertionState(n2, 0, 0)
+  const n0 = _createComponentWithFallback(_component_Comp1)
+  _setInsertionState(n2, 0, 1, true)
+  const n1 = _createComponentWithFallback(_component_Comp2)
+  return n2
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > prepend scenarios > single component prepend 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("<div><span>A", true)
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n1 = t0()
+  _setInsertionState(n1, 0, 0, true)
+  const n0 = _createComponentWithFallback(_component_Comp)
+  return n1
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > slot scenarios > slot append 1`] = `
+"import { setInsertionState as _setInsertionState, createSlot as _createSlot, template as _template } from 'vue';
+const t0 = _template("<div><span>A</span>", true)
+
+export function render(_ctx) {
+  const n1 = t0()
+  _setInsertionState(n1, null, 1, true)
+  const n0 = _createSlot("default", null)
+  return n1
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > slot scenarios > slot prepend 1`] = `
+"import { setInsertionState as _setInsertionState, createSlot as _createSlot, template as _template } from 'vue';
+const t0 = _template("<div><span>A", true)
+
+export function render(_ctx) {
+  const n1 = t0()
+  _setInsertionState(n1, 0, 0, true)
+  const n0 = _createSlot("default", null)
+  return n1
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > v-for scenarios > v-for append 1`] = `
+"import { setInsertionState as _setInsertionState, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div>")
+const t1 = _template("<div><span>A</span>", true)
+
+export function render(_ctx) {
+  const n3 = t1()
+  _setInsertionState(n3, null, 1, true)
+  const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
+    const n2 = t0()
+    return n2
+  }, (i) => (i))
+  return n3
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > v-for scenarios > v-for prepend 1`] = `
+"import { setInsertionState as _setInsertionState, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div>")
+const t1 = _template("<div><span>A", true)
+
+export function render(_ctx) {
+  const n3 = t1()
+  _setInsertionState(n3, 0, 0, true)
+  const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
+    const n2 = t0()
+    return n2
+  }, (i) => (i))
+  return n3
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > v-if scenarios > v-if append 1`] = `
+"import { setInsertionState as _setInsertionState, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>")
+const t1 = _template("<div><span>A</span>", true)
+
+export function render(_ctx) {
+  const n3 = t1()
+  _setInsertionState(n3, null, 1, true)
+  const n0 = _createIf(() => (_ctx.show), () => {
+    const n2 = t0()
+    return n2
+  })
+  return n3
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > v-if scenarios > v-if insert 1`] = `
+"import { child as _child, next as _next, setInsertionState as _setInsertionState, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>")
+const t1 = _template("<div><span>A</span><!><p>B", true)
+
+export function render(_ctx) {
+  const n4 = t1()
+  const n3 = _next(_child(n4), 1)
+  _setInsertionState(n4, n3, 1, true)
+  const n0 = _createIf(() => (_ctx.show), () => {
+    const n2 = t0()
+    return n2
+  })
+  return n4
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > v-if scenarios > v-if prepend 1`] = `
+"import { setInsertionState as _setInsertionState, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>")
+const t1 = _template("<div><span>A", true)
+
+export function render(_ctx) {
+  const n3 = t1()
+  _setInsertionState(n3, 0, 0, true)
+  const n0 = _createIf(() => (_ctx.show), () => {
+    const n2 = t0()
+    return n2
+  })
+  return n3
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > v-if/v-else-if/v-else scenarios > component + v-if/v-else + component 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>if")
+const t1 = _template("<div>else")
+const t2 = _template("<div>", true)
+
+export function render(_ctx) {
+  const _component_Comp1 = _resolveComponent("Comp1")
+  const _component_Comp2 = _resolveComponent("Comp2")
+  const n7 = t2()
+  _setInsertionState(n7, null, 0)
+  const n0 = _createComponentWithFallback(_component_Comp1)
+  _setInsertionState(n7, null, 1)
+  const n1 = _createIf(() => (_ctx.show), () => {
+    const n3 = t0()
+    return n3
+  }, () => {
+    const n5 = t1()
+    return n5
+  })
+  _setInsertionState(n7, null, 2, true)
+  const n6 = _createComponentWithFallback(_component_Comp2)
+  return n7
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > v-if/v-else-if/v-else scenarios > component followed by v-if/v-else 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>if")
+const t1 = _template("<div>else")
+const t2 = _template("<div><span>A", true)
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n6 = t2()
+  _setInsertionState(n6, 0, 0)
+  const n0 = _createComponentWithFallback(_component_Comp)
+  _setInsertionState(n6, 0, 1, true)
+  const n1 = _createIf(() => (_ctx.show), () => {
+    const n3 = t0()
+    return n3
+  }, () => {
+    const n5 = t1()
+    return n5
+  })
+  return n6
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > v-if/v-else-if/v-else scenarios > v-if with v-else should share same logicalIndex 1`] = `
+"import { child as _child, next as _next, setInsertionState as _setInsertionState, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>if")
+const t1 = _template("<div>else")
+const t2 = _template("<div><span>A</span><!><p>B", true)
+
+export function render(_ctx) {
+  const n6 = t2()
+  const n5 = _next(_child(n6), 1)
+  _setInsertionState(n6, n5, 1, true)
+  const n0 = _createIf(() => (_ctx.show), () => {
+    const n2 = t0()
+    return n2
+  }, () => {
+    const n4 = t1()
+    return n4
+  })
+  return n6
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > v-if/v-else-if/v-else scenarios > v-if with v-else-if and v-else should share same logicalIndex 1`] = `
+"import { child as _child, next as _next, setInsertionState as _setInsertionState, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>if")
+const t1 = _template("<div>else-if")
+const t2 = _template("<div>else")
+const t3 = _template("<div><span>A</span><!><p>B", true)
+
+export function render(_ctx) {
+  const n8 = t3()
+  const n7 = _next(_child(n8), 1)
+  _setInsertionState(n8, n7, 1, true)
+  const n0 = _createIf(() => (_ctx.a), () => {
+    const n2 = t0()
+    return n2
+  }, () => _createIf(() => (_ctx.b), () => {
+    const n4 = t1()
+    return n4
+  }, () => {
+    const n6 = t2()
+    return n6
+  }))
+  return n8
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > v-if/v-else-if/v-else scenarios > v-if/v-else append 1`] = `
+"import { setInsertionState as _setInsertionState, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>if")
+const t1 = _template("<div>else")
+const t2 = _template("<div><span>A</span>", true)
+
+export function render(_ctx) {
+  const n5 = t2()
+  _setInsertionState(n5, null, 1, true)
+  const n0 = _createIf(() => (_ctx.show), () => {
+    const n2 = t0()
+    return n2
+  }, () => {
+    const n4 = t1()
+    return n4
+  })
+  return n5
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > v-if/v-else-if/v-else scenarios > v-if/v-else followed by component 1`] = `
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createIf as _createIf, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template("<div>if")
+const t1 = _template("<div>else")
+const t2 = _template("<div><span>A</span>", true)
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n6 = t2()
+  _setInsertionState(n6, null, 1)
+  const n0 = _createIf(() => (_ctx.show), () => {
+    const n2 = t0()
+    return n2
+  }, () => {
+    const n4 = t1()
+    return n4
+  })
+  _setInsertionState(n6, null, 2, true)
+  const n5 = _createComponentWithFallback(_component_Comp)
+  return n6
+}"
+`;
+
+exports[`compiler: logicalIndex > setInsertionState scenarios > v-if/v-else-if/v-else scenarios > v-if/v-else prepend 1`] = `
+"import { setInsertionState as _setInsertionState, createIf as _createIf, template as _template } from 'vue';
+const t0 = _template("<div>if")
+const t1 = _template("<div>else")
+const t2 = _template("<div><span>A", true)
+
+export function render(_ctx) {
+  const n5 = t2()
+  _setInsertionState(n5, 0, 0, true)
+  const n0 = _createIf(() => (_ctx.show), () => {
+    const n2 = t0()
+    return n2
+  }, () => {
+    const n4 = t1()
+    return n4
+  })
+  return n5
+}"
+`;

+ 1 - 1
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap

@@ -8,7 +8,7 @@ const t1 = _template("<div><div></div><!><div>", true)
 export function render(_ctx) {
   const n4 = t1()
   const n3 = _next(_child(n4), 1)
-  _setInsertionState(n4, n3, true)
+  _setInsertionState(n4, n3, 1, true)
   const n0 = _createIf(() => (1), () => {
     const n2 = t0()
     return n2

+ 1 - 1
packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap

@@ -87,7 +87,7 @@ const t1 = _template("<div>")
 export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
     const n5 = t1()
-    _setInsertionState(n5, null, true)
+    _setInsertionState(n5, null, 0, true)
     const n2 = _createFor(() => (_for_item0.value), (_for_item1) => {
       const n4 = t0()
       const x4 = _txt(n4)

+ 2 - 2
packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap

@@ -276,12 +276,12 @@ const t3 = _template("<div>", true)
 
 export function render(_ctx) {
   const n8 = t3()
-  _setInsertionState(n8, null)
+  _setInsertionState(n8, null, 0)
   const n0 = _createIf(() => (_ctx.foo), () => {
     const n2 = t0()
     return n2
   })
-  _setInsertionState(n8, null, true)
+  _setInsertionState(n8, null, 1, true)
   const n3 = _createIf(() => (_ctx.bar), () => {
     const n5 = t1()
     return n5

+ 2 - 2
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap

@@ -42,7 +42,7 @@ const t0 = _template("<div>", true)
 export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
   const n1 = t0()
-  _setInsertionState(n1, null, true)
+  _setInsertionState(n1, null, 0, true)
   const n0 = _createComponentWithFallback(_component_Comp, { id: () => (_ctx.foo) }, null, null, true)
   return n1
 }"
@@ -66,7 +66,7 @@ const t0 = _template("<div>", true)
 
 export function render(_ctx) {
   const n1 = t0()
-  _setInsertionState(n1, null, true)
+  _setInsertionState(n1, null, 0, true)
   const n0 = _createSlot("default", null, null, null, true)
   return n1
 }"

+ 7 - 7
packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap

@@ -366,9 +366,9 @@ export function render(_ctx) {
   const _component_Foo = _resolveComponent("Foo")
   const _component_Bar = _resolveComponent("Bar")
   const n6 = t0()
-  _setInsertionState(n6, null)
+  _setInsertionState(n6, null, 0)
   const n0 = _createSlot("foo", null)
-  _setInsertionState(n6, null, true)
+  _setInsertionState(n6, null, 1, true)
   const n1 = _createIf(() => (true), () => {
     const n3 = _createComponentWithFallback(_component_Foo)
     return n3
@@ -592,7 +592,7 @@ export function render(_ctx) {
     "default": _withVaporCtx(() => {
       const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
         const n3 = t0()
-        _setInsertionState(n3, null, true)
+        _setInsertionState(n3, null, 0, true)
         const n2 = _createComponentWithFallback(_component_ChildComp)
         return n3
       })
@@ -614,7 +614,7 @@ export function render(_ctx) {
     "default": _withVaporCtx(() => {
       const n0 = _createIf(() => (_ctx.show), () => {
         const n3 = t0()
-        _setInsertionState(n3, null, true)
+        _setInsertionState(n3, null, 0, true)
         const n2 = _createComponentWithFallback(_component_ChildComp)
         return n3
       })
@@ -651,7 +651,7 @@ export function render(_ctx) {
     "default": _withVaporCtx(() => {
       const n0 = _createIf(() => (_ctx.show), () => {
         const n3 = t0()
-        _setInsertionState(n3, null, true)
+        _setInsertionState(n3, null, 0, true)
         const n2 = _createPlainElement("my-element")
         return n3
       })
@@ -689,10 +689,10 @@ export function render(_ctx) {
     "default": _withVaporCtx(() => {
       const n0 = _createIf(() => (_ctx.a), () => {
         const n6 = t1()
-        _setInsertionState(n6, null, true)
+        _setInsertionState(n6, null, 0, true)
         const n2 = _createIf(() => (_ctx.b), () => {
           const n5 = t0()
-          _setInsertionState(n5, null, true)
+          _setInsertionState(n5, null, 0, true)
           const n4 = _createComponentWithFallback(_component_ChildComp)
           return n5
         })

+ 427 - 0
packages/compiler-vapor/__tests__/transforms/logicalIndex.spec.ts

@@ -0,0 +1,427 @@
+import { makeCompile } from './_utils'
+import {
+  transformChildren,
+  transformElement,
+  transformSlotOutlet,
+  transformText,
+  transformVFor,
+  transformVIf,
+} from '../../src'
+import { transformVSlot } from '../../src/transforms/vSlot'
+
+const compileWithTransforms = makeCompile({
+  nodeTransforms: [
+    transformText,
+    transformVIf,
+    transformVFor,
+    transformVSlot,
+    transformSlotOutlet,
+    transformElement,
+    transformChildren,
+  ],
+})
+
+describe('compiler: logicalIndex', () => {
+  describe('child/nthChild/next with logicalIndex', () => {
+    test('next with logicalIndex for insert anchor', () => {
+      // <div><div /><Comp /><div /><div v-if="true" /></div>
+      // first div: 0, Comp: 1 (insert), second div: 2, v-if: 3 (append)
+      // The anchor for Comp is at logicalIndex=1
+      const { code } = compileWithTransforms(`
+        <div>
+          <div />
+          <Comp />
+          <div />
+          <div v-if="true" />
+        </div>
+      `)
+      // next(child(parent), logicalIndex=1) for the insert anchor
+      expect(code).toContain('_next(_child(n5), 1)')
+      expect(code).toMatchSnapshot()
+    })
+
+    test('nthChild with logicalIndex', () => {
+      // <div><div /><Comp /><div /><div v-if="true" /><div><button :disabled="foo" /></div></div>
+      // first div: 0, Comp: 1, second div: 2, v-if: 3, last div: 4
+      // But the v-if is append, so the anchor is at elementIndex=3, logicalIndex=3
+      const { code } = compileWithTransforms(`
+        <div>
+          <div />
+          <Comp />
+          <div />
+          <div v-if="true" />
+          <div>
+            <button :disabled="foo" />
+          </div>
+        </div>
+      `)
+      // nthChild(parent, elementIndex=3, logicalIndex=3) for the anchor
+      expect(code).toContain('_nthChild(n5, 3, 3)')
+      expect(code).toMatchSnapshot()
+    })
+
+    test('child with logicalIndex when prepend exists and insert anchor needed', () => {
+      // <div><Comp1 /><div /><Comp2 /><span /></div>
+      // Comp1: 0 (prepend), div: 1, Comp2: 2 (insert), span: 3
+      // The anchor is accessed via next(child(parent), logicalIndex)
+      const { code } = compileWithTransforms(`
+        <div>
+          <Comp1 />
+          <div />
+          <Comp2 />
+          <span />
+        </div>
+      `)
+      // next(child(parent), logicalIndex=2) for the anchor (<!> placeholder)
+      expect(code).toContain('_next(_child(n2), 2)')
+      expect(code).toMatchSnapshot()
+    })
+
+    test('multiple prepends affect logicalIndex', () => {
+      // <div><Comp1 /><Comp2 /><div /><Comp3 /><span /></div>
+      // Comp1: 0, Comp2: 1, div: 2, Comp3: 3 (insert), span: 4
+      const { code } = compileWithTransforms(`
+        <div>
+          <Comp1 />
+          <Comp2 />
+          <div />
+          <Comp3 />
+          <span />
+        </div>
+      `)
+      // next(child(parent), logicalIndex=3) for the anchor (<!> placeholder)
+      expect(code).toContain('_next(_child(n3), 3)')
+      expect(code).toMatchSnapshot()
+    })
+  })
+
+  describe('setInsertionState scenarios', () => {
+    describe('prepend scenarios', () => {
+      test('single component prepend', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <Comp />
+            <span>A</span>
+          </div>
+        `)
+        // setInsertionState(parent, anchor=0(prepend), logicalIndex=0, last=true)
+        expect(code).toContain('_setInsertionState(n1, 0, 0, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('multiple consecutive prepend', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <Comp1 />
+            <Comp2 />
+            <span>A</span>
+          </div>
+        `)
+        // Comp1
+        expect(code).toContain('_setInsertionState(n2, 0, 0)')
+        // Comp2
+        expect(code).toContain('_setInsertionState(n2, 0, 1, true)')
+        expect(code).toMatchSnapshot()
+      })
+    })
+
+    describe('insert scenarios', () => {
+      test('single component insert in middle', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <Comp />
+            <p>B</p>
+          </div>
+        `)
+        // setInsertionState(parent, anchor=n${id}, logicalIndex=1, last=true)
+        expect(code).toContain('_setInsertionState(n2, n1, 1, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('multiple consecutive insert in middle', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <Comp1 />
+            <Comp2 />
+            <p>B</p>
+          </div>
+        `)
+        // Comp1
+        expect(code).toContain('_setInsertionState(n3, n2, 1)')
+        // Comp2
+        expect(code).toContain('_setInsertionState(n3, n2, 2, true)')
+        expect(code).toMatchSnapshot()
+      })
+    })
+
+    describe('append scenarios', () => {
+      test('single component append', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <Comp />
+          </div>
+        `)
+        // setInsertionState(parent, null(append), logicalIndex=1, last=true)
+        expect(code).toContain('_setInsertionState(n1, null, 1, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('multiple consecutive append', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <Comp1 />
+            <Comp2 />
+          </div>
+        `)
+        // Comp1
+        expect(code).toContain('_setInsertionState(n2, null, 1)')
+        // Comp2
+        expect(code).toContain('_setInsertionState(n2, null, 2, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('only component (append with logicalIndex 0)', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <Comp />
+          </div>
+        `)
+        expect(code).toContain('_setInsertionState(n1, null, 0, true)')
+        expect(code).toMatchSnapshot()
+      })
+    })
+
+    describe('mixed scenarios', () => {
+      test('prepend + append', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <Comp1 />
+            <span>A</span>
+            <Comp2 />
+          </div>
+        `)
+        // Comp1: prepend, logicalIndex=0
+        expect(code).toContain('_setInsertionState(n2, 0, 0)')
+        // Comp2: append, logicalIndex=2
+        expect(code).toContain('_setInsertionState(n2, null, 2, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('prepend + insert + append', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <Comp1 />
+            <span>A</span>
+            <Comp2 />
+            <p>B</p>
+            <Comp3 />
+          </div>
+        `)
+        // Comp1: prepend, logicalIndex=0
+        expect(code).toContain('_setInsertionState(n3, 0, 0)')
+        // Comp2: insert, logicalIndex=2
+        expect(code).toContain('_setInsertionState(n3, n4, 2)')
+        // Comp3: append, logicalIndex=4
+        expect(code).toContain('_setInsertionState(n3, null, 4, true)')
+        expect(code).toMatchSnapshot()
+      })
+    })
+
+    describe('v-if scenarios', () => {
+      test('v-if prepend', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <div v-if="show" />
+            <span>A</span>
+          </div>
+        `)
+        expect(code).toContain('_setInsertionState(n3, 0, 0, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('v-if insert', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <div v-if="show" />
+            <p>B</p>
+          </div>
+        `)
+        expect(code).toContain('_setInsertionState(n4, n3, 1, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('v-if append', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <div v-if="show" />
+          </div>
+        `)
+        expect(code).toContain('_setInsertionState(n3, null, 1, true)')
+        expect(code).toMatchSnapshot()
+      })
+    })
+
+    describe('v-for scenarios', () => {
+      test('v-for prepend', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <div v-for="i in list" :key="i" />
+            <span>A</span>
+          </div>
+        `)
+        expect(code).toContain('_setInsertionState(n3, 0, 0, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('v-for append', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <div v-for="i in list" :key="i" />
+          </div>
+        `)
+        expect(code).toContain('_setInsertionState(n3, null, 1, true)')
+        expect(code).toMatchSnapshot()
+      })
+    })
+
+    describe('slot scenarios', () => {
+      test('slot prepend', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <slot />
+            <span>A</span>
+          </div>
+        `)
+        expect(code).toContain('_setInsertionState(n1, 0, 0, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('slot append', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <slot />
+          </div>
+        `)
+        expect(code).toContain('_setInsertionState(n1, null, 1, true)')
+        expect(code).toMatchSnapshot()
+      })
+    })
+
+    describe('v-if/v-else-if/v-else scenarios', () => {
+      test('v-if with v-else should share same logicalIndex', () => {
+        // v-if/v-else is a single logical unit
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <div v-if="show">if</div>
+            <div v-else>else</div>
+            <p>B</p>
+          </div>
+        `)
+        // The entire if/else block is logicalIndex = 1
+        expect(code).toContain('_setInsertionState(n6, n5, 1, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('v-if with v-else-if and v-else should share same logicalIndex', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <div v-if="a">if</div>
+            <div v-else-if="b">else-if</div>
+            <div v-else>else</div>
+            <p>B</p>
+          </div>
+        `)
+        // The entire if/else-if/else block is logicalIndex = 1
+        expect(code).toContain('_setInsertionState(n8, n7, 1, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('v-if/v-else prepend', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <div v-if="show">if</div>
+            <div v-else>else</div>
+            <span>A</span>
+          </div>
+        `)
+        // logicalIndex = 0 for prepend
+        expect(code).toContain('_setInsertionState(n5, 0, 0, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('v-if/v-else append', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <div v-if="show">if</div>
+            <div v-else>else</div>
+          </div>
+        `)
+        // logicalIndex = 1 for append
+        expect(code).toContain('_setInsertionState(n5, null, 1, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('v-if/v-else followed by component', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <span>A</span>
+            <div v-if="show">if</div>
+            <div v-else>else</div>
+            <Comp />
+          </div>
+        `)
+        // v-if/v-else: logicalIndex = 1
+        // Comp: logicalIndex = 2
+        expect(code).toContain('_setInsertionState(n6, null, 1)')
+        expect(code).toContain('_setInsertionState(n6, null, 2, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('component followed by v-if/v-else', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <Comp />
+            <div v-if="show">if</div>
+            <div v-else>else</div>
+            <span>A</span>
+          </div>
+        `)
+        // Comp: logicalIndex = 0
+        // v-if/v-else: logicalIndex = 1
+        // span: logicalIndex = 2
+        expect(code).toContain('_setInsertionState(n6, 0, 0)')
+        expect(code).toContain('_setInsertionState(n6, 0, 1, true)')
+        expect(code).toMatchSnapshot()
+      })
+
+      test('component + v-if/v-else + component', () => {
+        const { code } = compileWithTransforms(`
+          <div>
+            <Comp1 />
+            <div v-if="show">if</div>
+            <div v-else>else</div>
+            <Comp2 />
+          </div>
+        `)
+        // Comp1: logicalIndex = 0
+        // v-if/v-else: logicalIndex = 1
+        // Comp2: logicalIndex = 2
+        expect(code).toContain('_setInsertionState(n7, null, 0)')
+        expect(code).toContain('_setInsertionState(n7, null, 1)')
+        expect(code).toContain('_setInsertionState(n7, null, 2, true)')
+        expect(code).toMatchSnapshot()
+      })
+    })
+  })
+})

+ 1 - 1
packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts

@@ -70,7 +70,7 @@ describe('compiler: children transform', () => {
     )
     // ensure the insertion anchor is generated before the insertion statement
     expect(code).toMatch(`const n3 = _next(_child(n4), 1)`)
-    expect(code).toMatch(`_setInsertionState(n4, n3, true)`)
+    expect(code).toMatch(`_setInsertionState(n4, n3, 1, true)`)
     expect(code).toMatchSnapshot()
   })
 })

+ 5 - 7
packages/compiler-vapor/src/generators/operation.ts

@@ -166,7 +166,7 @@ function genInsertionState(
   operation: InsertionStateTypes,
   context: CodegenContext,
 ): CodeFragment[] {
-  const { parent, anchor, append, last } = operation
+  const { parent, anchor, logicalIndex, append, last } = operation
   return [
     NEWLINE,
     ...genCall(
@@ -176,13 +176,11 @@ function genInsertionState(
         ? undefined
         : anchor === -1 // -1 indicates prepend
           ? `0` // runtime anchor value for prepend
-          : append // -2 indicates append
-            ? // null or anchor > 0 for append
-              // anchor > 0 is the logical index of append node - used for locate node during hydration
-              anchor === 0
-              ? 'null'
-              : `${anchor}`
+          : append
+            ? // for append, always use null since we have logicalIndex
+              'null'
             : `n${anchor}`,
+      logicalIndex !== undefined ? String(logicalIndex) : undefined,
       last && 'true',
     ),
   ]

+ 8 - 18
packages/compiler-vapor/src/generators/template.ts

@@ -1,9 +1,5 @@
 import type { CodegenContext } from '../generate'
-import {
-  DynamicFlag,
-  type IRDynamicInfo,
-  type InsertionStateTypes,
-} from '../ir'
+import { DynamicFlag, type IRDynamicInfo } from '../ir'
 import { genDirectivesForElement } from './directive'
 import { genOperationWithInsertionState } from './operation'
 import {
@@ -71,15 +67,8 @@ export function genChildren(
 
   let offset = 0
   let prev: [variable: string, elementIndex: number] | undefined
-  let prependCount = 0
 
   for (const [index, child] of children.entries()) {
-    if (
-      child.operation &&
-      (child.operation as InsertionStateTypes).anchor === -1
-    ) {
-      prependCount++
-    }
     if (child.flags & DynamicFlag.NON_TEMPLATE) {
       offset--
     }
@@ -97,7 +86,8 @@ export function genChildren(
     }
 
     const elementIndex = index + offset
-    const logicalIndex = elementIndex + prependCount
+    const logicalIndex =
+      child.logicalIndex !== undefined ? String(child.logicalIndex) : undefined
     // p for "placeholder" variables that are meant for possible reuse by
     // other access paths
     const variable =
@@ -106,14 +96,14 @@ export function genChildren(
 
     if (prev) {
       if (elementIndex - prev[1] === 1) {
-        pushBlock(...genCall(helper('next'), prev[0], String(logicalIndex)))
+        pushBlock(...genCall(helper('next'), prev[0], logicalIndex))
       } else {
         pushBlock(
           ...genCall(
             helper('nthChild'),
             from,
             String(elementIndex),
-            String(logicalIndex),
+            logicalIndex,
           ),
         )
       }
@@ -123,20 +113,20 @@ export function genChildren(
           ...genCall(
             helper('child'),
             from,
-            logicalIndex !== 0 ? String(logicalIndex) : undefined,
+            child.logicalIndex !== 0 ? logicalIndex : undefined,
           ),
         )
       } else {
         // check if there's a node that we can reuse from
         let init = genCall(helper('child'), from)
         if (elementIndex === 1) {
-          init = genCall(helper('next'), init, String(logicalIndex))
+          init = genCall(helper('next'), init, logicalIndex)
         } else if (elementIndex > 1) {
           init = genCall(
             helper('nthChild'),
             from,
             String(elementIndex),
-            String(logicalIndex),
+            logicalIndex,
           )
         }
         pushBlock(...init)

+ 7 - 0
packages/compiler-vapor/src/ir/index.ts

@@ -78,6 +78,7 @@ export interface IfIRNode extends BaseIRNode {
   once?: boolean
   parent?: number
   anchor?: number
+  logicalIndex?: number
   append?: boolean
   last?: boolean
 }
@@ -99,6 +100,7 @@ export interface ForIRNode extends BaseIRNode, IRFor {
   onlyChild: boolean
   parent?: number
   anchor?: number
+  logicalIndex?: number
   append?: boolean
   last?: boolean
 }
@@ -203,6 +205,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
   isCustomElement: boolean
   parent?: number
   anchor?: number
+  logicalIndex?: number
   append?: boolean
   last?: boolean
 }
@@ -217,6 +220,7 @@ export interface SlotOutletIRNode extends BaseIRNode {
   once?: boolean
   parent?: number
   anchor?: number
+  logicalIndex?: number
   append?: boolean
   last?: boolean
 }
@@ -264,6 +268,9 @@ export interface IRDynamicInfo {
   id?: number
   flags: DynamicFlag
   anchor?: number
+  // logical index of this node among siblings (including dynamic nodes)
+  // used during hydration to locate the correct DOM node
+  logicalIndex?: number
   children: IRDynamicInfo[]
   template?: number
   hasDynamicChild?: boolean

+ 12 - 0
packages/compiler-vapor/src/transforms/transformChildren.ts

@@ -65,12 +65,21 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
   let lastInsertionChild: IRDynamicInfo | undefined
   const children = context.dynamic.children
 
+  // Track logical index for each child.
+  // logicalIndex represents the position in SSR DOM, used during hydration
+  // to locate the correct DOM node. Each child (static element, component,
+  // v-if/v-else-if/v-else chain, v-for, slot) counts as one logical unit.
+  let logicalIndex = 0
+
   for (const [index, child] of children.entries()) {
     if (child.flags & DynamicFlag.INSERT) {
+      child.logicalIndex = logicalIndex
       prevDynamics.push((lastInsertionChild = child))
+      logicalIndex++
     }
 
     if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
+      child.logicalIndex = logicalIndex
       if (prevDynamics.length) {
         if (staticCount) {
           context.childrenTemplate[index - prevDynamics.length] = `<!>`
@@ -84,6 +93,7 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
         prevDynamics = []
       }
       staticCount++
+      logicalIndex++
     }
   }
 
@@ -109,6 +119,7 @@ function registerInsertion(
   append?: boolean,
 ) {
   for (const child of dynamics) {
+    const logicalIndex = child.logicalIndex
     if (child.template != null) {
       // template node due to invalid nesting - generate actual insertion
       context.registerOperation({
@@ -121,6 +132,7 @@ function registerInsertion(
       // block types
       child.operation.parent = context.reference()
       child.operation.anchor = anchor
+      child.operation.logicalIndex = logicalIndex
       child.operation.append = append
     }
   }

+ 1 - 1
packages/runtime-vapor/__tests__/componentSlots.spec.ts

@@ -970,7 +970,7 @@ describe('component: slots', () => {
                   () => props.show,
                   () => {
                     const n5 = template('<div></div>')() as any
-                    setInsertionState(n5, null, true)
+                    setInsertionState(n5, null, 0, true)
                     createSlot('header', null, () => {
                       const n4 = template('default header')()
                       return n4

+ 1 - 1
packages/runtime-vapor/__tests__/components/Teleport.spec.ts

@@ -1307,7 +1307,7 @@ function runSharedTests(deferMode: boolean): void {
       setup() {
         const n0 = template('<div id="tt"></div>')()
         const n4 = template('<div></div>')() as any
-        setInsertionState(n4, null, true)
+        setInsertionState(n4, null, 0, true)
         createComponent(
           VaporTeleport,
           { to: () => '#tt' },

+ 3 - 3
packages/runtime-vapor/__tests__/customElement.spec.ts

@@ -125,7 +125,7 @@ describe('defineVaporCustomElement', () => {
       const containerComp = defineVaporComponent({
         setup() {
           const n1 = template('<div><div id="move"></div></div>', true)() as any
-          setInsertionState(n1, 0, true)
+          setInsertionState(n1, 0, 0, true)
           createPlainElement('my-el-input', {
             value: () => num.value,
             onInput: () => ($event: CustomEvent) => {
@@ -761,13 +761,13 @@ describe('defineVaporCustomElement', () => {
         const t0 = template('<div>fallback</div>')
         const t1 = template('<div></div>')
         const n3 = t1() as any
-        setInsertionState(n3, null, true)
+        setInsertionState(n3, null, 0, true)
         createSlot('default', null, () => {
           const n2 = t0()
           return n2
         })
         const n5 = t1() as any
-        setInsertionState(n5, null, true)
+        setInsertionState(n5, null, 0, true)
         createSlot('named', null)
         return [n3, n5]
       },

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

@@ -1207,6 +1207,40 @@ describe('Vapor Mode hydration', () => {
       )
     })
 
+    test('v-if/else with sibling components and elements', async () => {
+      const data = ref('a')
+      const { container } = await testHydration(
+        `<script setup>
+          const msg = _data
+          const { Comp } = _components
+        </script>
+        <template>
+          <div>
+            <Comp/>
+            <div>11</div>
+            <div v-if="msg === 'a'">foo</div>
+            <div v-else>baz</div>
+            <div>11</div>
+            <Comp/>
+          </div>
+        </template>`,
+        {
+          Comp: `<template><span>comp</span></template>`,
+        },
+        data,
+      )
+
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span>comp</span><div>11</div><div>foo</div><!--if--><div>11</div><span>comp</span></div>"`,
+      )
+
+      data.value = 'b'
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `"<div><span>comp</span><div>11</div><div>baz</div><!--if--><div>11</div><span>comp</span></div>"`,
+      )
+    })
+
     test('nested if', async () => {
       const data = reactive({ outer: true, inner: true })
       const { container } = await testHydration(
@@ -1590,6 +1624,135 @@ describe('Vapor Mode hydration', () => {
         `"<div><span></span>foo<!--dynamic-component--><!--if--><span></span></div>"`,
       )
     })
+
+    test('v-if with insertion parent + sibling component', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <span v-if="data">hello</span>
+          </div>
+          <components.Child/>
+        </template>`,
+        {
+          Child: `<template><div>child</div></template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div><span>hello</span><!--if--></div><div>child</div><!--]-->
+        "
+      `,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div><!--if--></div><div>child</div><!--]-->
+        "
+      `,
+      )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div><span>hello</span><!--if--></div><div>child</div><!--]-->
+        "
+      `,
+      )
+    })
+
+    test('v-if with static sibling + root sibling component', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <span v-if="data">hello</span>
+            <div>1</div>
+          </div>
+          <components.Child/>
+        </template>`,
+        {
+          Child: `<template><div>child</div></template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div><span>hello</span><!--if--><div>1</div></div><div>child</div><!--]-->
+        "
+      `,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div><!--if--><div>1</div></div><div>child</div><!--]-->
+        "
+      `,
+      )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div><span>hello</span><!--if--><div>1</div></div><div>child</div><!--]-->
+        "
+      `,
+      )
+    })
+
+    test('v-if + static sibling + root sibling component (flat)', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <span v-if="data">hello</span>
+          <span></span>
+          <components.Child/>
+        </template>`,
+        {
+          Child: `<template><div>child</div></template>`,
+        },
+        data,
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>hello</span><!--if--><span></span><div>child</div><!--]-->
+        "
+      `,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><!--if--><span></span><div>child</div><!--]-->
+        "
+      `,
+      )
+
+      data.value = true
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><span>hello</span><!--if--><span></span><div>child</div><!--]-->
+        "
+      `,
+      )
+    })
   })
 
   describe('for', () => {
@@ -1683,6 +1846,43 @@ describe('Vapor Mode hydration', () => {
       )
     })
 
+    test('v-for with static sibling + root sibling component', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+          <div>
+            <span v-for="item in data" :key="item">{{ item }}</span>
+            <div>1</div>
+          </div>
+          <components.Child/>
+        </template>`,
+        {
+          Child: `<template><div>{{data.length}}</div></template>`,
+        },
+        ref(['a', 'b', 'c']),
+      )
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div>
+        <!--[--><span>a</span><span>b</span><span>c</span><!--]-->
+        <div>1</div></div><div>3</div><!--]-->
+        "
+      `,
+      )
+
+      data.value.push('d')
+      await nextTick()
+      expect(formatHtml(container.innerHTML)).toMatchInlineSnapshot(
+        `
+        "
+        <!--[--><div>
+        <!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]-->
+        <div>1</div></div><div>4</div><!--]-->
+        "
+      `,
+      )
+    })
+
     test('v-for with insertion anchor', async () => {
       const { container, data } = await testHydration(
         `<template>

+ 1 - 1
packages/runtime-vapor/__tests__/scopeId.spec.ts

@@ -315,7 +315,7 @@ describe('scopeId', () => {
     const Child = defineVaporComponent({
       setup() {
         const n0 = template('<div>')() as any
-        setInsertionState(n0, null, true)
+        setInsertionState(n0, null, 0, true)
         createSlot('default')
         return n0
       },

+ 9 - 7
packages/runtime-vapor/src/apiCreateFor.ts

@@ -12,11 +12,7 @@ import {
   watch,
 } from '@vue/reactivity'
 import { isArray, isObject, isString } from '@vue/shared'
-import {
-  createComment,
-  createTextNode,
-  updateLastLogicalChild,
-} from './dom/node'
+import { createComment, createTextNode } from './dom/node'
 import {
   type Block,
   applyTransitionHooks,
@@ -39,7 +35,9 @@ import {
 } from './dom/hydration'
 import { ForFragment, VaporFragment } from './fragment'
 import {
+  type ChildItem,
   insertionAnchor,
+  insertionIndex,
   insertionParent,
   isLastInsertion,
   resetInsertionState,
@@ -97,6 +95,7 @@ export const createFor = (
 ): ForFragment => {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
+  const _insertionIndex = insertionIndex
   const _isLastInsertion = isLastInsertion
   if (isHydrating) {
     locateHydrationNode()
@@ -160,8 +159,11 @@ export const createFor = (
           )
         }
 
-        if (_insertionParent) {
-          updateLastLogicalChild(_insertionParent!, parentAnchor)
+        // optimization: cache the fragment end anchor as $llc (last logical child)
+        // so that locateChildByLogicalIndex can skip the entire fragment
+        if (_insertionParent && isComment(parentAnchor, ']')) {
+          ;(parentAnchor as any as ChildItem).$idx = _insertionIndex || 0
+          _insertionParent.$llc = parentAnchor
         }
       }
     } else {

+ 7 - 35
packages/runtime-vapor/src/dom/hydration.ts

@@ -1,7 +1,6 @@
 import { MismatchTypes, isMismatchAllowed, warn } from '@vue/runtime-dom'
 import {
-  type ChildItem,
-  insertionAnchor,
+  insertionIndex,
   insertionParent,
   resetInsertionState,
   setInsertionState,
@@ -53,10 +52,6 @@ function performHydration<T>(
     ;(Node.prototype as any).$pns = undefined
     ;(Node.prototype as any).$idx = undefined
     ;(Node.prototype as any).$llc = undefined
-    ;(Node.prototype as any).$lpn = undefined
-    ;(Node.prototype as any).$lan = undefined
-    ;(Node.prototype as any).$lin = undefined
-    ;(Node.prototype as any).$curIdx = undefined
 
     isOptimized = true
   }
@@ -162,38 +157,15 @@ export function locateNextNode(node: Node): Node | null {
 
 function locateHydrationNodeImpl(): void {
   let node: Node | null
-  if (insertionAnchor !== undefined) {
-    const { $lpn: lastPrepend, $lan: lastAppend, firstChild } = insertionParent!
-    // prepend
-    if (insertionAnchor === 0) {
-      node = insertionParent!.$lpn = lastPrepend
-        ? locateNextNode(lastPrepend)
-        : firstChild
-    }
-    // insert
-    else if (insertionAnchor instanceof Node) {
-      const { $lin: lastInsertedNode } = insertionAnchor as ChildItem
-      node = (insertionAnchor as ChildItem).$lin = lastInsertedNode
-        ? locateNextNode(lastInsertedNode)
-        : insertionAnchor
-    }
-    // append
-    else {
-      node = insertionParent!.$lan = lastAppend
-        ? locateNextNode(lastAppend)
-        : insertionAnchor === null
-          ? firstChild
-          : locateChildByLogicalIndex(insertionParent!, insertionAnchor)!
-    }
 
-    insertionParent!.$llc = node
-    ;(node as ChildItem).$idx = insertionParent!.$curIdx =
-      insertionParent!.$curIdx === undefined ? 0 : insertionParent!.$curIdx + 1
+  if (insertionIndex !== undefined) {
+    // use logicalIndex to locate the node
+    node = locateChildByLogicalIndex(insertionParent!, insertionIndex)
+  } else if (insertionParent) {
+    // no logicalIndex: withHydration entry initialization
+    node = insertionParent.firstChild
   } else {
     node = currentHydrationNode
-    if (insertionParent && (!node || node.parentNode !== insertionParent)) {
-      node = insertionParent.firstChild
-    }
   }
 
   if (__DEV__ && !node) {

+ 8 - 11
packages/runtime-vapor/src/dom/node.ts

@@ -144,6 +144,14 @@ export function locateChildByLogicalIndex(
   let child = (parent.$llc || parent.firstChild) as ChildItem
   let fromIndex = child.$idx || 0
 
+  // if target index is less than cached index, start from the beginning.
+  // this can happen when child/nthChild/next updates $llc to a later node
+  // before an earlier dynamic node is hydrated
+  if (logicalIndex < fromIndex) {
+    child = parent.firstChild as ChildItem
+    fromIndex = 0
+  }
+
   while (child) {
     if (fromIndex === logicalIndex) {
       child.$idx = logicalIndex
@@ -162,14 +170,3 @@ export function locateChildByLogicalIndex(
 
   return null
 }
-
-// use fragment end anchor as the logical child to avoid locateEndAnchor calls
-// in locateChildByLogicalIndex
-export function updateLastLogicalChild(
-  parent: InsertionParent,
-  child: Node,
-): void {
-  if (!isComment(child, ']')) return
-  ;(child as any as ChildItem).$idx = parent.$curIdx || 0
-  parent.$llc = child
-}

+ 13 - 15
packages/runtime-vapor/src/insertionState.ts

@@ -2,8 +2,6 @@ import { isHydrating } from './dom/hydration'
 export type ChildItem = ChildNode & {
   // logical index, used during hydration to locate the node
   $idx: number
-  // last inserted node
-  $lin?: Node | null
 }
 
 export type InsertionParent = ParentNode & {
@@ -12,15 +10,11 @@ export type InsertionParent = ParentNode & {
 
   // last located logical child
   $llc?: Node | null
-  // last prepend node
-  $lpn?: Node | null
-  // last append node
-  $lan?: Node | null
-  // the logical index of current hydration node
-  $curIdx?: number
 }
 export let insertionParent: InsertionParent | undefined
 export let insertionAnchor: Node | 0 | undefined | null
+// logical index for hydration
+export let insertionIndex: number | undefined
 
 // indicates whether the insertion is the last one in the parent.
 // if true, means no more nodes need to be hydrated after this insertion,
@@ -34,20 +28,20 @@ export let isLastInsertion: boolean | undefined
  */
 export function setInsertionState(
   parent: ParentNode & { $fc?: Node | null },
-  anchor?: Node | 0 | null | number,
+  anchor?: Node | 0 | null,
+  logicalIndex?: number,
   last?: boolean,
 ): void {
   insertionParent = parent
   isLastInsertion = last
+  insertionIndex = logicalIndex
 
   if (anchor !== undefined) {
     if (isHydrating) {
-      insertionAnchor = anchor as Node
+      // hydration uses logicalIndex, not anchor
+      insertionAnchor = undefined
     } else {
-      // special handling append anchor value to null
-      insertionAnchor =
-        typeof anchor === 'number' && anchor > 0 ? null : (anchor as Node)
-
+      insertionAnchor = anchor
       if (anchor === 0 && !parent.$fc) {
         parent.$fc = parent.firstChild
       }
@@ -58,5 +52,9 @@ export function setInsertionState(
 }
 
 export function resetInsertionState(): void {
-  insertionParent = insertionAnchor = isLastInsertion = undefined
+  insertionParent =
+    insertionAnchor =
+    insertionIndex =
+    isLastInsertion =
+      undefined
 }