component.ts 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198
  1. import {
  2. type AsyncComponentInternalOptions,
  3. type ComponentInternalOptions,
  4. type ComponentObjectPropsOptions,
  5. type ComponentPropsOptions,
  6. EffectScope,
  7. type EmitFn,
  8. type EmitsOptions,
  9. type EmitsToProps,
  10. ErrorCodes,
  11. type ExtractPropTypes,
  12. type GenericAppContext,
  13. type GenericComponentInstance,
  14. type LifecycleHook,
  15. NULL_DYNAMIC_COMPONENT,
  16. type NormalizedPropsOptions,
  17. type ObjectEmitsOptions,
  18. type ShallowUnwrapRef,
  19. type SuspenseBoundary,
  20. callWithErrorHandling,
  21. currentInstance,
  22. endMeasure,
  23. expose,
  24. getComponentName,
  25. getFunctionalFallthrough,
  26. invalidateMount,
  27. isAsyncWrapper,
  28. isKeepAlive,
  29. markAsyncBoundary,
  30. nextUid,
  31. popWarningContext,
  32. pushWarningContext,
  33. queuePostFlushCb,
  34. registerHMR,
  35. setCurrentInstance,
  36. startMeasure,
  37. unregisterHMR,
  38. warn,
  39. warnExtraneousAttributes,
  40. } from '@vue/runtime-dom'
  41. import { type Block, findBlockNode, insert, isBlock, remove } from './block'
  42. import {
  43. type ShallowRef,
  44. markRaw,
  45. onScopeDispose,
  46. proxyRefs,
  47. setActiveSub,
  48. toRaw,
  49. unref,
  50. } from '@vue/reactivity'
  51. import {
  52. EMPTY_OBJ,
  53. type Prettify,
  54. ShapeFlags,
  55. hasOwn,
  56. invokeArrayFns,
  57. isArray,
  58. isFunction,
  59. isPromise,
  60. isString,
  61. } from '@vue/shared'
  62. import {
  63. type DynamicPropsSource,
  64. type RawProps,
  65. getKeysFromRawProps,
  66. getPropsProxyHandlers,
  67. hasFallthroughAttrs,
  68. normalizePropsOptions,
  69. resolveDynamicProps,
  70. setupPropsValidation,
  71. } from './componentProps'
  72. import { type RenderEffect, renderEffect } from './renderEffect'
  73. import { emit, normalizeEmitsOptions } from './componentEmits'
  74. import { setDynamicProps } from './dom/prop'
  75. import {
  76. type DynamicSlotSource,
  77. type RawSlots,
  78. type StaticSlots,
  79. type VaporSlot,
  80. dynamicSlotsProxyHandlers,
  81. getScopeOwner,
  82. getSlot,
  83. setCurrentSlotOwner,
  84. } from './componentSlots'
  85. import { hmrReload, hmrRerender } from './hmr'
  86. import {
  87. adoptTemplate,
  88. advanceHydrationNode,
  89. currentHydrationNode,
  90. enterHydrationBoundary,
  91. isComment,
  92. isHydrating,
  93. locateEndAnchor,
  94. locateHydrationNode,
  95. locateNextNode,
  96. markHydrationAnchor,
  97. setCurrentHydrationNode,
  98. } from './dom/hydration'
  99. import { createComment, createElement, createTextNode } from './dom/node'
  100. import {
  101. type TeleportFragment,
  102. isTeleportFragment,
  103. isVaporTeleport,
  104. } from './components/Teleport'
  105. import type { KeepAliveInstance } from './components/KeepAlive'
  106. import {
  107. currentKeepAliveCtx,
  108. setCurrentKeepAliveCtx,
  109. } from './components/KeepAlive'
  110. import {
  111. insertionAnchor,
  112. insertionParent,
  113. isLastInsertion,
  114. resetInsertionState,
  115. } from './insertionState'
  116. import type {
  117. DefineVaporComponent,
  118. VaporRenderResult,
  119. } from './apiDefineComponent'
  120. import { DynamicFragment, isFragment } from './fragment'
  121. import type { VaporElement } from './apiDefineCustomElement'
  122. import { parentSuspense, setParentSuspense } from './components/Suspense'
  123. import { isInteropEnabled } from './vdomInteropState'
  124. import { setComponentScopeId, setScopeId } from './scopeId'
  125. export { currentInstance } from '@vue/runtime-dom'
  126. export type VaporComponent =
  127. | FunctionalVaporComponent<any>
  128. | VaporComponentOptions
  129. | DefineVaporComponent
  130. export type FunctionalVaporComponent<
  131. Props = {},
  132. Emits extends EmitsOptions = {},
  133. Slots extends StaticSlots = StaticSlots,
  134. Exposed extends Record<string, any> = Record<string, any>,
  135. > = ((
  136. props: Props & EmitsToProps<Emits>,
  137. ctx: {
  138. emit: EmitFn<Emits>
  139. slots: Slots
  140. attrs: Record<string, any>
  141. expose: <T extends Record<string, any> = Exposed>(exposed: T) => void
  142. },
  143. ) => VaporRenderResult) &
  144. Omit<
  145. VaporComponentOptions<ComponentPropsOptions<Props>, Emits, string, Slots>,
  146. 'setup'
  147. > & {
  148. displayName?: string
  149. } & SharedInternalOptions
  150. export interface VaporComponentOptions<
  151. Props = {},
  152. Emits extends EmitsOptions = {},
  153. RuntimeEmitsKeys extends string = string,
  154. Slots extends StaticSlots = StaticSlots,
  155. Exposed extends Record<string, any> = Record<string, any>,
  156. TypeBlock extends Block = Block,
  157. InferredProps = ComponentObjectPropsOptions extends Props
  158. ? {}
  159. : ExtractPropTypes<Props>,
  160. >
  161. extends
  162. ComponentInternalOptions,
  163. AsyncComponentInternalOptions<
  164. VaporComponentOptions,
  165. VaporComponentInstance
  166. >,
  167. SharedInternalOptions {
  168. inheritAttrs?: boolean
  169. props?: Props
  170. emits?: Emits | RuntimeEmitsKeys[]
  171. slots?: Slots
  172. setup?: (
  173. props: Readonly<InferredProps>,
  174. ctx: {
  175. emit: EmitFn<Emits>
  176. slots: Slots
  177. attrs: Record<string, any>
  178. expose: <T extends Record<string, any> = Exposed>(exposed: T) => void
  179. },
  180. ) => TypeBlock | Exposed | Promise<Exposed> | void
  181. render?(
  182. ctx: Exposed extends Block ? undefined : ShallowUnwrapRef<Exposed>,
  183. props: Readonly<InferredProps>,
  184. emit: EmitFn<Emits>,
  185. attrs: any,
  186. slots: Slots,
  187. ): VaporRenderResult<TypeBlock> | void
  188. name?: string
  189. vapor?: boolean
  190. components?: Record<string, VaporComponent>
  191. /**
  192. * @internal custom element interception hook
  193. */
  194. ce?: (instance: VaporComponentInstance) => void
  195. }
  196. interface SharedInternalOptions {
  197. /**
  198. * Cached normalized props options.
  199. * In vapor mode there are no mixins so normalized options can be cached
  200. * directly on the component
  201. */
  202. __propsOptions?: NormalizedPropsOptions
  203. /**
  204. * Cached normalized props proxy handlers.
  205. */
  206. __propsHandlers?: [ProxyHandler<any> | null, ProxyHandler<any>]
  207. /**
  208. * Cached normalized emits options.
  209. */
  210. __emitsOptions?: ObjectEmitsOptions
  211. }
  212. // In TypeScript, it is actually impossible to have a record type with only
  213. // specific properties that have a different type from the indexed type.
  214. // This makes our rawProps / rawSlots shape difficult to satisfy when calling
  215. // `createComponent` - luckily this is not user-facing, so we don't need to be
  216. // 100% strict. Here we use intentionally wider types to make `createComponent`
  217. // more ergonomic in tests and internal call sites, where we immediately cast
  218. // them into the stricter types.
  219. export type LooseRawProps = Record<
  220. string,
  221. (() => unknown) | DynamicPropsSource[]
  222. > & {
  223. $?: DynamicPropsSource[]
  224. }
  225. export type LooseRawSlots = Record<string, VaporSlot | DynamicSlotSource[]> & {
  226. $?: DynamicSlotSource[]
  227. }
  228. export function createComponent(
  229. component: VaporComponent,
  230. rawProps?: LooseRawProps | null,
  231. rawSlots?: LooseRawSlots | null,
  232. isSingleRoot?: boolean,
  233. once?: boolean,
  234. appContext: GenericAppContext = (currentInstance &&
  235. currentInstance.appContext) ||
  236. emptyContext,
  237. managedMount = false,
  238. ): VaporComponentInstance {
  239. const _insertionParent = insertionParent
  240. const _insertionAnchor = insertionAnchor
  241. const _isLastInsertion = isLastInsertion
  242. let hydrationClose: Node | null = null
  243. let exitHydrationBoundary: (() => void) | undefined
  244. let deferHydrationBoundary = false
  245. const finalizeHydrationBoundary = () => {
  246. exitHydrationBoundary && exitHydrationBoundary()
  247. if (hydrationClose && currentHydrationNode === hydrationClose) {
  248. advanceHydrationNode(hydrationClose)
  249. }
  250. }
  251. if (isHydrating) {
  252. locateHydrationNode()
  253. if (component.__multiRoot && isComment(currentHydrationNode!, '[')) {
  254. hydrationClose = locateEndAnchor(currentHydrationNode!)
  255. exitHydrationBoundary = enterHydrationBoundary(
  256. hydrationClose && markHydrationAnchor(hydrationClose),
  257. )
  258. setCurrentHydrationNode(currentHydrationNode!.nextSibling)
  259. }
  260. } else {
  261. resetInsertionState()
  262. }
  263. try {
  264. let prevSuspense: SuspenseBoundary | null = null
  265. if (__FEATURE_SUSPENSE__ && currentInstance && currentInstance.suspense) {
  266. prevSuspense = setParentSuspense(currentInstance.suspense)
  267. }
  268. if (
  269. (isSingleRoot ||
  270. // transition has attrs fallthrough
  271. (currentInstance && isVaporTransition(currentInstance!.type))) &&
  272. component.inheritAttrs !== false &&
  273. isVaporComponent(currentInstance) &&
  274. currentInstance.hasFallthrough
  275. ) {
  276. // check if we are the single root of the parent
  277. // if yes, inject parent attrs as dynamic props source
  278. const attrs = currentInstance.attrs
  279. if (rawProps && rawProps !== EMPTY_OBJ) {
  280. ;((rawProps as RawProps).$ || ((rawProps as RawProps).$ = [])).push(
  281. () => attrs,
  282. )
  283. } else {
  284. rawProps = { $: [() => attrs] } as RawProps
  285. }
  286. }
  287. // keep-alive
  288. if (
  289. currentInstance &&
  290. currentInstance.vapor &&
  291. isKeepAlive(currentInstance)
  292. ) {
  293. const cached = (
  294. currentInstance as KeepAliveInstance
  295. ).ctx.getCachedComponent(component)
  296. // @ts-expect-error
  297. if (cached) return cached
  298. }
  299. // vdom interop enabled and component is not an explicit vapor component
  300. if (isInteropEnabled && appContext.vapor && !component.__vapor) {
  301. const frag = appContext.vapor.vdomMount(
  302. component as any,
  303. currentInstance as any,
  304. rawProps,
  305. rawSlots,
  306. )
  307. if (!isHydrating) {
  308. if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
  309. } else {
  310. frag.hydrate()
  311. if (_isLastInsertion) {
  312. advanceHydrationNode(_insertionParent!)
  313. }
  314. }
  315. return frag
  316. }
  317. // teleport
  318. if (isVaporTeleport(component)) {
  319. const frag = component.process(rawProps!, rawSlots!)
  320. if (_insertionParent) {
  321. // Teleports mounted via insertion state are not part of the returned
  322. // block tree, so scope disposal must tear down their target-side state.
  323. onScopeDispose(() => frag.dispose(), true)
  324. }
  325. if (!isHydrating) {
  326. if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
  327. } else {
  328. frag.hydrate()
  329. if (_isLastInsertion) {
  330. advanceHydrationNode(_insertionParent!)
  331. }
  332. }
  333. return frag as any
  334. }
  335. const instance = new VaporComponentInstance(
  336. component,
  337. rawProps as RawProps,
  338. rawSlots as RawSlots,
  339. appContext,
  340. once,
  341. )
  342. // handle currentKeepAliveCtx for component boundary isolation
  343. // AsyncWrapper should NOT clear currentKeepAliveCtx so its internal
  344. // DynamicFragment can capture it
  345. if (currentKeepAliveCtx && !isAsyncWrapper(instance)) {
  346. currentKeepAliveCtx.processShapeFlag(instance)
  347. // clear currentKeepAliveCtx so child components don't associate
  348. // with parent's KeepAlive
  349. setCurrentKeepAliveCtx(null)
  350. }
  351. // reset currentSlotOwner to null to avoid affecting the child components
  352. const prevSlotOwner = setCurrentSlotOwner(null)
  353. // HMR
  354. if (__DEV__) {
  355. registerHMR(instance)
  356. instance.isSingleRoot = isSingleRoot
  357. instance.hmrRerender = hmrRerender.bind(null, instance)
  358. instance.hmrReload = hmrReload.bind(null, instance)
  359. pushWarningContext(instance)
  360. startMeasure(instance, `init`)
  361. // cache normalized options for dev only emit check
  362. instance.propsOptions = normalizePropsOptions(component)
  363. instance.emitsOptions = normalizeEmitsOptions(component)
  364. }
  365. // hydrating async component
  366. if (
  367. isHydrating &&
  368. isAsyncWrapper(instance) &&
  369. component.__asyncHydrate &&
  370. !component.__asyncResolved
  371. ) {
  372. component.__asyncHydrate(currentHydrationNode as Element, instance, () =>
  373. setupComponent(instance, component),
  374. )
  375. } else {
  376. setupComponent(instance, component)
  377. }
  378. if (__DEV__) {
  379. popWarningContext()
  380. endMeasure(instance, 'init')
  381. }
  382. if (__FEATURE_SUSPENSE__ && currentInstance && currentInstance.suspense) {
  383. setParentSuspense(prevSuspense)
  384. }
  385. // restore currentSlotOwner to previous value after setupFn is called
  386. setCurrentSlotOwner(prevSlotOwner)
  387. onScopeDispose(() => unmountComponent(instance), true)
  388. if (!managedMount && (_insertionParent || isHydrating)) {
  389. mountComponent(instance, _insertionParent!, _insertionAnchor)
  390. }
  391. if (isHydrating && _insertionAnchor !== undefined) {
  392. advanceHydrationNode(_insertionParent!)
  393. }
  394. if (
  395. isHydrating &&
  396. hydrationClose &&
  397. instance.suspense &&
  398. instance.asyncDep &&
  399. !instance.asyncResolved &&
  400. instance.restoreAsyncContext
  401. ) {
  402. deferHydrationBoundary = true
  403. instance.deferredHydrationBoundary = () => {
  404. if (
  405. instance.block &&
  406. hydrationClose &&
  407. findBlockNode(instance.block).nextNode === hydrationClose.nextSibling
  408. ) {
  409. setCurrentHydrationNode(hydrationClose)
  410. }
  411. finalizeHydrationBoundary()
  412. }
  413. }
  414. return instance
  415. } finally {
  416. if (isHydrating && !deferHydrationBoundary) {
  417. finalizeHydrationBoundary()
  418. }
  419. }
  420. }
  421. export function setupComponent(
  422. instance: VaporComponentInstance,
  423. component: VaporComponent,
  424. ): void {
  425. const prevInstance = setCurrentInstance(instance)
  426. const prevSub = setActiveSub()
  427. if (__DEV__) {
  428. setupPropsValidation(instance)
  429. }
  430. const setupFn = isFunction(component) ? component : component.setup
  431. const setupResult = setupFn
  432. ? callWithErrorHandling(setupFn, instance, ErrorCodes.SETUP_FUNCTION, [
  433. instance.props,
  434. instance,
  435. ]) || EMPTY_OBJ
  436. : EMPTY_OBJ
  437. const isAsyncSetup = isPromise(setupResult)
  438. if ((isAsyncSetup || instance.sp) && !isAsyncWrapper(instance)) {
  439. // async setup / serverPrefetch, mark as async boundary for useId()
  440. markAsyncBoundary(instance)
  441. }
  442. if (isAsyncSetup) {
  443. if (__FEATURE_SUSPENSE__) {
  444. // async setup returned Promise.
  445. // bail here and wait for re-entry.
  446. instance.asyncDep = setupResult
  447. if (__DEV__ && !instance.suspense) {
  448. const name = getComponentName(component) ?? 'Anonymous'
  449. warn(
  450. `Component <${name}>: setup function returned a promise, but no ` +
  451. `<Suspense> boundary was found in the parent component tree. ` +
  452. `A component with async setup() must be nested in a <Suspense> ` +
  453. `in order to be rendered.`,
  454. )
  455. }
  456. } else if (__DEV__) {
  457. warn(
  458. `setup() returned a Promise, but the version of Vue you are using ` +
  459. `does not support it yet.`,
  460. )
  461. }
  462. } else {
  463. handleSetupResult(setupResult, component, instance)
  464. }
  465. setActiveSub(prevSub)
  466. setCurrentInstance(...prevInstance)
  467. }
  468. export let isApplyingFallthroughProps = false
  469. export function applyFallthroughProps(
  470. el: Element,
  471. attrs: Record<string, any>,
  472. ): void {
  473. isApplyingFallthroughProps = true
  474. setDynamicProps(el, [attrs])
  475. isApplyingFallthroughProps = false
  476. }
  477. /**
  478. * dev only
  479. */
  480. function createDevSetupStateProxy(
  481. instance: VaporComponentInstance,
  482. ): Record<string, any> {
  483. const { setupState } = instance
  484. return new Proxy(setupState!, {
  485. get(target, key: string | symbol, receiver) {
  486. if (
  487. isString(key) &&
  488. !key.startsWith('__v') &&
  489. !hasOwn(toRaw(setupState)!, key)
  490. ) {
  491. warn(
  492. `Property ${JSON.stringify(key)} was accessed during render ` +
  493. `but is not defined on instance.`,
  494. )
  495. }
  496. return Reflect.get(target, key, receiver)
  497. },
  498. })
  499. }
  500. /**
  501. * dev only
  502. */
  503. export function devRender(instance: VaporComponentInstance): void {
  504. instance.block =
  505. (instance.type.render
  506. ? callWithErrorHandling(
  507. instance.type.render,
  508. instance,
  509. ErrorCodes.RENDER_FUNCTION,
  510. [
  511. instance.setupState,
  512. instance.props,
  513. instance.emit,
  514. instance.attrs,
  515. instance.slots,
  516. ],
  517. )
  518. : callWithErrorHandling(
  519. isFunction(instance.type) ? instance.type : instance.type.setup!,
  520. instance,
  521. ErrorCodes.SETUP_FUNCTION,
  522. [
  523. instance.props,
  524. {
  525. slots: instance.slots,
  526. attrs: instance.attrs,
  527. emit: instance.emit,
  528. expose: instance.expose,
  529. },
  530. ],
  531. )) || []
  532. }
  533. export const emptyContext: GenericAppContext = {
  534. app: null as any,
  535. config: {},
  536. provides: /*@__PURE__*/ Object.create(null),
  537. }
  538. export class VaporComponentInstance<
  539. Props extends Record<string, any> = {},
  540. Emits extends EmitsOptions = {},
  541. Slots extends StaticSlots = StaticSlots,
  542. Exposed extends Record<string, any> = Record<string, any>,
  543. TypeBlock extends Block = Block,
  544. TypeRefs extends Record<string, any> = Record<string, any>,
  545. > implements GenericComponentInstance {
  546. vapor: true
  547. uid: number
  548. type: VaporComponent
  549. root: GenericComponentInstance | null
  550. parent: GenericComponentInstance | null
  551. appContext: GenericAppContext
  552. block: TypeBlock
  553. scope: EffectScope
  554. rawProps: RawProps
  555. rawSlots: RawSlots
  556. props: Readonly<Props>
  557. attrs: Record<string, any>
  558. propsDefaults: Record<string, any> | null
  559. slots: Slots
  560. scopeId?: string | null
  561. // to hold vnode props / slots in vdom interop mode
  562. rawPropsRef?: ShallowRef<any>
  563. rawSlotsRef?: ShallowRef<any>
  564. emit: EmitFn<Emits>
  565. emitted: Record<string, boolean> | null
  566. expose: (<T extends Record<string, any> = Exposed>(exposed: T) => void) &
  567. // compatible with vdom components
  568. string[]
  569. exposed: Exposed | null
  570. exposeProxy: Prettify<ShallowUnwrapRef<Exposed>> | null
  571. // for useTemplateRef()
  572. refs: TypeRefs
  573. // for provide / inject
  574. provides: Record<string, any>
  575. // for useId
  576. ids: [string, number, number]
  577. // for suspense
  578. suspense: SuspenseBoundary | null
  579. suspenseId: number
  580. asyncDep: Promise<any> | null
  581. asyncResolved: boolean
  582. restoreAsyncContext?: () => void | (() => void)
  583. deferredHydrationBoundary?: () => void
  584. // for vapor custom element
  585. renderEffects?: RenderEffect[]
  586. hasFallthrough: boolean
  587. // for keep-alive
  588. shapeFlag?: number
  589. $key?: any
  590. // for v-once: caches props/attrs values to ensure they remain frozen
  591. // even when the component re-renders due to local state changes
  592. oncePropsCache?: Record<string | symbol, any>
  593. // lifecycle hooks
  594. isMounted: boolean
  595. isUnmounted: boolean
  596. isDeactivated: boolean
  597. isUpdating: boolean
  598. bc?: LifecycleHook // LifecycleHooks.BEFORE_CREATE
  599. c?: LifecycleHook // LifecycleHooks.CREATED
  600. bm?: LifecycleHook // LifecycleHooks.BEFORE_MOUNT
  601. m?: LifecycleHook // LifecycleHooks.MOUNTED
  602. bu?: LifecycleHook // LifecycleHooks.BEFORE_UPDATE
  603. u?: LifecycleHook // LifecycleHooks.UPDATED
  604. um?: LifecycleHook // LifecycleHooks.BEFORE_UNMOUNT
  605. bum?: LifecycleHook // LifecycleHooks.UNMOUNTED
  606. da?: LifecycleHook // LifecycleHooks.DEACTIVATED
  607. a?: LifecycleHook // LifecycleHooks.ACTIVATED
  608. rtg?: LifecycleHook // LifecycleHooks.RENDER_TRACKED
  609. rtc?: LifecycleHook // LifecycleHooks.RENDER_TRIGGERED
  610. ec?: LifecycleHook // LifecycleHooks.ERROR_CAPTURED
  611. sp?: LifecycleHook<() => Promise<unknown>> // LifecycleHooks.SERVER_PREFETCH
  612. // dev only
  613. setupState?: Exposed extends Block ? undefined : ShallowUnwrapRef<Exposed>
  614. devtoolsRawSetupState?: any
  615. hmrRerender?: () => void
  616. hmrReload?: (newComp: VaporComponent) => void
  617. parentTeleport?: TeleportFragment | null
  618. propsOptions?: NormalizedPropsOptions
  619. emitsOptions?: ObjectEmitsOptions | null
  620. isSingleRoot?: boolean
  621. /**
  622. * dev only flag to track whether $attrs was used during render.
  623. * If $attrs was used during render then the warning for failed attrs
  624. * fallthrough can be suppressed.
  625. */
  626. accessedAttrs: boolean = false
  627. // type only
  628. /**
  629. * @deprecated only used for JSX to detect props types.
  630. */
  631. // @ts-expect-error
  632. $props: Props
  633. constructor(
  634. comp: VaporComponent,
  635. rawProps?: RawProps | null,
  636. rawSlots?: RawSlots | null,
  637. appContext?: GenericAppContext,
  638. once?: boolean,
  639. ) {
  640. this.vapor = true
  641. this.uid = nextUid()
  642. this.type = comp
  643. this.parent = currentInstance
  644. if (currentInstance) {
  645. this.root = currentInstance.root
  646. this.appContext = currentInstance.appContext
  647. this.provides = currentInstance.provides
  648. this.ids = currentInstance.ids
  649. } else {
  650. this.root = this
  651. this.appContext = appContext || emptyContext
  652. this.provides = Object.create(this.appContext.provides)
  653. this.ids = ['', 0, 0]
  654. }
  655. this.block = null! // to be set
  656. this.scope = new EffectScope(true)
  657. this.emit = emit.bind(null, this) as EmitFn<Emits>
  658. this.expose = expose.bind(null, this) as any
  659. this.refs = EMPTY_OBJ as TypeRefs
  660. this.emitted = this.exposed = this.exposeProxy = this.propsDefaults = null
  661. // suspense related
  662. this.suspense = parentSuspense
  663. this.suspenseId = parentSuspense ? parentSuspense.pendingId : 0
  664. this.asyncDep = null
  665. this.asyncResolved = false
  666. this.isMounted =
  667. this.isUnmounted =
  668. this.isUpdating =
  669. this.isDeactivated =
  670. false
  671. // init props
  672. this.rawProps = rawProps || EMPTY_OBJ
  673. this.hasFallthrough = hasFallthroughAttrs(comp, rawProps)
  674. if (rawProps || comp.props) {
  675. const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp, once)
  676. this.attrs = new Proxy(this, attrsHandlers)
  677. this.props = (
  678. comp.props
  679. ? new Proxy(this, propsHandlers!)
  680. : isFunction(comp)
  681. ? this.attrs
  682. : EMPTY_OBJ
  683. ) as Props
  684. } else {
  685. this.props = this.attrs = EMPTY_OBJ as Props
  686. }
  687. // init slots
  688. this.rawSlots = rawSlots || EMPTY_OBJ
  689. this.slots = (
  690. rawSlots
  691. ? rawSlots.$
  692. ? new Proxy(rawSlots, dynamicSlotsProxyHandlers)
  693. : rawSlots
  694. : EMPTY_OBJ
  695. ) as Slots
  696. this.scopeId = getCurrentScopeId()
  697. // apply custom element special handling
  698. if (comp.ce) {
  699. comp.ce(this)
  700. }
  701. if (__DEV__) {
  702. // in dev, mark attrs accessed if optional props (attrs === props)
  703. if (this.props === this.attrs) {
  704. this.accessedAttrs = true
  705. } else {
  706. const attrs = this.attrs
  707. const instance = this
  708. this.attrs = new Proxy(attrs, {
  709. get(target, key, receiver) {
  710. instance.accessedAttrs = true
  711. return Reflect.get(target, key, receiver)
  712. },
  713. })
  714. }
  715. }
  716. }
  717. /**
  718. * Expose `getKeysFromRawProps` on the instance so it can be used in code
  719. * paths where it's needed, e.g. `useModel`
  720. */
  721. rawKeys(): string[] {
  722. return getKeysFromRawProps(this.rawProps)
  723. }
  724. }
  725. export function isVaporComponent(
  726. value: unknown,
  727. ): value is VaporComponentInstance {
  728. return value instanceof VaporComponentInstance
  729. }
  730. /**
  731. * Used when a component cannot be resolved at compile time
  732. * and needs rely on runtime resolution - where it might fallback to a plain
  733. * element if the resolution fails.
  734. */
  735. export function createComponentWithFallback(
  736. comp: VaporComponent | typeof NULL_DYNAMIC_COMPONENT | string,
  737. rawProps?: LooseRawProps | null,
  738. rawSlots?: LooseRawSlots | null,
  739. isSingleRoot?: boolean,
  740. once?: boolean,
  741. appContext?: GenericAppContext,
  742. ): HTMLElement | VaporComponentInstance {
  743. if (comp === NULL_DYNAMIC_COMPONENT) {
  744. if (isHydrating && currentHydrationNode) {
  745. if (isReusableNullComponentAnchor(currentHydrationNode)) {
  746. const node = currentHydrationNode
  747. if (isComment(node, '')) {
  748. advanceHydrationNode(node)
  749. }
  750. return node as any as HTMLElement
  751. }
  752. const nextAnchor = locateNextNode(currentHydrationNode)
  753. if (nextAnchor && isReusableNullComponentAnchor(nextAnchor)) {
  754. // Keep the cursor on the stale SSR node before `nextAnchor` so the
  755. // owning DynamicFragment can trim that range on hydrate exit and then
  756. // advance past the reused null-branch anchor in one place.
  757. return nextAnchor as any as HTMLElement
  758. }
  759. }
  760. return (__DEV__
  761. ? createComment('ndc')
  762. : createTextNode('')) as any as HTMLElement
  763. }
  764. if (!isString(comp)) {
  765. return createComponent(
  766. comp,
  767. rawProps,
  768. rawSlots,
  769. isSingleRoot,
  770. once,
  771. appContext,
  772. )
  773. }
  774. return createPlainElement(comp, rawProps, rawSlots, isSingleRoot, once)
  775. }
  776. function isReusableNullComponentAnchor(node: Node): boolean {
  777. return (
  778. isComment(node, '') ||
  779. isComment(node, 'dynamic-component') ||
  780. isComment(node, 'async component') ||
  781. isComment(node, 'keyed')
  782. )
  783. }
  784. export function createPlainElement(
  785. comp: string,
  786. rawProps?: LooseRawProps | null,
  787. rawSlots?: LooseRawSlots | null,
  788. isSingleRoot?: boolean,
  789. once?: boolean,
  790. ): HTMLElement {
  791. const _insertionParent = insertionParent
  792. const _insertionAnchor = insertionAnchor
  793. const _isLastInsertion = isLastInsertion
  794. if (isHydrating) {
  795. locateHydrationNode()
  796. } else {
  797. resetInsertionState()
  798. }
  799. const el = isHydrating
  800. ? (adoptTemplate(currentHydrationNode!, `<${comp}/>`) as HTMLElement)
  801. : createElement(comp)
  802. // mark single root
  803. ;(el as any).$root = isSingleRoot
  804. if (!isHydrating) {
  805. const scopeId = getCurrentScopeId()
  806. if (scopeId) setScopeId(el, [scopeId])
  807. }
  808. if (rawProps) {
  809. const setFn = () =>
  810. setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)])
  811. if (once) setFn()
  812. else renderEffect(setFn)
  813. }
  814. if (rawSlots) {
  815. let nextNode: Node | null = null
  816. if (isHydrating) {
  817. nextNode = locateNextNode(el)
  818. setCurrentHydrationNode(el.firstChild)
  819. }
  820. if (rawSlots.$) {
  821. // Dynamic element children don't own an SSR slot-range anchor.
  822. // Use an empty label so hydration treats this as a generic dynamic
  823. // fragment instead of trying to reuse SlotFragment-style anchors.
  824. const frag = new DynamicFragment(
  825. isHydrating ? '' : __DEV__ ? 'slot' : undefined,
  826. )
  827. renderEffect(() => frag.update(getSlot(rawSlots as RawSlots, 'default')))
  828. if (!isHydrating) insert(frag, el)
  829. } else {
  830. let slot = getSlot(rawSlots as RawSlots, 'default')
  831. if (slot) {
  832. const block = slot()
  833. if (!isHydrating) insert(block, el)
  834. }
  835. }
  836. if (isHydrating) {
  837. setCurrentHydrationNode(nextNode)
  838. }
  839. }
  840. if (!isHydrating) {
  841. if (_insertionParent) insert(el, _insertionParent, _insertionAnchor)
  842. } else {
  843. if (_isLastInsertion) {
  844. advanceHydrationNode(_insertionParent!)
  845. }
  846. }
  847. return el
  848. }
  849. export function mountComponent(
  850. instance: VaporComponentInstance,
  851. parent: ParentNode,
  852. anchor?: Node | null | 0,
  853. ): void {
  854. if (
  855. __FEATURE_SUSPENSE__ &&
  856. instance.suspense &&
  857. instance.asyncDep &&
  858. !instance.asyncResolved
  859. ) {
  860. const component = instance.type
  861. instance.suspense.registerDep(instance, setupResult => {
  862. // Final suspense retry after async setup resolves. Restore hydrating
  863. // mode so the last mount does not fall back to fresh DOM insertion.
  864. const reset =
  865. instance.restoreAsyncContext && instance.restoreAsyncContext()
  866. try {
  867. handleSetupResult(setupResult, component, instance)
  868. mountComponent(instance, parent, anchor)
  869. if (isHydrating) {
  870. instance.deferredHydrationBoundary &&
  871. instance.deferredHydrationBoundary()
  872. }
  873. } finally {
  874. if (isHydrating) {
  875. instance.deferredHydrationBoundary = undefined
  876. }
  877. instance.restoreAsyncContext = undefined
  878. if (reset) reset()
  879. }
  880. })
  881. return
  882. }
  883. if (instance.shapeFlag! & ShapeFlags.COMPONENT_KEPT_ALIVE) {
  884. ;(instance.parent as KeepAliveInstance)!.ctx.activate(
  885. instance,
  886. parent,
  887. anchor,
  888. )
  889. return
  890. }
  891. // custom element style injection
  892. const { root, type } = instance as GenericComponentInstance
  893. if (root && root.ce && (root.ce as VaporElement)._hasShadowRoot()) {
  894. root.ce!._injectChildStyle(
  895. type,
  896. instance.parent ? instance.parent.type : undefined,
  897. )
  898. }
  899. if (__DEV__) {
  900. startMeasure(instance, `mount`)
  901. }
  902. if (instance.bm) invokeArrayFns(instance.bm)
  903. if (!isHydrating) {
  904. insert(instance.block, parent, anchor)
  905. setComponentScopeId(instance)
  906. }
  907. if (instance.m) queuePostFlushCb(instance.m!)
  908. if (
  909. instance.shapeFlag! & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE &&
  910. instance.a
  911. ) {
  912. queuePostFlushCb(instance.a!)
  913. }
  914. instance.isMounted = true
  915. if (__DEV__) {
  916. endMeasure(instance, `mount`)
  917. }
  918. }
  919. export function unmountComponent(
  920. instance: VaporComponentInstance,
  921. parentNode?: ParentNode,
  922. ): void {
  923. // Skip unmount for kept-alive components - deactivate if called from remove()
  924. if (
  925. instance.shapeFlag! & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE &&
  926. instance.parent &&
  927. instance.parent.vapor &&
  928. (instance.parent as KeepAliveInstance).ctx
  929. ) {
  930. if (parentNode) {
  931. ;(instance.parent as KeepAliveInstance)!.ctx.deactivate(instance)
  932. }
  933. return
  934. }
  935. if (instance.isMounted && !instance.isUnmounted) {
  936. if (__DEV__) {
  937. unregisterHMR(instance)
  938. }
  939. invalidateMount(instance.m)
  940. invalidateMount(instance.a)
  941. if (instance.bum) {
  942. invokeArrayFns(instance.bum)
  943. }
  944. instance.scope.stop()
  945. if (instance.um) {
  946. queuePostFlushCb(instance.um!)
  947. }
  948. instance.isUnmounted = true
  949. }
  950. if (parentNode) {
  951. remove(instance.block, parentNode)
  952. }
  953. }
  954. export function getExposed(
  955. instance: GenericComponentInstance,
  956. ): Record<string, any> | undefined {
  957. if (instance.exposed) {
  958. return (
  959. instance.exposeProxy ||
  960. (instance.exposeProxy = new Proxy(markRaw(instance.exposed), {
  961. get: (target, key) => unref(target[key as any]),
  962. }))
  963. )
  964. }
  965. }
  966. export function getRootElement(
  967. block: Block,
  968. onDynamicFragment?: (frag: DynamicFragment) => void,
  969. recurse: boolean = true,
  970. ): Element | undefined {
  971. if (block instanceof Element) {
  972. return block
  973. }
  974. if (recurse && isVaporComponent(block)) {
  975. return getRootElement(block.block, onDynamicFragment, recurse)
  976. }
  977. if (isFragment(block) && !isTeleportFragment(block)) {
  978. if (block instanceof DynamicFragment && onDynamicFragment) {
  979. onDynamicFragment(block)
  980. }
  981. const { nodes } = block
  982. if (nodes instanceof Element && (nodes as any).$root) {
  983. return nodes
  984. }
  985. return getRootElement(nodes, onDynamicFragment, recurse)
  986. }
  987. // The root node contains comments. It is necessary to filter out
  988. // the comment nodes and return a single root node.
  989. // align with vdom behavior
  990. if (isArray(block)) {
  991. let singleRoot: Element | undefined
  992. let hasComment = false
  993. for (const b of block) {
  994. if (b instanceof Comment) {
  995. hasComment = true
  996. continue
  997. }
  998. const thisRoot = getRootElement(b, onDynamicFragment, recurse)
  999. // only return root if there is exactly one eligible root in the array
  1000. if (!thisRoot || singleRoot) {
  1001. return
  1002. }
  1003. singleRoot = thisRoot
  1004. }
  1005. return hasComment ? singleRoot : undefined
  1006. }
  1007. }
  1008. function isVaporTransition(component: VaporComponent): boolean {
  1009. return getComponentName(component) === 'VaporTransition'
  1010. }
  1011. function handleSetupResult(
  1012. setupResult: any,
  1013. component: VaporComponent,
  1014. instance: VaporComponentInstance,
  1015. ) {
  1016. if (__DEV__) {
  1017. pushWarningContext(instance)
  1018. }
  1019. if (__DEV__ && !isBlock(setupResult)) {
  1020. if (isFunction(component)) {
  1021. warn(`Functional vapor component must return a block directly.`)
  1022. instance.block = []
  1023. } else if (!component.render) {
  1024. warn(
  1025. `Vapor component setup() returned non-block value, and has no render function.`,
  1026. )
  1027. instance.block = []
  1028. } else {
  1029. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  1030. instance.devtoolsRawSetupState = setupResult
  1031. }
  1032. instance.setupState = proxyRefs(setupResult)
  1033. if (__DEV__) {
  1034. instance.setupState = createDevSetupStateProxy(instance)
  1035. }
  1036. devRender(instance)
  1037. }
  1038. } else {
  1039. // component has a render function but no setup function
  1040. // (typically components with only a template and no state)
  1041. if (setupResult === EMPTY_OBJ && component.render) {
  1042. instance.block = callWithErrorHandling(
  1043. component.render,
  1044. instance,
  1045. ErrorCodes.RENDER_FUNCTION,
  1046. )
  1047. } else {
  1048. // in prod result can only be block
  1049. instance.block = setupResult as Block
  1050. }
  1051. }
  1052. // single root, inherit attrs
  1053. if (
  1054. instance.hasFallthrough &&
  1055. component.inheritAttrs !== false &&
  1056. Object.keys(instance.attrs).length
  1057. ) {
  1058. const root = getRootElement(
  1059. instance.block,
  1060. // attach attrs to root dynamic fragments for applying during each update
  1061. frag => (frag.attrs = instance.attrs),
  1062. false,
  1063. )
  1064. if (root) {
  1065. renderEffect(() => {
  1066. const attrs =
  1067. isFunction(component) && !isVaporTransition(component)
  1068. ? getFunctionalFallthrough(instance.attrs)
  1069. : instance.attrs
  1070. if (attrs) applyFallthroughProps(root, attrs)
  1071. })
  1072. } else if (
  1073. __DEV__ &&
  1074. ((!instance.accessedAttrs &&
  1075. isArray(instance.block) &&
  1076. instance.block.length) ||
  1077. // preventing attrs fallthrough on Teleport
  1078. // consistent with VDOM Teleport behavior
  1079. isTeleportFragment(instance.block))
  1080. ) {
  1081. warnExtraneousAttributes(instance.attrs)
  1082. }
  1083. }
  1084. if (__DEV__) {
  1085. popWarningContext()
  1086. }
  1087. }
  1088. export function getCurrentScopeId(): string | undefined {
  1089. const scopeOwner = getScopeOwner()
  1090. return scopeOwner ? scopeOwner.type.__scopeId : undefined
  1091. }