import { ConcreteComponent, Data, validateComponentName, Component } from './component' import { ComponentOptions } from './componentOptions' import { ComponentPublicInstance } from './componentPublicInstance' import { Directive, validateDirectiveName } from './directives' import { RootRenderFunction } from './renderer' import { InjectionKey } from './apiInject' import { isFunction, NO, isObject } from '@vue/shared' import { warn } from './warning' import { createVNode, cloneVNode, VNode } from './vnode' import { RootHydrateFunction } from './hydration' import { devtoolsInitApp, devtoolsUnmountApp } from './devtools' import { version } from '.' export interface App { version: string config: AppConfig use(plugin: Plugin, ...options: any[]): this mixin(mixin: ComponentOptions): this component(name: string): Component | undefined component(name: string, component: Component): this directive(name: string): Directive | undefined directive(name: string, directive: Directive): this mount( rootContainer: HostElement | string, isHydrate?: boolean ): ComponentPublicInstance unmount(rootContainer: HostElement | string): void provide(key: InjectionKey | string, value: T): this // internal, but we need to expose these for the server-renderer and devtools _component: ConcreteComponent _props: Data | null _container: HostElement | null _context: AppContext } export type OptionMergeFunction = ( to: unknown, from: unknown, instance: any, key: string ) => any export interface AppConfig { // @private readonly isNativeTag?: (tag: string) => boolean performance: boolean optionMergeStrategies: Record globalProperties: Record isCustomElement: (tag: string) => boolean errorHandler?: ( err: unknown, instance: ComponentPublicInstance | null, info: string ) => void warnHandler?: ( msg: string, instance: ComponentPublicInstance | null, trace: string ) => void } export interface AppContext { app: App // for devtools config: AppConfig mixins: ComponentOptions[] components: Record directives: Record provides: Record reload?: () => void // HMR only } type PluginInstallFunction = (app: App, ...options: any[]) => any export type Plugin = | PluginInstallFunction & { install?: PluginInstallFunction } | { install: PluginInstallFunction } export function createAppContext(): AppContext { return { app: null as any, config: { isNativeTag: NO, performance: false, globalProperties: {}, optionMergeStrategies: {}, isCustomElement: NO, errorHandler: undefined, warnHandler: undefined }, mixins: [], components: {}, directives: {}, provides: Object.create(null) } } export type CreateAppFunction = ( rootComponent: Component, rootProps?: Data | null ) => App export function createAppAPI( render: RootRenderFunction, hydrate?: RootHydrateFunction ): CreateAppFunction { return function createApp(rootComponent, rootProps = null) { if (rootProps != null && !isObject(rootProps)) { __DEV__ && warn(`root props passed to app.mount() must be an object.`) rootProps = null } const context = createAppContext() const installedPlugins = new Set() let isMounted = false const app: App = (context.app = { _component: rootComponent as ConcreteComponent, _props: rootProps, _container: null, _context: context, version, get config() { return context.config }, set config(v) { if (__DEV__) { warn( `app.config cannot be replaced. Modify individual options instead.` ) } }, use(plugin: Plugin, ...options: any[]) { if (installedPlugins.has(plugin)) { __DEV__ && warn(`Plugin has already been applied to target app.`) } else if (plugin && isFunction(plugin.install)) { installedPlugins.add(plugin) plugin.install(app, ...options) } else if (isFunction(plugin)) { installedPlugins.add(plugin) plugin(app, ...options) } else if (__DEV__) { warn( `A plugin must either be a function or an object with an "install" ` + `function.` ) } return app }, mixin(mixin: ComponentOptions) { if (__FEATURE_OPTIONS_API__) { if (!context.mixins.includes(mixin)) { context.mixins.push(mixin) } else if (__DEV__) { warn( 'Mixin has already been applied to target app' + (mixin.name ? `: ${mixin.name}` : '') ) } } else if (__DEV__) { warn('Mixins are only available in builds supporting Options API') } return app }, component(name: string, component?: Component): any { if (__DEV__) { validateComponentName(name, context.config) } if (!component) { return context.components[name] } if (__DEV__ && context.components[name]) { warn(`Component "${name}" has already been registered in target app.`) } context.components[name] = component return app }, directive(name: string, directive?: Directive) { if (__DEV__) { validateDirectiveName(name) } if (!directive) { return context.directives[name] as any } if (__DEV__ && context.directives[name]) { warn(`Directive "${name}" has already been registered in target app.`) } context.directives[name] = directive return app }, mount(rootContainer: HostElement, isHydrate?: boolean): any { if (!isMounted) { const vnode = createVNode( rootComponent as ConcreteComponent, rootProps ) // store app context on the root VNode. // this will be set on the root instance on initial mount. vnode.appContext = context // HMR root reload if (__DEV__) { context.reload = () => { render(cloneVNode(vnode), rootContainer) } } if (isHydrate && hydrate) { hydrate(vnode as VNode, rootContainer as any) } else { render(vnode, rootContainer) } isMounted = true app._container = rootContainer // for devtools and telemetry ;(rootContainer as any).__vue_app__ = app if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { devtoolsInitApp(app, version) } return vnode.component!.proxy } else if (__DEV__) { warn( `App has already been mounted.\n` + `If you want to remount the same app, move your app creation logic ` + `into a factory function and create fresh app instances for each ` + `mount - e.g. \`const createMyApp = () => createApp(App)\`` ) } }, unmount() { if (isMounted) { render(null, app._container) devtoolsUnmountApp(app) } else if (__DEV__) { warn(`Cannot unmount an app that is not mounted.`) } }, provide(key, value) { if (__DEV__ && (key as string | symbol) in context.provides) { warn( `App already provides property with key "${String(key)}". ` + `It will be overwritten with the new value.` ) } // TypeScript doesn't allow symbols as index type // https://github.com/Microsoft/TypeScript/issues/24587 context.provides[key as string] = value return app } }) return app } }