diff --git a/packages/runtime-core/src/compat/instance.ts b/packages/runtime-core/src/compat/instance.ts index 5beaf5f5ebb..d310de49ae6 100644 --- a/packages/runtime-core/src/compat/instance.ts +++ b/packages/runtime-core/src/compat/instance.ts @@ -15,6 +15,7 @@ import { DeprecationTypes, assertCompatEnabled, isCompatEnabled, + warnDeprecation, } from './compatConfig' import { off, on, once } from './instanceEventEmitter' import { getCompatListeners } from './instanceListeners' @@ -121,50 +122,77 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) { $children: getCompatChildren, $listeners: getCompatListeners, + + // inject additional properties into $options for compat + // e.g. vuex needs this.$options.parent + $options: i => { + if (!isCompatEnabled(DeprecationTypes.PRIVATE_APIS, i)) { + return resolveMergedOptions(i) + } + if (i.resolvedOptions) { + return i.resolvedOptions + } + const res = (i.resolvedOptions = extend({}, resolveMergedOptions(i))) + Object.defineProperties(res, { + parent: { + get() { + warnDeprecation(DeprecationTypes.PRIVATE_APIS, i, '$options.parent') + return i.proxy!.$parent + }, + }, + propsData: { + get() { + warnDeprecation( + DeprecationTypes.PRIVATE_APIS, + i, + '$options.propsData', + ) + return i.vnode.props + }, + }, + }) + return res + }, } as PublicPropertiesMap) - /* istanbul ignore if */ - if (isCompatEnabled(DeprecationTypes.PRIVATE_APIS, null)) { - extend(map, { - // needed by many libs / render fns - $vnode: i => i.vnode, - - // inject additional properties into $options for compat - // e.g. vuex needs this.$options.parent - $options: i => { - const res = extend({}, resolveMergedOptions(i)) - res.parent = i.proxy!.$parent - res.propsData = i.vnode.props - return res - }, - - // some private properties that are likely accessed... - _self: i => i.proxy, - _uid: i => i.uid, - _data: i => i.data, - _isMounted: i => i.isMounted, - _isDestroyed: i => i.isUnmounted, - - // v2 render helpers - $createElement: () => compatH, - _c: () => compatH, - _o: () => legacyMarkOnce, - _n: () => looseToNumber, - _s: () => toDisplayString, - _l: () => renderList, - _t: i => legacyRenderSlot.bind(null, i), - _q: () => looseEqual, - _i: () => looseIndexOf, - _m: i => legacyRenderStatic.bind(null, i), - _f: () => resolveFilter, - _k: i => legacyCheckKeyCodes.bind(null, i), - _b: () => legacyBindObjectProps, - _v: () => createTextVNode, - _e: () => createCommentVNode, - _u: () => legacyresolveScopedSlots, - _g: () => legacyBindObjectListeners, - _d: () => legacyBindDynamicKeys, - _p: () => legacyPrependModifier, - } as PublicPropertiesMap) + const privateAPIs = { + // needed by many libs / render fns + $vnode: i => i.vnode, + + // some private properties that are likely accessed... + _self: i => i.proxy, + _uid: i => i.uid, + _data: i => i.data, + _isMounted: i => i.isMounted, + _isDestroyed: i => i.isUnmounted, + + // v2 render helpers + $createElement: () => compatH, + _c: () => compatH, + _o: () => legacyMarkOnce, + _n: () => looseToNumber, + _s: () => toDisplayString, + _l: () => renderList, + _t: i => legacyRenderSlot.bind(null, i), + _q: () => looseEqual, + _i: () => looseIndexOf, + _m: i => legacyRenderStatic.bind(null, i), + _f: () => resolveFilter, + _k: i => legacyCheckKeyCodes.bind(null, i), + _b: () => legacyBindObjectProps, + _v: () => createTextVNode, + _e: () => createCommentVNode, + _u: () => legacyresolveScopedSlots, + _g: () => legacyBindObjectListeners, + _d: () => legacyBindDynamicKeys, + _p: () => legacyPrependModifier, + } as PublicPropertiesMap + + for (const key in privateAPIs) { + map[key] = i => { + if (isCompatEnabled(DeprecationTypes.PRIVATE_APIS, i)) { + return privateAPIs[key](i) + } + } } } diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index a551529e667..6f30053cfc2 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -45,6 +45,7 @@ import { type Directive, validateDirectiveName } from './directives' import { type ComponentOptions, type ComputedOptions, + type MergedComponentOptions, type MethodOptions, applyOptions, resolveMergedOptions, @@ -524,6 +525,12 @@ export interface ComponentInternalInstance { * @internal */ getCssVars?: () => Record + + /** + * v2 compat only, for caching mutated $options + * @internal + */ + resolvedOptions?: MergedComponentOptions } const emptyAppContext = createAppContext() diff --git a/packages/vue-compat/__tests__/instance.spec.ts b/packages/vue-compat/__tests__/instance.spec.ts index 63ce5581230..1feccabd8f8 100644 --- a/packages/vue-compat/__tests__/instance.spec.ts +++ b/packages/vue-compat/__tests__/instance.spec.ts @@ -14,6 +14,7 @@ beforeEach(() => { Vue.configureCompat({ MODE: 2, GLOBAL_MOUNT: 'suppress-warning', + PRIVATE_APIS: 'suppress-warning', }) }) @@ -331,3 +332,43 @@ test('INSTANCE_ATTR_CLASS_STYLE', () => { )('Anonymous'), ).toHaveBeenWarned() }) + +test('$options mutation', () => { + const Comp = { + props: ['id'], + template: '
', + data() { + return { + foo: '', + } + }, + created(this: any) { + expect(this.$options.parent).toBeDefined() + expect(this.$options.test).toBeUndefined() + this.$options.test = this.id + expect(this.$options.test).toBe(this.id) + }, + } + + new Vue({ + template: `
`, + components: { Comp }, + }).$mount() +}) + +test('other private APIs', () => { + new Vue({ + created() { + expect(this.$createElement).toBeTruthy() + }, + }) + + new Vue({ + compatConfig: { + PRIVATE_APIS: false, + }, + created() { + expect(this.$createElement).toBeUndefined() + }, + }) +})