apiCreateApp.ts 14 KB

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