apiCreateApp.ts 15 KB

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