wip: tests for global api compat

This commit is contained in:
Evan You 2021-04-27 17:34:19 -04:00
parent 86703c23a6
commit 1d1af403ca
7 changed files with 466 additions and 67 deletions

View File

@ -1,21 +1,335 @@
import Vue from '@vue/compat' import Vue from '@vue/compat'
import { effect, isReactive } from '@vue/reactivity'
import {
DeprecationTypes,
deprecationData,
toggleDeprecationWarning
} from '../compatConfig'
describe('compat: global API', () => { beforeEach(() => {
beforeEach(() => Vue.configureCompat({ MODE: 2 })) Vue.configureCompat({ MODE: 2 })
afterEach(() => Vue.configureCompat({ MODE: 3 })) })
afterEach(() => {
Vue.configureCompat({ MODE: 3 })
toggleDeprecationWarning(false)
})
describe('GLOBAL_MOUNT', () => {
test('new Vue() with el', () => {
toggleDeprecationWarning(true)
test('should work', () => {
const el = document.createElement('div') const el = document.createElement('div')
el.innerHTML = `{{ msg }}` el.innerHTML = `{{ msg }}`
new Vue({ new Vue({
el, el,
compatConfig: { GLOBAL_MOUNT: true },
data() { data() {
return { return {
msg: 'hello' msg: 'hello'
} }
} }
}) })
expect('global app bootstrapping API has changed').toHaveBeenWarned() expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
).toHaveBeenWarned()
expect(el.innerHTML).toBe('hello')
})
test('new Vue() + $mount', () => {
const el = document.createElement('div')
el.innerHTML = `{{ msg }}`
new Vue({
data() {
return {
msg: 'hello'
}
}
}).$mount(el)
expect(el.innerHTML).toBe('hello') expect(el.innerHTML).toBe('hello')
}) })
}) })
describe('GLOBAL_MOUNT_CONTAINER', () => {
test('should warn', () => {
toggleDeprecationWarning(true)
const el = document.createElement('div')
el.innerHTML = `test`
el.setAttribute('v-bind:id', 'foo')
new Vue().$mount(el)
// warning only
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT_CONTAINER].message
).toHaveBeenWarned()
})
})
describe('GLOBAL_EXTEND', () => {
// https://github.com/vuejs/vue/blob/dev/test/unit/features/global-api/extend.spec.js
it('should correctly merge options', () => {
toggleDeprecationWarning(true)
const Test = Vue.extend({
name: 'test',
a: 1,
b: 2
})
expect(Test.options.a).toBe(1)
expect(Test.options.b).toBe(2)
expect(Test.super).toBe(Vue)
const t = new Test({
a: 2
})
expect(t.$options.a).toBe(2)
expect(t.$options.b).toBe(2)
// inheritance
const Test2 = Test.extend({
a: 2
})
expect(Test2.options.a).toBe(2)
expect(Test2.options.b).toBe(2)
const t2 = new Test2({
a: 3
})
expect(t2.$options.a).toBe(3)
expect(t2.$options.b).toBe(2)
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.GLOBAL_EXTEND].message
).toHaveBeenWarned()
})
it('should work when used as components', () => {
const foo = Vue.extend({
template: '<span>foo</span>'
})
const bar = Vue.extend({
template: '<span>bar</span>'
})
const vm = new Vue({
template: '<div><foo></foo><bar></bar></div>',
components: { foo, bar }
}).$mount()
expect(vm.$el.innerHTML).toBe('<span>foo</span><span>bar</span>')
})
it('should merge lifecycle hooks', () => {
const calls: number[] = []
const A = Vue.extend({
created() {
calls.push(1)
}
})
const B = A.extend({
created() {
calls.push(2)
}
})
new B({
created() {
calls.push(3)
}
})
expect(calls).toEqual([1, 2, 3])
})
it('should not merge nested mixins created with Vue.extend', () => {
const A = Vue.extend({
created: () => {}
})
const B = Vue.extend({
mixins: [A],
created: () => {}
})
const C = Vue.extend({
extends: B,
created: () => {}
})
const D = Vue.extend({
mixins: [C],
created: () => {}
})
expect(D.options.created!.length).toBe(4)
})
it('should merge methods', () => {
const A = Vue.extend({
methods: {
a() {
return this.n
}
}
})
const B = A.extend({
methods: {
b() {
return this.n + 1
}
}
})
const b = new B({
data: () => ({ n: 0 }),
methods: {
c() {
return this.n + 2
}
}
}) as any
expect(b.a()).toBe(0)
expect(b.b()).toBe(1)
expect(b.c()).toBe(2)
})
it('should merge assets', () => {
const A = Vue.extend({
components: {
aa: {
template: '<div>A</div>'
}
}
})
const B = A.extend({
components: {
bb: {
template: '<div>B</div>'
}
}
})
const b = new B({
template: '<div><aa></aa><bb></bb></div>'
}).$mount()
expect(b.$el.innerHTML).toBe('<div>A</div><div>B</div>')
})
it('caching', () => {
const options = {
template: '<div></div>'
}
const A = Vue.extend(options)
const B = Vue.extend(options)
expect(A).toBe(B)
})
it('extended options should use different identify from parent', () => {
const A = Vue.extend({ computed: {} })
const B = A.extend()
B.options.computed.b = () => 'foo'
expect(B.options.computed).not.toBe(A.options.computed)
expect(A.options.computed.b).toBeUndefined()
})
})
describe('GLOBAL_PROTOTYPE', () => {
test('plain properties', () => {
toggleDeprecationWarning(true)
Vue.prototype.$test = 1
const vm = new Vue() as any
expect(vm.$test).toBe(1)
delete Vue.prototype.$test
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.GLOBAL_PROTOTYPE].message
).toHaveBeenWarned()
})
test('method this context', () => {
Vue.prototype.$test = function() {
return this.msg
}
const vm = new Vue({
data() {
return { msg: 'method' }
}
}) as any
expect(vm.$test()).toBe('method')
delete Vue.prototype.$test
})
test('defined properties', () => {
Object.defineProperty(Vue.prototype, '$test', {
configurable: true,
get() {
return this.msg
}
})
const vm = new Vue({
data() {
return { msg: 'getter' }
}
}) as any
expect(vm.$test).toBe('getter')
delete Vue.prototype.$test
})
test('extended prototype', async () => {
const Foo = Vue.extend()
Foo.prototype.$test = 1
const vm = new Foo() as any
expect(vm.$test).toBe(1)
const plain = new Vue() as any
expect(plain.$test).toBeUndefined()
})
})
describe('GLOBAL_SET/DELETE', () => {
test('set', () => {
toggleDeprecationWarning(true)
const obj: any = {}
Vue.set(obj, 'foo', 1)
expect(obj.foo).toBe(1)
expect(
deprecationData[DeprecationTypes.GLOBAL_SET].message
).toHaveBeenWarned()
})
test('delete', () => {
toggleDeprecationWarning(true)
const obj: any = { foo: 1 }
Vue.delete(obj, 'foo')
expect('foo' in obj).toBe(false)
expect(
deprecationData[DeprecationTypes.GLOBAL_DELETE].message
).toHaveBeenWarned()
})
})
describe('GLOBAL_OBSERVABLE', () => {
test('should work', () => {
toggleDeprecationWarning(true)
const obj = Vue.observable({})
expect(isReactive(obj)).toBe(true)
expect(
deprecationData[DeprecationTypes.GLOBAL_OBSERVABLE].message
).toHaveBeenWarned()
})
})
describe('GLOBAL_PRIVATE_UTIL', () => {
test('defineReactive', () => {
toggleDeprecationWarning(true)
const obj: any = {}
// @ts-ignore
Vue.util.defineReactive(obj, 'test', 1)
let n
effect(() => {
n = obj.test
})
expect(n).toBe(1)
obj.test++
expect(n).toBe(2)
expect(
deprecationData[DeprecationTypes.GLOBAL_PRIVATE_UTIL].message
).toHaveBeenWarned()
})
})

