]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(vapor): component hydration
authorEvan You <evan@vuejs.org>
Mon, 10 Mar 2025 08:18:02 +0000 (16:18 +0800)
committerEvan You <evan@vuejs.org>
Mon, 10 Mar 2025 08:18:02 +0000 (16:18 +0800)
14 files changed:
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts
packages/compiler-vapor/src/generators/component.ts
packages/compiler-vapor/src/generators/operation.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/transformChildren.ts
packages/runtime-vapor/__tests__/hydration.spec.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/index.ts
packages/runtime-vapor/src/insertionState.ts [new file with mode: 0644]

index 04e7d793b8be5e5d174e61c8c6c2addab9df5718..fff7dd1272b4e3d8da880552c87103979607356e 100644 (file)
@@ -26,7 +26,7 @@ export function render(_ctx) {
 `;
 
 exports[`compile > custom directive > component 1`] = `
-"import { resolveComponent as _resolveComponent, resolveDirective as _resolveDirective, createComponentWithFallback as _createComponentWithFallback, withVaporDirectives as _withVaporDirectives, insert as _insert, createIf as _createIf, template as _template } from 'vue';
+"import { resolveComponent as _resolveComponent, resolveDirective as _resolveDirective, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, withVaporDirectives as _withVaporDirectives, createIf as _createIf, template as _template } from 'vue';
 const t0 = _template("<div></div>")
 
 export function render(_ctx) {
@@ -38,9 +38,9 @@ export function render(_ctx) {
     "default": () => {
       const n0 = _createIf(() => (true), () => {
         const n3 = t0()
+        _setInsertionState(n3)
         const n2 = _createComponentWithFallback(_component_Bar)
         _withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]])
-        _insert(n2, n3)
         return n3
       })
       return n0
@@ -149,7 +149,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
 `;
 
 exports[`compile > directives > v-pre > should not affect siblings after it 1`] = `
-"import { resolveComponent as _resolveComponent, child as _child, createComponentWithFallback as _createComponentWithFallback, prepend as _prepend, toDisplayString as _toDisplayString, setText as _setText, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { resolveComponent as _resolveComponent, child as _child, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, toDisplayString as _toDisplayString, setText as _setText, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>")
 const t1 = _template("<div> </div>")
 
@@ -158,8 +158,8 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
   const n0 = t0()
   const n3 = t1()
   const n2 = _child(n3)
+  _setInsertionState(n3, 0)
   const n1 = _createComponentWithFallback(_component_Comp)
-  _prepend(n3, n1)
   _renderEffect(() => {
     _setText(n2, _toDisplayString(_ctx.bar))
     _setProp(n3, "id", _ctx.foo)
index 02235ddd99baeba7852f2c6b6dde78704055a212..cb14f56afdbfd800e900c7388716ef2691b171be 100644 (file)
@@ -65,20 +65,20 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-for > nested v-for 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, insert as _insert, template as _template } from 'vue';
+"import { setInsertionState as _setInsertionState, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<span> </span>")
 const t1 = _template("<div></div>", true)
 
 export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
     const n5 = t1()
+    _setInsertionState(n5)
     const n2 = _createFor(() => (_for_item0.value), (_for_item1) => {
       const n4 = t0()
       const x4 = _child(n4)
       _renderEffect(() => _setText(x4, _toDisplayString(_for_item1.value+_for_item0.value)))
       return n4
     }, null, 1)
-    _insert(n2, n5)
     return n5
   })
   return n0
