apiCreateApp.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. import {
  2. type Component,
  3. type ComponentInternalInstance,
  4. type ConcreteComponent,
  5. type Data,
  6. type GenericComponent,
  7. type GenericComponentInstance,
  8. validateComponentName,
  9. } from './component'
  10. import type {
  11. ComponentOptions,
  12. MergedComponentOptions,
  13. RuntimeCompilerOptions,
  14. } from './componentOptions'
  15. import type {
  16. ComponentCustomProperties,
  17. ComponentPublicInstance,
  18. } from './componentPublicInstance'
  19. import { type Directive, validateDirectiveName } from './directives'
  20. import type {
  21. ElementNamespace,
  22. RootRenderFunction,
  23. UnmountComponentFn,
  24. } from './renderer'
  25. import type { InjectionKey } from './apiInject'
  26. import { warn } from './warning'
  27. import type { VNode } from './vnode'
  28. import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
  29. import { NO, extend, hasOwn, isFunction, isObject } from '@vue/shared'
  30. import { type SuspenseBoundary, type TransitionHooks, version } from '.'
  31. import { installAppCompatProperties } from './compat/global'
  32. import type { NormalizedPropsOptions } from './componentProps'
  33. import type { ObjectEmitsOptions } from './componentEmits'
  34. import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
  35. import type { DefineComponent } from './apiDefineComponent'
  36. export interface App<HostElement = any> {
  37. vapor?: boolean
  38. version: string
  39. config: AppConfig
  40. use<Options extends unknown[]>(
  41. plugin: Plugin<Options>,
  42. ...options: NoInfer<Options>
  43. ): this
  44. use<Options>(plugin: Plugin<Options>, options: NoInfer<Options>): this
  45. mixin(mixin: ComponentOptions): this
  46. component(name: string): Component | undefined
  47. component<T extends Component | DefineComponent>(
  48. name: string,
  49. component: T,
  50. ): this
  51. directive<
  52. HostElement = any,
  53. Value = any,
  54. Modifiers extends string = string,
  55. Arg = any,
  56. >(
  57. name: string,
  58. ): Directive<HostElement, Value, Modifiers, Arg> | undefined
  59. directive<
  60. HostElement = any,
  61. Value = any,
  62. Modifiers extends string = string,
  63. Arg = any,
  64. >(
  65. name: string,
  66. directive: Directive<HostElement, Value, Modifiers, Arg>,
  67. ): this
  68. mount(
  69. rootContainer: HostElement | string,
  70. /**
  71. * @internal
  72. */
  73. isHydrate?: boolean,
  74. /**
  75. * @internal
  76. */
  77. namespace?: boolean | ElementNamespace,
  78. /**
  79. * @internal
  80. */
  81. vnode?: VNode,
  82. ): ComponentPublicInstance
  83. unmount(): void
  84. onUnmount(cb: () => void): void
  85. provide<T, K = InjectionKey<T> | string | number>(
  86. key: K,
  87. value: K extends InjectionKey<infer V> ? V : T,
  88. ): this
  89. /**
  90. * Runs a function with the app as active instance. This allows using of `inject()` within the function to get access
  91. * to variables provided via `app.provide()`.
  92. *
  93. * @param fn - function to run with the app as active instance
  94. */
  95. runWithContext<T>(fn: () => T): T
  96. // internal, but we need to expose these for the server-renderer and devtools
  97. _uid: number
  98. _component: GenericComponent
  99. _props: Data | null
  100. _container: HostElement | null
  101. _context: AppContext
  102. _instance: GenericComponentInstance | null
  103. /**
  104. * @internal custom element vnode
  105. */
  106. _ceVNode?: VNode
  107. /**
  108. * @internal vapor custom element instance
  109. */
  110. _ceComponent?: GenericComponentInstance | null
  111. /**
  112. * v2 compat only
  113. */
  114. filter?(name: string): Function | undefined
  115. filter?(name: string, filter: Function): this
  116. /**
  117. * @internal v3 compat only
  118. */
  119. _createRoot?(options: ComponentOptions): ComponentPublicInstance
  120. }
  121. export type OptionMergeFunction = (to: unknown, from: unknown) => any
  122. /**
  123. * Shared app config between vdom and vapor
  124. */
  125. export interface GenericAppConfig {
  126. performance?: boolean
  127. errorHandler?: (
  128. err: unknown,
  129. instance: ComponentPublicInstance | null,
  130. info: string,
  131. ) => void
  132. warnHandler?: (
  133. msg: string,
  134. instance: ComponentPublicInstance | null,
  135. trace: string,
  136. ) => void
  137. /**
  138. * Whether to throw unhandled errors in production.
  139. * Default is `false` to avoid crashing on any error (and only logs it)
  140. * But in some cases, e.g. SSR, throwing might be more desirable.
  141. */
  142. throwUnhandledErrorInProduction?: boolean
  143. /**
  144. * Prefix for all useId() calls within this app
  145. */
  146. idPrefix?: string
  147. }
  148. export interface AppConfig extends GenericAppConfig {
  149. // @private
  150. readonly isNativeTag: (tag: string) => boolean
  151. optionMergeStrategies: Record<string, OptionMergeFunction>
  152. globalProperties: ComponentCustomProperties & Record<string, any>
  153. /**
  154. * Options to pass to `@vue/compiler-dom`.
  155. * Only supported in runtime compiler build.
  156. */
  157. compilerOptions: RuntimeCompilerOptions
  158. /**
  159. * @deprecated use config.compilerOptions.isCustomElement
  160. */
  161. isCustomElement?: (tag: string) => boolean
  162. }
  163. /**
  164. * The vapor in vdom implementation is in runtime-vapor/src/vdomInterop.ts
  165. */
  166. export interface VaporInteropInterface {
  167. mount(
  168. vnode: VNode,
  169. container: any,
  170. anchor: any,
  171. parentComponent: ComponentInternalInstance | null,
  172. parentSuspense: SuspenseBoundary | null,
  173. ): GenericComponentInstance // VaporComponentInstance
  174. update(n1: VNode, n2: VNode, shouldUpdate: boolean): void
  175. unmount(vnode: VNode, doRemove?: boolean): void
  176. move(vnode: VNode, container: any, anchor: any): void
  177. slot(
  178. n1: VNode | null,
  179. n2: VNode,
  180. container: any,
  181. anchor: any,
  182. parentComponent: ComponentInternalInstance | null,
  183. ): void
  184. hydrate(
  185. vnode: VNode,
  186. node: any,
  187. container: any,
  188. anchor: any,
  189. parentComponent: ComponentInternalInstance | null,
  190. parentSuspense: SuspenseBoundary | null,
  191. ): Node
  192. hydrateSlot(vnode: VNode, node: any): Node
  193. activate(
  194. vnode: VNode,
  195. container: any,
  196. anchor: any,
  197. parentComponent: ComponentInternalInstance,
  198. ): void
  199. deactivate(vnode: VNode, container: any): void
  200. setTransitionHooks(
  201. component: ComponentInternalInstance,
  202. transition: TransitionHooks,
  203. ): void
  204. vdomMount: (
  205. component: ConcreteComponent,
  206. parentComponent: any,
  207. props?: any,
  208. slots?: any,
  209. ) => any
  210. vdomUnmount: UnmountComponentFn
  211. vdomSlot: (
  212. slots: any,
  213. name: string | (() => string),
  214. props: Record<string, any>,
  215. parentComponent: any, // VaporComponentInstance
  216. fallback?: any, // VaporSlot
  217. ) => any
  218. }
  219. /**
  220. * Minimal app context shared between vdom and vapor
  221. */
  222. export interface GenericAppContext {
  223. app: App // for devtools
  224. config: GenericAppConfig
  225. provides: Record<string | symbol, any>
  226. components?: Record<string, Component>
  227. directives?: Record<string, Directive>
  228. /**
  229. * HMR only
  230. * @internal
  231. */
  232. reload?: () => void
  233. /**
  234. * @internal vapor interop only
  235. */
  236. vapor?: VaporInteropInterface
  237. }
  238. export interface AppContext extends GenericAppContext {
  239. config: AppConfig
  240. components: Record<string, Component>
  241. directives: Record<string, Directive>
  242. mixins: ComponentOptions[]
  243. /**
  244. * Cache for merged/normalized component options
  245. * Each app instance has its own cache because app-level global mixins and
  246. * optionMergeStrategies can affect merge behavior.
  247. * @internal
  248. */
  249. optionsCache: WeakMap<ComponentOptions, MergedComponentOptions>
  250. /**
  251. * Cache for normalized props options
  252. * @internal
  253. */
  254. propsCache: WeakMap<ConcreteComponent, NormalizedPropsOptions>
  255. /**
  256. * Cache for normalized emits options
  257. * @internal
  258. */
  259. emitsCache: WeakMap<ConcreteComponent, ObjectEmitsOptions | null>
  260. /**
  261. * v2 compat only
  262. * @internal
  263. */
  264. filters?: Record<string, Function>
  265. }
  266. type PluginInstallFunction<Options = any[]> = Options extends unknown[]
  267. ? (app: App, ...options: Options) => any
  268. : (app: App, options: Options) => any
  269. export type ObjectPlugin<Options = any[]> = {
  270. install: PluginInstallFunction<Options>
  271. }
  272. export type FunctionPlugin<Options = any[]> = PluginInstallFunction<Options> &
  273. Partial<ObjectPlugin<Options>>
  274. export type Plugin<
  275. Options = any[],
  276. // TODO: in next major Options extends unknown[] and remove P
  277. P extends unknown[] = Options extends unknown[] ? Options : [Options],
  278. > = FunctionPlugin<P> | ObjectPlugin<P>
  279. export function createAppContext(): AppContext {
  280. return {
  281. app: null as any,
  282. config: {
  283. isNativeTag: NO,
  284. performance: false,
  285. globalProperties: {},
  286. optionMergeStrategies: {},
  287. errorHandler: undefined,
  288. warnHandler: undefined,
  289. compilerOptions: {},
  290. },
  291. mixins: [],
  292. components: {},
  293. directives: {},
  294. provides: Object.create(null),
  295. optionsCache: new WeakMap(),
  296. propsCache: new WeakMap(),
  297. emitsCache: new WeakMap(),
  298. }
  299. }
  300. export type CreateAppFunction<HostElement, Comp = Component> = (
  301. rootComponent: Comp,
  302. rootProps?: Data | null,
  303. ) => App<HostElement>
  304. let uid = 0
  305. export type AppMountFn<HostElement> = (
  306. app: App,
  307. rootContainer: HostElement,
  308. isHydrate?: boolean,
  309. namespace?: boolean | ElementNamespace,
  310. ) => GenericComponentInstance
  311. export type AppUnmountFn = (app: App) => void
  312. /**
  313. * @internal
  314. */
  315. export function createAppAPI<HostElement, Comp = Component>(
  316. // render: RootRenderFunction<HostElement>,
  317. // hydrate?: RootHydrateFunction,
  318. mount: AppMountFn<HostElement>,
  319. unmount: AppUnmountFn,
  320. getPublicInstance: (instance: GenericComponentInstance) => any,
  321. render?: RootRenderFunction,
  322. ): CreateAppFunction<HostElement, Comp> {
  323. return function createApp(rootComponent, rootProps = null) {
  324. if (!isFunction(rootComponent)) {
  325. rootComponent = extend({}, rootComponent)
  326. }
  327. if (rootProps != null && !isObject(rootProps)) {
  328. __DEV__ && warn(`root props passed to app.mount() must be an object.`)
  329. rootProps = null
  330. }
  331. const context = createAppContext()
  332. const installedPlugins = new WeakSet()
  333. const pluginCleanupFns: Array<() => any> = []
  334. let isMounted = false
  335. const app: App = (context.app = {
  336. _uid: uid++,
  337. _component: rootComponent as ConcreteComponent,
  338. _props: rootProps,
  339. _container: null,
  340. _context: context,
  341. _instance: null,
  342. version,
  343. get config() {
  344. return context.config
  345. },
  346. set config(v) {
  347. if (__DEV__) {
  348. warn(
  349. `app.config cannot be replaced. Modify individual options instead.`,
  350. )
  351. }
  352. },
  353. use(plugin: Plugin, ...options: any[]) {
  354. if (installedPlugins.has(plugin)) {
  355. __DEV__ && warn(`Plugin has already been applied to target app.`)
  356. } else if (plugin && isFunction(plugin.install)) {
  357. installedPlugins.add(plugin)
  358. plugin.install(app, ...options)
  359. } else if (isFunction(plugin)) {
  360. installedPlugins.add(plugin)
  361. plugin(app, ...options)
  362. } else if (__DEV__) {
  363. warn(
  364. `A plugin must either be a function or an object with an "install" ` +
  365. `function.`,
  366. )
  367. }
  368. return app
  369. },
  370. mixin(mixin: ComponentOptions) {
  371. if (__FEATURE_OPTIONS_API__) {
  372. if (!context.mixins.includes(mixin)) {
  373. context.mixins.push(mixin)
  374. } else if (__DEV__) {
  375. warn(
  376. 'Mixin has already been applied to target app' +
  377. (mixin.name ? `: ${mixin.name}` : ''),
  378. )
  379. }
  380. } else if (__DEV__) {
  381. warn('Mixins are only available in builds supporting Options API')
  382. }
  383. return app
  384. },
  385. component(name: string, component?: Component): any {
  386. if (__DEV__) {
  387. validateComponentName(name, context.config)
  388. }
  389. if (!component) {
  390. return context.components[name]
  391. }
  392. if (__DEV__ && context.components[name]) {
  393. warn(`Component "${name}" has already been registered in target app.`)
  394. }
  395. context.components[name] = component
  396. return app
  397. },
  398. directive(name: string, directive?: Directive) {
  399. if (__DEV__) {
  400. validateDirectiveName(name)
  401. }
  402. if (!directive) {
  403. return context.directives[name] as any
  404. }
  405. if (__DEV__ && context.directives[name]) {
  406. warn(`Directive "${name}" has already been registered in target app.`)
  407. }
  408. context.directives[name] = directive
  409. return app
  410. },
  411. mount(
  412. rootContainer: HostElement & { __vue_app__?: App },
  413. isHydrate?: boolean,
  414. namespace?: boolean | ElementNamespace,
  415. ): any {
  416. if (!isMounted) {
  417. // #5571
  418. if (__DEV__ && rootContainer.__vue_app__) {
  419. warn(
  420. `There is already an app instance mounted on the host container.\n` +
  421. ` If you want to mount another app on the same host container,` +
  422. ` you need to unmount the previous app by calling \`app.unmount()\` first.`,
  423. )
  424. }
  425. const instance = mount(app, rootContainer, isHydrate, namespace)
  426. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  427. app._instance = instance
  428. devtoolsInitApp(app, version)
  429. }
  430. isMounted = true
  431. app._container = rootContainer
  432. // for devtools and telemetry
  433. rootContainer.__vue_app__ = app
  434. return getPublicInstance(instance)
  435. } else if (__DEV__) {
  436. warn(
  437. `App has already been mounted.\n` +
  438. `If you want to remount the same app, move your app creation logic ` +
  439. `into a factory function and create fresh app instances for each ` +
  440. `mount - e.g. \`const createMyApp = () => createApp(App)\``,
  441. )
  442. }
  443. },
  444. onUnmount(cleanupFn: () => void) {
  445. if (__DEV__ && typeof cleanupFn !== 'function') {
  446. warn(
  447. `Expected function as first argument to app.onUnmount(), ` +
  448. `but got ${typeof cleanupFn}`,
  449. )
  450. }
  451. pluginCleanupFns.push(cleanupFn)
  452. },
  453. unmount() {
  454. if (isMounted) {
  455. callWithAsyncErrorHandling(
  456. pluginCleanupFns,
  457. app._instance,
  458. ErrorCodes.APP_UNMOUNT_CLEANUP,
  459. )
  460. unmount(app)
  461. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  462. app._instance = null
  463. devtoolsUnmountApp(app)
  464. }
  465. delete app._container.__vue_app__
  466. } else if (__DEV__) {
  467. warn(`Cannot unmount an app that is not mounted.`)
  468. }
  469. },
  470. provide(key, value) {
  471. if (__DEV__ && (key as string | symbol) in context.provides) {
  472. if (hasOwn(context.provides, key as string | symbol)) {
  473. warn(
  474. `App already provides property with key "${String(key)}". ` +
  475. `It will be overwritten with the new value.`,
  476. )
  477. } else {
  478. // #13212, context.provides can inherit the provides object from parent on custom elements
  479. warn(
  480. `App already provides property with key "${String(key)}" inherited from its parent element. ` +
  481. `It will be overwritten with the new value.`,
  482. )
  483. }
  484. }
  485. context.provides[key as string | symbol] = value
  486. return app
  487. },
  488. runWithContext(fn) {
  489. const lastApp = currentApp
  490. currentApp = app
  491. try {
  492. return fn()
  493. } finally {
  494. currentApp = lastApp
  495. }
  496. },
  497. })
  498. if (__COMPAT__) {
  499. installAppCompatProperties(
  500. app,
  501. context,
  502. // vapor doesn't have compat mode so this is always passed
  503. render!,
  504. )
  505. }
  506. return app
  507. }
  508. }
  509. /**
  510. * @internal Used to identify the current app when using `inject()` within
  511. * `app.runWithContext()`.
  512. */
  513. export let currentApp: App<unknown> | null = null