From 6a3047a8ee560090d260c56c468a75986fd3fac5 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:16:46 -0400 Subject: [PATCH] Deprecate PromiseProxyMixin, Enumerable and Observable per RFC 1116 Follow-up to #21577, which deprecated `Mixin.create` itself. The advancement RFC (emberjs/rfcs#1143) still lists these three public mixins as open work. Each mixin records its own deprecation notice in a WeakMap through `deprecatedMixin`. `CoreObject.extend` and both `reopen` methods read that map and fire the notice for each mixin they are given. `EmberObject` reopens its `PrototypeMixin` with `Observable` directly, which is what `extend` does internally, so apps that never name the mixin get no notice. `MutableEnumerable` shares the `deprecate-enumerable` id, because leaving it alone would leave an obvious hole in the same module. The three notices are available but not enabled, so they only fire when `_ALL_DEPRECATIONS_ENABLED` is set. Test suite is green in the default, all-deprecations, deprecations-as-errors, optional-features and production variants. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01T3ZrBpLnFsT39EKiAe4897 --- .../@ember/-internals/deprecations/index.ts | 21 +++ .../integration/components/tracked-test.js | 23 ++- .../tests/mixins/promise_proxy_test.js | 65 +++++-- .../-internals/utils/lib/deprecated-mixin.ts | 44 +++++ packages/@ember/enumerable/index.ts | 10 +- packages/@ember/enumerable/mutable.ts | 10 +- packages/@ember/object/core.ts | 4 + packages/@ember/object/index.ts | 7 +- packages/@ember/object/observable.ts | 168 ++++++++++-------- packages/@ember/object/promise-proxy-mixin.ts | 55 +++--- .../object/tests/mixin/deprecation_test.js | 132 ++++++++++++++ .../@ember/object/tests/observable_test.js | 6 +- 12 files changed, 420 insertions(+), 125 deletions(-) create mode 100644 packages/@ember/-internals/utils/lib/deprecated-mixin.ts diff --git a/packages/@ember/-internals/deprecations/index.ts b/packages/@ember/-internals/deprecations/index.ts index e69c72d5ac4..2272a52fd39 100644 --- a/packages/@ember/-internals/deprecations/index.ts +++ b/packages/@ember/-internals/deprecations/index.ts @@ -140,6 +140,27 @@ export const DEPRECATIONS = { until: '8.0.0', url: 'https://deprecations.emberjs.com/id/deprecate-mixins', }), + DEPRECATE_PROMISE_PROXY_MIXIN: deprecation({ + id: 'deprecate-promise-proxy-mixin', + for: 'ember-source', + since: { available: '7.4.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-promise-proxy-mixin', + }), + DEPRECATE_ENUMERABLE: deprecation({ + id: 'deprecate-enumerable', + for: 'ember-source', + since: { available: '7.4.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-enumerable', + }), + DEPRECATE_OBSERVABLE: deprecation({ + id: 'deprecate-observable', + for: 'ember-source', + since: { available: '7.4.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-observable', + }), }; export function deprecateUntil(message: string, deprecation: DeprecationObject) { diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js index a0004a240d6..f98377418c1 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js @@ -7,7 +7,15 @@ import { computed, get, set } from '@ember/object'; import { Promise } from 'rsvp'; import { fn } from '@ember/helper'; import { on } from '@ember/modifier'; -import { moduleFor, RenderingTestCase, strip, runTask } from 'internal-test-helpers'; +import { + moduleFor, + RenderingTestCase, + strip, + runTask, + expectDeprecation, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; import GlimmerishComponent from '../../utils/glimmerish-component'; import Component from '@glimmer/component'; import { Component as EmberComponent } from '../../utils/helpers'; @@ -96,8 +104,17 @@ moduleFor( this.assertText('max jackson | max jackson'); } - '@test creating an array proxy inside a tracking context does not trigger backtracking assertion'() { - let PromiseArray = ArrayProxy.extend(PromiseProxyMixin); + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test creating an array proxy inside a tracking context does not trigger backtracking assertion`]() { + let PromiseArray; + expectDeprecation( + () => { + PromiseArray = ArrayProxy.extend(PromiseProxyMixin); + }, + /The `PromiseProxyMixin` is deprecated/, + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isEnabled + ); class LoaderComponent extends GlimmerishComponent { get data() { diff --git a/packages/@ember/-internals/runtime/tests/mixins/promise_proxy_test.js b/packages/@ember/-internals/runtime/tests/mixins/promise_proxy_test.js index 99822d559fd..2dd77635510 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/promise_proxy_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/promise_proxy_test.js @@ -5,7 +5,8 @@ import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; import EmberRSVP from '../../lib/ext/rsvp'; import { onerrorDefault } from '../../lib/ext/rsvp'; import * as RSVP from 'rsvp'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, ignoreDeprecation, testUnless } from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../../deprecations'; let ObjectPromiseProxy, proxy; @@ -13,7 +14,11 @@ moduleFor( 'Ember.PromiseProxy - ObjectProxy', class extends AbstractTestCase { beforeEach() { - ObjectPromiseProxy = ObjectProxy.extend(PromiseProxyMixin); + // The notice for applying `PromiseProxyMixin` has its own test. These + // tests count their own assertions, so the notice must stay silent here. + ignoreDeprecation(() => { + ObjectPromiseProxy = ObjectProxy.extend(PromiseProxyMixin); + }); } afterEach() { @@ -22,11 +27,15 @@ moduleFor( proxy = undefined; } - ['@test present on ember namespace'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test present on ember namespace`](assert) { assert.ok(PromiseProxyMixin, 'expected PromiseProxyMixin to exist'); } - ['@test no promise, invoking then should raise'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test no promise, invoking then should raise`](assert) { proxy = ObjectPromiseProxy.create(); assert.throws(function () { @@ -41,7 +50,9 @@ moduleFor( }, new RegExp("PromiseProxy's promise must be set")); } - ['@test fulfillment'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved)} @test fulfillment`]( + assert + ) { let value = { firstName: 'stef', lastName: 'penner', @@ -155,7 +166,9 @@ moduleFor( // rest of the promise semantics are tested in directly in RSVP } - ['@test rejection'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved)} @test rejection`]( + assert + ) { let reason = new Error('failure'); let deferred = RSVP.defer(); proxy = ObjectPromiseProxy.create({ @@ -259,7 +272,9 @@ moduleFor( } // https://github.com/emberjs/ember.js/issues/15694 - ['@test rejection without specifying reason'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test rejection without specifying reason`](assert) { let deferred = RSVP.defer(); proxy = ObjectPromiseProxy.create({ promise: deferred.promise, @@ -328,7 +343,9 @@ moduleFor( ); } - ["@test unhandled rejects still propagate to RSVP.on('error', ...) "](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test unhandled rejects still propagate to RSVP.on('error', ...) `](assert) { assert.expect(1); RSVP.on('error', onerror); @@ -361,7 +378,9 @@ moduleFor( RSVP.off('error', onerror); } - ['@test should work with promise inheritance'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test should work with promise inheritance`](assert) { class PromiseSubclass extends RSVP.Promise {} proxy = ObjectPromiseProxy.create({ @@ -371,7 +390,9 @@ moduleFor( assert.ok(proxy.then() instanceof PromiseSubclass, 'promise proxy respected inheritance'); } - ['@test should reset isFulfilled and isRejected when promise is reset'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test should reset isFulfilled and isRejected when promise is reset`](assert) { let deferred = EmberRSVP.defer(); proxy = ObjectPromiseProxy.create({ @@ -470,7 +491,9 @@ moduleFor( ); } - ['@test should have content when isFulfilled is set'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test should have content when isFulfilled is set`](assert) { let deferred = EmberRSVP.defer(); proxy = ObjectPromiseProxy.create({ @@ -482,7 +505,9 @@ moduleFor( run(deferred, 'resolve', true); } - ['@test should have reason when isRejected is set'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test should have reason when isRejected is set`](assert) { let error = new Error('Y U REJECT?!?'); let deferred = EmberRSVP.defer(); @@ -499,7 +524,9 @@ moduleFor( } } - ['@test should not error if promise is resolved after proxy has been destroyed'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test should not error if promise is resolved after proxy has been destroyed`](assert) { let deferred = EmberRSVP.defer(); proxy = ObjectPromiseProxy.create({ @@ -521,7 +548,9 @@ moduleFor( ); } - ['@test should not error if promise is rejected after proxy has been destroyed'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test should not error if promise is rejected after proxy has been destroyed`](assert) { let deferred = EmberRSVP.defer(); proxy = ObjectPromiseProxy.create({ @@ -543,7 +572,9 @@ moduleFor( ); } - ['@test promise chain is not broken if promised is resolved after proxy has been destroyed']( + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test promise chain is not broken if promised is resolved after proxy has been destroyed`]( assert ) { let deferred = EmberRSVP.defer(); @@ -575,7 +606,9 @@ moduleFor( ); } - ['@test promise chain is not broken if promised is rejected after proxy has been destroyed']( + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test promise chain is not broken if promised is rejected after proxy has been destroyed`]( assert ) { let deferred = EmberRSVP.defer(); diff --git a/packages/@ember/-internals/utils/lib/deprecated-mixin.ts b/packages/@ember/-internals/utils/lib/deprecated-mixin.ts new file mode 100644 index 00000000000..04685fe7a33 --- /dev/null +++ b/packages/@ember/-internals/utils/lib/deprecated-mixin.ts @@ -0,0 +1,44 @@ +/** + Registry of the deprecation notice that belongs to a single framework mixin. + + `Mixin.create` deprecates the act of authoring a mixin. A framework mixin + such as `Observable` is built through `INTERNAL_MIXIN_CREATE`, so it stays + silent while Ember applies it. Application code that applies the same mixin + must still get a notice for that specific mixin, so the public entry points + (`Mixin.create`, `CoreObject.extend`, `CoreObject.reopen`) look the mixin up + here and call its notice. + + The registry is a `WeakMap`, so the association is not visible on the mixin. + + @private +*/ +const NOTICES = new WeakMap void>(); + +/** + Records the deprecation notice for a framework mixin and returns the mixin. + + @private +*/ +export function deprecatedMixin(mixin: T, notice: () => void): T { + NOTICES.set(mixin, notice); + return mixin; +} + +/** + Calls the deprecation notice of every value that is a deprecated framework + mixin. Other values are ignored. + + @private +*/ +export function deprecateAppliedMixins(mixins: ArrayLike): void { + for (let i = 0; i < mixins.length; i++) { + let mixin = mixins[i]; + if (typeof mixin !== 'object' || mixin === null) { + continue; + } + let notice = NOTICES.get(mixin); + if (notice !== undefined) { + notice(); + } + } +} diff --git a/packages/@ember/enumerable/index.ts b/packages/@ember/enumerable/index.ts index 1e9ec615cfc..a0d56144394 100644 --- a/packages/@ember/enumerable/index.ts +++ b/packages/@ember/enumerable/index.ts @@ -1,5 +1,7 @@ import Mixin from '@ember/object/mixin'; import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { deprecatedMixin } from '@ember/-internals/utils/lib/deprecated-mixin'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; /** @module @ember/enumerable @@ -13,9 +15,15 @@ import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixi @class Enumerable @private + @deprecated Use native arrays and native array methods instead. */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface Enumerable {} -const Enumerable = Mixin[INTERNAL_MIXIN_CREATE](); +const Enumerable = deprecatedMixin(Mixin[INTERNAL_MIXIN_CREATE](), () => { + deprecateUntil( + 'The `Enumerable` mixin is deprecated. Use native arrays and native array methods instead.', + DEPRECATIONS.DEPRECATE_ENUMERABLE + ); +}); export default Enumerable; diff --git a/packages/@ember/enumerable/mutable.ts b/packages/@ember/enumerable/mutable.ts index 0fce726a0e0..d54b526663e 100644 --- a/packages/@ember/enumerable/mutable.ts +++ b/packages/@ember/enumerable/mutable.ts @@ -1,6 +1,8 @@ import Enumerable from '@ember/enumerable'; import Mixin from '@ember/object/mixin'; import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { deprecatedMixin } from '@ember/-internals/utils/lib/deprecated-mixin'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; /** @module ember @@ -15,9 +17,15 @@ import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixi @namespace Ember @uses Enumerable @private + @deprecated Use native arrays and native array methods instead. */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface MutableEnumerable extends Enumerable {} -const MutableEnumerable = Mixin[INTERNAL_MIXIN_CREATE](Enumerable); +const MutableEnumerable = deprecatedMixin(Mixin[INTERNAL_MIXIN_CREATE](Enumerable), () => { + deprecateUntil( + 'The `MutableEnumerable` mixin is deprecated. Use native arrays and native array methods instead.', + DEPRECATIONS.DEPRECATE_ENUMERABLE + ); +}); export default MutableEnumerable; diff --git a/packages/@ember/object/core.ts b/packages/@ember/object/core.ts index 792e6e2b52f..c9ad9281aa2 100644 --- a/packages/@ember/object/core.ts +++ b/packages/@ember/object/core.ts @@ -16,6 +16,7 @@ import { descriptorForProperty, isClassicDecorator } from '@ember/-internals/met import { DEBUG_INJECTION_FUNCTIONS } from '@ember/-internals/metal/lib/injected_property'; import Mixin, { applyMixin } from '@ember/object/mixin'; import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { deprecateAppliedMixins } from '@ember/-internals/utils/lib/deprecated-mixin'; import ActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler'; import makeArray from '@ember/array/make'; import { assert } from '@ember/debug'; @@ -311,6 +312,7 @@ class CoreObject { } reopen(...args: Array>): this { + deprecateAppliedMixins(args); applyMixin(this, args); return this; } @@ -713,6 +715,7 @@ class CoreObject { ...mixins: M ): Readonly & EmberClassConstructor & MergeArray; static extend(...mixins: any[]) { + deprecateAppliedMixins(mixins); let Class = class extends this {}; reopen.apply(Class.PrototypeMixin, mixins); return Class; @@ -838,6 +841,7 @@ class CoreObject { @public */ static reopen(this: C, ...args: any[]): C { + deprecateAppliedMixins(args); this.willReopen(); reopen.apply(this.PrototypeMixin, args); return this; diff --git a/packages/@ember/object/index.ts b/packages/@ember/object/index.ts index 1bf0587949a..39b16e43620 100644 --- a/packages/@ember/object/index.ts +++ b/packages/@ember/object/index.ts @@ -36,7 +36,12 @@ export { default as computed } from '@ember/-internals/metal/lib/computed'; */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface EmberObject extends Observable {} -class EmberObject extends CoreObject.extend(Observable) { +class EmberObject extends CoreObject { + static { + // `extend(Observable)` would fire the Observable deprecation for every app. + this.PrototypeMixin.reopen(Observable); + } + get _debugContainerKey() { let factory = getFactoryFor(this); return factory !== undefined && factory.fullName; diff --git a/packages/@ember/object/observable.ts b/packages/@ember/object/observable.ts index b928183b051..e538474ec83 100644 --- a/packages/@ember/object/observable.ts +++ b/packages/@ember/object/observable.ts @@ -17,6 +17,8 @@ import setProperties from '@ember/-internals/metal/lib/set_properties'; import Mixin from '@ember/object/mixin'; import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { deprecatedMixin } from '@ember/-internals/utils/lib/deprecated-mixin'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; import { assert } from '@ember/debug'; export type ObserverMethod = @@ -92,6 +94,8 @@ export type ObserverMethod = @class Observable @public + @deprecated Use `get` and `set` from `@ember/object`, and `@tracked` from + `@glimmer/tracking`, instead of applying this mixin. */ interface Observable { /** @@ -421,24 +425,25 @@ interface Observable { */ cacheFor(key: K): unknown; } -const Observable = Mixin[INTERNAL_MIXIN_CREATE]({ - get(keyName: string) { - return get(this, keyName); - }, +const Observable = deprecatedMixin( + Mixin[INTERNAL_MIXIN_CREATE]({ + get(keyName: string) { + return get(this, keyName); + }, - getProperties(...args: string[]) { - return getProperties(this, ...args); - }, + getProperties(...args: string[]) { + return getProperties(this, ...args); + }, - set(keyName: string, value: unknown) { - return set(this, keyName, value); - }, + set(keyName: string, value: unknown) { + return set(this, keyName, value); + }, - setProperties(hash: object) { - return setProperties(this, hash); - }, + setProperties(hash: object) { + return setProperties(this, hash); + }, - /** + /** Begins a grouping of property changes. You can use this method to group property changes so that notifications @@ -453,12 +458,12 @@ const Observable = Mixin[INTERNAL_MIXIN_CREATE]({ @return {Observable} @private */ - beginPropertyChanges() { - beginPropertyChanges(); - return this; - }, + beginPropertyChanges() { + beginPropertyChanges(); + return this; + }, - /** + /** Ends a grouping of property changes. You can use this method to group property changes so that notifications @@ -472,37 +477,37 @@ const Observable = Mixin[INTERNAL_MIXIN_CREATE]({ @return {Observable} @private */ - endPropertyChanges() { - endPropertyChanges(); - return this; - }, - - notifyPropertyChange(keyName: string) { - notifyPropertyChange(this, keyName); - return this; - }, - - addObserver( - key: string, - target: object | Function | null, - method?: string | Function, - sync?: boolean - ) { - addObserver(this, key, target, method, sync); - return this; - }, - - removeObserver( - key: string, - target: object | Function | null, - method?: string | Function, - sync?: boolean - ) { - removeObserver(this, key, target, method, sync); - return this; - }, - - /** + endPropertyChanges() { + endPropertyChanges(); + return this; + }, + + notifyPropertyChange(keyName: string) { + notifyPropertyChange(this, keyName); + return this; + }, + + addObserver( + key: string, + target: object | Function | null, + method?: string | Function, + sync?: boolean + ) { + addObserver(this, key, target, method, sync); + return this; + }, + + removeObserver( + key: string, + target: object | Function | null, + method?: string | Function, + sync?: boolean + ) { + removeObserver(this, key, target, method, sync); + return this; + }, + + /** Returns `true` if the object currently has observers registered for a particular key. You can use this method to potentially defer performing an expensive action until someone begins observing a particular property @@ -513,34 +518,41 @@ const Observable = Mixin[INTERNAL_MIXIN_CREATE]({ @return {Boolean} @private */ - hasObserverFor(key: string) { - return hasListeners(this, `${key}:change`); - }, - - incrementProperty(keyName: string, increment = 1) { - assert( - 'Must pass a numeric value to incrementProperty', - !isNaN(parseFloat(String(increment))) && isFinite(increment) - ); - return set(this, keyName, (parseFloat(get(this, keyName)) || 0) + increment); - }, - - decrementProperty(keyName: string, decrement = 1) { - assert( - 'Must pass a numeric value to decrementProperty', - (typeof decrement === 'number' || !isNaN(parseFloat(decrement))) && isFinite(decrement) + hasObserverFor(key: string) { + return hasListeners(this, `${key}:change`); + }, + + incrementProperty(keyName: string, increment = 1) { + assert( + 'Must pass a numeric value to incrementProperty', + !isNaN(parseFloat(String(increment))) && isFinite(increment) + ); + return set(this, keyName, (parseFloat(get(this, keyName)) || 0) + increment); + }, + + decrementProperty(keyName: string, decrement = 1) { + assert( + 'Must pass a numeric value to decrementProperty', + (typeof decrement === 'number' || !isNaN(parseFloat(decrement))) && isFinite(decrement) + ); + return set(this, keyName, (get(this, keyName) || 0) - decrement); + }, + + toggleProperty(keyName: string) { + return set(this, keyName, !get(this, keyName)); + }, + + cacheFor(keyName: string) { + let meta = peekMeta(this); + return meta !== null ? meta.valueFor(keyName) : undefined; + }, + }), + () => { + deprecateUntil( + 'The `Observable` mixin is deprecated. Use `get` and `set` from `@ember/object`, and `@tracked` from `@glimmer/tracking`, instead.', + DEPRECATIONS.DEPRECATE_OBSERVABLE ); - return set(this, keyName, (get(this, keyName) || 0) - decrement); - }, - - toggleProperty(keyName: string) { - return set(this, keyName, !get(this, keyName)); - }, - - cacheFor(keyName: string) { - let meta = peekMeta(this); - return meta !== null ? meta.valueFor(keyName) : undefined; - }, -}); + } +); export default Observable; diff --git a/packages/@ember/object/promise-proxy-mixin.ts b/packages/@ember/object/promise-proxy-mixin.ts index ac163d9ee7e..ee74df64d70 100644 --- a/packages/@ember/object/promise-proxy-mixin.ts +++ b/packages/@ember/object/promise-proxy-mixin.ts @@ -3,6 +3,8 @@ import setProperties from '@ember/-internals/metal/lib/set_properties'; import computed from '@ember/-internals/metal/lib/computed'; import Mixin from '@ember/object/mixin'; import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { deprecatedMixin } from '@ember/-internals/utils/lib/deprecated-mixin'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; import type { AnyFn, MethodNamesOf } from '@ember/-internals/utility-types'; import type RSVP from 'rsvp'; import type CoreObject from '@ember/object/core'; @@ -108,6 +110,7 @@ function tap(proxy: PromiseProxyMixin, promise: RSVP.Promise) { @class PromiseProxyMixin @public + @deprecated Track the state of the promise on your own class instead. */ interface PromiseProxyMixin { /** @@ -212,36 +215,44 @@ interface PromiseProxyMixin { */ finally: this['promise']['finally']; } -const PromiseProxyMixin = Mixin[INTERNAL_MIXIN_CREATE]({ - reason: null, +const PromiseProxyMixin = deprecatedMixin( + Mixin[INTERNAL_MIXIN_CREATE]({ + reason: null, - isPending: computed('isSettled', function () { - return !get(this, 'isSettled'); - }).readOnly(), + isPending: computed('isSettled', function () { + return !get(this, 'isSettled'); + }).readOnly(), - isSettled: computed('isRejected', 'isFulfilled', function () { - return get(this, 'isRejected') || get(this, 'isFulfilled'); - }).readOnly(), + isSettled: computed('isRejected', 'isFulfilled', function () { + return get(this, 'isRejected') || get(this, 'isFulfilled'); + }).readOnly(), - isRejected: false, + isRejected: false, - isFulfilled: false, + isFulfilled: false, - promise: computed({ - get() { - throw new Error("PromiseProxy's promise must be set"); - }, - set(_key, promise: RSVP.Promise) { - return tap(this, promise); - }, - }), + promise: computed({ + get() { + throw new Error("PromiseProxy's promise must be set"); + }, + set(_key, promise: RSVP.Promise) { + return tap(this, promise); + }, + }), - then: promiseAlias('then'), + then: promiseAlias('then'), - catch: promiseAlias('catch'), + catch: promiseAlias('catch'), - finally: promiseAlias('finally'), -}); + finally: promiseAlias('finally'), + }), + () => { + deprecateUntil( + 'The `PromiseProxyMixin` is deprecated. Track the state of the promise on your own class instead.', + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN + ); + } +); function promiseAlias>>(name: N) { return function (this: PromiseProxyMixin, ...args: Parameters[N]>) { diff --git a/packages/@ember/object/tests/mixin/deprecation_test.js b/packages/@ember/object/tests/mixin/deprecation_test.js index 9e1229a6530..2fdeb9778ce 100644 --- a/packages/@ember/object/tests/mixin/deprecation_test.js +++ b/packages/@ember/object/tests/mixin/deprecation_test.js @@ -1,5 +1,12 @@ import EmberObject from '@ember/object'; import Mixin from '@ember/object/mixin'; +import Observable from '@ember/object/observable'; +import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; +import Enumerable from '@ember/enumerable'; +import MutableEnumerable from '@ember/enumerable/mutable'; +import ObjectProxy from '@ember/object/proxy'; +import ArrayProxy from '@ember/array/proxy'; +import { A as emberA } from '@ember/array'; import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; import { moduleFor, @@ -88,3 +95,128 @@ moduleFor( } } ); + +moduleFor( + 'Framework mixin deprecation', + class extends AbstractTestCase { + [`${testUnless( + DEPRECATIONS.DEPRECATE_OBSERVABLE.isRemoved + )} @test extending with Observable deprecates`](assert) { + let Subclass; + + expectDeprecation( + () => { + Subclass = EmberObject.extend(Observable); + }, + /The `Observable` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_OBSERVABLE.isEnabled + ); + + let obj = Subclass.create({ foo: 'FOO' }); + assert.equal(obj.get('foo'), 'FOO', 'the mixin still applies while deprecated'); + obj.destroy(); + } + + [`${testUnless( + DEPRECATIONS.DEPRECATE_OBSERVABLE.isRemoved + )} @test reopening with Observable deprecates`](assert) { + let Subclass = EmberObject.extend(); + + expectDeprecation( + () => { + Subclass.reopen(Observable); + }, + /The `Observable` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_OBSERVABLE.isEnabled + ); + + let obj = Subclass.create({ foo: 'FOO' }); + assert.equal(obj.get('foo'), 'FOO', 'the reopened class works'); + obj.destroy(); + } + + [`${testUnless( + DEPRECATIONS.DEPRECATE_OBSERVABLE.isRemoved + )} @test EmberObject applies Observable without deprecating`](assert) { + // `EmberObject` is built from `Observable`, so apps that never name the + // mixin must not get a notice. + let obj; + + expectNoDeprecation(() => { + obj = EmberObject.extend({ foo: 'FOO' }).create(); + }); + + assert.equal(obj.get('foo'), 'FOO', 'the object works'); + obj.destroy(); + } + + [`${testUnless( + DEPRECATIONS.DEPRECATE_ENUMERABLE.isRemoved + )} @test extending with Enumerable deprecates`](assert) { + let Subclass; + + expectDeprecation( + () => { + Subclass = EmberObject.extend(Enumerable); + }, + /The `Enumerable` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_ENUMERABLE.isEnabled + ); + + let obj = Subclass.create(); + assert.ok(Enumerable.detect(obj), 'the mixin still applies while deprecated'); + obj.destroy(); + } + + [`${testUnless( + DEPRECATIONS.DEPRECATE_ENUMERABLE.isRemoved + )} @test extending with MutableEnumerable deprecates`](assert) { + let Subclass; + + expectDeprecation( + () => { + Subclass = EmberObject.extend(MutableEnumerable); + }, + /The `MutableEnumerable` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_ENUMERABLE.isEnabled + ); + + let obj = Subclass.create(); + assert.ok(MutableEnumerable.detect(obj), 'the mixin still applies while deprecated'); + obj.destroy(); + } + + [`${testUnless( + DEPRECATIONS.DEPRECATE_ENUMERABLE.isRemoved + )} @test Ember's own arrays apply Enumerable without deprecating`](assert) { + let array, proxy; + + expectNoDeprecation(() => { + array = emberA([1, 2, 3]); + proxy = ArrayProxy.create({ content: emberA([]) }); + }); + + assert.ok(Enumerable.detect(array), 'A() is Enumerable'); + assert.ok(Enumerable.detect(proxy), 'ArrayProxy is Enumerable'); + proxy.destroy(); + } + + [`${testUnless( + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isRemoved + )} @test extending with PromiseProxyMixin deprecates`](assert) { + let Subclass; + + expectDeprecation( + () => { + Subclass = ObjectProxy.extend(PromiseProxyMixin); + }, + /The `PromiseProxyMixin` is deprecated/, + DEPRECATIONS.DEPRECATE_PROMISE_PROXY_MIXIN.isEnabled + ); + + let proxy = Subclass.create(); + assert.ok(PromiseProxyMixin.detect(proxy), 'the mixin still applies while deprecated'); + proxy.destroy(); + } + } +); diff --git a/packages/@ember/object/tests/observable_test.js b/packages/@ember/object/tests/observable_test.js index 8b6ce290204..8e84e0fc304 100644 --- a/packages/@ember/object/tests/observable_test.js +++ b/packages/@ember/object/tests/observable_test.js @@ -2,7 +2,6 @@ import { context } from '@ember/-internals/environment'; import { run } from '@ember/runloop'; import { get, computed } from '@ember/object'; import EmberObject, { observer } from '@ember/object'; -import Observable from '@ember/object/observable'; import { A as emberA } from '@ember/array'; import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; @@ -33,7 +32,8 @@ import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpe let object, objectA, objectB, objectC, objectD, objectE, objectF, lookup; -const ObservableObject = EmberObject.extend(Observable); +// `EmberObject` already applies `Observable`. Applying it again is deprecated. +const ObservableObject = EmberObject.extend(); const originalLookup = context.lookup; class ObservableTestCase extends AbstractTestCase { @@ -58,7 +58,7 @@ moduleFor( 'object.get()', class extends ObservableTestCase { beforeEach() { - object = ObservableObject.extend(Observable, { + object = ObservableObject.extend({ computed: computed(function () { return 'value'; }),