import { isReactive, reactive, stop, track, TrackOpTypes, trigger, TriggerOpTypes } from '@vue/reactivity' import { isFunction, extend, NOOP, isArray, isObject, isString, invokeArrayFns } from '@vue/shared' import { warn } from '../warning' import { cloneVNode, createVNode } from '../vnode' import { RootRenderFunction } from '../renderer' import { App, AppConfig, AppContext, CreateAppFunction, Plugin } from '../apiCreateApp' import { Component, ComponentOptions, createComponentInstance, finishComponentSetup, isRuntimeOnly, setupComponent } from '../component' import { RenderFunction, mergeOptions } from '../componentOptions' import { ComponentPublicInstance } from '../componentPublicInstance' import { devtoolsInitApp, devtoolsUnmountApp } from '../devtools' import { Directive } from '../directives' import { nextTick } from '../scheduler' import { version } from '..' import { installLegacyConfigWarnings, installLegacyOptionMergeStrats, LegacyConfig, legacyOptionMergeStrats } from './globalConfig' import { LegacyDirective } from './customDirective' import { warnDeprecation, DeprecationTypes, assertCompatEnabled, configureCompat, isCompatEnabled, softAssertCompatEnabled } from './compatConfig' import { LegacyPublicInstance } from './instance' /** * @deprecated the default `Vue` export has been removed in Vue 3. The type for * the default export is provided only for migration purposes. Please use * named imports instead - e.g. `import { createApp } from 'vue'`. */ export type CompatVue = Pick & { configureCompat: typeof configureCompat // no inference here since these types are not meant for actual use - they // are merely here to provide type checks for internal implementation and // information for migration. new (options?: ComponentOptions): LegacyPublicInstance version: string config: AppConfig & LegacyConfig nextTick: typeof nextTick use(plugin: Plugin, ...options: any[]): CompatVue mixin(mixin: ComponentOptions): CompatVue component(name: string): Component | undefined component(name: string, component: Component): CompatVue directive(name: string): Directive | undefined directive(name: string, directive: Directive): CompatVue compile(template: string): RenderFunction /** * @deprecated */ extend: (options?: ComponentOptions) => CompatVue /** * @deprecated Vue 3 no longer needs set() for adding new properties. */ set(target: any, key: string | number | symbol, value: any): void /** * @deprecated Vue 3 no longer needs delete() for property deletions. */ delete(target: any, key: string | number | symbol): void /** * @deprecated use `reactive` instead. */ observable: typeof reactive /** * @deprecated filters have been removed from Vue 3. */ filter(name: string, arg?: any): null /** * @internal */ cid: number /** * @internal */ options: ComponentOptions /** * @internal */ util: any /** * @internal */ super: CompatVue } export let isCopyingConfig = false // exported only for test export let singletonApp: App let singletonCtor: CompatVue // Legacy global Vue constructor export function createCompatVue( createApp: CreateAppFunction, createSingletonApp: CreateAppFunction ): CompatVue { singletonApp = createSingletonApp({}) const Vue: CompatVue = (singletonCtor = function Vue( options: ComponentOptions = {} ) { return createCompatApp(options, Vue) } as any) function createCompatApp(options: ComponentOptions = {}, Ctor: any) { assertCompatEnabled(DeprecationTypes.GLOBAL_MOUNT, null) const { data } = options if ( data && !isFunction(data) && softAssertCompatEnabled(DeprecationTypes.OPTIONS_DATA_FN, null) ) { options.data = () => data } const app = createApp(options) if (Ctor !== Vue) { applySingletonPrototype(app, Ctor) } const vm = app._createRoot!(options) if (options.el) { return (vm as any).$mount(options.el) } else { return vm } } Vue.version = __VERSION__ Vue.config = singletonApp.config Vue.use = (p, ...options) => { if (p && isFunction(p.install)) { p.install(Vue as any, ...options) } else if (isFunction(p)) { p(Vue as any, ...options) } return Vue } Vue.mixin = m => { singletonApp.mixin(m) return Vue } Vue.component = ((name: string, comp: Component) => { if (comp) { singletonApp.component(name, comp) return Vue } else { return singletonApp.component(name) } }) as any Vue.directive = ((name: string, dir: Directive | LegacyDirective) => { if (dir) { singletonApp.directive(name, dir as Directive) return Vue } else { return singletonApp.directive(name) } }) as any Vue.options = { _base: Vue } let cid = 1 Vue.cid = cid Vue.nextTick = nextTick const extendCache = new WeakMap() function extendCtor(this: any, extendOptions: ComponentOptions = {}) { assertCompatEnabled(DeprecationTypes.GLOBAL_EXTEND, null) if (isFunction(extendOptions)) { extendOptions = extendOptions.options } if (extendCache.has(extendOptions)) { return extendCache.get(extendOptions) } const Super = this function SubVue(inlineOptions?: ComponentOptions) { if (!inlineOptions) { return createCompatApp(SubVue.options, SubVue) } else { return createCompatApp( mergeOptions( extend({}, SubVue.options), inlineOptions, null, legacyOptionMergeStrats as any ), SubVue ) } } SubVue.super = Super SubVue.prototype = Object.create(Vue.prototype) SubVue.prototype.constructor = SubVue // clone non-primitive base option values for edge case of mutating // extended options const mergeBase: any = {} for (const key in Super.options) { const superValue = Super.options[key] mergeBase[key] = isArray(superValue) ? superValue.slice() : isObject(superValue) ? extend(Object.create(null), superValue) : superValue } SubVue.options = mergeOptions( mergeBase, extendOptions, null, legacyOptionMergeStrats as any ) SubVue.options._base = SubVue SubVue.extend = extendCtor.bind(SubVue) SubVue.mixin = Super.mixin SubVue.use = Super.use SubVue.cid = ++cid extendCache.set(extendOptions, SubVue) return SubVue } Vue.extend = extendCtor.bind(Vue) as any Vue.set = (target, key, value) => { assertCompatEnabled(DeprecationTypes.GLOBAL_SET, null) target[key] = value } Vue.delete = (target, key) => { assertCompatEnabled(DeprecationTypes.GLOBAL_DELETE, null) delete target[key] } Vue.observable = (target: any) => { assertCompatEnabled(DeprecationTypes.GLOBAL_OBSERVABLE, null) return reactive(target) } Vue.filter = ((name: string, filter?: any) => { if (filter) { singletonApp.filter!(name, filter) return Vue } else { return singletonApp.filter!(name) } }) as any // internal utils - these are technically internal but some plugins use it. const util = { warn: __DEV__ ? warn : NOOP, extend, mergeOptions: (parent: any, child: any, vm?: ComponentPublicInstance) => mergeOptions( parent, child, vm && vm.$, vm ? undefined : (legacyOptionMergeStrats as any) ), defineReactive } Object.defineProperty(Vue, 'util', { get() { assertCompatEnabled(DeprecationTypes.GLOBAL_PRIVATE_UTIL, null) return util } }) Vue.configureCompat = configureCompat return Vue } export function installAppCompatProperties( app: App, context: AppContext, render: RootRenderFunction ) { installFilterMethod(app, context) installLegacyOptionMergeStrats(app.config) if (!singletonApp) { // this is the call of creating the singleton itself so the rest is // unnecessary return } installCompatMount(app, context, render) installLegacyAPIs(app) applySingletonAppMutations(app) if (__DEV__) installLegacyConfigWarnings(app.config) } function installFilterMethod(app: App, context: AppContext) { context.filters = {} app.filter = (name: string, filter?: Function): any => { assertCompatEnabled(DeprecationTypes.FILTERS, null) if (!filter) { return context.filters![name] } if (__DEV__ && context.filters![name]) { warn(`Filter "${name}" has already been registered.`) } context.filters![name] = filter return app } } function installLegacyAPIs(app: App) { // expose global API on app instance for legacy plugins Object.defineProperties(app, { // so that app.use() can work with legacy plugins that extend prototypes prototype: { get() { __DEV__ && warnDeprecation(DeprecationTypes.GLOBAL_PROTOTYPE, null) return app.config.globalProperties } }, nextTick: { value: nextTick }, extend: { value: singletonCtor.extend }, set: { value: singletonCtor.set }, delete: { value: singletonCtor.delete }, observable: { value: singletonCtor.observable }, util: { get() { return singletonCtor.util } } }) } function applySingletonAppMutations(app: App) { // copy over asset registries and deopt flag ;['mixins', 'components', 'directives', 'filters', 'deopt'].forEach(key => { // @ts-ignore app._context[key] = singletonApp._context[key] }) // copy over global config mutations isCopyingConfig = true for (const key in singletonApp.config) { if (key === 'isNativeTag') continue if ( isRuntimeOnly() && (key === 'isCustomElement' || key === 'compilerOptions') ) { continue } const val = singletonApp.config[key as keyof AppConfig] // @ts-ignore app.config[key] = val // compat for runtime ignoredElements -> isCustomElement if ( key === 'ignoredElements' && isCompatEnabled(DeprecationTypes.CONFIG_IGNORED_ELEMENTS, null) && !isRuntimeOnly() && isArray(val) ) { app.config.compilerOptions.isCustomElement = tag => { return val.some(v => (isString(v) ? v === tag : v.test(tag))) } } } isCopyingConfig = false applySingletonPrototype(app, singletonCtor) } function applySingletonPrototype(app: App, Ctor: Function) { // copy prototype augmentations as config.globalProperties const enabled = isCompatEnabled(DeprecationTypes.GLOBAL_PROTOTYPE, null) if (enabled) { app.config.globalProperties = Object.create(Ctor.prototype) } let hasPrototypeAugmentations = false const descriptors = Object.getOwnPropertyDescriptors(Ctor.prototype) for (const key in descriptors) { if (key !== 'constructor') { hasPrototypeAugmentations = true if (enabled) { Object.defineProperty( app.config.globalProperties, key, descriptors[key] ) } } } if (__DEV__ && hasPrototypeAugmentations) { warnDeprecation(DeprecationTypes.GLOBAL_PROTOTYPE, null) } } function installCompatMount( app: App, context: AppContext, render: RootRenderFunction ) { let isMounted = false /** * Vue 2 supports the behavior of creating a component instance but not * mounting it, which is no longer possible in Vue 3 - this internal * function simulates that behavior. */ app._createRoot = options => { const component = app._component const vnode = createVNode(component, options.propsData || null) vnode.appContext = context const hasNoRender = !isFunction(component) && !component.render && !component.template const emptyRender = () => {} // create root instance const instance = createComponentInstance(vnode, null, null) // suppress "missing render fn" warning since it can't be determined // until $mount is called if (hasNoRender) { instance.render = emptyRender } setupComponent(instance) vnode.component = instance vnode.isCompatRoot = true // $mount & $destroy // these are defined on ctx and picked up by the $mount/$destroy // public property getters on the instance proxy. // Note: the following assumes DOM environment since the compat build // only targets web. It essentially includes logic for app.mount from // both runtime-core AND runtime-dom. instance.ctx._compat_mount = (selectorOrEl?: string | Element) => { if (isMounted) { __DEV__ && warn(`Root instance is already mounted.`) return } let container: Element if (typeof selectorOrEl === 'string') { // eslint-disable-next-line const result = document.querySelector(selectorOrEl) if (!result) { __DEV__ && warn( `Failed to mount root instance: selector "${selectorOrEl}" returned null.` ) return } container = result } else { // eslint-disable-next-line container = selectorOrEl || document.createElement('div') } const isSVG = container instanceof SVGElement // HMR root reload if (__DEV__) { context.reload = () => { const cloned = cloneVNode(vnode) // compat mode will use instance if not reset to null cloned.component = null render(cloned, container, isSVG) } } // resolve in-DOM template if component did not provide render // and no setup/mixin render functions are provided (by checking // that the instance is still using the placeholder render fn) if (hasNoRender && instance.render === emptyRender) { // root directives check if (__DEV__) { for (let i = 0; i < container.attributes.length; i++) { const attr = container.attributes[i] if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) { warnDeprecation(DeprecationTypes.GLOBAL_MOUNT_CONTAINER, null) break } } } instance.render = null ;(component as ComponentOptions).template = container.innerHTML finishComponentSetup(instance, false, true /* skip options */) } // clear content before mounting container.innerHTML = '' // TODO hydration render(vnode, container, isSVG) if (container instanceof Element) { container.removeAttribute('v-cloak') container.setAttribute('data-v-app', '') } isMounted = true app._container = container // for devtools and telemetry ;(container as any).__vue_app__ = app if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { devtoolsInitApp(app, version) } return instance.proxy! } instance.ctx._compat_destroy = () => { if (isMounted) { render(null, app._container) if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { devtoolsUnmountApp(app) } delete app._container.__vue_app__ } else { const { bum, effects, um } = instance // beforeDestroy hooks if (bum) { invokeArrayFns(bum) } if (isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)) { instance.emit('hook:beforeDestroy') } // stop effects if (effects) { for (let i = 0; i < effects.length; i++) { stop(effects[i]) } } // unmounted hook if (um) { invokeArrayFns(um) } if (isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)) { instance.emit('hook:destroyed') } } } return instance.proxy! } } const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] const patched = new WeakSet() function defineReactive(obj: any, key: string, val: any) { // it's possible for the orignial object to be mutated after being defined // and expecting reactivity... we are covering it here because this seems to // be a bit more common. if (isObject(val) && !isReactive(val) && !patched.has(val)) { const reactiveVal = reactive(val) if (isArray(val)) { methodsToPatch.forEach(m => { // @ts-ignore val[m] = (...args: any[]) => { // @ts-ignore Array.prototype[m].call(reactiveVal, ...args) } }) } else { Object.keys(val).forEach(key => { try { defineReactiveSimple(val, key, val[key]) } catch (e) {} }) } } const i = obj.$ if (i && obj === i.proxy) { // target is a Vue instance - define on instance.ctx defineReactiveSimple(i.ctx, key, val) i.accessCache = Object.create(null) } else if (isReactive(obj)) { obj[key] = val } else { defineReactiveSimple(obj, key, val) } } function defineReactiveSimple(obj: any, key: string, val: any) { val = isObject(val) ? reactive(val) : val Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { track(obj, TrackOpTypes.GET, key) return val }, set(newVal) { val = isObject(newVal) ? reactive(newVal) : newVal trigger(obj, TriggerOpTypes.SET, key, newVal) } }) }