Bläddra i källkod

Merge branch 'minor' into rolldown

edison 3 månader sedan
förälder
incheckning
cad433b061

+ 48 - 0
packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap

@@ -270,6 +270,54 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`cache multiple access > should cache method call with same arguments 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  _renderEffect(() => {
+    const _msg_replace_1_2 = _ctx.msg.replace('1', '2')
+    _setProp(n0, "id", _msg_replace_1_2)
+    _setProp(n1, "id", _msg_replace_1_2)
+  })
+  return [n0, n1]
+}"
+`;
+
+exports[`cache multiple access > should cache optional call expression with same arguments 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  _renderEffect(() => {
+    const _obj_foo_bar = _ctx.obj[_ctx.foo?.(_ctx.bar)]
+    _setProp(n0, "id", _obj_foo_bar)
+    _setProp(n1, "id", _obj_foo_bar)
+  })
+  return [n0, n1]
+}"
+`;
+
+exports[`cache multiple access > should not cache method call with different arguments 1`] = `
+"import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  const n1 = t0()
+  _renderEffect(() => {
+    const _msg = _ctx.msg
+    _setProp(n0, "id", _msg.replace('1', '2'))
+    _setProp(n1, "id", _msg.replace('1', '3'))
+  })
+  return [n0, n1]
+}"
+`;
+
 exports[`cache multiple access > variable name substring edge cases 1`] = `
 "import { setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div></div>", true)

+ 29 - 0
packages/compiler-vapor/__tests__/transforms/vBind.spec.ts

@@ -954,4 +954,33 @@ describe('cache multiple access', () => {
     expect(code).matchSnapshot()
     expect(code).not.contains('const _bar = _ctx.bar')
   })
+
+  test('should not cache method call with different arguments', () => {
+    const { code } = compileWithVBind(`
+      <div :id="msg.replace('1', '2')"></div>
+      <div :id="msg.replace('1', '3')"></div>
+    `)
+    expect(code).matchSnapshot()
+    expect(code).contains('const _msg = _ctx.msg')
+    expect(code).not.contains('_ctx.msg.replace')
+  })
+
+  test('should cache method call with same arguments', () => {
+    const { code } = compileWithVBind(`
+      <div :id="msg.replace('1', '2')"></div>
+      <div :id="msg.replace('1', '2')"></div>
+    `)
+    expect(code).matchSnapshot()
+    expect(code).contains(`const _msg_replace_1_2 = _ctx.msg.replace('1', '2')`)
+    expect(code).not.contains('const _msg = _ctx.msg')
+  })
+
+  test('should cache optional call expression with same arguments', () => {
+    const { code } = compileWithVBind(`
+      <div :id="obj[foo?.(bar)]"></div>
+      <div :id="obj[foo?.(bar)]"></div>
+    `)
+    expect(code).matchSnapshot()
+    expect(code).contains(`const _obj_foo_bar = _ctx.obj[_ctx.foo?.(_ctx.bar)]`)
+  })
 })

+ 14 - 0
packages/compiler-vapor/src/generators/expression.ts

@@ -337,6 +337,12 @@ function analyzeExpressions(expressions: SimpleExpressionNode[]) {
             end: id.end!,
           })
         })
+
+        const parentOfMemberExp = parentStack[parentStack.length - 2]
+        if (parentOfMemberExp && isCallExpression(parentOfMemberExp)) {
+          return
+        }
+
         registerVariable(
           memberExp,
           exp,
@@ -671,6 +677,8 @@ function extractMemberExpression(
       return `${extractMemberExpression(exp.left, onIdentifier)} ${exp.operator} ${extractMemberExpression(exp.right, onIdentifier)}`
     case 'CallExpression': // foo[bar(baz)]
       return `${extractMemberExpression(exp.callee, onIdentifier)}(${exp.arguments.map(arg => extractMemberExpression(arg, onIdentifier)).join(', ')})`
+    case 'OptionalCallExpression': // foo[bar?.(baz)]
+      return `${extractMemberExpression(exp.callee, onIdentifier)}?.(${exp.arguments.map(arg => extractMemberExpression(arg, onIdentifier)).join(', ')})`
     case 'MemberExpression': // foo[bar.baz]
     case 'OptionalMemberExpression': // foo?.bar
       const object = extractMemberExpression(exp.object, onIdentifier)
@@ -685,6 +693,12 @@ function extractMemberExpression(
   }
 }
 
+const isCallExpression = (node: Node) => {
+  return (
+    node.type === 'CallExpression' || node.type === 'OptionalCallExpression'
+  )
+}
+
 const isMemberExpression = (node: Node) => {
   return (
     node.type === 'MemberExpression' ||

+ 1 - 1
packages/runtime-core/src/component.ts

@@ -722,7 +722,7 @@ export interface ComponentInternalInstance extends GenericComponentInstance {
   resolvedOptions?: MergedComponentOptions
 }
 
-const emptyAppContext = createAppContext()
+const emptyAppContext = /*@__PURE__*/ createAppContext()
 
 let uid = 0
 

+ 12 - 4
packages/runtime-core/src/hydrationStrategies.ts

@@ -3,10 +3,17 @@ import { DOMNodeTypes, isComment } from './hydration'
 
 // Polyfills for Safari support
 // see https://caniuse.com/requestidlecallback
-const requestIdleCallback: Window['requestIdleCallback'] =
-  getGlobalThis().requestIdleCallback || (cb => setTimeout(cb, 1))
-const cancelIdleCallback: Window['cancelIdleCallback'] =
-  getGlobalThis().cancelIdleCallback || (id => clearTimeout(id))
+// Use lazy initialization to allow tree-shaking when hydrateOnIdle is not used
+let requestIdleCallback: Window['requestIdleCallback']
+let cancelIdleCallback: Window['cancelIdleCallback']
+
+function ensureIdleCallbacks() {
+  if (!requestIdleCallback) {
+    const g = getGlobalThis()
+    requestIdleCallback = g.requestIdleCallback || (cb => setTimeout(cb, 1))
+    cancelIdleCallback = g.cancelIdleCallback || (id => clearTimeout(id))
+  }
+}
 
 /**
  * A lazy hydration strategy for async components.
@@ -29,6 +36,7 @@ export type HydrationStrategyFactory<Options> = (
 export const hydrateOnIdle: HydrationStrategyFactory<number> =
   (timeout = 10000) =>
   hydrate => {
+    ensureIdleCallbacks()
     const id = requestIdleCallback(hydrate, { timeout })
     return () => cancelIdleCallback(id)
   }

+ 48 - 0
packages/runtime-vapor/__tests__/dom/templateRef.spec.ts

@@ -1057,6 +1057,54 @@ describe('interop: template ref', () => {
     )
   })
 
+  test('vapor app: useTemplateRef with vdom child + insertionState', async () => {
+    const { container } = await testTemplateRefInterop(
+      `<script vapor>
+        import { useTemplateRef } from 'vue'
+        const components = _components;
+        const elRef = useTemplateRef('el')
+        function click() {
+          elRef.value.change()
+        }
+      </script>
+      <template>
+        <div>
+          <button class="btn" @click="click"></button>
+          <components.VDOMChild ref="el"/>
+        </div>
+      </template>`,
+      {
+        VDOMChild: {
+          code: `
+            <script setup>
+              import { ref } from 'vue'
+              const msg = ref('foo')
+              function change(){
+                msg.value = 'bar'
+              }
+              defineExpose({ change })
+            </script>
+            <template><div>{{msg}}</div></template>
+          `,
+          vapor: false,
+        },
+      },
+      undefined,
+      { vapor: true },
+    )
+
+    expect(container.innerHTML).toBe(
+      `<div><button class="btn"></button><div>foo</div></div>`,
+    )
+
+    const btn = container.querySelector('.btn')
+    triggerEvent('click', btn!)
+    await nextTick()
+    expect(container.innerHTML).toBe(
+      `<div><button class="btn"></button><div>bar</div></div>`,
+    )
+  })
+
   test('vapor app: static ref with vdom child', async () => {
     const { container } = await testTemplateRefInterop(
       `<script vapor>

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

@@ -17,7 +17,13 @@ import {
   createTextNode,
   updateLastLogicalChild,
 } from './dom/node'
-import { type Block, findBlockNode, insert, remove } from './block'
+import {
+  type Block,
+  applyTransitionHooks,
+  findBlockNode,
+  insert,
+  remove,
+} from './block'
 import { warn } from '@vue/runtime-dom'
 import { currentInstance, isVaporComponent } from './component'
 import type { DynamicSlot } from './componentSlots'
@@ -31,7 +37,6 @@ import {
   locateHydrationNode,
   setCurrentHydrationNode,
 } from './dom/hydration'
-import { applyTransitionHooks } from './components/Transition'
 import { ForFragment, VaporFragment } from './fragment'
 import {
   insertionAnchor,

+ 41 - 2
packages/runtime-vapor/src/block.ts

@@ -21,7 +21,7 @@ import {
   type VaporFragment,
   isFragment,
 } from './fragment'
-import { TeleportFragment } from './components/Teleport'
+import { isTeleportFragment } from './components/Teleport'
 
 export interface VaporTransitionHooks extends TransitionHooks {
   state: TransitionState
@@ -270,7 +270,7 @@ export function normalizeBlock(block: Block): Node[] {
   } else if (isVaporComponent(block)) {
     nodes.push(...normalizeBlock(block.block!))
   } else {
-    if (block instanceof TeleportFragment) {
+    if (isTeleportFragment(block)) {
       nodes.push(block.placeholder!, block.anchor!)
     } else {
       nodes.push(...normalizeBlock(block.nodes))
@@ -371,3 +371,42 @@ export function setComponentScopeId(instance: VaporComponentInstance): void {
     setScopeId(instance.block, scopeIds)
   }
 }
+
+// Transition hooks registry for tree-shaking
+// These are registered by Transition component when it's used
+type ApplyTransitionHooksFn = (
+  block: Block,
+  hooks: VaporTransitionHooks,
+) => VaporTransitionHooks
+type ApplyTransitionLeaveHooksFn = (
+  block: Block,
+  enterHooks: VaporTransitionHooks,
+  afterLeaveCb: () => void,
+) => void
+
+let _applyTransitionHooks: ApplyTransitionHooksFn | undefined
+let _applyTransitionLeaveHooks: ApplyTransitionLeaveHooksFn | undefined
+
+export function registerTransitionHooks(
+  applyHooks: ApplyTransitionHooksFn,
+  applyLeaveHooks: ApplyTransitionLeaveHooksFn,
+): void {
+  _applyTransitionHooks = applyHooks
+  _applyTransitionLeaveHooks = applyLeaveHooks
+}
+
+export function applyTransitionHooks(
+  block: Block,
+  hooks: VaporTransitionHooks,
+): VaporTransitionHooks {
+  return _applyTransitionHooks ? _applyTransitionHooks(block, hooks) : hooks
+}
+
+export function applyTransitionLeaveHooks(
+  block: Block,
+  enterHooks: VaporTransitionHooks,
+  afterLeaveCb: () => void,
+): void {
+  _applyTransitionLeaveHooks &&
+    _applyTransitionLeaveHooks(block, enterHooks, afterLeaveCb)
+}

+ 7 - 3
packages/runtime-vapor/src/component.ts

@@ -100,7 +100,11 @@ import {
   setCurrentHydrationNode,
 } from './dom/hydration'
 import { _next, createElement } from './dom/node'
-import { TeleportFragment, isVaporTeleport } from './components/Teleport'
+import {
+  type TeleportFragment,
+  isTeleportFragment,
+  isVaporTeleport,
+} from './components/Teleport'
 import {
   type KeepAliveInstance,
   findParentKeepAlive,
@@ -966,7 +970,7 @@ export function getRootElement(
     return getRootElement(block.block, onDynamicFragment, recurse)
   }
 
-  if (isFragment(block) && !(block instanceof TeleportFragment)) {
+  if (isFragment(block) && !isTeleportFragment(block)) {
     if (block instanceof DynamicFragment && onDynamicFragment) {
       onDynamicFragment(block)
     }
@@ -1073,7 +1077,7 @@ function handleSetupResult(
         instance.block.length) ||
         // preventing attrs fallthrough on Teleport
         // consistent with VDOM Teleport behavior
-        instance.block instanceof TeleportFragment)
+        isTeleportFragment(instance.block))
     ) {
       warnExtraneousAttributes(instance.attrs)
     }

+ 4 - 1
packages/runtime-vapor/src/components/KeepAlive.ts

@@ -54,7 +54,7 @@ type CacheKey = VaporComponent | VNode['type']
 type Cache = Map<CacheKey, VaporComponentInstance | VaporFragment>
 type Keys = Set<CacheKey>
 
-export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
+const KeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
   name: 'VaporKeepAlive',
   __isKeepAlive: true,
   props: {
@@ -278,6 +278,9 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
   },
 })
 
+export const VaporKeepAliveImpl: ObjectVaporComponent =
+  /*@__PURE__*/ KeepAliveImpl
+
 const shouldCache = (
   block: GenericComponentInstance | VaporFragment,
   props: KeepAliveProps,

+ 25 - 3
packages/runtime-vapor/src/components/Teleport.ts

@@ -11,7 +11,13 @@ import {
   resolveTeleportTarget,
   warn,
 } from '@vue/runtime-dom'
-import { type Block, type BlockFn, insert, remove } from '../block'
+import {
+  type Block,
+  type BlockFn,
+  applyTransitionHooks,
+  insert,
+  remove,
+} from '../block'
 import { createComment, createTextNode, querySelector } from '../dom/node'
 import {
   type LooseRawProps,
@@ -31,7 +37,6 @@ import {
   runWithoutHydration,
   setCurrentHydrationNode,
 } from '../dom/hydration'
-import { applyTransitionHooks } from './Transition'
 
 export const VaporTeleportImpl = {
   name: 'VaporTeleport',
@@ -44,6 +49,11 @@ export const VaporTeleportImpl = {
 }
 
 export class TeleportFragment extends VaporFragment {
+  /**
+   * @internal marker for duck typing to avoid direct instanceof check
+   * which prevents tree-shaking of TeleportFragment
+   */
+  readonly __isTeleportFragment = true
   anchor?: Node
   private rawProps?: LooseRawProps
   private resolvedProps?: TeleportProps
@@ -334,10 +344,22 @@ export class TeleportFragment extends VaporFragment {
   }
 }
 
+/**
+ * Use duck typing to check for VaporTeleport instead of direct reference
+ * to VaporTeleportImpl, allowing tree-shaking when Teleport is not used.
+ */
 export function isVaporTeleport(
   value: unknown,
 ): value is typeof VaporTeleportImpl {
-  return value === VaporTeleportImpl
+  return !!(value && (value as any).__isTeleport && (value as any).__vapor)
+}
+
+/**
+ * Use duck typing to check for TeleportFragment instead of instanceof,
+ * allowing tree-shaking when Teleport is not used.
+ */
+export function isTeleportFragment(value: unknown): value is TeleportFragment {
+  return !!(value && (value as any).__isTeleportFragment)
 }
 
 function locateTeleportEndAnchor(

+ 23 - 4
packages/runtime-vapor/src/components/Transition.ts

@@ -19,7 +19,12 @@ import {
   useTransitionState,
   warn,
 } from '@vue/runtime-dom'
-import type { Block, TransitionBlock, VaporTransitionHooks } from '../block'
+import {
+  type Block,
+  type TransitionBlock,
+  type VaporTransitionHooks,
+  registerTransitionHooks,
+} from '../block'
 import {
   type FunctionalVaporComponent,
   type VaporComponentInstance,
@@ -36,6 +41,17 @@ import {
 
 const displayName = 'VaporTransition'
 
+let registered = false
+export const ensureTransitionHooksRegistered = (): void => {
+  if (!registered) {
+    registered = true
+    registerTransitionHooks(
+      applyTransitionHooksImpl,
+      applyTransitionLeaveHooksImpl,
+    )
+  }
+}
+
 const decorate = (t: typeof VaporTransition) => {
   t.displayName = displayName
   t.props = TransitionPropsValidators
@@ -45,6 +61,9 @@ const decorate = (t: typeof VaporTransition) => {
 
 export const VaporTransition: FunctionalVaporComponent<TransitionProps> =
   /*@__PURE__*/ decorate((props, { slots }) => {
+    // Register transition hooks on first use
+    ensureTransitionHooksRegistered()
+
     // wrapped <transition appear>
     let resetDisplay: Function | undefined
     if (
@@ -82,7 +101,7 @@ export const VaporTransition: FunctionalVaporComponent<TransitionProps> =
     let resolvedProps: BaseTransitionProps<Element>
     renderEffect(() => (resolvedProps = resolveTransitionProps(props)))
 
-    const hooks = applyTransitionHooks(children, {
+    const hooks = applyTransitionHooksImpl(children, {
       state: useTransitionState(),
       // use proxy to keep props reference stable
       props: new Proxy({} as BaseTransitionProps<Element>, {
@@ -169,7 +188,7 @@ export function resolveTransitionHooks(
   return hooks
 }
 
-export function applyTransitionHooks(
+function applyTransitionHooksImpl(
   block: Block,
   hooks: VaporTransitionHooks,
 ): VaporTransitionHooks {
@@ -210,7 +229,7 @@ export function applyTransitionHooks(
   return resolvedHooks
 }
 
-export function applyTransitionLeaveHooks(
+function applyTransitionLeaveHooksImpl(
   block: Block,
   enterHooks: VaporTransitionHooks,
   afterLeaveCb: () => void,

+ 10 - 3
packages/runtime-vapor/src/components/TransitionGroup.ts

@@ -23,6 +23,7 @@ import {
 } from '../block'
 import { renderEffect } from '../renderEffect'
 import {
+  ensureTransitionHooksRegistered,
   resolveTransitionHooks,
   setTransitionHooks,
   setTransitionHooksOnFragment,
@@ -39,13 +40,13 @@ import { isFragment } from '../fragment'
 const positionMap = new WeakMap<TransitionBlock, DOMRect>()
 const newPositionMap = new WeakMap<TransitionBlock, DOMRect>()
 
-const decorate = (t: typeof VaporTransitionGroup) => {
+const decorate = <T extends ObjectVaporComponent>(t: T): T => {
   delete (t.props! as any).mode
   t.__vapor = true
   return t
 }
 
-export const VaporTransitionGroup: ObjectVaporComponent = decorate({
+const VaporTransitionGroupImpl: ObjectVaporComponent = {
   name: 'VaporTransitionGroup',
 
   props: /*@__PURE__*/ extend({}, TransitionPropsValidators, {
@@ -54,6 +55,9 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({
   }),
 
   setup(props: TransitionGroupProps, { slots }) {
+    // Register transition hooks on first use
+    ensureTransitionHooksRegistered()
+
     const instance = currentInstance as VaporComponentInstance
     const state = useTransitionState()
 
@@ -165,7 +169,10 @@ export const VaporTransitionGroup: ObjectVaporComponent = decorate({
       return slottedBlock
     }
   },
-})
+}
+
+export const VaporTransitionGroup: ObjectVaporComponent =
+  /*@__PURE__*/ decorate(VaporTransitionGroupImpl)
 
 function getTransitionBlocks(block: Block) {
   let children: TransitionBlock[] = []

+ 1 - 1
packages/runtime-vapor/src/dom/event.ts

@@ -60,7 +60,7 @@ type DelegatedHandler = {
 /**
  * Event delegation borrowed from solid
  */
-const delegatedEvents = Object.create(null)
+const delegatedEvents = /*@__PURE__*/ Object.create(null)
 
 export const delegateEvents = (...names: string[]): void => {
   for (const name of names) {

+ 79 - 2
packages/runtime-vapor/src/dom/hydration.ts

@@ -1,4 +1,9 @@
-import { MismatchTypes, isMismatchAllowed, warn } from '@vue/runtime-dom'
+import {
+  MismatchTypes,
+  isMismatchAllowed,
+  queuePostFlushCb,
+  warn,
+} from '@vue/runtime-dom'
 import {
   type ChildItem,
   insertionAnchor,
@@ -9,6 +14,7 @@ import {
 import {
   _child,
   _next,
+  createComment,
   createElement,
   createTextNode,
   disableHydrationNodeLookup,
@@ -16,12 +22,17 @@ import {
   locateChildByLogicalIndex,
   parentNode,
 } from './node'
-import { remove } from '../block'
+import { findBlockNode, remove } from '../block'
+import type { DynamicFragment } from '../fragment'
 
 const isHydratingStack = [] as boolean[]
 export let isHydrating = false
 export let currentHydrationNode: Node | null = null
 
+let _hydrateDynamicFragment:
+  | ((frag: DynamicFragment, isEmpty: boolean) => void)
+  | undefined
+
 function pushIsHydrating(value: boolean): void {
   isHydratingStack.push((isHydrating = value))
 }
@@ -50,6 +61,7 @@ function performHydration<T>(
   if (!isOptimized) {
     adoptTemplate = adoptTemplateImpl
     locateHydrationNode = locateHydrationNodeImpl
+    _hydrateDynamicFragment = hydrateDynamicFragmentImpl
     // optimize anchor cache lookup
     ;(Comment.prototype as any).$fe = undefined
     ;(Node.prototype as any).$pns = undefined
@@ -307,3 +319,68 @@ export function removeFragmentNodes(node: Node, endAnchor?: Node): void {
     }
   }
 }
+
+export function hydrateDynamicFragment(
+  frag: DynamicFragment,
+  isEmpty: boolean,
+): void {
+  _hydrateDynamicFragment && _hydrateDynamicFragment(frag, isEmpty)
+}
+
+// Hydrate implementation for DynamicFragment
+function hydrateDynamicFragmentImpl(
+  frag: DynamicFragment,
+  isEmpty: boolean,
+): void {
+  // avoid repeated hydration during fallback rendering
+  if (frag.anchor) return
+
+  if (frag.anchorLabel === 'if') {
+    // reuse the empty comment node as the anchor for empty if
+    // e.g. `<div v-if="false"></div>` -> `<!---->`
+    if (isEmpty) {
+      frag.anchor = locateFragmentEndAnchor('')!
+      if (__DEV__ && !frag.anchor) {
+        throw new Error(
+          'Failed to locate if anchor. this is likely a Vue internal bug.',
+        )
+      } else {
+        if (__DEV__) {
+          ;(frag.anchor as Comment).data = frag.anchorLabel
+        }
+        return
+      }
+    }
+  } else if (frag.anchorLabel === 'slot') {
+    // reuse the empty comment node for empty slot
+    // e.g. `<slot v-if="false"></slot>`
+    if (isEmpty && isComment(currentHydrationNode!, '')) {
+      frag.anchor = currentHydrationNode!
+      if (__DEV__) {
+        ;(frag.anchor as Comment).data = frag.anchorLabel!
+      }
+      return
+    }
+
+    // reuse the vdom fragment end anchor
+    frag.anchor = locateFragmentEndAnchor()!
+    if (__DEV__ && !frag.anchor) {
+      throw new Error(
+        'Failed to locate slot anchor. this is likely a Vue internal bug.',
+      )
+    } else {
+      return
+    }
+  }
+
+  const { parentNode: pn, nextNode } = findBlockNode(frag.nodes)!
+  // create an anchor
+  queuePostFlushCb(() => {
+    pn!.insertBefore(
+      (frag.anchor = __DEV__
+        ? createComment(frag.anchorLabel!)
+        : createTextNode()),
+      nextNode,
+    )
+  })
+}

+ 4 - 60
packages/runtime-vapor/src/fragment.ts

@@ -5,7 +5,8 @@ import {
   type BlockFn,
   type TransitionOptions,
   type VaporTransitionHooks,
-  findBlockNode,
+  applyTransitionHooks,
+  applyTransitionLeaveHooks,
   insert,
   isValidBlock,
   remove,
@@ -15,21 +16,14 @@ import {
   type TransitionHooks,
   type VNode,
   currentInstance,
-  queuePostFlushCb,
   setCurrentInstance,
   warnExtraneousAttributes,
 } from '@vue/runtime-dom'
 import { type VaporComponentInstance, applyFallthroughProps } from './component'
 import type { NodeRef } from './apiTemplateRef'
 import {
-  applyTransitionHooks,
-  applyTransitionLeaveHooks,
-} from './components/Transition'
-import {
-  currentHydrationNode,
-  isComment,
+  hydrateDynamicFragment,
   isHydrating,
-  locateFragmentEndAnchor,
   locateHydrationNode,
 } from './dom/hydration'
 import { isArray } from '@vue/shared'
@@ -280,57 +274,7 @@ export class DynamicFragment extends VaporFragment {
   }
 
   hydrate = (isEmpty = false): void => {
-    // avoid repeated hydration during fallback rendering
-    if (this.anchor) return
-
-    if (this.anchorLabel === 'if') {
-      // reuse the empty comment node as the anchor for empty if
-      // e.g. `<div v-if="false"></div>` -> `<!---->`
-      if (isEmpty) {
-        this.anchor = locateFragmentEndAnchor('')!
-        if (__DEV__ && !this.anchor) {
-          throw new Error(
-            'Failed to locate if anchor. this is likely a Vue internal bug.',
-          )
-        } else {
-          if (__DEV__) {
-            ;(this.anchor as Comment).data = this.anchorLabel
-          }
-          return
-        }
-      }
-    } else if (this.anchorLabel === 'slot') {
-      // reuse the empty comment node for empty slot
-      // e.g. `<slot v-if="false"></slot>`
-      if (isEmpty && isComment(currentHydrationNode!, '')) {
-        this.anchor = currentHydrationNode!
-        if (__DEV__) {
-          ;(this.anchor as Comment).data = this.anchorLabel!
-        }
-        return
-      }
-
-      // reuse the vdom fragment end anchor
-      this.anchor = locateFragmentEndAnchor()!
-      if (__DEV__ && !this.anchor) {
-        throw new Error(
-          'Failed to locate slot anchor. this is likely a Vue internal bug.',
-        )
-      } else {
-        return
-      }
-    }
-
-    const { parentNode, nextNode } = findBlockNode(this.nodes)!
-    // create an anchor
-    queuePostFlushCb(() => {
-      parentNode!.insertBefore(
-        (this.anchor = __DEV__
-          ? createComment(this.anchorLabel!)
-          : createTextNode()),
-        nextNode,
-      )
-    })
+    hydrateDynamicFragment(this, isEmpty)
   }
 }
 

+ 4 - 0
packages/runtime-vapor/src/vdomInterop.ts

@@ -421,6 +421,10 @@ function createVDOMComponent(
       },
       instance as any,
     )
+
+    if (isMounted && rawRef) {
+      vdomSetRef(rawRef, null, null, vnode)
+    }
   }
 
   return frag