vdomInterop.ts 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019
  1. import {
  2. type App,
  3. type ComponentInternalInstance,
  4. type ConcreteComponent,
  5. Fragment,
  6. type FunctionalComponent,
  7. type HydrationRenderer,
  8. type KeepAliveContext,
  9. MoveType,
  10. type Plugin,
  11. type RendererElement,
  12. type RendererInternals,
  13. type RendererNode,
  14. type ShallowRef,
  15. type Slots,
  16. Static,
  17. type SuspenseBoundary,
  18. type TransitionHooks,
  19. type VNode,
  20. type VNodeArrayChildren,
  21. type VNodeNormalizedRef,
  22. type VaporInteropInterface,
  23. createInternalObject,
  24. createVNode,
  25. currentInstance,
  26. ensureHydrationRenderer,
  27. ensureRenderer,
  28. ensureVaporSlotFallback,
  29. isEmitListener,
  30. isKeepAlive,
  31. isVNode,
  32. isHydrating as isVdomHydrating,
  33. normalizeRef,
  34. onScopeDispose,
  35. queuePostFlushCb,
  36. renderSlot,
  37. setTransitionHooks as setVNodeTransitionHooks,
  38. shallowReactive,
  39. shallowRef,
  40. simpleSetCurrentInstance,
  41. activate as vdomActivate,
  42. deactivate as vdomDeactivate,
  43. setRef as vdomSetRef,
  44. warn,
  45. } from '@vue/runtime-dom'
  46. import {
  47. type LooseRawProps,
  48. type LooseRawSlots,
  49. type VaporComponent,
  50. VaporComponentInstance,
  51. createComponent,
  52. getCurrentScopeId,
  53. getRootElement,
  54. mountComponent,
  55. unmountComponent,
  56. } from './component'
  57. import {
  58. type Block,
  59. type BlockFn,
  60. type VaporTransitionHooks,
  61. insert,
  62. isValidBlock,
  63. move,
  64. remove,
  65. } from './block'
  66. import {
  67. EMPTY_OBJ,
  68. ShapeFlags,
  69. extend,
  70. isArray,
  71. isFunction,
  72. isReservedProp,
  73. } from '@vue/shared'
  74. import { type RawProps, rawPropsProxyHandlers } from './componentProps'
  75. import type { RawSlots, VaporSlot } from './componentSlots'
  76. import {
  77. currentSlotOwner,
  78. currentSlotScopeIds,
  79. setCurrentSlotOwner,
  80. } from './componentSlots'
  81. import { renderEffect } from './renderEffect'
  82. import { _next, createTextNode } from './dom/node'
  83. import { optimizePropertyLookup } from './dom/prop'
  84. import {
  85. advanceHydrationNode,
  86. currentHydrationNode,
  87. isComment,
  88. isHydrating,
  89. setCurrentHydrationNode,
  90. hydrateNode as vaporHydrateNode,
  91. } from './dom/hydration'
  92. import {
  93. VaporFragment,
  94. attachSlotFallback,
  95. isFragment,
  96. renderSlotFallback,
  97. } from './fragment'
  98. import type { NodeRef } from './apiTemplateRef'
  99. import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition'
  100. import { setInteropEnabled } from './vdomInteropState'
  101. import {
  102. type KeepAliveInstance,
  103. activate,
  104. currentKeepAliveCtx,
  105. deactivate,
  106. setCurrentKeepAliveCtx,
  107. } from './components/KeepAlive'
  108. import {
  109. parentSuspense as currentParentSuspense,
  110. setParentSuspense,
  111. } from './components/Suspense'
  112. export const interopKey: unique symbol = Symbol(`interop`)
  113. function filterReservedProps(props: VNode['props']): VNode['props'] {
  114. const filtered: VNode['props'] = {}
  115. for (const key in props) {
  116. if (!isReservedProp(key)) {
  117. filtered[key] = props[key]
  118. }
  119. }
  120. return filtered
  121. }
  122. // mounting vapor components and slots in vdom
  123. const vaporInteropImpl: Omit<
  124. VaporInteropInterface,
  125. 'vdomMount' | 'vdomUnmount' | 'vdomSlot' | 'vdomMountVNode'
  126. > = {
  127. mount(
  128. vnode,
  129. container,
  130. anchor,
  131. parentComponent,
  132. parentSuspense,
  133. onBeforeMount,
  134. ) {
  135. let selfAnchor = (vnode.anchor = createTextNode())
  136. if (isHydrating) {
  137. // avoid vdom hydration children mismatch by the selfAnchor, delay its insertion
  138. queuePostFlushCb(() => container.insertBefore(selfAnchor, anchor))
  139. } else {
  140. vnode.el = selfAnchor
  141. container.insertBefore(selfAnchor, anchor)
  142. }
  143. const prev = currentInstance
  144. simpleSetCurrentInstance(parentComponent)
  145. const propsRef = shallowRef(filterReservedProps(vnode.props))
  146. const slotsRef = shallowRef(vnode.children)
  147. let prevSuspense: SuspenseBoundary | null = null
  148. if (__FEATURE_SUSPENSE__ && parentSuspense) {
  149. prevSuspense = setParentSuspense(parentSuspense)
  150. }
  151. const dynamicPropSource: (() => any)[] & { [interopKey]?: boolean } = [
  152. () => propsRef.value,
  153. ]
  154. // mark as interop props
  155. dynamicPropSource[interopKey] = true
  156. // @ts-expect-error
  157. const instance = (vnode.component = createComponent(
  158. vnode.type as any as VaporComponent,
  159. {
  160. $: dynamicPropSource,
  161. } as RawProps,
  162. {
  163. _: slotsRef, // pass the slots ref
  164. } as any as RawSlots,
  165. undefined,
  166. undefined,
  167. (parentComponent ? parentComponent.appContext : vnode.appContext) as any,
  168. ))
  169. instance.rawPropsRef = propsRef
  170. instance.rawSlotsRef = slotsRef
  171. // copy the shape flag from the vdom component if inside a keep-alive
  172. if (isKeepAlive(parentComponent)) instance.shapeFlag = vnode.shapeFlag
  173. if (vnode.transition) {
  174. setVaporTransitionHooks(
  175. instance,
  176. vnode.transition as VaporTransitionHooks,
  177. )
  178. }
  179. if (__FEATURE_SUSPENSE__ && parentSuspense) {
  180. setParentSuspense(prevSuspense)
  181. }
  182. const rootEl = getRootElement(instance)
  183. if (rootEl) {
  184. vnode.el = rootEl
  185. }
  186. // invoke directive hooks only when we have a valid root element
  187. if (vnode.dirs) {
  188. if (rootEl) {
  189. onBeforeMount && onBeforeMount()
  190. } else {
  191. if (__DEV__) {
  192. warn(
  193. `Runtime directive used on component with non-element root node. ` +
  194. `The directives will not function as intended.`,
  195. )
  196. }
  197. vnode.dirs = null
  198. }
  199. }
  200. mountComponent(instance, container, selfAnchor)
  201. simpleSetCurrentInstance(prev)
  202. return instance
  203. },
  204. update(n1, n2, shouldUpdate, onBeforeUpdate) {
  205. n2.component = n1.component
  206. n2.el = n2.anchor = n1.anchor
  207. const instance = n2.component as any as VaporComponentInstance
  208. const rootEl = getRootElement(instance)
  209. if (rootEl) {
  210. n2.el = rootEl
  211. }
  212. // invoke directive hooks only when we have a valid root element
  213. if (n2.dirs) {
  214. if (rootEl) {
  215. onBeforeUpdate && onBeforeUpdate()
  216. } else {
  217. n2.dirs = null
  218. }
  219. }
  220. if (shouldUpdate) {
  221. instance.rawPropsRef!.value = filterReservedProps(n2.props)
  222. instance.rawSlotsRef!.value = n2.children
  223. }
  224. },
  225. unmount(vnode, doRemove) {
  226. const container = doRemove ? vnode.anchor!.parentNode : undefined
  227. const instance = vnode.component as any as VaporComponentInstance
  228. if (instance) {
  229. // the async component may not be resolved yet, block is null
  230. if (instance.block) {
  231. unmountComponent(instance, container)
  232. }
  233. } else if (vnode.vb) {
  234. remove(vnode.vb, container)
  235. }
  236. remove(vnode.anchor as Node, container)
  237. },
  238. /**
  239. * vapor slot in vdom
  240. */
  241. slot(
  242. n1: VNode,
  243. n2: VNode,
  244. container,
  245. anchor,
  246. parentComponent,
  247. parentSuspense,
  248. ) {
  249. if (!n1) {
  250. const prev = currentInstance
  251. let prevSuspense: SuspenseBoundary | null = null
  252. simpleSetCurrentInstance(parentComponent)
  253. if (__FEATURE_SUSPENSE__ && parentSuspense) {
  254. prevSuspense = setParentSuspense(parentSuspense)
  255. }
  256. // mount
  257. let selfAnchor: Node | undefined
  258. const { slot, fallback } = n2.vs!
  259. const propsRef = (n2.vs!.ref = shallowRef(n2.props))
  260. let slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
  261. if (fallback) {
  262. const vaporFallback = createVaporFallback(fallback, parentComponent)
  263. const emptyFrag = attachSlotFallback(slotBlock, vaporFallback)
  264. if (!isValidBlock(slotBlock)) {
  265. slotBlock = renderSlotFallback(slotBlock, vaporFallback, emptyFrag)
  266. }
  267. }
  268. if (isFragment(slotBlock)) {
  269. // use fragment's anchor when possible
  270. selfAnchor = slotBlock.anchor
  271. }
  272. if (__FEATURE_SUSPENSE__ && parentSuspense) {
  273. setParentSuspense(prevSuspense)
  274. }
  275. simpleSetCurrentInstance(prev)
  276. if (!selfAnchor) selfAnchor = createTextNode()
  277. insert((n2.el = n2.anchor = selfAnchor), container, anchor)
  278. insert((n2.vb = slotBlock), container, selfAnchor)
  279. } else {
  280. // update
  281. n2.el = n2.anchor = n1.anchor
  282. n2.vb = n1.vb
  283. ;(n2.vs!.ref = n1.vs!.ref)!.value = n2.props
  284. }
  285. },
  286. move(vnode, container, anchor, moveType) {
  287. move(vnode.vb || (vnode.component as any), container, anchor, moveType)
  288. move(vnode.anchor as any, container, anchor, moveType)
  289. },
  290. hydrate(vnode, node, container, anchor, parentComponent, parentSuspense) {
  291. // Check both vapor's isHydrating (for createVaporSSRApp) and
  292. // VDOM's isVdomHydrating (for createSSRApp).
  293. // In CSR (createApp/createVaporApp + vaporInteropPlugin), both are false,
  294. // so this logic is tree-shaken.
  295. if (!isHydrating && !isVdomHydrating) return node
  296. vaporHydrateNode(node, () =>
  297. this.mount(vnode, container, anchor, parentComponent, parentSuspense),
  298. )
  299. return _next(node)
  300. },
  301. hydrateSlot(vnode, node) {
  302. if (!isHydrating && !isVdomHydrating) return node
  303. const { slot } = vnode.vs!
  304. const propsRef = (vnode.vs!.ref = shallowRef(vnode.props))
  305. vaporHydrateNode(node, () => {
  306. vnode.vb = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
  307. vnode.anchor = vnode.el = currentHydrationNode!
  308. if (__DEV__ && !vnode.anchor) {
  309. throw new Error(
  310. `Failed to locate slot anchor. this is likely a Vue internal bug.`,
  311. )
  312. }
  313. })
  314. // For fragment-wrapped slot content (`<!--[-->...<!--]-->`), return the
  315. // node after the end anchor to avoid hydrateChildren() treating `<!--]-->`
  316. // as an extra child of the current container.
  317. return isComment(node, '[')
  318. ? (vnode.anchor as Node).nextSibling
  319. : (vnode.anchor as Node)
  320. },
  321. setTransitionHooks(component, hooks) {
  322. setVaporTransitionHooks(component as any, hooks as VaporTransitionHooks)
  323. },
  324. activate(vnode, container, anchor, parentComponent) {
  325. const cached = (parentComponent.ctx as KeepAliveContext).getCachedComponent(
  326. vnode,
  327. )
  328. vnode.el = cached.el
  329. vnode.component = cached.component
  330. vnode.anchor = cached.anchor
  331. activate(vnode.component as any, container, anchor)
  332. insert(vnode.anchor as any, container, anchor)
  333. },
  334. deactivate(vnode, container) {
  335. deactivate(vnode.component as any, container)
  336. insert(vnode.anchor as any, container)
  337. },
  338. }
  339. const vaporSlotPropsProxyHandler: ProxyHandler<
  340. ShallowRef<Record<string, any>>
  341. > = {
  342. get(target, key: any) {
  343. return target.value[key]
  344. },
  345. has(target, key: any) {
  346. return key in target.value
  347. },
  348. ownKeys(target) {
  349. return Reflect.ownKeys(target.value)
  350. },
  351. getOwnPropertyDescriptor(target, key: any) {
  352. if (key in target.value) {
  353. return {
  354. enumerable: true,
  355. configurable: true,
  356. }
  357. }
  358. },
  359. }
  360. const vaporSlotsProxyHandler: ProxyHandler<any> = {
  361. get(target, key) {
  362. const slot = target[key]
  363. if (isFunction(slot)) {
  364. slot.__vapor = true
  365. // Create a wrapper that internally uses renderSlot for proper vapor slot handling
  366. // This ensures that calling slots.default() works the same as renderSlot(slots, 'default')
  367. const wrapped = (props?: Record<string, any>) => [
  368. renderSlot({ [key]: slot }, key as string, props),
  369. ]
  370. ;(wrapped as any).__vs = slot
  371. return wrapped
  372. }
  373. return slot
  374. },
  375. }
  376. let vdomHydrateNode: HydrationRenderer['hydrateNode'] | undefined
  377. // Static/Fragment vnodes always represent a contiguous range [el..anchor].
  378. // For component vnodes, only treat them as a range when their hydrated subTree
  379. // is Static/Fragment (multi-root component case).
  380. function resolveVNodeRange(vnode: VNode): [Node, Node] | undefined {
  381. const { type, shapeFlag, el, anchor } = vnode
  382. if (shapeFlag & ShapeFlags.TELEPORT && el && anchor && anchor !== el) {
  383. return [el as Node, anchor as Node]
  384. }
  385. if ((type === Static || type === Fragment) && el && anchor && anchor !== el) {
  386. return [el as Node, anchor as Node]
  387. }
  388. if (!(shapeFlag & ShapeFlags.COMPONENT)) {
  389. return
  390. }
  391. const subTree = vnode.component && vnode.component.subTree
  392. const subEl = subTree && subTree.el
  393. const subAnchor = subTree && subTree.anchor
  394. if (
  395. subTree &&
  396. (subTree.type === Static || subTree.type === Fragment) &&
  397. subEl &&
  398. subAnchor &&
  399. subAnchor !== subEl
  400. ) {
  401. return [subEl as Node, subAnchor as Node]
  402. }
  403. }
  404. function resolveVNodeNodes(vnode: VNode): Block {
  405. const vnodeRange = resolveVNodeRange(vnode)
  406. if (vnodeRange) {
  407. const nodeRange: Node[] = []
  408. let n: Node | null = vnodeRange[0]
  409. while (n) {
  410. nodeRange.push(n)
  411. if (n === vnodeRange[1]) break
  412. n = n.nextSibling
  413. }
  414. return nodeRange
  415. }
  416. return vnode.el as Block
  417. }
  418. /**
  419. * Mount VNode in vapor
  420. */
  421. function mountVNode(
  422. internals: RendererInternals,
  423. vnode: VNode,
  424. parentComponent: VaporComponentInstance | null,
  425. ): VaporFragment {
  426. const suspense =
  427. currentParentSuspense || (parentComponent && parentComponent.suspense)
  428. const frag = new VaporFragment<Block>([])
  429. frag.vnode = vnode
  430. let isMounted = false
  431. const unmount = (parentNode?: ParentNode, transition?: TransitionHooks) => {
  432. if (transition) setVNodeTransitionHooks(vnode, transition)
  433. if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
  434. if ((vnode.type as any).__vapor) {
  435. deactivate(
  436. vnode.component as any,
  437. (parentComponent as KeepAliveInstance)!.ctx.getStorageContainer(),
  438. )
  439. } else {
  440. vdomDeactivate(
  441. vnode,
  442. (parentComponent as KeepAliveInstance)!.ctx.getStorageContainer(),
  443. internals,
  444. parentComponent as any,
  445. null,
  446. )
  447. }
  448. } else {
  449. internals.um(vnode, parentComponent as any, null, !!parentNode)
  450. }
  451. }
  452. frag.hydrate = () => {
  453. if (!isHydrating) return
  454. hydrateVNode(vnode, parentComponent as any)
  455. onScopeDispose(unmount, true)
  456. isMounted = true
  457. frag.nodes = resolveVNodeNodes(vnode)
  458. }
  459. frag.insert = (parentNode, anchor, transition) => {
  460. if (isHydrating) return
  461. if (vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
  462. if ((vnode.type as any).__vapor) {
  463. activate(vnode.component as any, parentNode, anchor)
  464. } else {
  465. vdomActivate(
  466. vnode,
  467. parentNode,
  468. anchor,
  469. internals,
  470. parentComponent as any,
  471. null,
  472. undefined,
  473. false,
  474. )
  475. }
  476. return
  477. } else {
  478. const prev = currentInstance
  479. simpleSetCurrentInstance(parentComponent)
  480. if (!isMounted) {
  481. if (transition) setVNodeTransitionHooks(vnode, transition)
  482. internals.p(
  483. null,
  484. vnode,
  485. parentNode,
  486. anchor,
  487. parentComponent as any,
  488. suspense,
  489. undefined, // namespace
  490. vnode.slotScopeIds,
  491. )
  492. onScopeDispose(unmount, true)
  493. isMounted = true
  494. } else {
  495. // move
  496. internals.m(
  497. vnode,
  498. parentNode,
  499. anchor,
  500. MoveType.REORDER,
  501. parentComponent as any,
  502. )
  503. }
  504. simpleSetCurrentInstance(prev)
  505. }
  506. frag.nodes = resolveVNodeNodes(vnode)
  507. if (isMounted && frag.onUpdated) frag.onUpdated.forEach(m => m())
  508. }
  509. frag.remove = unmount
  510. return frag
  511. }
  512. /**
  513. * Mount vdom component in vapor
  514. */
  515. function createVDOMComponent(
  516. internals: RendererInternals,
  517. component: ConcreteComponent,
  518. parentComponent: VaporComponentInstance | null,
  519. rawProps?: LooseRawProps | null,
  520. rawSlots?: LooseRawSlots | null,
  521. ): VaporFragment {
  522. const suspense =
  523. currentParentSuspense || (parentComponent && parentComponent.suspense)
  524. const useBridge = shouldUseRendererBridge(component)
  525. const comp = useBridge ? ensureRendererBridge(component) : component
  526. const frag = new VaporFragment<Block>([])
  527. const vnode = (frag.vnode = createVNode(
  528. comp,
  529. rawProps && extend({}, new Proxy(rawProps, rawPropsProxyHandlers)),
  530. ))
  531. if (currentKeepAliveCtx) {
  532. currentKeepAliveCtx.processShapeFlag(frag)
  533. setCurrentKeepAliveCtx(null)
  534. }
  535. const wrapper = new VaporComponentInstance(
  536. useBridge ? (comp as any) : { props: component.props },
  537. rawProps as RawProps,
  538. rawSlots as RawSlots,
  539. parentComponent ? parentComponent.appContext : undefined,
  540. undefined,
  541. )
  542. // overwrite how the vdom instance handles props
  543. vnode.vi = (instance: ComponentInternalInstance) => {
  544. // ensure props are shallow reactive to align with VDOM behavior.
  545. instance.props = shallowReactive(wrapper.props)
  546. const attrs = createInternalObject()
  547. const isFilteredEmit = (key: string | symbol): boolean =>
  548. typeof key === 'string' && isEmitListener(instance.emitsOptions, key)
  549. instance.attrs = new Proxy(attrs, {
  550. get(_, key: string | symbol) {
  551. if (isFilteredEmit(key)) return
  552. return wrapper.attrs[key as any]
  553. },
  554. has(_, key: string | symbol) {
  555. return !isFilteredEmit(key) && key in wrapper.attrs
  556. },
  557. ownKeys() {
  558. return Reflect.ownKeys(wrapper.attrs).filter(
  559. key => !isFilteredEmit(key),
  560. )
  561. },
  562. getOwnPropertyDescriptor(_, key: string | symbol) {
  563. if (!isFilteredEmit(key) && key in wrapper.attrs) {
  564. return {
  565. enumerable: true,
  566. configurable: true,
  567. }
  568. }
  569. },
  570. })
  571. instance.slots =
  572. wrapper.slots === EMPTY_OBJ
  573. ? EMPTY_OBJ
  574. : new Proxy(wrapper.slots, vaporSlotsProxyHandler)
  575. }
  576. let rawRef: VNodeNormalizedRef | null = null
  577. let isMounted = false
  578. const unmount = (parentNode?: ParentNode, transition?: TransitionHooks) => {
  579. // unset ref
  580. if (rawRef) vdomSetRef(rawRef, null, null, vnode, true)
  581. if (transition) setVNodeTransitionHooks(vnode, transition)
  582. if (vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
  583. vdomDeactivate(
  584. vnode,
  585. (parentComponent as KeepAliveInstance)!.ctx.getStorageContainer(),
  586. internals,
  587. parentComponent as any,
  588. null,
  589. )
  590. return
  591. }
  592. internals.umt(vnode.component!, null, !!parentNode)
  593. }
  594. frag.hydrate = () => {
  595. if (!isHydrating) return
  596. hydrateVNode(vnode, parentComponent as any)
  597. onScopeDispose(unmount, true)
  598. isMounted = true
  599. frag.nodes = resolveVNodeNodes(vnode)
  600. }
  601. vnode.scopeId = getCurrentScopeId() || null
  602. vnode.slotScopeIds = currentSlotScopeIds
  603. frag.insert = (parentNode, anchor, transition) => {
  604. if (isHydrating) return
  605. if (vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
  606. vdomActivate(
  607. vnode,
  608. parentNode,
  609. anchor,
  610. internals,
  611. parentComponent as any,
  612. null,
  613. undefined,
  614. false,
  615. )
  616. } else {
  617. const prev = currentInstance
  618. simpleSetCurrentInstance(parentComponent)
  619. if (!isMounted) {
  620. if (transition) setVNodeTransitionHooks(vnode, transition)
  621. internals.mt(
  622. vnode,
  623. parentNode,
  624. anchor,
  625. parentComponent as any,
  626. suspense,
  627. undefined,
  628. false,
  629. )
  630. // set ref
  631. if (rawRef) vdomSetRef(rawRef, null, null, vnode)
  632. onScopeDispose(unmount, true)
  633. isMounted = true
  634. } else {
  635. // move
  636. internals.m(
  637. vnode,
  638. parentNode,
  639. anchor,
  640. MoveType.REORDER,
  641. parentComponent as any,
  642. )
  643. }
  644. simpleSetCurrentInstance(prev)
  645. }
  646. frag.nodes = resolveVNodeNodes(vnode)
  647. if (isMounted && frag.onUpdated) frag.onUpdated.forEach(m => m())
  648. }
  649. frag.remove = unmount
  650. frag.setRef = (
  651. instance: VaporComponentInstance,
  652. ref: NodeRef,
  653. refFor: boolean,
  654. refKey: string | undefined,
  655. ): void => {
  656. const oldRawRef = rawRef
  657. rawRef = normalizeRef(
  658. {
  659. ref: ref as any,
  660. ref_for: refFor,
  661. ref_key: refKey,
  662. },
  663. instance as any,
  664. )
  665. if (isMounted) {
  666. if (rawRef) {
  667. vdomSetRef(rawRef, oldRawRef, null, vnode)
  668. } else if (oldRawRef) {
  669. vdomSetRef(oldRawRef, null, null, vnode, true)
  670. }
  671. }
  672. }
  673. return frag
  674. }
  675. const rendererBridgeCache = new WeakMap<
  676. ConcreteComponent,
  677. FunctionalComponent
  678. >()
  679. /**
  680. * Teleport/Suspense are renderer primitives (`__isTeleport` / `__isSuspense`),
  681. * not regular components with their own render pipeline.
  682. *
  683. * We wrap them with a tiny functional bridge so they can pass through the
  684. * interop component mount path while preserving built-in vnode semantics.
  685. */
  686. function shouldUseRendererBridge(
  687. component: ConcreteComponent & {
  688. __isTeleport?: boolean
  689. __isSuspense?: boolean
  690. },
  691. ): boolean {
  692. return !!(component.__isTeleport || component.__isSuspense)
  693. }
  694. function ensureRendererBridge(
  695. component: ConcreteComponent,
  696. ): FunctionalComponent {
  697. let bridge = rendererBridgeCache.get(component)
  698. if (!bridge) {
  699. rendererBridgeCache.set(
  700. component,
  701. (bridge = (props, { slots }) => createVNode(component, props, slots)),
  702. )
  703. }
  704. return bridge
  705. }
  706. /**
  707. * Mount vdom slot in vapor
  708. */
  709. function renderVDOMSlot(
  710. internals: RendererInternals,
  711. slotsRef: ShallowRef<Slots>,
  712. name: string | (() => string),
  713. props: Record<string, any>,
  714. parentComponent: VaporComponentInstance,
  715. fallback?: VaporSlot,
  716. ): VaporFragment {
  717. const suspense = currentParentSuspense || parentComponent.suspense
  718. const frag = new VaporFragment([])
  719. const instance = currentInstance
  720. const slotOwner = currentSlotOwner
  721. if (fallback && !frag.fallback) frag.fallback = fallback
  722. let isMounted = false
  723. let currentBlock: Block | null = null
  724. let currentVNode: VNode | null = null
  725. frag.insert = (parentNode, anchor) => {
  726. if (isHydrating) return
  727. if (!isMounted) {
  728. render(parentNode, anchor)
  729. isMounted = true
  730. } else {
  731. if (currentVNode) {
  732. // move vdom content
  733. internals.m(
  734. currentVNode,
  735. parentNode,
  736. anchor,
  737. MoveType.REORDER,
  738. parentComponent as any,
  739. )
  740. } else if (currentBlock) {
  741. // move vapor content
  742. insert(currentBlock, parentNode, anchor)
  743. }
  744. }
  745. frag.remove = parentNode => {
  746. if (currentBlock) {
  747. remove(currentBlock, parentNode)
  748. } else if (currentVNode) {
  749. internals.um(currentVNode, parentComponent as any, null, true)
  750. }
  751. }
  752. if (isMounted && frag.onUpdated) frag.onUpdated.forEach(m => m())
  753. }
  754. const render = (parentNode?: ParentNode, anchor?: Node | null) => {
  755. const prev = currentInstance
  756. simpleSetCurrentInstance(instance)
  757. try {
  758. renderEffect(() => {
  759. const prevSlotOwner = setCurrentSlotOwner(slotOwner)
  760. try {
  761. const effectiveFallback = frag.fallback || fallback
  762. let slotContent: VNode | Block | undefined
  763. let isEmpty = true
  764. let emptyFrag: VaporFragment | null = null
  765. if (slotsRef.value) {
  766. slotContent = renderSlot(
  767. slotsRef.value,
  768. isFunction(name) ? name() : name,
  769. props,
  770. )
  771. if (isVNode(slotContent)) {
  772. const children = slotContent.children as VNode[]
  773. // handle forwarded vapor slot without its own fallback
  774. // use the fallback provided by the slot outlet
  775. ensureVaporSlotFallback(
  776. children,
  777. effectiveFallback as () => VNodeArrayChildren,
  778. )
  779. isEmpty = children.length === 0
  780. } else {
  781. if (effectiveFallback && slotContent) {
  782. emptyFrag = attachSlotFallback(slotContent, () =>
  783. effectiveFallback(internals, parentComponent),
  784. )
  785. }
  786. isEmpty = !isValidBlock(slotContent)
  787. }
  788. }
  789. let resolved = slotContent
  790. if (isEmpty && effectiveFallback) {
  791. if (isVNode(slotContent)) {
  792. resolved = effectiveFallback(internals, parentComponent)
  793. } else if (slotContent) {
  794. resolved = renderSlotFallback(
  795. slotContent,
  796. () => effectiveFallback(internals, parentComponent),
  797. emptyFrag,
  798. )
  799. } else {
  800. resolved = effectiveFallback(internals, parentComponent)
  801. }
  802. }
  803. if (isHydrating) {
  804. if (isVNode(resolved)) {
  805. hydrateVNode(resolved, parentComponent as any)
  806. currentVNode = resolved
  807. currentBlock = null
  808. frag.nodes = resolved.el as any
  809. } else if (resolved) {
  810. currentBlock = resolved as Block
  811. currentVNode = null
  812. frag.nodes = resolved as any
  813. } else {
  814. currentBlock = null
  815. currentVNode = null
  816. frag.nodes = []
  817. }
  818. return
  819. }
  820. if (isVNode(resolved)) {
  821. if (currentBlock) {
  822. remove(currentBlock, parentNode)
  823. currentBlock = null
  824. }
  825. internals.p(
  826. currentVNode,
  827. resolved,
  828. parentNode!,
  829. anchor,
  830. parentComponent as any,
  831. suspense,
  832. undefined, // namespace
  833. resolved.slotScopeIds, // pass slotScopeIds for :slotted styles
  834. )
  835. currentVNode = resolved
  836. frag.nodes = resolved.el as any
  837. return
  838. }
  839. if (resolved) {
  840. if (currentVNode) {
  841. internals.um(currentVNode, parentComponent as any, null, true)
  842. currentVNode = null
  843. }
  844. if (currentBlock) {
  845. remove(currentBlock, parentNode)
  846. }
  847. insert(resolved, parentNode!, anchor)
  848. currentBlock = resolved
  849. frag.nodes = resolved as any
  850. return
  851. }
  852. if (currentBlock) {
  853. remove(currentBlock, parentNode)
  854. currentBlock = null
  855. }
  856. if (currentVNode) {
  857. internals.um(currentVNode, parentComponent as any, null, true)
  858. currentVNode = null
  859. }
  860. // mark as empty
  861. frag.nodes = []
  862. } finally {
  863. setCurrentSlotOwner(prevSlotOwner)
  864. }
  865. })
  866. } finally {
  867. simpleSetCurrentInstance(prev)
  868. }
  869. }
  870. frag.hydrate = () => {
  871. if (!isHydrating) return
  872. render()
  873. isMounted = true
  874. }
  875. return frag
  876. }
  877. export const vaporInteropPlugin: Plugin = app => {
  878. setInteropEnabled()
  879. const internals = ensureRenderer().internals
  880. app._context.vapor = extend(vaporInteropImpl, {
  881. vdomMount: createVDOMComponent.bind(null, internals),
  882. vdomUnmount: internals.umt,
  883. vdomSlot: renderVDOMSlot.bind(null, internals),
  884. vdomMountVNode: mountVNode.bind(null, internals),
  885. })
  886. const mount = app.mount
  887. app.mount = ((...args) => {
  888. optimizePropertyLookup()
  889. return mount(...args)
  890. }) satisfies App['mount']
  891. }
  892. function hydrateVNode(
  893. vnode: VNode,
  894. parentComponent: ComponentInternalInstance | null,
  895. ) {
  896. const node = currentHydrationNode!
  897. if (!vdomHydrateNode) vdomHydrateNode = ensureHydrationRenderer().hydrateNode!
  898. const nextNode = vdomHydrateNode(
  899. node,
  900. vnode,
  901. parentComponent,
  902. null,
  903. null,
  904. false,
  905. )
  906. if (nextNode) setCurrentHydrationNode(nextNode)
  907. else advanceHydrationNode(node)
  908. }
  909. function createVaporFallback(
  910. fallback: () => any,
  911. parentComponent: ComponentInternalInstance | null,
  912. ): BlockFn {
  913. const internals = ensureRenderer().internals
  914. return () => createFallback(fallback)(internals, parentComponent)
  915. }
  916. const createFallback =
  917. (fallback: () => any) =>
  918. (
  919. internals: RendererInternals<RendererNode, RendererElement>,
  920. parentComponent: ComponentInternalInstance | null,
  921. ) => {
  922. const fallbackNodes = fallback()
  923. // vnode content, wrap it as a VaporFragment
  924. if (isArray(fallbackNodes) && fallbackNodes.every(isVNode)) {
  925. const frag = new VaporFragment([])
  926. frag.insert = (parentNode, anchor) => {
  927. fallbackNodes.forEach(vnode => {
  928. internals.p(null, vnode, parentNode, anchor, parentComponent)
  929. })
  930. }
  931. frag.remove = parentNode => {
  932. fallbackNodes.forEach(vnode => {
  933. internals.um(vnode, parentComponent, null, true)
  934. })
  935. }
  936. return frag
  937. }
  938. // vapor block
  939. return fallbackNodes as Block
  940. }