index d7ec3ceed1edbad4a477d2fa859e439b01195664..ab3ade45b602fb013a66fa37922b53f2b2ee92bc 100644 (file)
@@ -36,14 +36,14 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-once > on component 1`] = `
-"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, insert as _insert, template as _template } from 'vue';
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
 const t0 = _template("<div></div>", true)
 
 export function render(_ctx) {
   const _component_Comp = _resolveComponent("Comp")
   const n1 = t0()
+  _setInsertionState(n1)
   const n0 = _createComponentWithFallback(_component_Comp, { id: () => (_ctx.foo) }, null, null, true)
-  _insert(n0, n1)
   return n1
 }"
 `;
index a3fb18aa1cb1e9224db2a0b8be01a72aed9bd581..43077bf2eb98652f5f90f812d9ae00fc4695a623 100644 (file)
@@ -132,10 +132,6 @@ describe('compiler: v-once', () => {
         id: 0,
         tag: 'Comp',
         once: true,
-      },
-      {
-        type: IRNodeTypes.INSERT_NODE,
-        elements: [0],
         parent: 1,
       },
     ])
index 73e23150fa1d432fb317cb66225052c2ffec7afd..7c232db754be5c6574fd04b8098eeff1e2cb368f 100644 (file)
@@ -51,6 +51,7 @@ export function genCreateComponent(
   const rawSlots = genRawSlots(slots, context)
   const [ids, handlers] = processInlineHandlers(props, context)
   const rawProps = context.withId(() => genRawProps(props, context), ids)
+
   const inlineHandlers: CodeFragment[] = handlers.reduce<CodeFragment[]>(
     (acc, { name, value }) => {
       const handler = genEventHandler(context, value, undefined, false)
index 93eff05846d580bc47afe8324b5486b9ec8ed1c3..64b2a568ee1fa85b4578f9728892eb8c12ce83d6 100644 (file)
@@ -1,4 +1,10 @@
-import { type IREffect, IRNodeTypes, type OperationNode } from '../ir'
+import {
+  type IREffect,
+  IRNodeTypes,
+  type InsertionStateTypes,
+  type OperationNode,
+  isTypeThatNeedsInsertionState,
+} from '../ir'
 import type { CodegenContext } from '../generate'
 import { genInsertNode, genPrependNode } from './dom'
 import { genSetDynamicEvents, genSetEvent } from './event'
@@ -14,6 +20,7 @@ import {
   INDENT_START,
   NEWLINE,
   buildCodeFragment,
+  genCall,
 } from './utils'
 import { genCreateComponent } from './component'
 import { genSlotOutlet } from './slotOutlet'
@@ -26,6 +33,9 @@ export function genOperations(
 ): CodeFragment[] {
   const [frag, push] = buildCodeFragment()
   for (const operation of opers) {
+    if (isTypeThatNeedsInsertionState(operation) && operation.parent) {
+      push(...genInsertionstate(operation, context))
+    }
     push(...genOperation(operation, context))
   }
   return frag
@@ -134,3 +144,21 @@ export function genEffect(
 
   return frag
 }
+
+function genInsertionstate(
+  operation: InsertionStateTypes,
+  context: CodegenContext,
+): CodeFragment[] {
+  return [
+    NEWLINE,
+    ...genCall(
+      context.helper('setInsertionState'),
+      `n${operation.parent}`,
+      operation.anchor == null
+        ? undefined
+        : operation.anchor === -1 // -1 indicates prepend
+          ? `0` // runtime anchor value for prepend
+          : `n${operation.anchor}`,
+    ),
+  ]
+}
index 6616e35e91ce38f81ff36b18adf5c6037878c78f..d4beb1e3f0a19765a929115863bc14460f9a6602 100644 (file)
@@ -76,6 +76,8 @@ export interface IfIRNode extends BaseIRNode {
   positive: BlockIRNode
   negative?: BlockIRNode | IfIRNode
   once?: boolean
+  parent?: number
+  anchor?: number
 }
 
 export interface IRFor {
@@ -93,6 +95,8 @@ export interface ForIRNode extends BaseIRNode, IRFor {
   once: boolean
   component: boolean
   onlyChild: boolean
+  parent?: number
+  anchor?: number
 }
 
 export interface SetPropIRNode extends BaseIRNode {
@@ -158,6 +162,7 @@ export interface SetTemplateRefIRNode extends BaseIRNode {
   effect: boolean
 }
 
+// TODO remove, no longer needed
 export interface CreateTextNodeIRNode extends BaseIRNode {
   type: IRNodeTypes.CREATE_TEXT_NODE
   id: number
@@ -198,6 +203,8 @@ export interface CreateComponentIRNode extends BaseIRNode {
   root: boolean
   once: boolean
   dynamic?: SimpleExpressionNode
+  parent?: number
+  anchor?: number
 }
 
 export interface DeclareOldRefIRNode extends BaseIRNode {
@@ -211,6 +218,8 @@ export interface SlotOutletIRNode extends BaseIRNode {
   name: SimpleExpressionNode
   props: IRProps[]
   fallback?: BlockIRNode
+  parent?: number
+  anchor?: number
 }
 
 export interface GetTextChildIRNode extends BaseIRNode {
@@ -288,3 +297,21 @@ export type VaporDirectiveNode = Overwrite<
     arg: Exclude<DirectiveNode['arg'], CompoundExpressionNode>
   }
 >
+
+export type InsertionStateTypes =
+  | IfIRNode
+  | ForIRNode
+  | SlotOutletIRNode
+  | CreateComponentIRNode
+
+export function isTypeThatNeedsInsertionState(
+  op: OperationNode,
+): op is InsertionStateTypes {
+  const type = op.type
+  return (
+    type === IRNodeTypes.CREATE_COMPONENT_NODE ||
+    type === IRNodeTypes.SLOT_OUTLET_NODE ||
+    type === IRNodeTypes.IF ||
+    type === IRNodeTypes.FOR
+  )
+}
index 9b76d86f3283bcbb79c7d706192745542bc3f5a9..8952036c04bf50cde64fcfef2039092460b61326 100644 (file)
@@ -4,7 +4,12 @@ import {
   type TransformContext,
   transformNode,
 } from '../transform'
-import { DynamicFlag, type IRDynamicInfo, IRNodeTypes } from '../ir'
+import {
+  DynamicFlag,
+  type IRDynamicInfo,
+  IRNodeTypes,
+  isTypeThatNeedsInsertionState as isBlockOperation,
+} from '../ir'
 
 export const transformChildren: NodeTransform = (node, context) => {
   const isFragment =
@@ -66,21 +71,11 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
       if (prevDynamics.length) {
         if (hasStaticTemplate) {
           context.childrenTemplate[index - prevDynamics.length] = `<!>`
-
           prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
           const anchor = (prevDynamics[0].anchor = context.increaseId())
-          context.registerOperation({
-            type: IRNodeTypes.INSERT_NODE,
-            elements: prevDynamics.map(child => child.id!),
-            parent: context.reference(),
-            anchor,
-          })
+          registerInsertion(prevDynamics, context, anchor)
         } else {
-          context.registerOperation({
-            type: IRNodeTypes.PREPEND_NODE,
-            elements: prevDynamics.map(child => child.id!),
-            parent: context.reference(),
-          })
+          registerInsertion(prevDynamics, context, -1 /* prepend */)
         }
         prevDynamics = []
       }
@@ -89,10 +84,32 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
   }
 
   if (prevDynamics.length) {
-    context.registerOperation({
-      type: IRNodeTypes.INSERT_NODE,
-      elements: prevDynamics.map(child => child.id!),
-      parent: context.reference(),
-    })
+    registerInsertion(prevDynamics, context)
+  }
+}
+
+function registerInsertion(
+  dynamics: IRDynamicInfo[],
+  context: TransformContext,
+  anchor?: number,
+) {
+  for (const child of dynamics) {
+    if (child.template != null) {
+      // template node due to invalid nesting - generate actual insertion
+      context.registerOperation({
+        type: IRNodeTypes.INSERT_NODE,
+        elements: dynamics.map(child => child.id!),
+        parent: context.reference(),
+        anchor,
+      })
+    } else {
+      // block types
+      for (const op of context.block.operation) {
+        if (isBlockOperation(op) && op.id === child.id) {
+          op.parent = context.reference()
+          op.anchor = anchor
+        }
+      }
+    }
   }
 }
index 8abf1570d52afce2cbf75a17150ef090b0bdbba8..3345debc34dcd86130a2a2bab8e5dbdfa3db94b4 100644 (file)
@@ -1,11 +1,13 @@
 // import { type SSRContext, renderToString } from '@vue/server-renderer'
 import {
   child,
+  createComponent,
   createVaporSSRApp,
   delegateEvents,
   next,
   renderEffect,
   setClass,
+  setInsertionState,
   setText,
   template,
 } from '../src'
@@ -144,6 +146,117 @@ describe('SSR hydration', () => {
     )
   })
 
+  test('basic component', async () => {
+    const t0 = template(' ')
+    const msg = ref('foo')
+    const Comp = {
+      setup() {
+        const n0 = t0() as Text
+        renderEffect(() => setText(n0, toDisplayString(msg.value)))
+        return n0
+      },
+    }
+
+    const t1 = template('<div><span></span></div>', true)
+    const { container } = mountWithHydration(
+      '<div><span></span>foo</div>',
+      () => {
+        const n1 = t1() as Element
+        setInsertionState(n1)
+        createComponent(Comp)
+        return n1
+      },
+    )
+
+    expect(container.innerHTML).toBe(`<div><span></span>foo</div>`)
+
+    msg.value = 'bar'
+    await nextTick()
+    expect(container.innerHTML).toBe(`<div><span></span>bar</div>`)
+  })
+
+  test('fragment component', async () => {
+    const t0 = template('<div> </div>')
+    const t1 = template(' ')
+    const msg = ref('foo')
+    const Comp = {
+      setup() {
+        const n0 = t0() as Element
+        const n1 = t1() as Text
+        const x0 = child(n0) as Text
+        renderEffect(() => {
+          const _msg = msg.value
+
+          setText(x0, toDisplayString(_msg))
+          setText(n1, toDisplayString(_msg))
+        })
+        return [n0, n1]
+      },
+    }
+
+    const t2 = template('<div><span></span></div>', true)
+    const { container } = mountWithHydration(
+      '<div><span></span><!--[--><div>foo</div>foo<!--]--></div>',
+      () => {
+        const n1 = t2() as Element
+        setInsertionState(n1)
+        createComponent(Comp)
+        return n1
+      },
+    )
+
+    expect(container.innerHTML).toBe(
+      `<div><span></span><!--[--><div>foo</div>foo<!--]--></div>`,
+    )
+
+    msg.value = 'bar'
+    await nextTick()
+    expect(container.innerHTML).toBe(
+      `<div><span></span><!--[--><div>bar</div>bar<!--]--></div>`,
+    )
+  })
+
+  test('fragment component with prepend', async () => {
+    const t0 = template('<div> </div>')
+    const t1 = template(' ')
+    const msg = ref('foo')
+    const Comp = {
+      setup() {
+        const n0 = t0() as Element
+        const n1 = t1() as Text
+        const x0 = child(n0) as Text
+        renderEffect(() => {
+          const _msg = msg.value
+
+          setText(x0, toDisplayString(_msg))
+          setText(n1, toDisplayString(_msg))
+        })
+        return [n0, n1]
+      },
+    }
+
+    const t2 = template('<div><span></span></div>', true)
+    const { container } = mountWithHydration(
+      '<div><!--[--><div>foo</div>foo<!--]--><span></span></div>',
+      () => {
+        const n1 = t2() as Element
+        setInsertionState(n1, 0)
+        createComponent(Comp)
+        return n1
+      },
+    )
+
+    expect(container.innerHTML).toBe(
+      `<div><!--[--><div>foo</div>foo<!--]--><span></span></div>`,
+    )
+
+    msg.value = 'bar'
+    await nextTick()
+    expect(container.innerHTML).toBe(
+      `<div><!--[--><div>bar</div>bar<!--]--><span></span></div>`,
+    )
+  })
+
   // test('element with ref', () => {
   //   const el = ref()
   //   const { vnode, container } = mountWithHydration('<div></div>', () =>
index 3716ac7ae4754c27f8eca0708459a8cb04ba1fec..17cbc0c3b9bdb1cd946355dacb34561d8d6701b5 100644 (file)
@@ -58,6 +58,12 @@ import {
   getSlot,
 } from './componentSlots'
 import { hmrReload, hmrRerender } from './hmr'
+import { isHydrating, locateHydrationNode } from './dom/hydration'
+import {
+  insertionAnchor,
+  insertionParent,
+  resetInsertionState,
+} from './insertionState'
 
 export { currentInstance } from '@vue/runtime-dom'
 
@@ -136,6 +142,10 @@ export function createComponent(
     currentInstance.appContext) ||
     emptyContext,
 ): VaporComponentInstance {
+  if (isHydrating) {
+    locateHydrationNode()
+  }
+
   // vdom interop enabled and component is not an explicit vapor component
   if (appContext.vapor && !component.__vapor) {
     return appContext.vapor.vdomMount(component as any, rawProps, rawSlots)
@@ -253,6 +263,11 @@ export function createComponent(
 
   onScopeDispose(() => unmountComponent(instance), true)
 
+  if (!isHydrating && insertionParent) {
+    insert(instance.block, insertionParent, insertionAnchor)
+    resetInsertionState()
+  }
+
   return instance
 }
 
index af6fe0ec18951a0380e88a5abd794bcf2f1e2ade..db16e61b2035212a0fc7d42b983e6db3d616f948 100644 (file)
@@ -1,3 +1,10 @@
+import { warn } from '@vue/runtime-dom'
+import {
+  insertionAnchor,
+  insertionParent,
+  resetInsertionState,
+  setInsertionState,
+} from '../insertionState'
 import { child, next } from './node'
 
 export let isHydrating = false
@@ -10,31 +17,28 @@ export function setCurrentHydrationNode(node: Node | null): void {
 let isOptimized = false
 
 export function withHydration(container: ParentNode, fn: () => void): void {
-  adoptHydrationNode = adoptHydrationNodeImpl
+  adoptTemplate = adoptTemplateImpl
+  locateHydrationNode = locateHydrationNodeImpl
   if (!isOptimized) {
     // optimize anchor cache lookup
-    const proto = Comment.prototype as any
-    proto.$p = proto.$e = undefined
+    ;(Comment.prototype as any).$fs = undefined
     isOptimized = true
   }
   isHydrating = true
-  currentHydrationNode = child(container)
+  setInsertionState(container, 0)
   const res = fn()
+  resetInsertionState()
   isHydrating = false
-  currentHydrationNode = null
   return res
 }
 
-export let adoptHydrationNode: (
-  node: Node | null,
-  template?: string,
-) => Node | null
+export let adoptTemplate: (node: Node, template: string) => Node | null
+export let locateHydrationNode: () => void
 
 type Anchor = Comment & {
-  // previous open anchor
-  $p?: Anchor
-  // matching end anchor
-  $e?: Anchor
+  // cached matching fragment start to avoid repeated traversal
+  // on nested fragments
+  $fs?: Anchor
 }
 
 const isComment = (node: Node, data: string): node is Anchor =>
@@ -44,84 +48,82 @@ const isComment = (node: Node, data: string): node is Anchor =>
  * Locate the first non-fragment-comment node and locate the next node
  * while handling potential fragments.
  */
-function adoptHydrationNodeImpl(
-  node: Node | null,
-  template?: string,
-): Node | null {
-  if (!isHydrating || !node) {
-    return node
+function adoptTemplateImpl(node: Node, template: string): Node | null {
+  if (!(template[0] === '<' && template[1] === '!')) {
+    while (node.nodeType === 8) node = next(node)
   }
 
-  let adopted: Node | undefined
-  let end: Node | undefined | null
-
-  if (template) {
-    if (template[0] !== '<' && template[1] !== '!') {
-      while (node.nodeType === 8) node = next(node)
-    }
-    adopted = end = node
-  } else if (isComment(node, '[')) {
-    // fragment
-    let start = node
-    let cur: Node = node
-    let fragmentDepth = 1
-    // previously recorded fragment end
-    if (!end && node.$e) {
-      end = node.$e
-    }
-    while (true) {
-      cur = next(cur)
-      if (isComment(cur, '[')) {
-        // previously recorded fragment end
-        if (!end && node.$e) {
-          end = node.$e
-        }
-        fragmentDepth++
-        cur.$p = start
-        start = cur
-      } else if (isComment(cur, ']')) {
-        fragmentDepth--
-        // record fragment end on start node for later traversal
-        start.$e = cur
-        start = start.$p!
-        if (!fragmentDepth) {
-          // fragment end
-          end = cur
-          break
-        }
-      } else if (!adopted) {
-        adopted = cur
-        if (end) {
-          break
-        }
-      }
-    }
-    if (!adopted) {
-      throw new Error('hydration mismatch')
-    }
-  } else {
-    adopted = end = node
-  }
-
-  if (__DEV__ && template) {
-    const type = adopted.nodeType
+  if (__DEV__) {
+    const type = node.nodeType
     if (
       (type === 8 && !template.startsWith('<!')) ||
       (type === 1 &&
-        !template.startsWith(
-          `<` + (adopted as Element).tagName.toLowerCase(),
-        )) ||
+        !template.startsWith(`<` + (node as Element).tagName.toLowerCase())) ||
       (type === 3 &&
         template.trim() &&
-        !template.startsWith((adopted as Text).data))
+        !template.startsWith((node as Text).data))
     ) {
       // TODO recover and provide more info
-      console.error(`adopted: `, adopted)
-      console.error(`template: ${template}`)
-      throw new Error('hydration mismatch!')
+      warn(`adopted: `, node)
+      warn(`template: ${template}`)
+      warn('hydration mismatch!')
     }
   }
 
-  currentHydrationNode = next(end!)
-  return adopted
+  currentHydrationNode = next(node)
+  return node
+}
+
+function locateHydrationNodeImpl() {
+  if (__DEV__ && !insertionParent) {
+    warn('Hydration error: missing insertion state.')
+  }
+
+  let node: Node | null
+
+  // prepend / firstChild
+  if (insertionAnchor === 0) {
+    node = child(insertionParent!)
+  } else {
+    node = insertionAnchor
+      ? insertionAnchor.previousSibling
+      : insertionParent!.lastChild
+
+    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++
+            }
+          }
+        }
+      }
+    }
+  }
+
+  currentHydrationNode = node
+
+  if (__DEV__ && !currentHydrationNode) {
+    // TODO more info
+    warn('Hydration mismatch in ', insertionParent)
+  }
 }
index dbe83364150ea1608978192feef356ab33ea8758..b78ca4e52cfb8fa988d1de60ad7879a00f8f3566 100644 (file)
@@ -1,8 +1,4 @@
-import {
-  adoptHydrationNode,
-  currentHydrationNode,
-  isHydrating,
-} from './hydration'
+import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration'
 import { child, createTextNode } from './node'
 
 let t: HTMLTemplateElement
@@ -16,7 +12,7 @@ export function template(html: string, root?: boolean) {
         // TODO this should not happen
         throw new Error('No current hydration node')
       }
-      return adoptHydrationNode(currentHydrationNode, html)!
+      return adoptTemplate(currentHydrationNode!, html)!
     }
     // fast path for text nodes
     if (html[0] !== '<') {
index 16aeab9766f75527ce888e5a1ce2d46a74be2411..682532fa4d80aa01a23a948bbb538b049715a9ff 100644 (file)
@@ -6,6 +6,7 @@ export type { VaporDirective } from './directives/custom'
 
 // compiler-use only
 export { insert, prepend, remove, isFragment, VaporFragment } from './block'
+export { setInsertionState } from './insertionState'
 export { createComponent, createComponentWithFallback } from './component'
 export { renderEffect } from './renderEffect'
 export { createSlot } from './componentSlots'
diff --git a/packages/runtime-vapor/src/insertionState.ts b/packages/runtime-vapor/src/insertionState.ts
new file mode 100644 (file)
index 0000000..4b06c04
--- /dev/null
@@ -0,0 +1,19 @@
+import { setCurrentHydrationNode } from './dom/hydration'
+
+export let insertionParent: ParentNode | undefined
+export let insertionAnchor: Node | 0 | undefined
+
+/**
+ * This function is called before a block type that requires insertion
+ * (component, slot outlet, if, for) is created. The state is used for actual
+ * insertion on client-side render, and used for node adoption during hydration.
+ */
+export function setInsertionState(parent: ParentNode, anchor?: Node | 0): void {
+  insertionParent = parent
+  insertionAnchor = anchor
+}
+
+export function resetInsertionState(): void {
+  insertionParent = insertionAnchor = undefined
+  setCurrentHydrationNode(null)
+}