diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index ed1971cb..bda1501b 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -16,7 +16,7 @@ import { devtoolsInitApp, devtoolsUnmountApp } from './devtools' import { isFunction, NO, isObject } from '@vue/shared' import { version } from '.' import { installCompatMount } from './compat/global' -import { installLegacyConfigTraps } from './compat/globalConfig' +import { installLegacyConfigProperties } from './compat/globalConfig' export interface App { version: string @@ -307,7 +307,7 @@ export function createAppAPI( if (__COMPAT__) { installCompatMount(app, context, render, hydrate) - if (__DEV__) installLegacyConfigTraps(app.config) + if (__DEV__) installLegacyConfigProperties(app.config) } return app diff --git a/packages/runtime-core/src/compat/deprecations.ts b/packages/runtime-core/src/compat/deprecations.ts index e91ea920..f33f360d 100644 --- a/packages/runtime-core/src/compat/deprecations.ts +++ b/packages/runtime-core/src/compat/deprecations.ts @@ -18,6 +18,7 @@ export const enum DeprecationTypes { GLOBAL_SET = 'GLOBAL_SET', GLOBAL_DELETE = 'GLOBAL_DELETE', GLOBAL_OBSERVABLE = 'GLOBAL_OBSERVABLE', + GLOBAL_UTIL = 'GLOBAL_UTIL', CONFIG_SILENT = 'CONFIG_SILENT', CONFIG_DEVTOOLS = 'CONFIG_DEVTOOLS', @@ -113,6 +114,12 @@ const deprecationData: Record = { link: `https://v3.vuejs.org/api/basic-reactivity.html` }, + [DeprecationTypes.GLOBAL_UTIL]: { + message: + `Vue.util has been removed. Please refactor to avoid its usage ` + + `since it was an internal API even in Vue 2.` + }, + [DeprecationTypes.CONFIG_SILENT]: { message: `config.silent has been removed because it is not good practice to ` + diff --git a/packages/runtime-core/src/compat/global.ts b/packages/runtime-core/src/compat/global.ts index 93490b2c..13cb05c8 100644 --- a/packages/runtime-core/src/compat/global.ts +++ b/packages/runtime-core/src/compat/global.ts @@ -1,5 +1,19 @@ -import { reactive } from '@vue/reactivity' -import { isFunction } from '@vue/shared' +import { + isReactive, + reactive, + track, + TrackOpTypes, + trigger, + TriggerOpTypes +} from '@vue/reactivity' +import { + isFunction, + extend, + NOOP, + EMPTY_OBJ, + isArray, + isObject +} from '@vue/shared' import { warn } from '../warning' import { cloneVNode, createVNode } from '../vnode' import { RootRenderFunction } from '../renderer' @@ -20,7 +34,7 @@ import { isRuntimeOnly, setupComponent } from '../component' -import { RenderFunction } from '../componentOptions' +import { RenderFunction, mergeOptions } from '../componentOptions' import { ComponentPublicInstance } from '../componentPublicInstance' import { devtoolsInitApp } from '../devtools' import { Directive } from '../directives' @@ -129,17 +143,14 @@ export function createCompatVue( isCopyingConfig = false // copy prototype augmentations as config.globalProperties - const isPrototypeEnabled = isCompatEnabled( - DeprecationTypes.GLOBAL_PROTOTYPE, - null - ) + if (isCompatEnabled(DeprecationTypes.GLOBAL_PROTOTYPE, null)) { + app.config.globalProperties = Ctor.prototype + } let hasPrototypeAugmentations = false for (const key in Ctor.prototype) { if (key !== 'constructor') { hasPrototypeAugmentations = true - } - if (isPrototypeEnabled) { - app.config.globalProperties[key] = Ctor.prototype[key] + break } } if (__DEV__ && hasPrototypeAugmentations) { @@ -228,6 +239,21 @@ export function createCompatVue( // TODO compiler warning for filters (maybe behavior compat?) }) 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.$), + defineReactive + } + Object.defineProperty(Vue, 'util', { + get() { + assertCompatEnabled(DeprecationTypes.GLOBAL_UTIL, null) + return util + } + }) + Vue.configureCompat = configureCompat return Vue @@ -358,3 +384,67 @@ export function installCompatMount( 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 => { + defineReactiveSimple(val, key, val[key]) + }) + } + } + + const i = obj.$ + if (i && obj === i.proxy) { + // Vue instance, add it to data + if (i.data === EMPTY_OBJ) { + i.data = reactive({}) + } + i.data[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) + } + }) +} diff --git a/packages/runtime-core/src/compat/globalConfig.ts b/packages/runtime-core/src/compat/globalConfig.ts index cf081c3e..fab0196f 100644 --- a/packages/runtime-core/src/compat/globalConfig.ts +++ b/packages/runtime-core/src/compat/globalConfig.ts @@ -1,7 +1,8 @@ -import { isArray, isString } from '@vue/shared' +import { extend, isArray, isString } from '@vue/shared' import { AppConfig } from '../apiCreateApp' import { isRuntimeOnly } from '../component' import { isCompatEnabled } from './compatConfig' +import { deepMergeData } from './data' import { DeprecationTypes, warnDeprecation } from './deprecations' import { isCopyingConfig } from './global' @@ -34,7 +35,7 @@ export type LegacyConfig = { } // dev only -export function installLegacyConfigTraps(config: AppConfig) { +export function installLegacyConfigProperties(config: AppConfig) { const legacyConfigOptions: Record = { silent: DeprecationTypes.CONFIG_SILENT, devtools: DeprecationTypes.CONFIG_DEVTOOLS, @@ -72,4 +73,44 @@ export function installLegacyConfigTraps(config: AppConfig) { } }) }) + + // Internal merge strats which are no longer needed in v3, but we need to + // expose them because some v2 plugins will reuse these internal strats to + // merge their custom options. + const strats = config.optionMergeStrategies as any + strats.data = deepMergeData + // lifecycle hooks + strats.beforeCreate = mergeHook + strats.created = mergeHook + strats.beforeMount = mergeHook + strats.mounted = mergeHook + strats.beforeUpdate = mergeHook + strats.updated = mergeHook + strats.beforeDestroy = mergeHook + strats.destroyed = mergeHook + strats.activated = mergeHook + strats.deactivated = mergeHook + strats.errorCaptured = mergeHook + strats.serverPrefetch = mergeHook + // assets + strats.components = mergeObjectOptions + strats.directives = mergeObjectOptions + strats.filters = mergeObjectOptions + // objects + strats.props = mergeObjectOptions + strats.methods = mergeObjectOptions + strats.inject = mergeObjectOptions + strats.computed = mergeObjectOptions + // watch has special merge behavior in v2, but isn't actually needed in v3. + // since we are only exposing these for compat and nobody should be relying + // on the watch-specific behavior, just expose the object merge strat. + strats.watch = mergeObjectOptions +} + +function mergeHook(to: Function[] | undefined, from: Function | Function[]) { + return Array.from(new Set([...(to || []), from])) +} + +function mergeObjectOptions(to: Object | undefined, from: Object | undefined) { + return to ? extend(extend(Object.create(null), to), from) : from } diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 8b029380..3ba0b9d3 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -991,8 +991,12 @@ export function resolveMergedOptions( return (raw.__merged = options) } -function mergeOptions(to: any, from: any, instance: ComponentInternalInstance) { - const strats = instance.appContext.config.optionMergeStrategies +export function mergeOptions( + to: any, + from: any, + instance?: ComponentInternalInstance +) { + const strats = instance && instance.appContext.config.optionMergeStrategies const { mixins, extends: extendsOptions } = from extendsOptions && mergeOptions(to, extendsOptions, instance) @@ -1000,8 +1004,8 @@ function mergeOptions(to: any, from: any, instance: ComponentInternalInstance) { mixins.forEach((m: ComponentOptionsMixin) => mergeOptions(to, m, instance)) for (const key in from) { - if (strats && hasOwn(strats, key)) { - to[key] = strats[key](to[key], from[key], instance.proxy, key) + if (strats && hasOwn(to, key) && hasOwn(strats, key)) { + to[key] = strats[key](to[key], from[key], instance && instance.proxy, key) } else { to[key] = from[key] } diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index f9d95aac..22022317 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -497,20 +497,6 @@ export function createRenderContext(instance: ComponentInternalInstance) { }) }) - // expose global properties - const { globalProperties } = instance.appContext.config - Object.keys(globalProperties).forEach(key => { - Object.defineProperty(target, key, { - configurable: true, - enumerable: false, - get: () => { - const val = globalProperties[key] - return __COMPAT__ && isFunction(val) ? val.bind(instance.proxy) : val - }, - set: NOOP - }) - }) - return target as ComponentRenderContext }