View File

@ -17,7 +17,7 @@ export const enum DeprecationTypes {
GLOBAL_SET = 'GLOBAL_SET', GLOBAL_SET = 'GLOBAL_SET',
GLOBAL_DELETE = 'GLOBAL_DELETE', GLOBAL_DELETE = 'GLOBAL_DELETE',
GLOBAL_OBSERVABLE = 'GLOBAL_OBSERVABLE', GLOBAL_OBSERVABLE = 'GLOBAL_OBSERVABLE',
GLOBAL_UTIL = 'GLOBAL_UTIL', GLOBAL_PRIVATE_UTIL = 'GLOBAL_PRIVATE_UTIL',
CONFIG_SILENT = 'CONFIG_SILENT', CONFIG_SILENT = 'CONFIG_SILENT',
CONFIG_DEVTOOLS = 'CONFIG_DEVTOOLS', CONFIG_DEVTOOLS = 'CONFIG_DEVTOOLS',
@ -70,7 +70,7 @@ type DeprecationData = {
link?: string link?: string
} }
const deprecationData: Record<DeprecationTypes, DeprecationData> = { export const deprecationData: Record<DeprecationTypes, DeprecationData> = {
[DeprecationTypes.GLOBAL_MOUNT]: { [DeprecationTypes.GLOBAL_MOUNT]: {
message: message:
`The global app bootstrapping API has changed: vm.$mount() and the "el" ` + `The global app bootstrapping API has changed: vm.$mount() and the "el" ` +
@ -119,7 +119,7 @@ const deprecationData: Record<DeprecationTypes, DeprecationData> = {
link: `https://v3.vuejs.org/api/basic-reactivity.html` link: `https://v3.vuejs.org/api/basic-reactivity.html`
}, },
[DeprecationTypes.GLOBAL_UTIL]: { [DeprecationTypes.GLOBAL_PRIVATE_UTIL]: {
message: message:
`Vue.util has been removed. Please refactor to avoid its usage ` + `Vue.util has been removed. Please refactor to avoid its usage ` +
`since it was an internal API even in Vue 2.` `since it was an internal API even in Vue 2.`
@ -437,6 +437,13 @@ const deprecationData: Record<DeprecationTypes, DeprecationData> = {
const instanceWarned: Record<string, true> = Object.create(null) const instanceWarned: Record<string, true> = Object.create(null)
const warnCount: Record<string, number> = Object.create(null) const warnCount: Record<string, number> = Object.create(null)
// test only
let warningEnabled = true
export function toggleDeprecationWarning(flag: boolean) {
warningEnabled = flag
}
export function warnDeprecation( export function warnDeprecation(
key: DeprecationTypes, key: DeprecationTypes,
instance: ComponentInternalInstance | null, instance: ComponentInternalInstance | null,
@ -445,6 +452,9 @@ export function warnDeprecation(
if (!__DEV__) { if (!__DEV__) {
return return
} }
if (__TEST__ && !warningEnabled) {
return
}
instance = instance || getCurrentInstance() instance = instance || getCurrentInstance()
@ -463,14 +473,14 @@ export function warnDeprecation(
// skip if the same warning is emitted for the same component type // skip if the same warning is emitted for the same component type
const componentDupKey = dupKey + compId const componentDupKey = dupKey + compId
if (componentDupKey in instanceWarned) { if (!__TEST__ && componentDupKey in instanceWarned) {
return return
} }
instanceWarned[componentDupKey] = true instanceWarned[componentDupKey] = true
// same warning, but different component. skip the long message and just // same warning, but different component. skip the long message and just
// log the key and count. // log the key and count.
if (dupKey in warnCount) { if (!__TEST__ && dupKey in warnCount) {
warn(`(deprecation ${key}) (${++warnCount[dupKey] + 1})`) warn(`(deprecation ${key}) (${++warnCount[dupKey] + 1})`)
return return
} }

View File

@ -1,5 +1,6 @@
import { isPlainObject } from '@vue/shared' import { isFunction, isPlainObject } from '@vue/shared'
import { ComponentInternalInstance } from '../component' import { ComponentInternalInstance } from '../component'
import { ComponentPublicInstance } from '../componentPublicInstance'
import { DeprecationTypes, warnDeprecation } from './compatConfig' import { DeprecationTypes, warnDeprecation } from './compatConfig'
export function deepMergeData( export function deepMergeData(
@ -19,3 +20,19 @@ export function deepMergeData(
} }
} }
} }
export function mergeDataOption(to: any, from: any) {
if (!from) {
return to
}
if (!to) {
return from
}
return function mergedDataFn(this: ComponentPublicInstance) {
return deepMergeData(
isFunction(to) ? to.call(this, this) : to,
isFunction(from) ? from.call(this, this) : from,
this.$
)
}
}

View File

@ -25,7 +25,6 @@ import {
CreateAppFunction, CreateAppFunction,
Plugin Plugin
} from '../apiCreateApp' } from '../apiCreateApp'
import { defineComponent } from '../apiDefineComponent'
import { import {
Component, Component,
ComponentOptions, ComponentOptions,
@ -40,7 +39,7 @@ import { devtoolsInitApp } from '../devtools'
import { Directive } from '../directives' import { Directive } from '../directives'
import { nextTick } from '../scheduler' import { nextTick } from '../scheduler'
import { version } from '..' import { version } from '..'
import { LegacyConfig } from './globalConfig' import { LegacyConfig, legacyOptionMergeStrats } from './globalConfig'
import { LegacyDirective } from './customDirective' import { LegacyDirective } from './customDirective'
import { import {
warnDeprecation, warnDeprecation,
@ -50,6 +49,7 @@ import {
isCompatEnabled, isCompatEnabled,
softAssertCompatEnabled softAssertCompatEnabled
} from './compatConfig' } from './compatConfig'
import { LegacyPublicInstance } from './instance'
/** /**
* @deprecated the default `Vue` export has been removed in Vue 3. The type for * @deprecated the default `Vue` export has been removed in Vue 3. The type for
@ -57,15 +57,17 @@ import {
* named imports instead - e.g. `import { createApp } from 'vue'`. * named imports instead - e.g. `import { createApp } from 'vue'`.
*/ */
export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & { export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & {
configureCompat: typeof configureCompat
// no inference here since these types are not meant for actual use - they // no inference here since these types are not meant for actual use - they
// are merely here to provide type checks for internal implementation and // are merely here to provide type checks for internal implementation and
// information for migration. // information for migration.
new (options?: ComponentOptions): ComponentPublicInstance new (options?: ComponentOptions): LegacyPublicInstance
version: string version: string
config: AppConfig & LegacyConfig config: AppConfig & LegacyConfig
extend: typeof defineComponent extend: (options?: ComponentOptions) => CompatVue
nextTick: typeof nextTick nextTick: typeof nextTick
use(plugin: Plugin, ...options: any[]): CompatVue use(plugin: Plugin, ...options: any[]): CompatVue
@ -102,8 +104,10 @@ export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & {
* @internal * @internal
*/ */
options: ComponentOptions options: ComponentOptions
/**
configureCompat: typeof configureCompat * @internal
*/
super: CompatVue
} }
export let isCopyingConfig = false export let isCopyingConfig = false
@ -184,32 +188,55 @@ export function createCompatVue(
let cid = 1 let cid = 1
Vue.cid = cid Vue.cid = cid
const extendCache = new WeakMap()
function extendCtor(this: any, extendOptions: ComponentOptions = {}) { function extendCtor(this: any, extendOptions: ComponentOptions = {}) {
assertCompatEnabled(DeprecationTypes.GLOBAL_EXTEND, null) assertCompatEnabled(DeprecationTypes.GLOBAL_EXTEND, null)
if (isFunction(extendOptions)) { if (isFunction(extendOptions)) {
extendOptions = extendOptions.options extendOptions = extendOptions.options
} }
if (extendCache.has(extendOptions)) {
return extendCache.get(extendOptions)
}
const Super = this const Super = this
function SubVue(inlineOptions?: ComponentOptions) { function SubVue(inlineOptions?: ComponentOptions) {
if (!inlineOptions) { if (!inlineOptions) {
return createCompatApp(extendOptions, SubVue) return createCompatApp(SubVue.options, SubVue)
} else { } else {
return createCompatApp( return createCompatApp(
{ mergeOptions(
el: inlineOptions.el, extend({}, SubVue.options),
extends: extendOptions, inlineOptions,
mixins: [inlineOptions] null,
}, legacyOptionMergeStrats as any
),
SubVue SubVue
) )
} }
} }
SubVue.super = Super
SubVue.prototype = Object.create(Vue.prototype) SubVue.prototype = Object.create(Vue.prototype)
SubVue.prototype.constructor = SubVue 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( SubVue.options = mergeOptions(
extend({}, Super.options) as ComponentOptions, mergeBase,
extendOptions extendOptions,
null,
legacyOptionMergeStrats as any
) )
SubVue.options._base = SubVue SubVue.options._base = SubVue
@ -217,6 +244,8 @@ export function createCompatVue(
SubVue.mixin = Super.mixin SubVue.mixin = Super.mixin
SubVue.use = Super.use SubVue.use = Super.use
SubVue.cid = ++cid SubVue.cid = ++cid
extendCache.set(extendOptions, SubVue)
return SubVue return SubVue
} }
@ -279,12 +308,17 @@ export function createCompatVue(
warn: __DEV__ ? warn : NOOP, warn: __DEV__ ? warn : NOOP,
extend, extend,
mergeOptions: (parent: any, child: any, vm?: ComponentPublicInstance) => mergeOptions: (parent: any, child: any, vm?: ComponentPublicInstance) =>
mergeOptions(parent, child, vm && vm.$), mergeOptions(
parent,
child,
vm && vm.$,
vm ? undefined : (legacyOptionMergeStrats as any)
),
defineReactive defineReactive
} }
Object.defineProperty(Vue, 'util', { Object.defineProperty(Vue, 'util', {
get() { get() {
assertCompatEnabled(DeprecationTypes.GLOBAL_UTIL, null) assertCompatEnabled(DeprecationTypes.GLOBAL_PRIVATE_UTIL, null)
return util return util
} }
}) })
@ -332,7 +366,7 @@ export function installCompatMount(
// Note: the following assumes DOM environment since the compat build // Note: the following assumes DOM environment since the compat build
// only targets web. It essentially includes logic for app.mount from // only targets web. It essentially includes logic for app.mount from
// both runtime-core AND runtime-dom. // both runtime-core AND runtime-dom.
instance.ctx._compat_mount = (selectorOrEl: string | Element) => { instance.ctx._compat_mount = (selectorOrEl?: string | Element) => {
if (isMounted) { if (isMounted) {
__DEV__ && warn(`Root instance is already mounted.`) __DEV__ && warn(`Root instance is already mounted.`)
return return
@ -351,14 +385,8 @@ export function installCompatMount(
} }
container = result container = result
} else { } else {
if (!selectorOrEl) { // eslint-disable-next-line
__DEV__ && container = selectorOrEl || document.createElement('div')
warn(
`Failed to mount root instance: invalid mount target ${selectorOrEl}.`
)
return
}
container = selectorOrEl
} }
const isSVG = container instanceof SVGElement const isSVG = container instanceof SVGElement

View File

@ -1,7 +1,7 @@
import { extend, isArray, isString } from '@vue/shared' import { extend, isArray, isString } from '@vue/shared'
import { AppConfig } from '../apiCreateApp' import { AppConfig } from '../apiCreateApp'
import { isRuntimeOnly } from '../component' import { isRuntimeOnly } from '../component'
import { deepMergeData } from './data' import { mergeDataOption } from './data'
import { import {
DeprecationTypes, DeprecationTypes,
warnDeprecation, warnDeprecation,
@ -80,34 +80,36 @@ export function installLegacyConfigProperties(config: AppConfig) {
// Internal merge strats which are no longer needed in v3, but we need to // 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 // expose them because some v2 plugins will reuse these internal strats to
// merge their custom options. // merge their custom options.
const strats = config.optionMergeStrategies as any extend(config.optionMergeStrategies, legacyOptionMergeStrats)
strats.data = deepMergeData }
// lifecycle hooks
strats.beforeCreate = mergeHook export const legacyOptionMergeStrats = {
strats.created = mergeHook data: mergeDataOption,
strats.beforeMount = mergeHook beforeCreate: mergeHook,
strats.mounted = mergeHook created: mergeHook,
strats.beforeUpdate = mergeHook beforeMount: mergeHook,
strats.updated = mergeHook mounted: mergeHook,
strats.beforeDestroy = mergeHook beforeUpdate: mergeHook,
strats.destroyed = mergeHook updated: mergeHook,
strats.activated = mergeHook beforeDestroy: mergeHook,
strats.deactivated = mergeHook destroyed: mergeHook,
strats.errorCaptured = mergeHook activated: mergeHook,
strats.serverPrefetch = mergeHook deactivated: mergeHook,
errorCaptured: mergeHook,
serverPrefetch: mergeHook,
// assets // assets
strats.components = mergeObjectOptions components: mergeObjectOptions,
strats.directives = mergeObjectOptions directives: mergeObjectOptions,
strats.filters = mergeObjectOptions filters: mergeObjectOptions,
// objects // objects
strats.props = mergeObjectOptions props: mergeObjectOptions,
strats.methods = mergeObjectOptions methods: mergeObjectOptions,
strats.inject = mergeObjectOptions inject: mergeObjectOptions,
strats.computed = mergeObjectOptions computed: mergeObjectOptions,
// watch has special merge behavior in v2, but isn't actually needed in v3. // 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 // since we are only exposing these for compat and nobody should be relying
// on the watch-specific behavior, just expose the object merge strat. // on the watch-specific behavior, just expose the object merge strat.
strats.watch = mergeObjectOptions watch: mergeObjectOptions
} }
function mergeHook( function mergeHook(

View File

@ -6,7 +6,10 @@ import {
toDisplayString, toDisplayString,
toNumber toNumber
} from '@vue/shared' } from '@vue/shared'
import { PublicPropertiesMap } from '../componentPublicInstance' import {
ComponentPublicInstance,
PublicPropertiesMap
} from '../componentPublicInstance'
import { getCompatChildren } from './instanceChildren' import { getCompatChildren } from './instanceChildren'
import { import {
DeprecationTypes, DeprecationTypes,
@ -33,6 +36,23 @@ import {
} from './renderHelpers' } from './renderHelpers'
import { resolveFilter } from '../helpers/resolveAssets' import { resolveFilter } from '../helpers/resolveAssets'
import { resolveMergedOptions } from '../componentOptions' import { resolveMergedOptions } from '../componentOptions'
import { Slots } from '../componentSlots'
export type LegacyPublicInstance = ComponentPublicInstance &
LegacyPublicProperties
export interface LegacyPublicProperties {
$set(target: object, key: string, value: any): void
$delete(target: object, key: string): void
$mount(el?: string | Element): this
$destroy(): void
$scopedSlots: Slots
$on(event: string | string[], fn: Function): this
$once(event: string, fn: Function): this
$off(event?: string, fn?: Function): this
$children: LegacyPublicProperties[]
$listeners: Record<string, Function | Function[]>
}
export function installCompatInstanceProperties(map: PublicPropertiesMap) { export function installCompatInstanceProperties(map: PublicPropertiesMap) {
const set = (target: any, key: any, val: any) => { const set = (target: any, key: any, val: any) => {

View File

@ -893,7 +893,13 @@ function callHookWithMixinAndExtends(
} }
} }
if (selfHook) { if (selfHook) {
callWithAsyncErrorHandling(selfHook.bind(instance.proxy!), instance, type) callWithAsyncErrorHandling(
__COMPAT__ && isArray(selfHook)
? selfHook.map(h => h.bind(instance.proxy!))
: selfHook.bind(instance.proxy!),
instance,
type
)
} }
} }
@ -1007,21 +1013,23 @@ export function resolveMergedOptions(
export function mergeOptions( export function mergeOptions(
to: any, to: any,
from: any, from: any,
instance?: ComponentInternalInstance instance?: ComponentInternalInstance | null,
strats = instance && instance.appContext.config.optionMergeStrategies
) { ) {
if (__COMPAT__ && isFunction(from)) { if (__COMPAT__ && isFunction(from)) {
from = from.options from = from.options
} }
const strats = instance && instance.appContext.config.optionMergeStrategies
const { mixins, extends: extendsOptions } = from const { mixins, extends: extendsOptions } = from
extendsOptions && mergeOptions(to, extendsOptions, instance) extendsOptions && mergeOptions(to, extendsOptions, instance, strats)
mixins && mixins &&
mixins.forEach((m: ComponentOptionsMixin) => mergeOptions(to, m, instance)) mixins.forEach((m: ComponentOptionsMixin) =>
mergeOptions(to, m, instance, strats)
)
for (const key in from) { for (const key in from) {
if (strats && hasOwn(to, key) && hasOwn(strats, key)) { if (strats && hasOwn(strats, key)) {
to[key] = strats[key](to[key], from[key], instance && instance.proxy, key) to[key] = strats[key](to[key], from[key], instance && instance.proxy, key)
} else { } else {
to[key] = from[key] to[key] = from[key]