54727f9874
e.g. by replacing `__VUE_OPTIONS_API__` to `false` using webpack's `DefinePlugin`, the final bundle will drop all code supporting the options API. This does not break existing usage, but requires the user to explicitly configure the feature flags via bundlers to properly tree-shake the disabled branches. As a result, users will see a console warning if the flags have not been properly configured.
275 lines
7.8 KiB
TypeScript
275 lines
7.8 KiB
TypeScript
import {
|
|
Component,
|
|
Data,
|
|
validateComponentName,
|
|
PublicAPIComponent
|
|
} from './component'
|
|
import { ComponentOptions } from './componentOptions'
|
|
import { ComponentPublicInstance } from './componentProxy'
|
|
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<HostElement = any> {
|
|
version: string
|
|
config: AppConfig
|
|
use(plugin: Plugin, ...options: any[]): this
|
|
mixin(mixin: ComponentOptions): this
|
|
component(name: string): PublicAPIComponent | undefined
|
|
component(name: string, component: PublicAPIComponent): 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<T>(key: InjectionKey<T> | string, value: T): this
|
|
|
|
// internal, but we need to expose these for the server-renderer and devtools
|
|
_component: Component
|
|
_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<string, OptionMergeFunction>
|
|
globalProperties: Record<string, any>
|
|
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<string, PublicAPIComponent>
|
|
directives: Record<string, Directive>
|
|
provides: Record<string | symbol, any>
|
|
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<HostElement> = (
|
|
rootComponent: PublicAPIComponent,
|
|
rootProps?: Data | null
|
|
) => App<HostElement>
|
|
|
|
export function createAppAPI<HostElement>(
|
|
render: RootRenderFunction,
|
|
hydrate?: RootHydrateFunction
|
|
): CreateAppFunction<HostElement> {
|
|
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 Component,
|
|
_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?: PublicAPIComponent): 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 Component, 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<Node, Element>, 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 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
|
|
}
|
|
}
|