import Vue from '@vue/compat'
+import { effect, isReactive } from '@vue/reactivity'
+import {
+ DeprecationTypes,
+ deprecationData,
+ toggleDeprecationWarning
+} from '../compatConfig'
-describe('compat: global API', () => {
- beforeEach(() => Vue.configureCompat({ MODE: 2 }))
- afterEach(() => Vue.configureCompat({ MODE: 3 }))
+beforeEach(() => {
+ Vue.configureCompat({ MODE: 2 })
+})
+
+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')
el.innerHTML = `{{ msg }}`
new Vue({
el,
+ compatConfig: { GLOBAL_MOUNT: true },
data() {
return {
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')
+ })
+})
+
+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()
+ })
})
GLOBAL_SET = 'GLOBAL_SET',
GLOBAL_DELETE = 'GLOBAL_DELETE',
GLOBAL_OBSERVABLE = 'GLOBAL_OBSERVABLE',
- GLOBAL_UTIL = 'GLOBAL_UTIL',
+ GLOBAL_PRIVATE_UTIL = 'GLOBAL_PRIVATE_UTIL',
CONFIG_SILENT = 'CONFIG_SILENT',
CONFIG_DEVTOOLS = 'CONFIG_DEVTOOLS',
link?: string
}
-const deprecationData: Record<DeprecationTypes, DeprecationData> = {
+export const deprecationData: Record<DeprecationTypes, DeprecationData> = {
[DeprecationTypes.GLOBAL_MOUNT]: {
message:
`The global app bootstrapping API has changed: vm.$mount() and the "el" ` +
link: `https://v3.vuejs.org/api/basic-reactivity.html`
},
- [DeprecationTypes.GLOBAL_UTIL]: {
+ [DeprecationTypes.GLOBAL_PRIVATE_UTIL]: {
message:
`Vue.util has been removed. Please refactor to avoid its usage ` +
`since it was an internal API even in Vue 2.`
const instanceWarned: Record<string, true> = 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(
key: DeprecationTypes,
instance: ComponentInternalInstance | null,
if (!__DEV__) {
return
}
+ if (__TEST__ && !warningEnabled) {
+ return
+ }
instance = instance || getCurrentInstance()
// skip if the same warning is emitted for the same component type
const componentDupKey = dupKey + compId
- if (componentDupKey in instanceWarned) {
+ if (!__TEST__ && componentDupKey in instanceWarned) {
return
}
instanceWarned[componentDupKey] = true
// same warning, but different component. skip the long message and just
// log the key and count.
- if (dupKey in warnCount) {
+ if (!__TEST__ && dupKey in warnCount) {
warn(`(deprecation ${key}) (${++warnCount[dupKey] + 1})`)
return
}
-import { isPlainObject } from '@vue/shared'
+import { isFunction, isPlainObject } from '@vue/shared'
import { ComponentInternalInstance } from '../component'
+import { ComponentPublicInstance } from '../componentPublicInstance'
import { DeprecationTypes, warnDeprecation } from './compatConfig'
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.$
+ )
+ }
+}
CreateAppFunction,
Plugin
} from '../apiCreateApp'
-import { defineComponent } from '../apiDefineComponent'
import {
Component,
ComponentOptions,
import { Directive } from '../directives'
import { nextTick } from '../scheduler'
import { version } from '..'
-import { LegacyConfig } from './globalConfig'
+import { LegacyConfig, legacyOptionMergeStrats } from './globalConfig'
import { LegacyDirective } from './customDirective'
import {
warnDeprecation,
isCompatEnabled,
softAssertCompatEnabled
} from './compatConfig'
+import { LegacyPublicInstance } from './instance'
/**
* @deprecated the default `Vue` export has been removed in Vue 3. The type for
* named imports instead - e.g. `import { createApp } from 'vue'`.
*/
export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & {
+ 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): ComponentPublicInstance
+ new (options?: ComponentOptions): LegacyPublicInstance
version: string
config: AppConfig & LegacyConfig
- extend: typeof defineComponent
+ extend: (options?: ComponentOptions) => CompatVue
nextTick: typeof nextTick
use(plugin: Plugin, ...options: any[]): CompatVue
* @internal
*/
options: ComponentOptions
-
- configureCompat: typeof configureCompat
+ /**
+ * @internal
+ */
+ super: CompatVue
}
export let isCopyingConfig = false
let cid = 1
Vue.cid = cid
+ 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(extendOptions, SubVue)
+ return createCompatApp(SubVue.options, SubVue)
} else {
return createCompatApp(
- {
- el: inlineOptions.el,
- extends: extendOptions,
- mixins: [inlineOptions]
- },
+ 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(
- extend({}, Super.options) as ComponentOptions,
- extendOptions
+ mergeBase,
+ extendOptions,
+ null,
+ legacyOptionMergeStrats as any
)
SubVue.options._base = SubVue
SubVue.mixin = Super.mixin
SubVue.use = Super.use
SubVue.cid = ++cid
+
+ extendCache.set(extendOptions, SubVue)
return SubVue
}
warn: __DEV__ ? warn : NOOP,
extend,
mergeOptions: (parent: any, child: any, vm?: ComponentPublicInstance) =>
- mergeOptions(parent, child, vm && vm.$),
+ mergeOptions(
+ parent,
+ child,
+ vm && vm.$,
+ vm ? undefined : (legacyOptionMergeStrats as any)
+ ),
defineReactive
}
Object.defineProperty(Vue, 'util', {
get() {
- assertCompatEnabled(DeprecationTypes.GLOBAL_UTIL, null)
+ assertCompatEnabled(DeprecationTypes.GLOBAL_PRIVATE_UTIL, null)
return util
}
})
// 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) => {
+ instance.ctx._compat_mount = (selectorOrEl?: string | Element) => {
if (isMounted) {
__DEV__ && warn(`Root instance is already mounted.`)
return
}
container = result
} else {
- if (!selectorOrEl) {
- __DEV__ &&
- warn(
- `Failed to mount root instance: invalid mount target ${selectorOrEl}.`
- )
- return
- }
- container = selectorOrEl
+ // eslint-disable-next-line
+ container = selectorOrEl || document.createElement('div')
}
const isSVG = container instanceof SVGElement
import { extend, isArray, isString } from '@vue/shared'
import { AppConfig } from '../apiCreateApp'
import { isRuntimeOnly } from '../component'
-import { deepMergeData } from './data'
+import { mergeDataOption } from './data'
import {
DeprecationTypes,
warnDeprecation,
// 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
+ extend(config.optionMergeStrategies, legacyOptionMergeStrats)
+}
+
+export const legacyOptionMergeStrats = {
+ data: mergeDataOption,
+ beforeCreate: mergeHook,
+ created: mergeHook,
+ beforeMount: mergeHook,
+ mounted: mergeHook,
+ beforeUpdate: mergeHook,
+ updated: mergeHook,
+ beforeDestroy: mergeHook,
+ destroyed: mergeHook,
+ activated: mergeHook,
+ deactivated: mergeHook,
+ errorCaptured: mergeHook,
+ serverPrefetch: mergeHook,
// assets
- strats.components = mergeObjectOptions
- strats.directives = mergeObjectOptions
- strats.filters = mergeObjectOptions
+ components: mergeObjectOptions,
+ directives: mergeObjectOptions,
+ filters: mergeObjectOptions,
// objects
- strats.props = mergeObjectOptions
- strats.methods = mergeObjectOptions
- strats.inject = mergeObjectOptions
- strats.computed = mergeObjectOptions
+ props: mergeObjectOptions,
+ methods: mergeObjectOptions,
+ inject: mergeObjectOptions,
+ 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
+ watch: mergeObjectOptions
}
function mergeHook(
toDisplayString,
toNumber
} from '@vue/shared'
-import { PublicPropertiesMap } from '../componentPublicInstance'
+import {
+ ComponentPublicInstance,
+ PublicPropertiesMap
+} from '../componentPublicInstance'
import { getCompatChildren } from './instanceChildren'
import {
DeprecationTypes,
} from './renderHelpers'
import { resolveFilter } from '../helpers/resolveAssets'
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) {
const set = (target: any, key: any, val: any) => {
}
}
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
+ )
}
}
export function mergeOptions(
to: any,
from: any,
- instance?: ComponentInternalInstance
+ instance?: ComponentInternalInstance | null,
+ strats = instance && instance.appContext.config.optionMergeStrategies
) {
if (__COMPAT__ && isFunction(from)) {
from = from.options
}
- const strats = instance && instance.appContext.config.optionMergeStrategies
const { mixins, extends: extendsOptions } = from
- extendsOptions && mergeOptions(to, extendsOptions, instance)
+ extendsOptions && mergeOptions(to, extendsOptions, instance, strats)
mixins &&
- mixins.forEach((m: ComponentOptionsMixin) => mergeOptions(to, m, instance))
+ mixins.forEach((m: ComponentOptionsMixin) =>
+ mergeOptions(to, m, instance, strats)
+ )
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)
} else {
to[key] = from[key]