]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: hydration edison/testVapor 13383/head
authordaiwei <daiwei521@126.com>
Wed, 25 Jun 2025 14:55:37 +0000 (22:55 +0800)
committerdaiwei <daiwei521@126.com>
Mon, 30 Jun 2025 09:55:38 +0000 (17:55 +0800)
17 files changed:
packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts
packages/compiler-ssr/__tests__/ssrVIf.spec.ts
packages/compiler-ssr/src/transforms/ssrVIf.ts
packages/compiler-vapor/src/generators/template.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/transformChildren.ts
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/hydration.ts
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/dom/hydration.ts
packages/runtime-vapor/src/dom/template.ts
packages/runtime-vapor/src/fragment.ts
packages/runtime-vapor/src/insertionState.ts
packages/runtime-vapor/src/vdomInterop.ts
packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts

index 0666e8949cc935eda4d60f284e4934b3a9e592da..e8e7dfb253d764f751bd0947165cf436a43024ed 100644 (file)
@@ -77,6 +77,7 @@ describe('ssr: inject <style vars>', () => {
           }></div><div\${
             _ssrRenderAttrs(_cssVars)
           }></div><!--]-->\`)
+          _push(\`<!--if-->\`)
         }
       }"
     `)
index 840d485088ba0fe196db35937a12f260cb7137a5..485d52352a373fdabf9929bbd04b7dab8931f2c9 100644 (file)
@@ -43,6 +43,7 @@ describe('ssr: v-if', () => {
             _push(\`<!--if-->\`)
           } else {
             _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
+            _push(\`<!--if-->\`)
           }
         }"
       `)
@@ -81,6 +82,7 @@ describe('ssr: v-if', () => {
             _push(\`<!--if-->\`)
           } else {
             _push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`)
+            _push(\`<!--if-->\`)
           }
         }"
       `)
@@ -170,6 +172,7 @@ describe('ssr: v-if', () => {
           _push(\`<!--if-->\`)
         } else {
           _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
+          _push(\`<!--if-->\`)
         }
       }"
     `)
index 811252c0fab903cb8d432a338845e05c2b94f8cf..4e7f5ca1b1c10a11f4b18b89fed0d1464e442cf7 100644 (file)
@@ -80,11 +80,11 @@ function processIfBranch(
     context,
     needFragmentWrapper,
   )
-  if (branch.condition) {
-    // v-if/v-else-if anchor for vapor hydration
-    statement.body.push(
-      createCallExpression(`_push`, [`\`<!--${IF_ANCHOR_LABEL}-->\``]),
-    )
-  }
+
+  // v-if/v-else-if/v-else anchor for vapor hydration
+  statement.body.push(
+    createCallExpression(`_push`, [`\`<!--${IF_ANCHOR_LABEL}-->\``]),
+  )
+
   return statement
 }
index 5a066b09e9a622435465988ab2af5cb4736222e6..9e2b810610b2ea35f24c2565fa53f777800998f1 100644 (file)
@@ -24,10 +24,10 @@ export function genSelf(
   context: CodegenContext,
 ): CodeFragment[] {
   const [frag, push] = buildCodeFragment()
-  const { id, template, operation } = dynamic
+  const { id, template, operation, dynamicChildOffset } = dynamic
 
   if (id !== undefined && template !== undefined) {
-    push(NEWLINE, `const n${id} = t${template}()`)
+    push(NEWLINE, `const n${id} = t${template}(${dynamicChildOffset || ''})`)
     push(...genDirectivesForElement(id, context))
   }
 
index 5af12457b2f8d60522ee8c319f624ec67c856d03..0e499c040e900138fe3d24716fecc9ab2dadf101 100644 (file)
@@ -266,6 +266,7 @@ export interface IRDynamicInfo {
   children: IRDynamicInfo[]
   template?: number
   hasDynamicChild?: boolean
+  dynamicChildOffset?: number
   operation?: OperationNode
   needsKey?: boolean
 }
index da47438c2a88c3d737854ea0eb74cb6c6e1e77c2..1baf7a9428239ca22c2225e0b8941e5135c7625f 100644 (file)
@@ -59,7 +59,7 @@ export const transformChildren: NodeTransform = (node, context) => {
 
 function processDynamicChildren(context: TransformContext<ElementNode>) {
   let prevDynamics: IRDynamicInfo[] = []
-  let hasStaticTemplate = false
+  let staticCount = 0
   const children = context.dynamic.children
 
   for (const [index, child] of children.entries()) {
@@ -69,7 +69,7 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
 
     if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
       if (prevDynamics.length) {
-        if (hasStaticTemplate) {
+        if (staticCount) {
           // 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++) {
@@ -92,12 +92,13 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
         }
         prevDynamics = []
       }
-      hasStaticTemplate = true
+      staticCount++
     }
   }
 
   if (prevDynamics.length) {
-    registerInsertion(prevDynamics, context)
+    registerInsertion(prevDynamics, context, undefined)
+    context.dynamic.dynamicChildOffset = staticCount
   }
 }
 
index 4b6fbaded5a019e47b9a242eca08af887bb5d142..2e70778fe9668cad7fe573cb80ecd13df15aafca 100644 (file)
@@ -191,6 +191,7 @@ export interface VaporInteropInterface {
     transition: TransitionHooks,
   ): void
   hydrate(node: Node, fn: () => void): void
+  hydrateSlot(vnode: VNode, container: any): void
 
   vdomMount: (
     component: ConcreteComponent,
index 8ed7b6af18953170d7a75cd9ebbbb8321984cc02..62bcb7686744738d359ea459a6130cb5d96dd78a 100644 (file)
@@ -5,6 +5,7 @@ import {
   Comment as VComment,
   type VNode,
   type VNodeHook,
+  VaporSlot,
   createTextVNode,
   createVNode,
   invokeVNodeHook,
@@ -276,6 +277,12 @@ export function createHydrationFunctions(
           )
         }
         break
+      case VaporSlot:
+        getVaporInterface(parentComponent, vnode).hydrateSlot(
+          vnode,
+          parentNode(node)!,
+        )
+        break
       default:
         if (shapeFlag & ShapeFlags.ELEMENT) {
           if (
index fbc27f1d4192f156d0eea023a38f57f2b6f8ec2f..0c0919ae405e00ae7c3261f33ec470f1e86498d1 100644 (file)
@@ -476,6 +476,34 @@ describe('Vapor Mode hydration', () => {
       )
     })
 
+    test('consecutive components with insertion parent', async () => {
+      const data = reactive({ foo: 'foo', bar: 'bar' })
+      const { container } = await testHydration(
+        `<template>
+        <div>
+          <components.Child1/>
+          <components.Child2/>
+        </div>
+      </template>
+      `,
+        {
+          Child1: `<template><span>{{ data.foo }}</span></template>`,
+          Child2: `<template><span>{{ data.bar }}</span></template>`,
+        },
+        data,
+      )
+      expect(container.innerHTML).toBe(
+        `<div><span>foo</span><span>bar</span></div>`,
+      )
+
+      data.foo = 'foo1'
+      data.bar = 'bar1'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div><span>foo1</span><span>bar1</span></div>`,
+      )
+    })
+
     test('nested consecutive components with anchor insertion', async () => {
       const { container, data } = await testHydration(
         `
@@ -1046,6 +1074,27 @@ describe('Vapor Mode hydration', () => {
           `</div>`,
       )
     })
+
+    test('dynamic component fallback', async () => {
+      const { container, data } = await testHydration(
+        `<template>
+            <component :is="'button'">
+              <span>{{ data }}</span>
+            </component>
+          </template>`,
+        {},
+        ref('foo'),
+      )
+
+      expect(container.innerHTML).toBe(
+        `<button><span>foo</span></button><!--${anchorLabel}-->`,
+      )
+      data.value = 'bar'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<button><span>bar</span></button><!--${anchorLabel}-->`,
+      )
+    })
   })
 
   describe('if', () => {
@@ -1314,6 +1363,38 @@ describe('Vapor Mode hydration', () => {
       )
     })
 
+    test('consecutive component with insertion parent', async () => {
+      const data = reactive({
+        show: true,
+        foo: 'foo',
+        bar: 'bar',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <div v-if="data.show">
+            <components.Child/>
+            <components.Child2/>
+          </div>
+        </template>`,
+        {
+          Child: `<template><span>{{data.foo}}</span></template>`,
+          Child2: `<template><span>{{data.bar}}</span></template>`,
+        },
+        data,
+      )
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<span>foo</span>` +
+          `<span>bar</span>` +
+          `</div>` +
+          `<!--${anchorLabel}-->`,
+      )
+
+      data.show = false
+      await nextTick()
+      expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
+    })
+
     test('consecutive v-if on component with anchor insertion', async () => {
       const data = ref(true)
       const { container } = await testHydration(
@@ -2354,6 +2435,31 @@ describe('Vapor Mode hydration', () => {
           `</div>`,
       )
     })
+
+    test('slot fallback', async () => {
+      const data = reactive({
+        foo: 'foo',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><slot><span>{{data.foo}}</span></slot></template>`,
+        },
+        data,
+      )
+      expect(container.innerHTML).toBe(
+        `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
+      )
+
+      data.foo = 'bar'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`,
+      )
+    })
   })
 
   describe.todo('transition', async () => {
@@ -3912,6 +4018,76 @@ describe('VDOM hydration interop', () => {
     expect(container.innerHTML).toMatchInlineSnapshot(`"false"`)
   })
 
+  test('nested components (VDOM -> Vapor -> VDOM (with slot fallback))', async () => {
+    const data = ref(true)
+    const { container } = await testHydrationInterop(
+      `<script setup>const data = _data; const components = _components;</script>
+      <template>
+        <components.VaporChild/>
+      </template>`,
+      {
+        VaporChild: {
+          code: `<template><components.VdomChild/></template>`,
+          vapor: true,
+        },
+        VdomChild: {
+          code: `<script setup>const data = _data;</script>
+            <template><slot><span>{{data}}</span></slot></template>`,
+          vapor: false,
+        },
+      },
+      data,
+    )
+
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<!--[--><span>true</span><!--]--><!--slot-->"`,
+    )
+
+    data.value = false
+    await nextTick()
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<!--[--><span>false</span><!--]--><!--slot-->"`,
+    )
+  })
+
+  test('nested components (VDOM -> Vapor(with slot fallback) -> VDOM)', async () => {
+    const data = ref(true)
+    const { container } = await testHydrationInterop(
+      `<script setup>const data = _data; const components = _components;</script>
+          <template>
+            <components.VaporChild/>
+          </template>`,
+      {
+        VaporChild: {
+          code: `<template>
+            <components.VdomChild>
+              <template #default>
+                <span>{{data}} vapor fallback</span>
+              </template>
+            </components.VdomChild>
+          </template>`,
+          vapor: true,
+        },
+        VdomChild: {
+          code: `<script setup>const data = _data;</script>
+            <template><slot><span>vdom fallback</span></slot></template>`,
+          vapor: false,
+        },
+      },
+      data,
+    )
+
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<!--[--><span>true vapor fallback</span><!--]--><!--slot-->"`,
+    )
+
+    data.value = false
+    await nextTick()
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<!--[--><span>false vapor fallback</span><!--]--><!--slot-->"`,
+    )
+  })
+
   test('vapor slot render vdom component', async () => {
     const data = ref(true)
     const { container } = await testHydrationInterop(
index b6c1429675223456c3249885f140ae2c5bb4f071..f609543afc8284503c91aae617d58014791f91fe 100644 (file)
@@ -30,6 +30,7 @@ import {
   isHydrating,
   locateHydrationNode,
   locateVaporFragmentAnchor,
+  updateNextChildToHydrate,
 } from './dom/hydration'
 import { VaporFragment } from './fragment'
 import {
@@ -86,7 +87,7 @@ export const createFor = (
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
   if (isHydrating) {
-    locateHydrationNode(true)
+    locateHydrationNode()
   } else {
     resetInsertionState()
   }
@@ -129,6 +130,10 @@ export const createFor = (
     if (!isMounted) {
       isMounted = true
       for (let i = 0; i < newLength; i++) {
+        // TODO add tests
+        if (isHydrating && i > 0 && _insertionParent) {
+          updateNextChildToHydrate(_insertionParent)
+        }
         mount(source, i)
       }
     } else {
index 4ce03f5f7f32da3661ee8ee38940281c3e8fba51..cfda7ced9d692a06ea3514d11cfd549f9b525d50 100644 (file)
@@ -66,7 +66,13 @@ import {
 } from './componentSlots'
 import { hmrReload, hmrRerender } from './hmr'
 import { createElement } from './dom/node'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
+import {
+  adoptTemplate,
+  currentHydrationNode,
+  isHydrating,
+  locateHydrationNode,
+  setCurrentHydrationNode,
+} from './dom/hydration'
 import { isVaporTeleport } from './components/Teleport'
 import {
   insertionAnchor,
@@ -533,7 +539,9 @@ export function createComponentWithFallback(
     resetInsertionState()
   }
 
-  const el = createElement(comp)
+  const el = isHydrating
+    ? (adoptTemplate(currentHydrationNode!, `<${comp}/>`) as HTMLElement)
+    : createElement(comp)
   // mark single root
   ;(el as any).$root = isSingleRoot
 
@@ -547,6 +555,7 @@ export function createComponentWithFallback(
   }
 
   if (rawSlots) {
+    isHydrating && setCurrentHydrationNode(el.firstChild)
     if (rawSlots.$) {
       // TODO dynamic slot fragment
     } else {
index e3f666b5b2652708168a97ecda58b8efeb37a5b7..0d4a118f19afe1e83f029aa74a0740b5db509439 100644 (file)
@@ -6,11 +6,12 @@ import {
   setInsertionState,
 } from '../insertionState'
 import {
+  __next,
+  _nthChild,
   disableHydrationNodeLookup,
   enableHydrationNodeLookup,
-  next,
 } from './node'
-import { isVaporAnchors, isVaporFragmentAnchor } from '@vue/shared'
+import { isVaporAnchors } from '@vue/shared'
 
 export let isHydrating = false
 export let currentHydrationNode: Node | null = null
@@ -29,9 +30,9 @@ function performHydration<T>(
   if (!isOptimized) {
     adoptTemplate = adoptTemplateImpl
     locateHydrationNode = locateHydrationNodeImpl
-
     // optimize anchor cache lookup
-    ;(Comment.prototype as any).$fs = undefined
+    ;(Comment.prototype as any).$fe = undefined
+    ;(Node.prototype as any).$dp = undefined
     isOptimized = true
   }
   enableHydrationNodeLookup()
@@ -58,12 +59,12 @@ export function hydrateNode(node: Node, fn: () => void): void {
 }
 
 export let adoptTemplate: (node: Node, template: string) => Node | null
-export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void
+export let locateHydrationNode: (isFragment?: boolean) => void
 
 type Anchor = Comment & {
-  // cached matching fragment start to avoid repeated traversal
+  // cached matching fragment end to avoid repeated traversal
   // on nested fragments
-  $fs?: Anchor
+  $fe?: Anchor
 }
 
 export const isComment = (node: Node, data: string): node is Anchor =>
@@ -95,13 +96,23 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
     }
   }
 
-  currentHydrationNode = next(node)
+  currentHydrationNode = __next(node)
   return node
 }
 
-const hydrationPositionMap = new WeakMap<ParentNode, Node>()
+const childToHydrateMap = new WeakMap<ParentNode, Node>()
+
+export function updateNextChildToHydrate(parent: ParentNode): void {
+  let nextNode = childToHydrateMap.get(parent)
+  if (nextNode) {
+    nextNode = __next(nextNode)
+    if (nextNode) {
+      childToHydrateMap.set(parent, (currentHydrationNode = nextNode))
+    }
+  }
+}
 
-function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
+function locateHydrationNodeImpl(isFragment?: boolean): void {
   let node: Node | null
   // prepend / firstChild
   if (insertionAnchor === 0) {
@@ -112,52 +123,25 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
     // SSR Output: `...<span/>Content<span/>...`// `insertionAnchor` is the actual node
     node = insertionAnchor
   } else {
-    node = insertionParent
-      ? hydrationPositionMap.get(insertionParent) || insertionParent.lastChild
-      : currentHydrationNode
-
-    // if node is a vapor fragment anchor, find the previous one
-    if (hasFragmentAnchor && node && isVaporFragmentAnchor(node)) {
-      node = node.previousSibling
-      if (__DEV__ && !node) {
-        // this should not happen
-        throw new Error(`vapor fragment anchor previous node was not found.`)
-      }
+    node = currentHydrationNode
+    if (insertionParent && (!node || node.parentNode !== insertionParent)) {
+      node =
+        childToHydrateMap.get(insertionParent) ||
+        _nthChild(insertionParent, insertionParent.$dp || 0)
     }
 
-    if (node && isComment(node, ']')) {
-      // fragment backward search
-      if (node.$fs) {
-        // already cached matching fragment start
-        node = node.$fs
-      } else {
-        let cur: Node | null = node
-        let curFragEnd = node
-        let fragDepth = 0
-        node = null
-        while (cur) {
-          cur = cur.previousSibling
-          if (cur) {
-            if (isComment(cur, '[')) {
-              curFragEnd.$fs = cur
-              if (!fragDepth) {
-                node = cur
-                break
-              } else {
-                fragDepth--
-              }
-            } else if (isComment(cur, ']')) {
-              curFragEnd = cur
-              fragDepth++
-            }
-          }
-        }
+    // locate slot fragment start anchor
+    if (isFragment && node && !isComment(node, '[')) {
+      node = locateVaporFragmentAnchor(node, '[')!
+    } else {
+      while (node && isNonHydrationNode(node)) {
+        node = node.nextSibling!
       }
     }
 
     if (insertionParent && node) {
-      const prev = node.previousSibling
-      if (prev) hydrationPositionMap.set(insertionParent, prev)
+      const nextNode = node.nextSibling
+      if (nextNode) childToHydrateMap.set(insertionParent, nextNode)
     }
   }
 
@@ -171,24 +155,28 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
 }
 
 export function locateEndAnchor(
-  node: Node | null,
+  node: Anchor,
   open = '[',
   close = ']',
 ): Node | null {
-  let match = 0
-  while (node) {
-    node = node.nextSibling
-    if (node && node.nodeType === 8) {
-      if ((node as Comment).data === open) match++
-      if ((node as Comment).data === close) {
-        if (match === 0) {
-          return node
-        } else {
-          match--
-        }
+  // already cached matching end
+  if (node.$fe) {
+    return node.$fe
+  }
+
+  const stack: Anchor[] = [node]
+  while ((node = node.nextSibling as Anchor) && stack.length > 0) {
+    if (node.nodeType === 8) {
+      if (node.data === open) {
+        stack.push(node)
+      } else if (node.data === close) {
+        const matchingOpen = stack.pop()!
+        matchingOpen.$fe = node
+        if (stack.length === 0) return node
       }
     }
   }
+
   return null
 }
 
index f6cdc3ff4873304f31110f0cbbeb6da54842d7b4..809314111099b75a36a3a095f4f92af91187bd5e 100644 (file)
@@ -6,13 +6,16 @@ let t: HTMLTemplateElement
 /*! #__NO_SIDE_EFFECTS__ */
 export function template(html: string, root?: boolean) {
   let node: Node
-  return (): Node & { $root?: true } => {
+  return (n?: number): Node & { $root?: true } => {
     if (isHydrating) {
       if (__DEV__ && !currentHydrationNode) {
         // TODO this should not happen
         throw new Error('No current hydration node')
       }
-      return adoptTemplate(currentHydrationNode!, html)!
+      node = adoptTemplate(currentHydrationNode!, html)!
+      // dynamic node position, default is 0
+      ;(node as any).$dp = n || 0
+      return node
     }
     // fast path for text nodes
     if (html[0] !== '<') {
index 1e4328ac7fae166f7f47a754696ee2ec819e80a0..03c212f728ea47c64a8c45597af01c5269684889 100644 (file)
@@ -16,6 +16,7 @@ import {
   isHydrating,
   locateHydrationNode,
   locateVaporFragmentAnchor,
+  setCurrentHydrationNode,
 } from './dom/hydration'
 import {
   applyTransitionHooks,
@@ -60,7 +61,7 @@ export class DynamicFragment extends VaporFragment {
   constructor(anchorLabel?: string) {
     super([])
     if (isHydrating) {
-      locateHydrationNode(true)
+      locateHydrationNode(anchorLabel === 'slot')
       this.hydrate(anchorLabel!)
     } else {
       this.anchor =
@@ -117,16 +118,23 @@ export class DynamicFragment extends VaporFragment {
       parent && insert(this.nodes, parent, this.anchor)
     }
 
+    if (isHydrating) {
+      setCurrentHydrationNode(this.anchor.nextSibling)
+    }
     resetTracking()
   }
 
   hydrate(label: string): void {
     // for `v-if="false"` the node will be an empty comment, use it as the anchor.
     // otherwise, find next sibling vapor fragment anchor
-    if (isComment(currentHydrationNode!, '')) {
+    if (label === 'if' && isComment(currentHydrationNode!, '')) {
       this.anchor = currentHydrationNode
     } else {
-      const anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
+      let anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)!
+      if (!anchor && (label === 'slot' || label === 'if')) {
+        // fallback to fragment end anchor for ssr vdom slot
+        anchor = locateVaporFragmentAnchor(currentHydrationNode!, ']')!
+      }
       if (anchor) {
         this.anchor = anchor
       } else if (__DEV__) {
index c8c7ffbcd1de3b1000cbc9fa62f50ee9c8c3a0b3..5c4c41fe23c2b2ea8edb6395fab4acf9441edb3a 100644 (file)
@@ -1,4 +1,16 @@
-export let insertionParent: ParentNode | undefined
+export let insertionParent:
+  | (ParentNode & {
+      // dynamic node position - hydration only
+      // indicates the position where dynamic nodes begin within the parent
+      // during hydration, static nodes before this index are skipped
+      //
+      // Example:
+      // const t0 = _template("<div><span></span><span></span></div>", true)
+      // const n4 = t0(2) // n4.$dp = 2
+      // The first 2 nodes are static, dynamic nodes start from index 2
+      $dp?: number
+    })
+  | undefined
 export let insertionAnchor: Node | 0 | undefined
 
 /**
index 5d6d9ebb229b799ae45550694d6e343ff3553b18..6b94261f1da16e8f3f9a28e39a74129fe65a421b 100644 (file)
@@ -48,13 +48,15 @@ import {
 import { type RawProps, rawPropsProxyHandlers } from './componentProps'
 import type { RawSlots, VaporSlot } from './componentSlots'
 import { renderEffect } from './renderEffect'
-import { createTextNode } from './dom/node'
+import { __next, createTextNode } from './dom/node'
 import { optimizePropertyLookup } from './dom/prop'
 import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition'
 import {
   currentHydrationNode,
   isHydrating,
   locateHydrationNode,
+  locateVaporFragmentAnchor,
+  setCurrentHydrationNode,
   hydrateNode as vaporHydrateNode,
 } from './dom/hydration'
 import { DynamicFragment, VaporFragment, isFragment } from './fragment'
@@ -171,6 +173,16 @@ const vaporInteropImpl: Omit<
   },
 
   hydrate: vaporHydrateNode,
+  hydrateSlot(vnode, container) {
+    const { slot } = vnode.vs!
+    const propsRef = (vnode.vs!.ref = shallowRef(vnode.props))
+    const slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
+    vaporHydrateNode(slotBlock, () => {
+      const anchor = locateVaporFragmentAnchor(currentHydrationNode!, 'slot')!
+      vnode.el = vnode.anchor = anchor
+      insert((vnode.vb = slotBlock), container, anchor)
+    })
+  },
 }
 
 const vaporSlotPropsProxyHandler: ProxyHandler<
@@ -261,17 +273,7 @@ function createVDOMComponent(
       if (transition) setVNodeTransitionHooks(vnode, transition)
 
       if (isHydrating) {
-        ;(
-          vdomHydrateNode ||
-          (vdomHydrateNode = ensureHydrationRenderer().hydrateNode!)
-        )(
-          currentHydrationNode!,
-          vnode,
-          parentInstance as any,
-          null,
-          null,
-          false,
-        )
+        hydrateVNode(vnode, parentInstance as any)
       } else {
         internals.mt(
           vnode,
@@ -344,36 +346,26 @@ function renderVDOMSlot(
           ensureVaporSlotFallback(children, fallback as any)
           isValidSlot = children.length > 0
         }
-
         if (isValidSlot) {
           if (isHydrating) {
-            locateHydrationNode(true)
-            ;(
-              vdomHydrateNode ||
-              (vdomHydrateNode = ensureHydrationRenderer().hydrateNode!)
-            )(
-              currentHydrationNode!,
+            hydrateVNode(vnode!, parentComponent as any)
+          } else {
+            if (fallbackNodes) {
+              remove(fallbackNodes, parentNode)
+              fallbackNodes = undefined
+            }
+            internals.p(
+              oldVNode,
               vnode!,
+              parentNode,
+              anchor,
               parentComponent as any,
               null,
+              undefined,
               null,
               false,
             )
-          } else if (fallbackNodes) {
-            remove(fallbackNodes, parentNode)
-            fallbackNodes = undefined
           }
-          internals.p(
-            oldVNode,
-            vnode!,
-            parentNode,
-            anchor,
-            parentComponent as any,
-            null,
-            undefined,
-            null,
-            false,
-          )
           oldVNode = vnode!
         } else {
           // for forwarded slot without its own fallback, use the fallback
@@ -390,6 +382,14 @@ function renderVDOMSlot(
               parentNode,
               anchor,
             )
+          } else if (isHydrating) {
+            // update hydration node to the next sibling of the slot anchor
+            locateHydrationNode()
+            const nextNode = locateVaporFragmentAnchor(
+              currentHydrationNode!,
+              'slot',
+            )
+            if (nextNode) setCurrentHydrationNode(__next(nextNode))
           }
           oldVNode = null
         }
@@ -397,13 +397,15 @@ function renderVDOMSlot(
       isMounted = true
     } else {
       // move
-      internals.m(
-        oldVNode!,
-        parentNode,
-        anchor,
-        MoveType.REORDER,
-        parentComponent as any,
-      )
+      if (oldVNode && !isHydrating) {
+        internals.m(
+          oldVNode,
+          parentNode,
+          anchor,
+          MoveType.REORDER,
+          parentComponent as any,
+        )
+      }
     }
 
     frag.remove = parentNode => {
@@ -451,7 +453,12 @@ const createFallback =
       const frag = new VaporFragment([])
       frag.insert = (parentNode, anchor) => {
         fallbackNodes.forEach(vnode => {
-          internals.p(null, vnode, parentNode, anchor, parentComponent)
+          // hydrate fallback
+          if (isHydrating) {
+            hydrateVNode(vnode, parentComponent as any)
+          } else {
+            internals.p(null, vnode, parentNode, anchor, parentComponent)
+          }
         })
       }
       frag.remove = parentNode => {
@@ -465,3 +472,22 @@ const createFallback =
     // vapor slot
     return fallbackNodes as Block
   }
+
+function hydrateVNode(
+  vnode: VNode,
+  parentComponent: ComponentInternalInstance | null,
+) {
+  // keep fragment start anchor, hydrateNode uses it to
+  // determine if node is a fragmentStart
+  locateHydrationNode()
+  if (!vdomHydrateNode) vdomHydrateNode = ensureHydrationRenderer().hydrateNode!
+  const nextNode = vdomHydrateNode(
+    currentHydrationNode!,
+    vnode,
+    parentComponent,
+    null,
+    null,
+    false,
+  )
+  setCurrentHydrationNode(nextNode)
+}
index cbf13db254ee31e6c30b498b8edcada84ddfb2df..70939bb4c8d4dbfa61ebb1ac0d29fc24fa164721 100644 (file)
@@ -28,7 +28,7 @@ describe('ssr: attr fallthrough', () => {
       `<div class="foo bar"></div><!--if-->`,
     )
     expect(await renderToString(createApp(Parent, { ok: false }))).toBe(
-      `<span class="bar"></span>`,
+      `<span class="bar"></span><!--if-->`,
     )
   })