]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compat): fix $options mutation + adjust private API initialization
authorEvan You <yyx990803@gmail.com>
Mon, 15 Apr 2024 11:28:37 +0000 (19:28 +0800)
committerEvan You <yyx990803@gmail.com>
Mon, 15 Apr 2024 11:28:37 +0000 (19:28 +0800)
close #10626
close #10636

packages/runtime-core/src/compat/instance.ts
packages/runtime-core/src/component.ts
packages/vue-compat/__tests__/instance.spec.ts

index 5beaf5f5ebb6cb197b60993d222277d1c06db2ad..d310de49ae64a8752e35107b689879a700629252 100644 (file)
@@ -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)
+      }
+    }
   }
 }
index a551529e6675e851ae1015974fcb3af428af798e..6f30053cfc2a65fbc524ca597626618990943631 100644 (file)
@@ -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<string, string>
+
+  /**
+   * v2 compat only, for caching mutated $options
+   * @internal
+   */
+  resolvedOptions?: MergedComponentOptions
 }
 
 const emptyAppContext = createAppContext()
index 63ce55812304022f832e45ad482dcaf323e51418..1feccabd8f820bc3c7f67ec9a31a038adf7102a3 100644 (file)
@@ -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: '<div/>',
+    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: `<div><Comp id="1"/><Comp id="2"/></div>`,
+    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()
+    },
+  })
+})