From 269916ab21d14f2cc986c590d0d473b4c893893b Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:48:07 -0400 Subject: [PATCH 01/19] Move internally-used mixins to internal copies RFC #1116 deprecates applying Ember's mixins, but Ember's own internals are built on several of them. Those mixins need to exist twice: an internal copy that applies silently, and a public copy that emits the deprecation. This is the mechanical half -- the six mixins under @ember/-internals move to `-internal` filenames with their contents unchanged, and their importers follow. The public, deprecating copies are added in the next commit. Add deprecating public mixin wrappers (RFC #1116 follow-up) Every mixin Ember applies to its own classes now exists twice: the internal copy applies silently, and the public copy wraps it and emits the deprecation from `init`. The public copies keep the API documentation; the internal copies carry the implementation. `Mixin` itself gets the same treatment -- `InternalMixin` holds the machinery and `Mixin.create` is the deprecating subclass -- which lets the `INTERNAL_MIXIN_CREATE` symbol go away entirely. Because the two copies are distinct objects, `meta.hasMixin` would no longer match a public mixin against an object that only ever had the internal copy applied. `DeprecatedMixin` overrides `detect` to look through to the wrapped copy, so `EmberArray.detect(someArrayProxy)` keeps working while the deprecation is live. Ember's own tests that exercise a mixin's behavior now apply the internal copy, matching what the framework does; tests that specifically cover the deprecation keep using the public copy. `EmberObject` already applies the internal `Observable`, so `observable_test` no longer re-applies the public one on top. The internal copies are kept out of `renamed-modules` so they are not advertised as importable module paths. Move deprecations to 7.5 Set deprecate flags, and update untils --- .../@ember/-internals/deprecations/index.ts | 77 ++ .../-internals/glimmer/lib/component.ts | 10 +- .../-internals/glimmer/lib/helpers/each-in.ts | 2 +- .../tests/integration/helpers/tracked-test.js | 2 +- .../runtime/lib/mixins/-proxy-internal.ts | 123 +++ .../-internals/runtime/lib/mixins/-proxy.ts | 105 +-- .../lib/mixins/action_handler-internal.ts | 51 ++ .../runtime/lib/mixins/action_handler.ts | 48 +- .../runtime/lib/mixins/comparable.ts | 5 +- .../lib/mixins/container_proxy-internal.ts | 43 ++ .../runtime/lib/mixins/container_proxy.ts | 39 +- .../lib/mixins/registry_proxy-internal.ts | 56 ++ .../runtime/lib/mixins/registry_proxy.ts | 50 +- .../mixins/target_action_support-internal.ts | 112 +++ .../lib/mixins/target_action_support.ts | 104 +-- .../runtime/tests/mixins/array_test.js | 3 +- .../tests/mixins/container_proxy_test.js | 2 +- .../mixins/target_action_support_test.js | 2 +- .../utils/lib/internal-mixin-create.ts | 11 - .../lib/mixins/action_support-internal.ts | 51 ++ .../views/lib/mixins/action_support.ts | 49 +- .../-internals/views/lib/views/core_view.ts | 6 +- packages/@ember/array/index-internal.ts | 641 ++++++++++++++++ packages/@ember/array/index.ts | 644 +--------------- packages/@ember/array/proxy.ts | 10 +- .../@ember/controller/controller-internal.ts | 91 +++ packages/@ember/controller/index.ts | 91 +-- packages/@ember/engine/index.ts | 5 +- packages/@ember/engine/instance.ts | 11 +- packages/@ember/enumerable/-internal.ts | 14 + packages/@ember/enumerable/index.ts | 18 +- .../@ember/enumerable/mutable-internal.ts | 15 + packages/@ember/enumerable/mutable.ts | 19 +- packages/@ember/object/core.ts | 19 +- packages/@ember/object/evented.ts | 14 +- packages/@ember/object/index.ts | 6 +- .../lib/computed/reduce_computed_macros.ts | 9 +- packages/@ember/object/mixin-internal.ts | 710 ++++++++++++++++++ packages/@ember/object/mixin.ts | 680 +---------------- packages/@ember/object/observable-internal.ts | 194 +++++ packages/@ember/object/observable.ts | 152 +--- packages/@ember/object/promise-proxy-mixin.ts | 5 +- packages/@ember/object/proxy.ts | 5 +- packages/@ember/object/tests/evented_test.js | 16 +- packages/@ember/object/tests/events_test.js | 68 +- .../object/tests/mixin/deprecation_test.js | 4 +- .../@ember/object/tests/observable_test.js | 5 +- packages/@ember/routing/route.ts | 6 +- .../ember/tests/service_injection_test.js | 2 +- rollup.config.mjs | 13 +- 50 files changed, 2499 insertions(+), 1919 deletions(-) create mode 100644 packages/@ember/-internals/runtime/lib/mixins/-proxy-internal.ts create mode 100644 packages/@ember/-internals/runtime/lib/mixins/action_handler-internal.ts create mode 100644 packages/@ember/-internals/runtime/lib/mixins/container_proxy-internal.ts create mode 100644 packages/@ember/-internals/runtime/lib/mixins/registry_proxy-internal.ts create mode 100644 packages/@ember/-internals/runtime/lib/mixins/target_action_support-internal.ts delete mode 100644 packages/@ember/-internals/utils/lib/internal-mixin-create.ts create mode 100644 packages/@ember/-internals/views/lib/mixins/action_support-internal.ts create mode 100644 packages/@ember/array/index-internal.ts create mode 100644 packages/@ember/controller/controller-internal.ts create mode 100644 packages/@ember/enumerable/-internal.ts create mode 100644 packages/@ember/enumerable/mutable-internal.ts create mode 100644 packages/@ember/object/mixin-internal.ts create mode 100644 packages/@ember/object/observable-internal.ts diff --git a/packages/@ember/-internals/deprecations/index.ts b/packages/@ember/-internals/deprecations/index.ts index 7c18ab9cda3..51a5f91651c 100644 --- a/packages/@ember/-internals/deprecations/index.ts +++ b/packages/@ember/-internals/deprecations/index.ts @@ -175,6 +175,83 @@ export const DEPRECATIONS = { until: '9.0.0', url: 'https://deprecations.emberjs.com/id/deprecate-computed-macros', }), + DEPRECATE_OBSERVABLE_MIXIN: deprecation({ + id: 'deprecate-observable-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-observable-mixin', + }), + DEPRECATE_ENUMERABLE_MIXIN: deprecation({ + id: 'deprecate-enumerable-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-enumerable-mixin', + }), + DEPRECATE_MUTABLE_ENUMERABLE_MIXIN: deprecation({ + id: 'deprecate-mutable-enumerable-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-mutable-enumerable-mixin', + }), + DEPRECATE_EMBER_ARRAY_MIXIN: deprecation({ + id: 'deprecate-ember-array-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-ember-array-mixin', + }), + DEPRECATE_MUTABLE_ARRAY_MIXIN: deprecation({ + id: 'deprecate-mutable-array-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-mutable-array-mixin', + }), + DEPRECATE_NATIVE_ARRAY_MIXIN: deprecation({ + id: 'deprecate-native-array-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-native-array-mixin', + }), + DEPRECATE_CONTROLLER_MIXIN: deprecation({ + id: 'deprecate-controller-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-controller-mixin', + }), + DEPRECATE_ACTION_HANDLER_MIXIN: deprecation({ + id: 'deprecate-action-handler-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-action-handler-mixin', + }), + DEPRECATE_PROXY_MIXIN: deprecation({ + id: 'deprecate-proxy-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-proxy-mixin', + }), + DEPRECATE_CONTAINER_PROXY_MIXIN: deprecation({ + id: 'deprecate-container-proxy-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-container-proxy-mixin', + }), + DEPRECATE_REGISTRY_PROXY_MIXIN: deprecation({ + id: 'deprecate-registry-proxy-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-registry-proxy-mixin', + }), }; export function deprecateUntil(message: string, deprecation: DeprecationObject) { diff --git a/packages/@ember/-internals/glimmer/lib/component.ts b/packages/@ember/-internals/glimmer/lib/component.ts index 5c0735cd780..490a6595227 100644 --- a/packages/@ember/-internals/glimmer/lib/component.ts +++ b/packages/@ember/-internals/glimmer/lib/component.ts @@ -4,9 +4,11 @@ import { get } from '@ember/-internals/metal/lib/property_get'; import { PROPERTY_DID_CHANGE } from '@ember/-internals/metal/lib/property_events'; import type { PropertyDidChange } from '@ember/-internals/metal/lib/property_events'; import { getOwner } from '@ember/-internals/owner'; -import TargetActionSupport from '@ember/-internals/runtime/lib/mixins/target_action_support'; +import type TargetActionSupport from '@ember/-internals/runtime/lib/mixins/target_action_support'; +import InternalTargetActionSupport from '@ember/-internals/runtime/lib/mixins/target_action_support-internal'; import type ViewStates from '@ember/-internals/views/lib/views/states'; -import ActionSupport from '@ember/-internals/views/lib/mixins/action_support'; +import type ActionSupport from '@ember/-internals/views/lib/mixins/action_support'; +import InternalActionSupport from '@ember/-internals/views/lib/mixins/action_support-internal'; import { addChildView, getChildViews, @@ -803,8 +805,8 @@ interface Component class Component extends CoreView.extend( - TargetActionSupport, - ActionSupport, + InternalTargetActionSupport, + InternalActionSupport, { // These need to be overridable via extend/create but should still // have a default. Defining them here is the best way to achieve that. diff --git a/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts b/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts index 745442038d9..ed10579f268 100644 --- a/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts +++ b/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts @@ -2,7 +2,7 @@ @module @ember/helper */ import { tagForObject } from '@ember/-internals/metal/lib/tags'; -import { contentFor as _contentFor } from '@ember/-internals/runtime/lib/mixins/-proxy'; +import { contentFor as _contentFor } from '@ember/-internals/runtime/lib/mixins/-proxy-internal'; import { isProxy } from '@ember/-internals/utils/lib/is_proxy'; import { assert } from '@ember/debug'; import type { CapturedArguments } from '@glimmer/interfaces'; diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js index a2a12be40cc..2aed389bace 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js @@ -1,7 +1,7 @@ import EmberObject from '@ember/object'; import { A } from '@ember/array'; -import MutableArray from '@ember/array/mutable'; import { tracked, notifyPropertyChange } from '@ember/-internals/metal'; +import { InternalMutableArray as MutableArray } from '@ember/array/index-internal'; import Service, { service } from '@ember/service'; import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers'; import { precompileTemplate } from '@ember/template-compilation'; diff --git a/packages/@ember/-internals/runtime/lib/mixins/-proxy-internal.ts b/packages/@ember/-internals/runtime/lib/mixins/-proxy-internal.ts new file mode 100644 index 00000000000..27592c3f89f --- /dev/null +++ b/packages/@ember/-internals/runtime/lib/mixins/-proxy-internal.ts @@ -0,0 +1,123 @@ +/** +@module ember +*/ + +import { meta } from '@ember/-internals/meta/lib/meta'; +import { InternalMixin } from '@ember/object/mixin-internal'; +import { get } from '@ember/-internals/metal/lib/property_get'; +import { set } from '@ember/-internals/metal/lib/property_set'; +import { defineProperty } from '@ember/-internals/metal/lib/properties'; +import { tagForObject, tagForProperty } from '@ember/-internals/metal/lib/tags'; +import computed from '@ember/-internals/metal/lib/computed'; +import { setProxy, isProxy } from '@ember/-internals/utils/lib/is_proxy'; +import { setupMandatorySetter } from '@ember/-internals/utils/lib/mandatory-setter'; +import { isObject } from '@ember/-internals/utils/lib/spec'; +import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; +import { setCustomTagFor } from '@glimmer/manager/lib/util/args-proxy'; +import type { UpdatableTag, Tag } from '@glimmer/interfaces'; +import { combine, UPDATE_TAG as updateTag } from '@glimmer/validator/lib/validators'; +import { tagFor, tagMetaFor } from '@glimmer/validator/lib/meta'; + +export function contentFor(proxy: { content: T | null }): T | null { + let content = get(proxy, 'content'); + // SAFETY: Ideally we'd assert instead of casting, but @glimmer/validator doesn't give us + // sufficient public types for this. Previously this code was .js and worked correctly so + // hopefully this is sufficiently reliable. + updateTag(tagForObject(proxy) as UpdatableTag, tagForObject(content)); + return content; +} + +function customTagForProxy(proxy: object, key: string, addMandatorySetter?: boolean): Tag { + assert('Expected a proxy', isProxy(proxy)); + + let meta = tagMetaFor(proxy); + let tag = tagFor(proxy, key, meta); + + if (DEBUG) { + // TODO: Replace this with something more first class for tracking tags in DEBUG + // SAFETY: This is not an officially supported property but setting shouldn't cause issues. + (tag as any)._propertyKey = key; + } + + if (key in proxy) { + if (DEBUG && addMandatorySetter) { + assert('[BUG] setupMandatorySetter should be set when debugging', setupMandatorySetter); + setupMandatorySetter(tag, proxy, key); + } + + return tag; + } else { + let tags: Tag[] = [tag, tagFor(proxy, 'content', meta)]; + + let content = contentFor(proxy); + + if (isObject(content)) { + tags.push(tagForProperty(content, key, addMandatorySetter)); + } + + return combine(tags); + } +} + +/** + The internal counterpart to the public `ProxyMixin`. Ember's own internals + apply this so that they do not trigger the deprecation that the public + mixin emits. The public API documentation lives on the public copy. + + @internal +*/ +const InternalProxyMixin = /*@__PURE__*/ InternalMixin.create({ + /** + The object whose properties will be forwarded. + + @property content + @type {unknown} + @default null + */ + content: null, + + init() { + this._super(...arguments); + setProxy(this); + tagForObject(this); + setCustomTagFor(this, customTagForProxy); + }, + + willDestroy() { + this.set('content', null); + this._super(...arguments); + }, + + isTruthy: computed('content', function () { + return Boolean(get(this, 'content')); + }), + + unknownProperty(key: string) { + let content = contentFor(this); + return content ? get(content, key) : undefined; + }, + + setUnknownProperty(key: string, value: unknown) { + let m = meta(this); + + if (m.isInitializing() || m.isPrototypeMeta(this)) { + // if marked as prototype or object is initializing then just + // defineProperty rather than delegate + defineProperty(this, key, null, value); + return value; + } + + let content = contentFor(this); + + assert( + `Cannot delegate set('${key}', ${value}) to the 'content' property of object proxy ${this}: its 'content' is undefined.`, + content + ); + + // SAFETY: We don't actually guarantee that this is an object, so this isn't necessarily safe :( + return set(content as object, key, value); + }, +}); + +export default InternalProxyMixin; diff --git a/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts b/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts index cf3e23234e6..2e43d3c85a3 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts @@ -2,69 +2,17 @@ @module ember */ -import { meta } from '@ember/-internals/meta/lib/meta'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import { get } from '@ember/-internals/metal/lib/property_get'; -import { set } from '@ember/-internals/metal/lib/property_set'; -import { defineProperty } from '@ember/-internals/metal/lib/properties'; -import { tagForObject, tagForProperty } from '@ember/-internals/metal/lib/tags'; -import computed from '@ember/-internals/metal/lib/computed'; -import { setProxy, isProxy } from '@ember/-internals/utils/lib/is_proxy'; -import { setupMandatorySetter } from '@ember/-internals/utils/lib/mandatory-setter'; -import { isObject } from '@ember/-internals/utils/lib/spec'; -import { assert } from '@ember/debug'; -import { DEBUG } from '@glimmer/env'; -import { setCustomTagFor } from '@glimmer/manager/lib/util/args-proxy'; -import type { UpdatableTag, Tag } from '@glimmer/interfaces'; -import { combine, UPDATE_TAG as updateTag } from '@glimmer/validator/lib/validators'; -import { tagFor, tagMetaFor } from '@glimmer/validator/lib/meta'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalProxyMixin from '@ember/-internals/runtime/lib/mixins/-proxy-internal'; -export function contentFor(proxy: ProxyMixin): T | null { - let content = get(proxy, 'content'); - // SAFETY: Ideally we'd assert instead of casting, but @glimmer/validator doesn't give us - // sufficient public types for this. Previously this code was .js and worked correctly so - // hopefully this is sufficiently reliable. - updateTag(tagForObject(proxy) as UpdatableTag, tagForObject(content)); - return content; -} - -function customTagForProxy(proxy: object, key: string, addMandatorySetter?: boolean): Tag { - assert('Expected a proxy', isProxy(proxy)); - - let meta = tagMetaFor(proxy); - let tag = tagFor(proxy, key, meta); - - if (DEBUG) { - // TODO: Replace this with something more first class for tracking tags in DEBUG - // SAFETY: This is not an officially supported property but setting shouldn't cause issues. - (tag as any)._propertyKey = key; - } - - if (key in proxy) { - if (DEBUG && addMandatorySetter) { - assert('[BUG] setupMandatorySetter should be set when debugging', setupMandatorySetter); - setupMandatorySetter(tag, proxy, key); - } - - return tag; - } else { - let tags: Tag[] = [tag, tagFor(proxy, 'content', meta)]; - - let content = contentFor(proxy); - - if (isObject(content)) { - tags.push(tagForProperty(content, key, addMandatorySetter)); - } - - return combine(tags); - } -} +export { contentFor } from '@ember/-internals/runtime/lib/mixins/-proxy-internal'; /** `ProxyMixin` forwards all properties not defined by the proxy itself to a proxied `content` object. See ObjectProxy for more details. + @deprecated @class ProxyMixin @namespace Ember @private @@ -91,7 +39,7 @@ interface ProxyMixin { setUnknownProperty(key: string, value: V): V; } -const ProxyMixin = /*@__PURE__*/ Mixin[INTERNAL_MIXIN_CREATE]({ +const ProxyMixin = /*@__PURE__*/ DeprecatedMixin.create(InternalProxyMixin, { /** The object whose properties will be forwarded. @@ -100,48 +48,13 @@ const ProxyMixin = /*@__PURE__*/ Mixin[INTERNAL_MIXIN_CREATE]({ @default null @public */ - content: null, init() { this._super(...arguments); - setProxy(this); - tagForObject(this); - setCustomTagFor(this, customTagForProxy); - }, - - willDestroy() { - this.set('content', null); - this._super(...arguments); - }, - - isTruthy: computed('content', function () { - return Boolean(get(this, 'content')); - }), - - unknownProperty(key: string) { - let content = contentFor(this); - return content ? get(content, key) : undefined; - }, - - setUnknownProperty(key: string, value: unknown) { - let m = meta(this); - - if (m.isInitializing() || m.isPrototypeMeta(this)) { - // if marked as prototype or object is initializing then just - // defineProperty rather than delegate - defineProperty(this, key, null, value); - return value; - } - - let content = contentFor(this); - - assert( - `Cannot delegate set('${key}', ${value}) to the 'content' property of object proxy ${this}: its 'content' is undefined.`, - content + deprecateUntil( + 'The `ProxyMixin` mixin is deprecated. Access the underlying object directly instead.', + DEPRECATIONS.DEPRECATE_PROXY_MIXIN ); - - // SAFETY: We don't actually guarantee that this is an object, so this isn't necessarily safe :( - return set(content as object, key, value); }, }); diff --git a/packages/@ember/-internals/runtime/lib/mixins/action_handler-internal.ts b/packages/@ember/-internals/runtime/lib/mixins/action_handler-internal.ts new file mode 100644 index 00000000000..38cb4c78409 --- /dev/null +++ b/packages/@ember/-internals/runtime/lib/mixins/action_handler-internal.ts @@ -0,0 +1,51 @@ +/** +@module ember +*/ + +import { InternalMixin } from '@ember/object/mixin-internal'; +import { get } from '@ember/-internals/metal/lib/property_get'; +import { assert } from '@ember/debug'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; + +/** + The internal counterpart to the public `ActionHandler` mixin. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. The public API documentation lives on the public copy. + + @internal +*/ +interface InternalActionHandler { + actions?: Record unknown>; + send(actionName: string, ...args: unknown[]): void; +} +const InternalActionHandler = InternalMixin.create({ + mergedProperties: ['actions'], + + send(actionName: string, ...args: any[]) { + deprecateUntil( + `Calling \`.send()\` on ${this} is deprecated. Invoke the corresponding method directly.`, + DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT + ); + assert( + `Attempted to call .send() with the action '${actionName}' on the destroyed object '${this}'.`, + !this.isDestroying && !this.isDestroyed + ); + if (this.actions && this.actions[actionName]) { + let shouldBubble = this.actions[actionName].apply(this, args) === true; + if (!shouldBubble) { + return; + } + } + + let target = get(this, 'target'); + if (target) { + assert( + `The \`target\` for ${this} (${target}) does not have a \`send\` method`, + typeof target.send === 'function' + ); + target.send(...arguments); + } + }, +}); + +export default InternalActionHandler; diff --git a/packages/@ember/-internals/runtime/lib/mixins/action_handler.ts b/packages/@ember/-internals/runtime/lib/mixins/action_handler.ts index 0270c1e32df..f3e4b13e8bf 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/action_handler.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/action_handler.ts @@ -2,11 +2,9 @@ @module ember */ -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import { get } from '@ember/-internals/metal/lib/property_get'; -import { assert } from '@ember/debug'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler-internal'; /** `ActionHandler` is available on some familiar classes including @@ -15,16 +13,21 @@ import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; and `Route` and available to the above classes through inheritance.) + @deprecated @class ActionHandler @namespace Ember @private */ -interface ActionHandler { - actions?: Record unknown>; - send(actionName: string, ...args: unknown[]): void; -} -const ActionHandler = Mixin[INTERNAL_MIXIN_CREATE]({ - mergedProperties: ['actions'], +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface ActionHandler extends InternalActionHandler {} +const ActionHandler = DeprecatedMixin.create(InternalActionHandler, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `ActionHandler` mixin is deprecated. Use the `@action` decorator and direct method calls instead.', + DEPRECATIONS.DEPRECATE_ACTION_HANDLER_MIXIN + ); + }, /** The collection of functions, keyed by name, available on this @@ -205,31 +208,6 @@ const ActionHandler = Mixin[INTERNAL_MIXIN_CREATE]({ @param {*} context a context to send with the action @public */ - send(actionName: string, ...args: any[]) { - deprecateUntil( - `Calling \`.send()\` on ${this} is deprecated. Invoke the corresponding method directly.`, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT - ); - assert( - `Attempted to call .send() with the action '${actionName}' on the destroyed object '${this}'.`, - !this.isDestroying && !this.isDestroyed - ); - if (this.actions && this.actions[actionName]) { - let shouldBubble = this.actions[actionName].apply(this, args) === true; - if (!shouldBubble) { - return; - } - } - - let target = get(this, 'target'); - if (target) { - assert( - `The \`target\` for ${this} (${target}) does not have a \`send\` method`, - typeof target.send === 'function' - ); - target.send(...arguments); - } - }, }); export default ActionHandler; diff --git a/packages/@ember/-internals/runtime/lib/mixins/comparable.ts b/packages/@ember/-internals/runtime/lib/mixins/comparable.ts index 20047cfcb00..ea1662d67ae 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/comparable.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/comparable.ts @@ -1,5 +1,4 @@ -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { InternalMixin } from '@ember/object/mixin-internal'; import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; /** @@ -20,7 +19,7 @@ import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; interface Comparable { compare: ((a: unknown, b: unknown) => -1 | 0 | 1) | null; } -const Comparable = Mixin[INTERNAL_MIXIN_CREATE]({ +const Comparable = InternalMixin.create({ /** __Required.__ You must implement this method to apply this mixin. diff --git a/packages/@ember/-internals/runtime/lib/mixins/container_proxy-internal.ts b/packages/@ember/-internals/runtime/lib/mixins/container_proxy-internal.ts new file mode 100644 index 00000000000..eb3c3313e1c --- /dev/null +++ b/packages/@ember/-internals/runtime/lib/mixins/container_proxy-internal.ts @@ -0,0 +1,43 @@ +import { schedule, join } from '@ember/runloop'; +/** +@module ember +*/ +import { InternalMixin } from '@ember/object/mixin-internal'; + +/** + The internal counterpart to the public `ContainerProxyMixin`. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. The public API documentation lives on the public copy. + + @internal +*/ +const InternalContainerProxyMixin = InternalMixin.create({ + __container__: null, + + ownerInjection() { + return this.__container__.ownerInjection(); + }, + + lookup(fullName: string, options: object) { + return this.__container__.lookup(fullName, options); + }, + + destroy() { + let container = this.__container__; + + if (container) { + join(() => { + container.destroy(); + schedule('destroy', container, 'finalizeDestroy'); + }); + } + + this._super(); + }, + + factoryFor(fullName: string) { + return this.__container__.factoryFor(fullName); + }, +}); + +export default InternalContainerProxyMixin; diff --git a/packages/@ember/-internals/runtime/lib/mixins/container_proxy.ts b/packages/@ember/-internals/runtime/lib/mixins/container_proxy.ts index 34ca8f41fba..f67d30d8ebd 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/container_proxy.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/container_proxy.ts @@ -1,11 +1,11 @@ -import { schedule, join } from '@ember/runloop'; /** @module ember */ import type Container from '@ember/-internals/container/lib/container'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; import type { ContainerProxy } from '@ember/-internals/owner'; +import InternalContainerProxyMixin from '@ember/-internals/runtime/lib/mixins/container_proxy-internal'; // This is defined as a separate interface so that it can be used in the definition of // `Owner` without also including the `__container__` property. @@ -17,43 +17,26 @@ import type { ContainerProxy } from '@ember/-internals/owner'; @class ContainerProxyMixin @extends ContainerProxy @private + @deprecated Use the owner API from `@ember/owner` instead. */ interface ContainerProxyMixin extends ContainerProxy { /** @internal */ __container__: Container; } -const ContainerProxyMixin = Mixin[INTERNAL_MIXIN_CREATE]({ +const ContainerProxyMixin = DeprecatedMixin.create(InternalContainerProxyMixin, { /** The container stores state. @private @property {Ember.Container} __container__ */ - __container__: null, - ownerInjection() { - return this.__container__.ownerInjection(); - }, - - lookup(fullName: string, options: object) { - return this.__container__.lookup(fullName, options); - }, - - destroy() { - let container = this.__container__; - - if (container) { - join(() => { - container.destroy(); - schedule('destroy', container, 'finalizeDestroy'); - }); - } - - this._super(); - }, - - factoryFor(fullName: string) { - return this.__container__.factoryFor(fullName); + init() { + this._super(...arguments); + deprecateUntil( + 'The `ContainerProxyMixin` mixin is deprecated. Use the owner API from `@ember/owner` instead.', + DEPRECATIONS.DEPRECATE_CONTAINER_PROXY_MIXIN + ); }, }); diff --git a/packages/@ember/-internals/runtime/lib/mixins/registry_proxy-internal.ts b/packages/@ember/-internals/runtime/lib/mixins/registry_proxy-internal.ts new file mode 100644 index 00000000000..9c198c1ceb3 --- /dev/null +++ b/packages/@ember/-internals/runtime/lib/mixins/registry_proxy-internal.ts @@ -0,0 +1,56 @@ +/** +@module ember +*/ + +import type Registry from '@ember/-internals/container/lib/registry'; +import type { AnyFn } from '@ember/-internals/utility-types'; + +import { assert } from '@ember/debug'; +import { InternalMixin } from '@ember/object/mixin-internal'; + +/** + The internal counterpart to the public `RegistryProxyMixin`. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. The public API documentation lives on the public copy. + + @internal +*/ +const InternalRegistryProxyMixin = InternalMixin.create({ + __registry__: null, + + resolveRegistration(fullName: string) { + assert('fullName must be a proper full name', this.__registry__.isValidFullName(fullName)); + return this.__registry__.resolve(fullName); + }, + + register: registryAlias('register'), + unregister: registryAlias('unregister'), + hasRegistration: registryAlias('has'), + registeredOption: registryAlias('getOption'), + registerOptions: registryAlias('options'), + registeredOptions: registryAlias('getOptions'), + registerOptionsForType: registryAlias('optionsForType'), + registeredOptionsForType: registryAlias('getOptionsForType'), +}); + +type AliasMethods = + | 'register' + | 'unregister' + | 'has' + | 'getOption' + | 'options' + | 'getOptions' + | 'optionsForType' + | 'getOptionsForType'; + +function registryAlias(name: N) { + return function (this: { __registry__: Registry }, ...args: Parameters) { + // We need this cast because `Parameters` is deferred so that it is not + // possible for TS to see it will always produce the right type. However, + // since `AnyFn` has a rest type, it is allowed. See discussion on [this + // issue](https://github.com/microsoft/TypeScript/issues/47615). + return (this.__registry__[name] as AnyFn)(...args); + }; +} + +export default InternalRegistryProxyMixin; diff --git a/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts b/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts index 9081904d5d7..ab2936d320a 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts @@ -4,11 +4,10 @@ import type Registry from '@ember/-internals/container/lib/registry'; import type { RegistryProxy } from '@ember/-internals/owner'; -import type { AnyFn } from '@ember/-internals/utility-types'; -import { assert } from '@ember/debug'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalRegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy-internal'; /** RegistryProxyMixin is used to provide public access to specific @@ -17,47 +16,20 @@ import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixi @class RegistryProxyMixin @extends RegistryProxy @private + @deprecated Use the owner API from `@ember/owner` instead. */ interface RegistryProxyMixin extends RegistryProxy { /** @internal */ __registry__: Registry; } -const RegistryProxyMixin = Mixin[INTERNAL_MIXIN_CREATE]({ - __registry__: null, - - resolveRegistration(fullName: string) { - assert('fullName must be a proper full name', this.__registry__.isValidFullName(fullName)); - return this.__registry__.resolve(fullName); +const RegistryProxyMixin = DeprecatedMixin.create(InternalRegistryProxyMixin, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `RegistryProxyMixin` mixin is deprecated. Use the owner API from `@ember/owner` instead.', + DEPRECATIONS.DEPRECATE_REGISTRY_PROXY_MIXIN + ); }, - - register: registryAlias('register'), - unregister: registryAlias('unregister'), - hasRegistration: registryAlias('has'), - registeredOption: registryAlias('getOption'), - registerOptions: registryAlias('options'), - registeredOptions: registryAlias('getOptions'), - registerOptionsForType: registryAlias('optionsForType'), - registeredOptionsForType: registryAlias('getOptionsForType'), }); -type AliasMethods = - | 'register' - | 'unregister' - | 'has' - | 'getOption' - | 'options' - | 'getOptions' - | 'optionsForType' - | 'getOptionsForType'; - -function registryAlias(name: N) { - return function (this: RegistryProxyMixin, ...args: Parameters) { - // We need this cast because `Parameters` is deferred so that it is not - // possible for TS to see it will always produce the right type. However, - // since `AnyFn` has a rest type, it is allowed. See discussion on [this - // issue](https://github.com/microsoft/TypeScript/issues/47615). - return (this.__registry__[name] as AnyFn)(...args); - }; -} - export default RegistryProxyMixin; diff --git a/packages/@ember/-internals/runtime/lib/mixins/target_action_support-internal.ts b/packages/@ember/-internals/runtime/lib/mixins/target_action_support-internal.ts new file mode 100644 index 00000000000..780c91659ab --- /dev/null +++ b/packages/@ember/-internals/runtime/lib/mixins/target_action_support-internal.ts @@ -0,0 +1,112 @@ +/** +@module ember +*/ + +import { context } from '@ember/-internals/environment/lib/context'; +import { get } from '@ember/-internals/metal/lib/property_get'; +import computed from '@ember/-internals/metal/lib/computed'; +import { InternalMixin } from '@ember/object/mixin-internal'; +import { assert } from '@ember/debug'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import { DEBUG } from '@glimmer/env'; + +/** + The internal counterpart to the public `TargetActionSupport` mixin. Ember's + own internals apply this so that they do not trigger the deprecation that + the public mixin emits. The public API documentation lives on the public + copy. + + @internal +*/ +const InternalTargetActionSupport = InternalMixin.create({ + target: null, + action: null, + actionContext: null, + + actionContextObject: computed('actionContext', function () { + let actionContext = get(this, 'actionContext'); + + if (typeof actionContext === 'string') { + let value = get(this, actionContext); + if (value === undefined) { + value = get(context.lookup, actionContext); + } + return value; + } else { + return actionContext; + } + }), + + triggerAction(opts: { action?: string; target?: unknown; actionContext?: unknown } = {}) { + deprecateUntil( + `Calling \`triggerAction\` on ${this} is deprecated. Invoke the target method directly.`, + DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT + ); + + let { action, target, actionContext } = opts; + action = action || get(this, 'action'); + target = target || getTarget(this); + + if (actionContext === undefined) { + actionContext = get(this, 'actionContextObject') || this; + } + + let context = Array.isArray(actionContext) ? actionContext : [actionContext]; + + if (target && action) { + let ret; + + if (isSendable(target)) { + ret = target.send(action, ...context); + } else { + assert( + `The action '${action}' did not exist on ${target}`, + typeof (target as any)[action] === 'function' + ); + ret = (target as any)[action](...context); + } + + if (ret !== false) { + return true; + } + } + + return false; + }, +}); + +interface Sendable { + send(action: string, ...context: unknown[]): unknown; +} + +function isSendable(obj: unknown): obj is Sendable { + return obj != null && typeof obj === 'object' && typeof (obj as Sendable).send === 'function'; +} + +function getTarget(instance: { _target?: unknown }) { + let target = get(instance, 'target'); + if (target) { + if (typeof target === 'string') { + let value = get(instance, target); + if (value === undefined) { + value = get(context.lookup, target); + } + + return value; + } else { + return target; + } + } + + if (instance._target) { + return instance._target; + } + + return null; +} + +if (DEBUG) { + Object.seal(InternalTargetActionSupport); +} + +export default InternalTargetActionSupport; diff --git a/packages/@ember/-internals/runtime/lib/mixins/target_action_support.ts b/packages/@ember/-internals/runtime/lib/mixins/target_action_support.ts index 6682e3b80e3..858a668f559 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/target_action_support.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/target_action_support.ts @@ -2,14 +2,10 @@ @module ember */ -import { context } from '@ember/-internals/environment/lib/context'; -import { get } from '@ember/-internals/metal/lib/property_get'; -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 { assert } from '@ember/debug'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; import { DEBUG } from '@glimmer/env'; +import InternalTargetActionSupport from '@ember/-internals/runtime/lib/mixins/target_action_support-internal'; /** `TargetActionSupport` is a mixin that can be included in a class @@ -22,36 +18,13 @@ doing more complex event handling in Components. @namespace Ember @extends Mixin @private +@deprecated Use a direct method call or closure action instead. */ interface TargetActionSupport { target: unknown; action: string | null; actionContext: unknown; actionContextObject: unknown; - triggerAction(opts?: object): unknown; - - /** @internal */ - _target?: unknown; -} -const TargetActionSupport = Mixin[INTERNAL_MIXIN_CREATE]({ - target: null, - action: null, - actionContext: null, - - actionContextObject: computed('actionContext', function () { - let actionContext = get(this, 'actionContext'); - - if (typeof actionContext === 'string') { - let value = get(this, actionContext); - if (value === undefined) { - value = get(context.lookup, actionContext); - } - return value; - } else { - return actionContext; - } - }), - /** The following is private and vestigial. Send an `action` with an `actionContext` to a `target`. The action, actionContext @@ -111,74 +84,21 @@ const TargetActionSupport = Mixin[INTERNAL_MIXIN_CREATE]({ @return {Boolean} true if the action was sent successfully and did not return false @private */ - triggerAction(opts: { action?: string; target?: unknown; actionContext?: unknown } = {}) { + triggerAction(opts?: object): unknown; + + /** @internal */ + _target?: unknown; +} +const TargetActionSupport = DeprecatedMixin.create(InternalTargetActionSupport, { + init() { + this._super(...arguments); deprecateUntil( - `Calling \`triggerAction\` on ${this} is deprecated. Invoke the target method directly.`, + 'The `TargetActionSupport` mixin is deprecated. Invoke the target method directly instead.', DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT ); - - let { action, target, actionContext } = opts; - action = action || get(this, 'action'); - target = target || getTarget(this); - - if (actionContext === undefined) { - actionContext = get(this, 'actionContextObject') || this; - } - - let context = Array.isArray(actionContext) ? actionContext : [actionContext]; - - if (target && action) { - let ret; - - if (isSendable(target)) { - ret = target.send(action, ...context); - } else { - assert( - `The action '${action}' did not exist on ${target}`, - typeof (target as any)[action] === 'function' - ); - ret = (target as any)[action](...context); - } - - if (ret !== false) { - return true; - } - } - - return false; }, }); -interface Sendable { - send(action: string, ...context: unknown[]): unknown; -} - -function isSendable(obj: unknown): obj is Sendable { - return obj != null && typeof obj === 'object' && typeof (obj as Sendable).send === 'function'; -} - -function getTarget(instance: TargetActionSupport) { - let target = get(instance, 'target'); - if (target) { - if (typeof target === 'string') { - let value = get(instance, target); - if (value === undefined) { - value = get(context.lookup, target); - } - - return value; - } else { - return target; - } - } - - if (instance._target) { - return instance._target; - } - - return null; -} - if (DEBUG) { Object.seal(TargetActionSupport); } diff --git a/packages/@ember/-internals/runtime/tests/mixins/array_test.js b/packages/@ember/-internals/runtime/tests/mixins/array_test.js index bf05aa13c7d..3ec1baed666 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/array_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/array_test.js @@ -7,7 +7,8 @@ import { arrayContentWillChange, } from '@ember/-internals/metal'; import EmberObject, { get, set, computed, observer as emberObserver } from '@ember/object'; -import EmberArray, { A as emberA } from '@ember/array'; +import { A as emberA } from '@ember/array'; +import { InternalEmberArray as EmberArray } from '@ember/array/index-internal'; import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; /* diff --git a/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js b/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js index 1f9cbcfa175..445e4eceedc 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js @@ -1,6 +1,6 @@ import { getOwner } from '@ember/-internals/owner'; import { Container, Registry } from '@ember/-internals/container'; -import ContainerProxy from '../../lib/mixins/container_proxy'; +import ContainerProxy from '../../lib/mixins/container_proxy-internal'; import EmberObject from '@ember/object'; import { run, schedule } from '@ember/runloop'; import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; diff --git a/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js b/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js index 13fae5111ed..e2b205a93db 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js @@ -1,6 +1,6 @@ import { context } from '@ember/-internals/environment'; import EmberObject from '@ember/object'; -import TargetActionSupport from '../../lib/mixins/target_action_support'; +import TargetActionSupport from '../../lib/mixins/target_action_support-internal'; import { expectDeprecation, moduleFor, AbstractTestCase, testUnless } from 'internal-test-helpers'; import { DEPRECATIONS } from '../../../deprecations'; diff --git a/packages/@ember/-internals/utils/lib/internal-mixin-create.ts b/packages/@ember/-internals/utils/lib/internal-mixin-create.ts deleted file mode 100644 index c01119d15aa..00000000000 --- a/packages/@ember/-internals/utils/lib/internal-mixin-create.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - Key for the internal Mixin constructor. - - Ember's own internals are built on mixins, so they need a way to construct - one without triggering the mixin deprecation that `Mixin.create` emits. This - is deliberately a Symbol so that it does not show up as a discoverable - property name and cannot be reached by name from application code. - - @private -*/ -export const INTERNAL_MIXIN_CREATE = Symbol('__internal__mixin__'); diff --git a/packages/@ember/-internals/views/lib/mixins/action_support-internal.ts b/packages/@ember/-internals/views/lib/mixins/action_support-internal.ts new file mode 100644 index 00000000000..6e22243b742 --- /dev/null +++ b/packages/@ember/-internals/views/lib/mixins/action_support-internal.ts @@ -0,0 +1,51 @@ +/** + @module ember +*/ +import { get } from '@ember/-internals/metal/lib/property_get'; +import { InternalMixin } from '@ember/object/mixin-internal'; +import inspect from '@ember/debug/lib/inspect'; +import { assert } from '@ember/debug'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; + +/** + The internal counterpart to the public `ActionSupport` mixin. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. The public API documentation lives on the public copy. + + @internal +*/ +const InternalActionSupport = InternalMixin.create({ + send(actionName: string, ...args: unknown[]) { + deprecateUntil( + `Calling \`.send()\` on ${this} is deprecated. Invoke the corresponding method directly.`, + DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT + ); + + assert( + `Attempted to call .send() with the action '${actionName}' on the destroyed object '${this}'.`, + !this.isDestroying && !this.isDestroyed + ); + + let action = this.actions && this.actions[actionName]; + + if (action) { + let shouldBubble = action.apply(this, args) === true; + if (!shouldBubble) { + return; + } + } + + let target = get(this, 'target'); + if (target) { + assert( + `The \`target\` for ${this} (${target}) does not have a \`send\` method`, + typeof target.send === 'function' + ); + target.send(...arguments); + } else { + assert(`${inspect(this)} had no action handler for: ${actionName}`, action); + } + }, +}); + +export default InternalActionSupport; diff --git a/packages/@ember/-internals/views/lib/mixins/action_support.ts b/packages/@ember/-internals/views/lib/mixins/action_support.ts index 1db3b87fa79..27885c83e9c 100644 --- a/packages/@ember/-internals/views/lib/mixins/action_support.ts +++ b/packages/@ember/-internals/views/lib/mixins/action_support.ts @@ -1,52 +1,35 @@ /** @module ember */ -import { get } from '@ember/-internals/metal/lib/property_get'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import inspect from '@ember/debug/lib/inspect'; -import { assert } from '@ember/debug'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalActionSupport from '@ember/-internals/views/lib/mixins/action_support-internal'; /** @class ActionSupport @namespace Ember @private + @deprecated Invoke the corresponding method directly instead. */ interface ActionSupport { + /** + Calls an action passed to a component. + + @method send + @deprecated Invoke the corresponding method directly instead. + @param {String} actionName The action to trigger + @param {*} args Arguments to pass on with the action + @private + */ send(actionName: string, ...args: unknown[]): void; } -const ActionSupport = Mixin[INTERNAL_MIXIN_CREATE]({ - send(actionName: string, ...args: unknown[]) { +const ActionSupport = DeprecatedMixin.create(InternalActionSupport, { + init() { + this._super(...arguments); deprecateUntil( - `Calling \`.send()\` on ${this} is deprecated. Invoke the corresponding method directly.`, + 'The `ActionSupport` mixin is deprecated. Invoke the corresponding method directly instead.', DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT ); - - assert( - `Attempted to call .send() with the action '${actionName}' on the destroyed object '${this}'.`, - !this.isDestroying && !this.isDestroyed - ); - - let action = this.actions && this.actions[actionName]; - - if (action) { - let shouldBubble = action.apply(this, args) === true; - if (!shouldBubble) { - return; - } - } - - let target = get(this, 'target'); - if (target) { - assert( - `The \`target\` for ${this} (${target}) does not have a \`send\` method`, - typeof target.send === 'function' - ); - target.send(...arguments); - } else { - assert(`${inspect(this)} had no action handler for: ${actionName}`, action); - } }, }); diff --git a/packages/@ember/-internals/views/lib/views/core_view.ts b/packages/@ember/-internals/views/lib/views/core_view.ts index c1a7d1c0a15..ca811fbd822 100644 --- a/packages/@ember/-internals/views/lib/views/core_view.ts +++ b/packages/@ember/-internals/views/lib/views/core_view.ts @@ -8,7 +8,7 @@ import { eventedOff, eventedHas, } from '@ember/-internals/metal/lib/evented-methods'; -import ActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler'; +import InternalActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler-internal'; import Evented from '@ember/object/evented'; import { FrameworkObject } from '@ember/object/-internals'; import type { ViewState } from './states'; @@ -31,8 +31,8 @@ import states from './states'; @private */ -interface CoreView extends ActionHandler, View {} -class CoreView extends FrameworkObject.extend(ActionHandler) { +interface CoreView extends InternalActionHandler, View {} +class CoreView extends FrameworkObject.extend(InternalActionHandler) { static { // The deprecated Evented mixin is no longer applied, but instances still // provide its methods, so `Evented.detect` must keep returning true. diff --git a/packages/@ember/array/index-internal.ts b/packages/@ember/array/index-internal.ts new file mode 100644 index 00000000000..2746d544132 --- /dev/null +++ b/packages/@ember/array/index-internal.ts @@ -0,0 +1,641 @@ +/** +@module @ember/array +*/ +import { objectAt } from '@ember/-internals/metal/lib/object-at'; +import { replaceInNativeArray, replace } from '@ember/-internals/metal/lib/array'; +import computed from '@ember/-internals/metal/lib/computed'; +import { + beginPropertyChanges, + endPropertyChanges, +} from '@ember/-internals/metal/lib/property_events'; +import { get } from '@ember/-internals/metal/lib/property_get'; +import { set } from '@ember/-internals/metal/lib/property_set'; +import { InternalMixin } from '@ember/object/mixin-internal'; +import { assert } from '@ember/debug'; +import InternalEnumerable from '@ember/enumerable/-internal'; +import InternalMutableEnumerable from '@ember/enumerable/mutable-internal'; +import sortCompare from './lib/sort-compare'; +import InternalObservable from '@ember/object/observable-internal'; +import type { ComputedPropertyCallback } from '@ember/-internals/metal/lib/computed'; +import { isEmberArray, setEmberArray } from '@ember/array/-internals'; +import isArray from './lib/is-array'; +import type EmberArray from '@ember/array'; +import type { MutableArray, NativeArray } from '@ember/array'; + +const EMPTY_ARRAY = Object.freeze([] as const); + +const identityFunction = (item: T) => item; + +export function uniqBy( + array: T[] | EmberArray, + keyOrFunc: string | ((item: T) => unknown) = identityFunction +): T[] | EmberArray { + assert(`first argument passed to \`uniqBy\` should be array`, isArray(array)); + + let ret = A(); + let seen = new Set(); + let getter = typeof keyOrFunc === 'function' ? keyOrFunc : (item: T) => get(item, keyOrFunc); + + array.forEach((item) => { + let val = getter(item); + if (!seen.has(val)) { + seen.add(val); + ret.push(item); + } + }); + + return ret; +} + +function iter(key: string): (item: T) => boolean; +function iter(key: string, value: unknown): (item: T) => boolean; +function iter(...args: [key: string] | [key: string, value: unknown]) { + let valueProvided = args.length === 2; + let [key, value] = args; + + return valueProvided + ? (item: T) => value === get(item, key) + : (item: T) => Boolean(get(item, key)); +} + +function findIndex( + array: EmberArray, + predicate: (item: T, index: number, arr: EmberArray) => unknown, + startAt: number +): number { + let len = array.length; + for (let index = startAt; index < len; index++) { + // SAFETY: Because we're checking the index this value should always be set. + let item = objectAt(array, index)!; + if (predicate(item, index, array)) { + return index; + } + } + return -1; +} + +function find( + array: EmberArray, + callback: (this: Target | null, item: T, index: number, arr: EmberArray) => unknown, + target: Target | null = null +) { + let predicate = callback.bind(target); + let index = findIndex(array, predicate, 0); + return index === -1 ? undefined : objectAt(array, index); +} + +function any( + array: EmberArray, + callback: (this: Target | null, item: T, index: number, arr: EmberArray) => unknown, + target: Target | null = null +) { + let predicate = callback.bind(target); + return findIndex(array, predicate, 0) !== -1; +} + +function every( + array: EmberArray, + callback: (this: Target | null | void, item: T, index: number, arr: EmberArray) => unknown, + target: Target | null = null +) { + let cb = callback.bind(target); + let predicate = (item: T, index: number, array: EmberArray) => !cb(item, index, array); + return findIndex(array, predicate, 0) === -1; +} + +function indexOf(array: EmberArray, val: T, startAt = 0, withNaNCheck: boolean) { + let len = array.length; + + if (startAt < 0) { + startAt += len; + } + + // SameValueZero comparison (NaN !== NaN) + let predicate = + withNaNCheck && val !== val ? (item: T) => item !== item : (item: T) => item === val; + return findIndex(array, predicate, startAt); +} + +export function removeAt>( + array: A, + index: number, + len?: number +): A { + assert(`\`removeAt\` index provided is out of range`, index > -1 && index < array.length); + replace(array, index, len ?? 1, EMPTY_ARRAY); + return array; +} + +function insertAt(array: MutableArray, index: number, item: T) { + assert(`\`insertAt\` index provided is out of range`, index > -1 && index <= array.length); + replace(array, index, 0, [item]); + return item; +} + +export { isArray }; + +/* + This allows us to define computed properties that are not enumerable. +*/ +function nonEnumerableComputed(callback: ComputedPropertyCallback) { + let property = computed(callback); + property.enumerable = false; + return property; +} + +function mapBy(this: EmberArray, key: string) { + return this.map((next) => get(next, key)); +} + +// .......................................................... +// ARRAY +// + +/** + The internal counterparts to the public `EmberArray`, `MutableArray` and + `NativeArray` mixins. Ember's own internals apply these so that they do not + trigger the deprecations the public mixins emit. The public API + documentation lives on the public copies in `@ember/array`. + + @internal +*/ +const InternalEmberArray = InternalMixin.create(InternalEnumerable, { + init() { + this._super(...arguments); + setEmberArray(this); + }, + + objectsAt(indexes: number[]) { + return indexes.map((idx) => objectAt(this, idx)); + }, + + '[]': nonEnumerableComputed({ + get() { + return this; + }, + set(_key, value) { + this.replace(0, this.length, value); + return this; + }, + }), + + firstObject: nonEnumerableComputed(function () { + return objectAt(this, 0); + }).readOnly(), + + lastObject: nonEnumerableComputed(function () { + return objectAt(this, this.length - 1); + }).readOnly(), + + // Add any extra methods to EmberArray that are native to the built-in Array. + slice(beginIndex = 0, endIndex?: number) { + let ret = A(); + let length = this.length; + + if (beginIndex < 0) { + beginIndex = length + beginIndex; + } + + let validatedEndIndex: number; + if (endIndex === undefined || endIndex > length) { + validatedEndIndex = length; + } else if (endIndex < 0) { + validatedEndIndex = length + endIndex; + } else { + validatedEndIndex = endIndex; + } + + while (beginIndex < validatedEndIndex) { + ret[ret.length] = objectAt(this, beginIndex++); + } + + return ret; + }, + + indexOf(object: T, startAt?: number) { + return indexOf(this, object, startAt, false); + }, + + lastIndexOf(object: T, startAt?: number) { + let len = this.length; + + if (startAt === undefined || startAt >= len) { + startAt = len - 1; + } + + if (startAt < 0) { + startAt += len; + } + + for (let idx = startAt; idx >= 0; idx--) { + if (objectAt(this, idx) === object) { + return idx; + } + } + + return -1; + }, + + forEach(callback: (item: T, index: number, arr: EmberArray) => void, target = null) { + assert('`forEach` expects a function as first argument.', typeof callback === 'function'); + + let length = this.length; + + for (let index = 0; index < length; index++) { + let item = this.objectAt(index); + callback.call(target, item, index, this); + } + + return this; + }, + + getEach: mapBy, + + setEach(key: string, value: unknown) { + return this.forEach((item: object) => set(item, key, value)); + }, + + map( + this: EmberArray, + callback: (item: T, index: number, arr: EmberArray) => unknown, + target = null + ) { + assert('`map` expects a function as first argument.', typeof callback === 'function'); + + let ret = A(); + + this.forEach((x, idx, i) => (ret[idx] = callback.call(target, x, idx, i))); + + return ret; + }, + + mapBy, + + filter( + this: EmberArray, + callback: (item: T, index: number, arr: EmberArray) => unknown, + target = null + ) { + assert('`filter` expects a function as first argument.', typeof callback === 'function'); + + let ret = A(); + + this.forEach((x, idx, i) => { + if (callback.call(target, x, idx, i)) { + ret.push(x); + } + }); + + return ret; + }, + + reject( + this: EmberArray, + callback: (item: T, index: number, arr: EmberArray) => unknown, + target = null + ) { + assert('`reject` expects a function as first argument.', typeof callback === 'function'); + return this.filter(function () { + // @ts-expect-error TS doesn't like us using arguments like this + return !callback.apply(target, arguments); + }); + }, + + filterBy() { + // @ts-expect-error TS doesn't like the ...arguments spread here. + return this.filter(iter(...arguments)); + }, + + rejectBy() { + // @ts-expect-error TS doesn't like the ...arguments spread here. + return this.reject(iter(...arguments)); + }, + + find(callback: (item: T, index: number, arr: EmberArray) => unknown, target = null) { + assert('`find` expects a function as first argument.', typeof callback === 'function'); + return find(this, callback, target); + }, + + findBy() { + // @ts-expect-error TS doesn't like the ...arguments spread here. + let callback = iter(...arguments); + return find(this, callback); + }, + + every(callback: (item: T, index: number, arr: EmberArray) => unknown, target = null) { + assert('`every` expects a function as first argument.', typeof callback === 'function'); + return every(this, callback, target); + }, + + isEvery() { + // @ts-expect-error TS doesn't like the ...arguments spread here. + let callback = iter(...arguments); + return every(this, callback); + }, + + any(callback: (item: T, index: number, arr: EmberArray) => unknown, target = null) { + assert('`any` expects a function as first argument.', typeof callback === 'function'); + return any(this, callback, target); + }, + + isAny() { + // @ts-expect-error TS doesn't like us using arguments like this + let callback = iter(...arguments); + return any(this, callback); + }, + + // FIXME: When called without initialValue, behavior does not match native behavior + reduce( + this: EmberArray, + callback: (summation: V, current: T, index: number, arr: EmberArray) => V, + initialValue?: V + ) { + assert('`reduce` expects a function as first argument.', typeof callback === 'function'); + + let hasInitialValue = arguments.length > 1; + let ret: any = initialValue; + let startIndex = 0; + + if (!hasInitialValue) { + if (this.length === 0) { + throw new TypeError('Reduce of empty array with no initial value'); + } + ret = this.objectAt(0); + startIndex = 1; + } + + for (let i = startIndex; i < this.length; i++) { + let item = this.objectAt(i) as T; + ret = callback(ret, item, i, this); + } + + return ret; + }, + + invoke(this: EmberArray, methodName: string, ...args: unknown[]) { + let ret = A(); + + // SAFETY: This is not entirely safe and the code will not work with Ember proxies + this.forEach((item: T) => ret.push((item as any)[methodName]?.(...args))); + + return ret; + }, + + toArray(this: EmberArray) { + return this.map((item: T) => item); + }, + + compact(this: EmberArray) { + return this.filter((value: T) => value != null); + }, + + includes(this: EmberArray, object: T, startAt?: number) { + return indexOf(this, object, startAt, true) !== -1; + }, + + sortBy(this: EmberArray) { + let sortKeys = arguments; + + return this.toArray().sort((a: T, b: T) => { + for (let i = 0; i < sortKeys.length; i++) { + let key = sortKeys[i]; + let propA = get(a, key); + let propB = get(b, key); + // return 1 or -1 else continue to the next sortKey + let compareValue = sortCompare(propA, propB); + + if (compareValue) { + return compareValue; + } + } + return 0; + }); + }, + + uniq() { + return uniqBy(this); + }, + + uniqBy(key: string) { + return uniqBy(this, key); + }, + + without(this: EmberArray, value: T) { + if (!this.includes(value)) { + return this; // nothing to do + } + + // SameValueZero comparison (NaN !== NaN) + let predicate = value === value ? (item: T) => item !== value : (item: T) => item === item; + return this.filter(predicate); + }, +}); + +const InternalMutableArray = InternalMixin.create(InternalEmberArray, InternalMutableEnumerable, { + clear() { + let len = this.length; + if (len === 0) { + return this; + } + + this.replace(0, len, EMPTY_ARRAY); + return this; + }, + + insertAt(idx: number, object: unknown) { + insertAt(this, idx, object); + return this; + }, + + removeAt(start: number, len?: number) { + return removeAt(this, start, len); + }, + + pushObject(this: MutableArray, obj: T) { + return insertAt(this, this.length, obj); + }, + + pushObjects(this: MutableArray, objects: T[]) { + this.replace(this.length, 0, objects); + return this; + }, + + popObject() { + let len = this.length; + if (len === 0) { + return null; + } + + let ret = objectAt(this, len - 1); + this.removeAt(len - 1, 1); + return ret; + }, + + shiftObject() { + if (this.length === 0) { + return null; + } + + let ret = objectAt(this, 0); + this.removeAt(0); + return ret; + }, + + unshiftObject(this: MutableArray, obj: T) { + return insertAt(this, 0, obj); + }, + + unshiftObjects(this: MutableArray, objects: T[]) { + this.replace(0, 0, objects); + return this; + }, + + reverseObjects() { + let len = this.length; + if (len === 0) { + return this; + } + + let objects = this.toArray().reverse(); + this.replace(0, len, objects); + return this; + }, + + setObjects(this: MutableArray, objects: T[]) { + if (objects.length === 0) { + return this.clear(); + } + + let len = this.length; + this.replace(0, len, objects); + return this; + }, + + removeObject(this: MutableArray, obj: T) { + let loc = this.length || 0; + while (--loc >= 0) { + let curObject = objectAt(this, loc); + + if (curObject === obj) { + this.removeAt(loc); + } + } + return this; + }, + + removeObjects(this: MutableArray, objects: T[]) { + beginPropertyChanges(); + for (let i = objects.length - 1; i >= 0; i--) { + // SAFETY: Due to the loop structure we know this will always exist. + this.removeObject(objects[i]!); + } + endPropertyChanges(); + return this; + }, + + addObject(this: MutableArray, obj: T) { + let included = this.includes(obj); + + if (!included) { + this.pushObject(obj); + } + + return this; + }, + + addObjects(this: MutableArray, objects: T[]) { + beginPropertyChanges(); + objects.forEach((obj) => this.addObject(obj)); + endPropertyChanges(); + return this; + }, +}); + +/** + Creates an `NativeArray` from an Array-like object. + Does not modify the original object's contents. + + This exists primarily for historic reasons and should not be used + in new code. Prefer native [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) + or [trackedArray](/ember/release/functions/@ember%2Freactive%2Fcollections/trackedArray). + + Example + + ```app/components/my-component.js + import Component from '@ember/component'; + import { A } from '@ember/array'; + + export default Component.extend({ + tagName: 'ul', + classNames: ['pagination'], + + init() { + this._super(...arguments); + + if (!this.get('content')) { + this.set('content', A()); + this.set('otherContent', A([1,2,3])); + } + } + }); + ``` + + @method A + @static + @for @ember/array + @return {Ember.NativeArray} +*/ + +// Add Ember.Array to Array.prototype. Remove methods with native +// implementations and supply some more optimized versions of generic methods +// because they are so common. +/** +@module ember +*/ + +let InternalNativeArray = InternalMixin.create(InternalMutableArray, InternalObservable, { + objectAt(idx: number) { + return this[idx]; + }, + + // primitive for array support. + replace(start: number, deleteCount: number, items = EMPTY_ARRAY) { + assert('The third argument to replace needs to be an array.', Array.isArray(items)); + + replaceInNativeArray(this, start, deleteCount, items); + + return this; + }, +}); + +// Remove any methods implemented natively so we don't override them +const ignore = ['length']; +InternalNativeArray.keys().forEach((methodName) => { + // SAFETY: It's safe to read unknown properties from an object + if ((Array.prototype as any)[methodName]) { + ignore.push(methodName); + } +}); + +InternalNativeArray = InternalNativeArray.without(...ignore); + +let A: (arr?: Array) => NativeArray; + +A = function (this: unknown, arr?: Array) { + assert( + 'You cannot create an Ember Array with `new A()`, please update to calling A as a function: `A()`', + !(this instanceof A) + ); + + if (isEmberArray(arr)) { + // SAFETY: If it's a true native array and it is also an EmberArray then it should be an Ember NativeArray + return arr as unknown as NativeArray; + } else { + // SAFETY: This will return an NativeArray but TS can't infer that. + return InternalNativeArray.apply(arr ?? []) as NativeArray; + } +}; + +export { A, InternalEmberArray, InternalMutableArray, InternalNativeArray }; + +export default InternalEmberArray; diff --git a/packages/@ember/array/index.ts b/packages/@ember/array/index.ts index b7ab885ebc6..882e71fe473 100644 --- a/packages/@ember/array/index.ts +++ b/packages/@ember/array/index.ts @@ -1,159 +1,25 @@ /** @module @ember/array */ -import { objectAt } from '@ember/-internals/metal/lib/object-at'; -import { replaceInNativeArray, replace } from '@ember/-internals/metal/lib/array'; -import computed from '@ember/-internals/metal/lib/computed'; -import { - beginPropertyChanges, - endPropertyChanges, -} from '@ember/-internals/metal/lib/property_events'; -import { get } from '@ember/-internals/metal/lib/property_get'; -import { set } from '@ember/-internals/metal/lib/property_set'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import { assert } from '@ember/debug'; -import Enumerable from '@ember/enumerable'; -import MutableEnumerable from '@ember/enumerable/mutable'; -import sortCompare from './lib/sort-compare'; -import Observable from '@ember/object/observable'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import type Enumerable from '@ember/enumerable'; +import type MutableEnumerable from '@ember/enumerable/mutable'; +import type Observable from '@ember/object/observable'; import type { MethodNamesOf, MethodParams, MethodReturns } from '@ember/-internals/utility-types'; -import type { ComputedPropertyCallback } from '@ember/-internals/metal/lib/computed'; -import { isEmberArray, setEmberArray } from '@ember/array/-internals'; import isArray from './lib/is-array'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalEmberArrayMixin, { + A, + InternalMutableArray as InternalMutableArrayMixin, + InternalNativeArray as InternalNativeArrayMixin, +} from '@ember/array/index-internal'; export { default as makeArray } from './make'; - -export type EmberArrayLike = EmberArray | NativeArray; - -const EMPTY_ARRAY = Object.freeze([] as const); - -const identityFunction = (item: T) => item; - -export function uniqBy( - array: T[] | EmberArray, - keyOrFunc: string | ((item: T) => unknown) = identityFunction -): T[] | EmberArray { - assert(`first argument passed to \`uniqBy\` should be array`, isArray(array)); - - let ret = A(); - let seen = new Set(); - let getter = typeof keyOrFunc === 'function' ? keyOrFunc : (item: T) => get(item, keyOrFunc); - - array.forEach((item) => { - let val = getter(item); - if (!seen.has(val)) { - seen.add(val); - ret.push(item); - } - }); - - return ret; -} - -function iter(key: string): (item: T) => boolean; -function iter(key: string, value: unknown): (item: T) => boolean; -function iter(...args: [key: string] | [key: string, value: unknown]) { - let valueProvided = args.length === 2; - let [key, value] = args; - - return valueProvided - ? (item: T) => value === get(item, key) - : (item: T) => Boolean(get(item, key)); -} - -function findIndex( - array: EmberArray, - predicate: (item: T, index: number, arr: EmberArray) => unknown, - startAt: number -): number { - let len = array.length; - for (let index = startAt; index < len; index++) { - // SAFETY: Because we're checking the index this value should always be set. - let item = objectAt(array, index)!; - if (predicate(item, index, array)) { - return index; - } - } - return -1; -} - -function find( - array: EmberArray, - callback: (this: Target | null, item: T, index: number, arr: EmberArray) => unknown, - target: Target | null = null -) { - let predicate = callback.bind(target); - let index = findIndex(array, predicate, 0); - return index === -1 ? undefined : objectAt(array, index); -} - -function any( - array: EmberArray, - callback: (this: Target | null, item: T, index: number, arr: EmberArray) => unknown, - target: Target | null = null -) { - let predicate = callback.bind(target); - return findIndex(array, predicate, 0) !== -1; -} - -function every( - array: EmberArray, - callback: (this: Target | null | void, item: T, index: number, arr: EmberArray) => unknown, - target: Target | null = null -) { - let cb = callback.bind(target); - let predicate = (item: T, index: number, array: EmberArray) => !cb(item, index, array); - return findIndex(array, predicate, 0) === -1; -} - -function indexOf(array: EmberArray, val: T, startAt = 0, withNaNCheck: boolean) { - let len = array.length; - - if (startAt < 0) { - startAt += len; - } - - // SameValueZero comparison (NaN !== NaN) - let predicate = - withNaNCheck && val !== val ? (item: T) => item !== item : (item: T) => item === val; - return findIndex(array, predicate, startAt); -} - -export function removeAt>( - array: A, - index: number, - len?: number -): A { - assert(`\`removeAt\` index provided is out of range`, index > -1 && index < array.length); - replace(array, index, len ?? 1, EMPTY_ARRAY); - return array; -} - -function insertAt(array: MutableArray, index: number, item: T) { - assert(`\`insertAt\` index provided is out of range`, index > -1 && index <= array.length); - replace(array, index, 0, [item]); - return item; -} - +export { uniqBy, removeAt } from '@ember/array/index-internal'; export { isArray }; -/* - This allows us to define computed properties that are not enumerable. -*/ -function nonEnumerableComputed(callback: ComputedPropertyCallback) { - let property = computed(callback); - property.enumerable = false; - return property; -} - -function mapBy(this: EmberArray, key: string) { - return this.map((next) => get(next, key)); -} +export type EmberArrayLike = EmberArray | NativeArray; -// .......................................................... -// ARRAY -// /** This mixin implements Observer-friendly Array-like behavior. It is not a concrete implementation, but it can be used up by other classes that want @@ -1140,275 +1006,13 @@ interface EmberArray extends Enumerable { */ without(value: T): NativeArray; } -const EmberArray = Mixin[INTERNAL_MIXIN_CREATE](Enumerable, { +const EmberArray = DeprecatedMixin.create(InternalEmberArrayMixin, { init() { this._super(...arguments); - setEmberArray(this); - }, - - objectsAt(indexes: number[]) { - return indexes.map((idx) => objectAt(this, idx)); - }, - - '[]': nonEnumerableComputed({ - get() { - return this; - }, - set(_key, value) { - this.replace(0, this.length, value); - return this; - }, - }), - - firstObject: nonEnumerableComputed(function () { - return objectAt(this, 0); - }).readOnly(), - - lastObject: nonEnumerableComputed(function () { - return objectAt(this, this.length - 1); - }).readOnly(), - - // Add any extra methods to EmberArray that are native to the built-in Array. - slice(beginIndex = 0, endIndex?: number) { - let ret = A(); - let length = this.length; - - if (beginIndex < 0) { - beginIndex = length + beginIndex; - } - - let validatedEndIndex: number; - if (endIndex === undefined || endIndex > length) { - validatedEndIndex = length; - } else if (endIndex < 0) { - validatedEndIndex = length + endIndex; - } else { - validatedEndIndex = endIndex; - } - - while (beginIndex < validatedEndIndex) { - ret[ret.length] = objectAt(this, beginIndex++); - } - - return ret; - }, - - indexOf(object: T, startAt?: number) { - return indexOf(this, object, startAt, false); - }, - - lastIndexOf(object: T, startAt?: number) { - let len = this.length; - - if (startAt === undefined || startAt >= len) { - startAt = len - 1; - } - - if (startAt < 0) { - startAt += len; - } - - for (let idx = startAt; idx >= 0; idx--) { - if (objectAt(this, idx) === object) { - return idx; - } - } - - return -1; - }, - - forEach(callback: (item: T, index: number, arr: EmberArray) => void, target = null) { - assert('`forEach` expects a function as first argument.', typeof callback === 'function'); - - let length = this.length; - - for (let index = 0; index < length; index++) { - let item = this.objectAt(index); - callback.call(target, item, index, this); - } - - return this; - }, - - getEach: mapBy, - - setEach(key: string, value: unknown) { - return this.forEach((item: object) => set(item, key, value)); - }, - - map( - this: EmberArray, - callback: (item: T, index: number, arr: EmberArray) => unknown, - target = null - ) { - assert('`map` expects a function as first argument.', typeof callback === 'function'); - - let ret = A(); - - this.forEach((x, idx, i) => (ret[idx] = callback.call(target, x, idx, i))); - - return ret; - }, - - mapBy, - - filter( - this: EmberArray, - callback: (item: T, index: number, arr: EmberArray) => unknown, - target = null - ) { - assert('`filter` expects a function as first argument.', typeof callback === 'function'); - - let ret = A(); - - this.forEach((x, idx, i) => { - if (callback.call(target, x, idx, i)) { - ret.push(x); - } - }); - - return ret; - }, - - reject( - this: EmberArray, - callback: (item: T, index: number, arr: EmberArray) => unknown, - target = null - ) { - assert('`reject` expects a function as first argument.', typeof callback === 'function'); - return this.filter(function () { - // @ts-expect-error TS doesn't like us using arguments like this - return !callback.apply(target, arguments); - }); - }, - - filterBy() { - // @ts-expect-error TS doesn't like the ...arguments spread here. - return this.filter(iter(...arguments)); - }, - - rejectBy() { - // @ts-expect-error TS doesn't like the ...arguments spread here. - return this.reject(iter(...arguments)); - }, - - find(callback: (item: T, index: number, arr: EmberArray) => unknown, target = null) { - assert('`find` expects a function as first argument.', typeof callback === 'function'); - return find(this, callback, target); - }, - - findBy() { - // @ts-expect-error TS doesn't like the ...arguments spread here. - let callback = iter(...arguments); - return find(this, callback); - }, - - every(callback: (item: T, index: number, arr: EmberArray) => unknown, target = null) { - assert('`every` expects a function as first argument.', typeof callback === 'function'); - return every(this, callback, target); - }, - - isEvery() { - // @ts-expect-error TS doesn't like the ...arguments spread here. - let callback = iter(...arguments); - return every(this, callback); - }, - - any(callback: (item: T, index: number, arr: EmberArray) => unknown, target = null) { - assert('`any` expects a function as first argument.', typeof callback === 'function'); - return any(this, callback, target); - }, - - isAny() { - // @ts-expect-error TS doesn't like us using arguments like this - let callback = iter(...arguments); - return any(this, callback); - }, - - // FIXME: When called without initialValue, behavior does not match native behavior - reduce( - this: EmberArray, - callback: (summation: V, current: T, index: number, arr: EmberArray) => V, - initialValue?: V - ) { - assert('`reduce` expects a function as first argument.', typeof callback === 'function'); - - let hasInitialValue = arguments.length > 1; - let ret: any = initialValue; - let startIndex = 0; - - if (!hasInitialValue) { - if (this.length === 0) { - throw new TypeError('Reduce of empty array with no initial value'); - } - ret = this.objectAt(0); - startIndex = 1; - } - - for (let i = startIndex; i < this.length; i++) { - let item = this.objectAt(i) as T; - ret = callback(ret, item, i, this); - } - - return ret; - }, - - invoke(this: EmberArray, methodName: string, ...args: unknown[]) { - let ret = A(); - - // SAFETY: This is not entirely safe and the code will not work with Ember proxies - this.forEach((item: T) => ret.push((item as any)[methodName]?.(...args))); - - return ret; - }, - - toArray(this: EmberArray) { - return this.map((item: T) => item); - }, - - compact(this: EmberArray) { - return this.filter((value: T) => value != null); - }, - - includes(this: EmberArray, object: T, startAt?: number) { - return indexOf(this, object, startAt, true) !== -1; - }, - - sortBy(this: EmberArray) { - let sortKeys = arguments; - - return this.toArray().sort((a: T, b: T) => { - for (let i = 0; i < sortKeys.length; i++) { - let key = sortKeys[i]; - let propA = get(a, key); - let propB = get(b, key); - // return 1 or -1 else continue to the next sortKey - let compareValue = sortCompare(propA, propB); - - if (compareValue) { - return compareValue; - } - } - return 0; - }); - }, - - uniq() { - return uniqBy(this); - }, - - uniqBy(key: string) { - return uniqBy(this, key); - }, - - without(this: EmberArray, value: T) { - if (!this.includes(value)) { - return this; // nothing to do - } - - // SameValueZero comparison (NaN !== NaN) - let predicate = value === value ? (item: T) => item !== value : (item: T) => item === item; - return this.filter(predicate); + deprecateUntil( + 'The `EmberArray` mixin is deprecated. Use native arrays and array methods instead.', + DEPRECATIONS.DEPRECATE_EMBER_ARRAY_MIXIN + ); }, }); @@ -1688,169 +1292,16 @@ interface MutableArray extends EmberArray, MutableEnumerable { */ addObjects(objects: T[]): this; } -const MutableArray = Mixin[INTERNAL_MIXIN_CREATE](EmberArray, MutableEnumerable, { - clear() { - let len = this.length; - if (len === 0) { - return this; - } - - this.replace(0, len, EMPTY_ARRAY); - return this; - }, - - insertAt(idx: number, object: unknown) { - insertAt(this, idx, object); - return this; - }, - - removeAt(start: number, len?: number) { - return removeAt(this, start, len); - }, - - pushObject(this: MutableArray, obj: T) { - return insertAt(this, this.length, obj); - }, - - pushObjects(this: MutableArray, objects: T[]) { - this.replace(this.length, 0, objects); - return this; - }, - - popObject() { - let len = this.length; - if (len === 0) { - return null; - } - - let ret = objectAt(this, len - 1); - this.removeAt(len - 1, 1); - return ret; - }, - - shiftObject() { - if (this.length === 0) { - return null; - } - - let ret = objectAt(this, 0); - this.removeAt(0); - return ret; - }, - - unshiftObject(this: MutableArray, obj: T) { - return insertAt(this, 0, obj); - }, - - unshiftObjects(this: MutableArray, objects: T[]) { - this.replace(0, 0, objects); - return this; - }, - - reverseObjects() { - let len = this.length; - if (len === 0) { - return this; - } - - let objects = this.toArray().reverse(); - this.replace(0, len, objects); - return this; - }, - - setObjects(this: MutableArray, objects: T[]) { - if (objects.length === 0) { - return this.clear(); - } - - let len = this.length; - this.replace(0, len, objects); - return this; - }, - - removeObject(this: MutableArray, obj: T) { - let loc = this.length || 0; - while (--loc >= 0) { - let curObject = objectAt(this, loc); - - if (curObject === obj) { - this.removeAt(loc); - } - } - return this; - }, - - removeObjects(this: MutableArray, objects: T[]) { - beginPropertyChanges(); - for (let i = objects.length - 1; i >= 0; i--) { - // SAFETY: Due to the loop structure we know this will always exist. - this.removeObject(objects[i]!); - } - endPropertyChanges(); - return this; - }, - - addObject(this: MutableArray, obj: T) { - let included = this.includes(obj); - - if (!included) { - this.pushObject(obj); - } - - return this; - }, - - addObjects(this: MutableArray, objects: T[]) { - beginPropertyChanges(); - objects.forEach((obj) => this.addObject(obj)); - endPropertyChanges(); - return this; +const MutableArray = DeprecatedMixin.create(InternalMutableArrayMixin, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `MutableArray` mixin is deprecated. Use native arrays and array methods instead.', + DEPRECATIONS.DEPRECATE_MUTABLE_ARRAY_MIXIN + ); }, }); -/** - Creates an `NativeArray` from an Array-like object. - Does not modify the original object's contents. - - This exists primarily for historic reasons and should not be used - in new code. Prefer native [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) - or [trackedArray](/ember/release/functions/@ember%2Freactive%2Fcollections/trackedArray). - - Example - - ```app/components/my-component.js - import Component from '@ember/component'; - import { A } from '@ember/array'; - - export default Component.extend({ - tagName: 'ul', - classNames: ['pagination'], - - init() { - this._super(...arguments); - - if (!this.get('content')) { - this.set('content', A()); - this.set('otherContent', A([1,2,3])); - } - } - }); - ``` - - @method A - @static - @for @ember/array - @return {Ember.NativeArray} - @public -*/ - -// Add Ember.Array to Array.prototype. Remove methods with native -// implementations and supply some more optimized versions of generic methods -// because they are so common. -/** -@module ember -*/ - type AnyArray = EmberArray | Array | ReadonlyArray; /** @@ -2024,49 +1475,16 @@ interface MutableArrayWithoutNative extends Omit< */ interface NativeArray extends Array, Observable, MutableArrayWithoutNative {} -let NativeArray = Mixin[INTERNAL_MIXIN_CREATE](MutableArray, Observable, { - objectAt(idx: number) { - return this[idx]; - }, - - // primitive for array support. - replace(start: number, deleteCount: number, items = EMPTY_ARRAY) { - assert('The third argument to replace needs to be an array.', Array.isArray(items)); - - replaceInNativeArray(this, start, deleteCount, items); - - return this; +const NativeArray = DeprecatedMixin.create(InternalNativeArrayMixin, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `NativeArray` mixin is deprecated. Use native arrays and array methods instead.', + DEPRECATIONS.DEPRECATE_NATIVE_ARRAY_MIXIN + ); }, }); -// Remove any methods implemented natively so we don't override them -const ignore = ['length']; -NativeArray.keys().forEach((methodName) => { - // SAFETY: It's safe to read unknown properties from an object - if ((Array.prototype as any)[methodName]) { - ignore.push(methodName); - } -}); - -NativeArray = NativeArray.without(...ignore); - -let A: (arr?: Array) => NativeArray; - -A = function (this: unknown, arr?: Array) { - assert( - 'You cannot create an Ember Array with `new A()`, please update to calling A as a function: `A()`', - !(this instanceof A) - ); - - if (isEmberArray(arr)) { - // SAFETY: If it's a true native array and it is also an EmberArray then it should be an Ember NativeArray - return arr as unknown as NativeArray; - } else { - // SAFETY: This will return an NativeArray but TS can't infer that. - return NativeArray.apply(arr ?? []) as NativeArray; - } -}; - export { A, NativeArray, MutableArray }; export default EmberArray; diff --git a/packages/@ember/array/proxy.ts b/packages/@ember/array/proxy.ts index 192c55215ee..7489ad40d8b 100644 --- a/packages/@ember/array/proxy.ts +++ b/packages/@ember/array/proxy.ts @@ -15,8 +15,10 @@ import { get } from '@ember/-internals/metal/lib/property_get'; import type { PropertyDidChange } from '@ember/-internals/metal/lib/property_events'; import { isObject } from '@ember/-internals/utils/lib/spec'; import EmberObject from '@ember/object'; -import EmberArray, { type NativeArray } from '@ember/array'; -import MutableArray from '@ember/array/mutable'; +import type EmberArray from '@ember/array'; +import type { NativeArray } from '@ember/array'; +import type MutableArray from '@ember/array/mutable'; +import { InternalEmberArray, InternalMutableArray } from '@ember/array/index-internal'; import { assert } from '@ember/debug'; import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; import { setCustomTagFor } from '@glimmer/manager/lib/util/args-proxy'; @@ -330,7 +332,7 @@ class ArrayProxy extends EmberObject implements PropertyDidChange { assert( `ArrayProxy expects a native Array, EmberArray, or ArrayProxy, but you passed ${typeof arrangedContent}`, (function (arr: unknown): arr is EmberArray { - return Array.isArray(arr) || EmberArray.detect(arr); + return Array.isArray(arr) || InternalEmberArray.detect(arr); })(arrangedContent) ); assert( @@ -412,7 +414,7 @@ class ArrayProxy extends EmberObject implements PropertyDidChange { } } -ArrayProxy.reopen(MutableArray, { +ArrayProxy.reopen(InternalMutableArray, { arrangedContent: alias('content'), }); diff --git a/packages/@ember/controller/controller-internal.ts b/packages/@ember/controller/controller-internal.ts new file mode 100644 index 00000000000..ad50466ef13 --- /dev/null +++ b/packages/@ember/controller/controller-internal.ts @@ -0,0 +1,91 @@ +import { getOwner } from '@ember/-internals/owner'; // This is imported from -internals to avoid circularity +import computed from '@ember/-internals/metal/lib/computed'; +import { get } from '@ember/-internals/metal/lib/property_get'; +import { InternalMixin } from '@ember/object/mixin-internal'; +import InternalActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler-internal'; + +const MODEL = Symbol('MODEL'); + +/** + The internal counterpart to the public `ControllerMixin`. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. The public API documentation lives on the public copy + in `@ember/controller`. + + @internal +*/ +const InternalControllerMixin = InternalMixin.create(InternalActionHandler, { + /* ducktype as a controller */ + isController: true, + + concatenatedProperties: ['queryParams'], + + target: null, + + store: null, + + init() { + this._super(...arguments); + let owner = getOwner(this); + if (owner) { + this.namespace = owner.lookup('application:main'); + this.target = owner.lookup('router:main'); + } + }, + + model: computed({ + get() { + return this[MODEL]; + }, + + set(_key, value) { + return (this[MODEL] = value); + }, + }), + + queryParams: null, + + /** + This property is updated to various different callback functions depending on + the current "state" of the backing route. It is used by + `Controller.prototype._qpChanged`. + + The methods backing each state can be found in the `Route.prototype._qp` computed + property return value (the `.states` property). The current values are listed here for + the sanity of future travelers: + + * `inactive` - This state is used when this controller instance is not part of the active + route hierarchy. Set in `Route.prototype._reset` (a `router.js` microlib hook) and + `Route.prototype.actions.finalizeQueryParamChange`. + * `active` - This state is used when this controller instance is part of the active + route hierarchy. Set in `Route.prototype.actions.finalizeQueryParamChange`. + * `allowOverrides` - This state is used in `Route.prototype.setup` (`route.js` microlib hook). + + @method _qpDelegate + @private + */ + _qpDelegate: null, // set by route + + /** + During `Route#setup` observers are created to invoke this method + when any of the query params declared in `Controller#queryParams` property + are changed. + + When invoked this method uses the currently active query param update delegate + (see `Controller.prototype._qpDelegate` for details) and invokes it with + the QP key/value being changed. + + @method _qpChanged + @private + */ + _qpChanged(controller: any, _prop: string) { + let dotIndex = _prop.indexOf('.[]'); + let prop = dotIndex === -1 ? _prop : _prop.slice(0, dotIndex); + + let delegate = controller._qpDelegate; + let value = get(controller, prop); + delegate(prop, value); + }, +}); + +export default InternalControllerMixin; diff --git a/packages/@ember/controller/index.ts b/packages/@ember/controller/index.ts index b83b4e6ae98..3ea52f076de 100644 --- a/packages/@ember/controller/index.ts +++ b/packages/@ember/controller/index.ts @@ -1,6 +1,3 @@ -import { getOwner } from '@ember/-internals/owner'; // This is imported from -internals to avoid circularity -import computed from '@ember/-internals/metal/lib/computed'; -import { get } from '@ember/-internals/metal/lib/property_get'; import { FrameworkObject } from '@ember/object/-internals'; import metalInject from '@ember/-internals/metal/lib/injected_property'; import type { InjectionDecorator } from '@ember/-internals/metal/lib/decorator-util'; @@ -8,10 +5,11 @@ import type { DecoratorPropertyDescriptor, ElementDescriptor, } from '@ember/-internals/metal/lib/decorator'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; import type { RouteArgs } from '@ember/routing/-internals'; -import ActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler'; +import type InternalActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalControllerMixin from '@ember/controller/controller-internal'; import type { Transition } from 'router_js'; export type ControllerQueryParamType = 'boolean' | 'number' | 'array' | 'string'; @@ -20,8 +18,6 @@ export type ControllerQueryParam = | Record | Record; -const MODEL = Symbol('MODEL'); - /** @module @ember/controller */ @@ -31,8 +27,9 @@ const MODEL = Symbol('MODEL'); @namespace Ember @uses Ember.ActionHandler @private + @deprecated Extend `Controller` from `@ember/controller` instead. */ -interface ControllerMixin extends ActionHandler { +interface ControllerMixin extends InternalActionHandler { /** @internal */ _qpDelegate: unknown | null; @@ -235,77 +232,13 @@ interface ControllerMixin extends ActionHandler { */ replaceRoute(...args: RouteArgs): Transition; } -const ControllerMixin = Mixin[INTERNAL_MIXIN_CREATE](ActionHandler, { - /* ducktype as a controller */ - isController: true, - - concatenatedProperties: ['queryParams'], - - target: null, - - store: null, - +const ControllerMixin = DeprecatedMixin.create(InternalControllerMixin, { init() { this._super(...arguments); - let owner = getOwner(this); - if (owner) { - this.namespace = owner.lookup('application:main'); - this.target = owner.lookup('router:main'); - } - }, - - model: computed({ - get() { - return this[MODEL]; - }, - - set(_key, value) { - return (this[MODEL] = value); - }, - }), - - queryParams: null, - - /** - This property is updated to various different callback functions depending on - the current "state" of the backing route. It is used by - `Controller.prototype._qpChanged`. - - The methods backing each state can be found in the `Route.prototype._qp` computed - property return value (the `.states` property). The current values are listed here for - the sanity of future travelers: - - * `inactive` - This state is used when this controller instance is not part of the active - route hierarchy. Set in `Route.prototype._reset` (a `router.js` microlib hook) and - `Route.prototype.actions.finalizeQueryParamChange`. - * `active` - This state is used when this controller instance is part of the active - route hierarchy. Set in `Route.prototype.actions.finalizeQueryParamChange`. - * `allowOverrides` - This state is used in `Route.prototype.setup` (`route.js` microlib hook). - - @method _qpDelegate - @private - */ - _qpDelegate: null, // set by route - - /** - During `Route#setup` observers are created to invoke this method - when any of the query params declared in `Controller#queryParams` property - are changed. - - When invoked this method uses the currently active query param update delegate - (see `Controller.prototype._qpDelegate` for details) and invokes it with - the QP key/value being changed. - - @method _qpChanged - @private - */ - _qpChanged(controller: any, _prop: string) { - let dotIndex = _prop.indexOf('.[]'); - let prop = dotIndex === -1 ? _prop : _prop.slice(0, dotIndex); - - let delegate = controller._qpDelegate; - let value = get(controller, prop); - delegate(prop, value); + deprecateUntil( + 'The `ControllerMixin` mixin is deprecated. Extend `Controller` from `@ember/controller` instead.', + DEPRECATIONS.DEPRECATE_CONTROLLER_MIXIN + ); }, }); @@ -317,7 +250,7 @@ const ControllerMixin = Mixin[INTERNAL_MIXIN_CREATE](ActionHandler, { @public */ interface Controller<_T = unknown> extends FrameworkObject, ControllerMixin<_T> {} -class Controller<_T = unknown> extends FrameworkObject.extend(ControllerMixin) {} +class Controller<_T = unknown> extends FrameworkObject.extend(InternalControllerMixin) {} /** Creates a property that lazily looks up another controller in the container. diff --git a/packages/@ember/engine/index.ts b/packages/@ember/engine/index.ts index 478de9e9b13..5f244c81f01 100644 --- a/packages/@ember/engine/index.ts +++ b/packages/@ember/engine/index.ts @@ -14,7 +14,8 @@ import type { EngineInstanceOptions } from '@ember/engine/instance'; import EngineInstance from '@ember/engine/instance'; import { RoutingService } from '@ember/routing/-internals'; import { setupEngineRegistry } from '@ember/-internals/glimmer/lib/setup-registry'; -import RegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy'; +import type RegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy'; +import InternalRegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy-internal'; import { StrictResolver } from './lib/strict-resolver'; function props(obj: object) { @@ -56,7 +57,7 @@ export interface Initializer { */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface Engine extends RegistryProxyMixin {} -class Engine extends Namespace.extend(RegistryProxyMixin) { +class Engine extends Namespace.extend(InternalRegistryProxyMixin) { static initializers: Record> = Object.create(null); static instanceInitializers: Record> = Object.create(null); diff --git a/packages/@ember/engine/instance.ts b/packages/@ember/engine/instance.ts index b49d1280121..0bedeef2162 100644 --- a/packages/@ember/engine/instance.ts +++ b/packages/@ember/engine/instance.ts @@ -8,8 +8,10 @@ import { assert } from '@ember/debug'; import { default as Registry, privatize as P } from '@ember/-internals/container/lib/registry'; import { guidFor } from '@ember/-internals/utils/lib/guid'; import { ENGINE_PARENT, getEngineParent, setEngineParent } from './parent'; -import ContainerProxyMixin from '@ember/-internals/runtime/lib/mixins/container_proxy'; -import RegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy'; +import type ContainerProxyMixin from '@ember/-internals/runtime/lib/mixins/container_proxy'; +import type RegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy'; +import InternalContainerProxyMixin from '@ember/-internals/runtime/lib/mixins/container_proxy-internal'; +import InternalRegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy-internal'; import type { InternalOwner } from '@ember/-internals/owner'; import type Owner from '@ember/-internals/owner'; import { type FullName, isFactory } from '@ember/-internals/owner'; @@ -59,7 +61,10 @@ export interface EngineInstanceOptions { // type checking, we have broken part of our public API contract. Medium-term, // the goal here is to `EngineInstance` simple be `Owner`. interface EngineInstance extends RegistryProxyMixin, ContainerProxyMixin, InternalOwner, Owner {} -class EngineInstance extends EmberObject.extend(RegistryProxyMixin, ContainerProxyMixin) { +class EngineInstance extends EmberObject.extend( + InternalRegistryProxyMixin, + InternalContainerProxyMixin +) { /** @private @method setupRegistry diff --git a/packages/@ember/enumerable/-internal.ts b/packages/@ember/enumerable/-internal.ts new file mode 100644 index 00000000000..0a1c573248b --- /dev/null +++ b/packages/@ember/enumerable/-internal.ts @@ -0,0 +1,14 @@ +import { InternalMixin } from '@ember/object/mixin-internal'; + +/** + The internal counterpart to the public `Enumerable` mixin. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. + + @internal +*/ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface InternalEnumerable {} +const InternalEnumerable = InternalMixin.create(); + +export default InternalEnumerable; diff --git a/packages/@ember/enumerable/index.ts b/packages/@ember/enumerable/index.ts index 1e9ec615cfc..d3e5c2f3a30 100644 --- a/packages/@ember/enumerable/index.ts +++ b/packages/@ember/enumerable/index.ts @@ -1,5 +1,6 @@ -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalEnumerable from '@ember/enumerable/-internal'; /** @module @ember/enumerable @@ -13,9 +14,18 @@ import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixi @class Enumerable @private + @deprecated Use native arrays and array methods instead. */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface Enumerable {} -const Enumerable = Mixin[INTERNAL_MIXIN_CREATE](); +interface Enumerable extends InternalEnumerable {} +const Enumerable = DeprecatedMixin.create(InternalEnumerable, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `Enumerable` mixin is deprecated. Use native arrays and array methods instead.', + DEPRECATIONS.DEPRECATE_ENUMERABLE_MIXIN + ); + }, +}); export default Enumerable; diff --git a/packages/@ember/enumerable/mutable-internal.ts b/packages/@ember/enumerable/mutable-internal.ts new file mode 100644 index 00000000000..96631833036 --- /dev/null +++ b/packages/@ember/enumerable/mutable-internal.ts @@ -0,0 +1,15 @@ +import InternalEnumerable from '@ember/enumerable/-internal'; +import { InternalMixin } from '@ember/object/mixin-internal'; + +/** + The internal counterpart to the public `MutableEnumerable` mixin. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. + + @internal +*/ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface InternalMutableEnumerable extends InternalEnumerable {} +const InternalMutableEnumerable = InternalMixin.create(InternalEnumerable); + +export default InternalMutableEnumerable; diff --git a/packages/@ember/enumerable/mutable.ts b/packages/@ember/enumerable/mutable.ts index 0fce726a0e0..38acfe00ff6 100644 --- a/packages/@ember/enumerable/mutable.ts +++ b/packages/@ember/enumerable/mutable.ts @@ -1,6 +1,6 @@ -import Enumerable from '@ember/enumerable'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import InternalMutableEnumerable from '@ember/enumerable/mutable-internal'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; /** @module ember @@ -15,9 +15,18 @@ import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixi @namespace Ember @uses Enumerable @private + @deprecated Use native arrays and array methods instead. */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface MutableEnumerable extends Enumerable {} -const MutableEnumerable = Mixin[INTERNAL_MIXIN_CREATE](Enumerable); +interface MutableEnumerable extends InternalMutableEnumerable {} +const MutableEnumerable = DeprecatedMixin.create(InternalMutableEnumerable, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `MutableEnumerable` mixin is deprecated. Use native arrays and array methods instead.', + DEPRECATIONS.DEPRECATE_MUTABLE_ENUMERABLE_MIXIN + ); + }, +}); export default MutableEnumerable; diff --git a/packages/@ember/object/core.ts b/packages/@ember/object/core.ts index 2dcec80ec1c..e68f0dcb3c6 100644 --- a/packages/@ember/object/core.ts +++ b/packages/@ember/object/core.ts @@ -2,6 +2,7 @@ @module @ember/object/core */ +import { InternalMixin } from '@ember/object/mixin-internal'; import { getFactoryFor, setFactoryFor } from '@ember/-internals/container/lib/container'; import { type default as Owner, getOwner } from '@ember/-internals/owner'; import { guidFor } from '@ember/-internals/utils/lib/guid'; @@ -15,9 +16,9 @@ import { defineProperty } from '@ember/-internals/metal/lib/properties'; import { descriptorForProperty, isClassicDecorator } from '@ember/-internals/metal/lib/decorator'; import { DEBUG_INJECTION_FUNCTIONS } from '@ember/-internals/metal/lib/injected_property'; import { finalizeDecoratedClass } from '@ember/-internals/metal/lib/decorator-util'; -import Mixin, { applyMixin } from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import ActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler'; +import type Mixin from '@ember/object/mixin'; +import { applyMixin } from '@ember/object/mixin'; +import InternalActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler-internal'; import makeArray from '@ember/array/make'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; @@ -53,7 +54,7 @@ function hasToStringExtension(val: unknown): val is HasToStringExtension { typeof (val as HasToStringExtension).toStringExtension === 'function' ); } -const reopen = Mixin.prototype.reopen; +const reopen = InternalMixin.prototype.reopen; const wasApplied = new WeakSet(); const prototypeMixinMap = new WeakMap(); @@ -80,7 +81,7 @@ function initialize(obj: CoreObject, properties?: unknown) { assert( 'EmberObject.create no longer supports mixing in other ' + 'definitions, use .extend & .create separately instead.', - !(properties instanceof Mixin) + !(properties instanceof InternalMixin) ); let concatenatedProperties = obj.concatenatedProperties; @@ -107,7 +108,7 @@ function initialize(obj: CoreObject, properties?: unknown) { assert( '`actions` must be provided at extend time, not at create time, ' + 'when Ember.ActionHandler is used (i.e. views, controllers & routes).', - !(keyName === 'actions' && ActionHandler.detect(obj)) + !(keyName === 'actions' && InternalActionHandler.detect(obj)) ); let possibleDesc = descriptorForProperty(obj, keyName, m); @@ -853,7 +854,7 @@ class CoreObject { // make sure that it gets properly applied. Reusing the same mixin after // the first `proto` call will cause it to get skipped. if (prototypeMixinMap.has(this)) { - prototypeMixinMap.set(this, Mixin[INTERNAL_MIXIN_CREATE](this.PrototypeMixin)); + prototypeMixinMap.set(this, InternalMixin.create(this.PrototypeMixin)); } } } @@ -1012,7 +1013,7 @@ class CoreObject { static get PrototypeMixin() { let prototypeMixin = prototypeMixinMap.get(this); if (prototypeMixin === undefined) { - prototypeMixin = Mixin[INTERNAL_MIXIN_CREATE](); + prototypeMixin = InternalMixin.create(); prototypeMixin.ownerConstructor = this; prototypeMixinMap.set(this, prototypeMixin); } @@ -1066,7 +1067,7 @@ function flattenProps(this: typeof CoreObject, ...props: Array { - return Array.isArray(obj) || EmberArray.detect(obj); + return Array.isArray(obj) || InternalEmberArray.detect(obj); } function reduceMacro( diff --git a/packages/@ember/object/mixin-internal.ts b/packages/@ember/object/mixin-internal.ts new file mode 100644 index 00000000000..5e40f1ad1d9 --- /dev/null +++ b/packages/@ember/object/mixin-internal.ts @@ -0,0 +1,710 @@ +import { INIT_FACTORY } from '@ember/-internals/container/lib/container'; +import type { Meta } from '@ember/-internals/meta/lib/meta'; +import { meta as metaFor, peekMeta } from '@ember/-internals/meta/lib/meta'; +import { observerListenerMetaFor, ROOT, wrap } from '@ember/-internals/utils/lib/super'; +import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; +import type { + ComputedDecorator, + ComputedPropertyGetter, + ComputedPropertyObj, + ComputedPropertySetter, +} from '@ember/-internals/metal/lib/computed'; +import { type ComputedDescriptor, isClassicDecorator } from '@ember/-internals/metal/lib/decorator'; +import { ComputedProperty } from '@ember/-internals/metal/lib/computed'; +import { + descriptorForDecorator, + makeComputedDecorator, + nativeDescDecorator, +} from '@ember/-internals/metal/lib/decorator'; +import { setUnprocessedMixins } from '@ember/-internals/metal/lib/namespace_search'; +import { + addObserver, + removeObserver, + revalidateObservers, +} from '@ember/-internals/metal/lib/observer'; +import { defineDecorator, defineValue } from '@ember/-internals/metal/lib/properties'; +import { addListener, removeListener } from '@ember/-internals/metal/lib/events'; + +const a_concat = Array.prototype.concat; +const { isArray } = Array; + +function extractAccessors(properties: { [key: string]: any } | undefined) { + if (properties !== undefined) { + for (let key of Object.keys(properties)) { + let desc = Object.getOwnPropertyDescriptor(properties, key)!; + + if (desc.get !== undefined || desc.set !== undefined) { + Object.defineProperty(properties, key, { value: nativeDescDecorator(desc) }); + } + } + } + + return properties; +} + +function concatenatedMixinProperties( + concatProp: string, + props: { [key: string]: any }, + values: { [key: string]: any }, + base: { [key: string]: any } +) { + // reset before adding each new mixin to pickup concats from previous + let concats = values[concatProp] || base[concatProp]; + if (props[concatProp]) { + concats = concats ? a_concat.call(concats, props[concatProp]) : props[concatProp]; + } + return concats; +} + +function giveDecoratorSuper( + key: string, + decorator: ComputedDecorator, + property: ComputedProperty | true, + descs: { [key: string]: any } +): ComputedDecorator { + if (property === true) { + return decorator; + } + + let originalGetter = property._getter; + + if (originalGetter === undefined) { + return decorator; + } + + let superDesc = descs[key]; + + // Check to see if the super property is a decorator first, if so load its descriptor + let superProperty: ComputedProperty | true | undefined = + typeof superDesc === 'function' ? descriptorForDecorator(superDesc) : superDesc; + + if (superProperty === undefined || superProperty === true) { + return decorator; + } + + let superGetter = superProperty._getter; + + if (superGetter === undefined) { + return decorator; + } + + let get = wrap(originalGetter, superGetter) as ComputedPropertyGetter; + let set; + let originalSetter = property._setter; + let superSetter = superProperty._setter; + + if (superSetter !== undefined) { + if (originalSetter !== undefined) { + set = wrap(originalSetter, superSetter) as ComputedPropertySetter; + } else { + // If the super property has a setter, we default to using it no matter what. + // This is clearly very broken and weird, but it's what was here so we have + // to keep it until the next major at least. + // + // TODO: Add a deprecation here. + set = superSetter; + } + } else { + set = originalSetter; + } + + // only create a new CP if we must + if (get !== originalGetter || set !== originalSetter) { + // Since multiple mixins may inherit from the same parent, we need + // to clone the computed property so that other mixins do not receive + // the wrapped version. + let dependentKeys = property._dependentKeys || []; + let newProperty = new ComputedProperty([ + ...dependentKeys, + { + get, + set, + } as ComputedPropertyObj, + ]); + + newProperty._readOnly = property._readOnly; + newProperty._meta = property._meta; + newProperty.enumerable = property.enumerable; + + // SAFETY: We passed in the impl for this class + return makeComputedDecorator(newProperty, ComputedProperty) as ComputedDecorator; + } + + return decorator; +} + +function giveMethodSuper( + key: string, + method: Function, + values: { [key: string]: any }, + descs: { [key: string]: any } +) { + // Methods overwrite computed properties, and do not call super to them. + if (descs[key] !== undefined) { + return method; + } + + // Find the original method in a parent mixin + let superMethod = values[key]; + + // Only wrap the new method if the original method was a function + if (typeof superMethod === 'function') { + return wrap(method, superMethod); + } + + return method; +} + +function simpleMakeArray(value: unknown) { + if (!value) { + return []; + } else if (!Array.isArray(value)) { + return [value]; + } else { + return value; + } +} + +function applyConcatenatedProperties(key: string, value: any, values: { [key: string]: any }) { + let baseValue = values[key]; + let ret = simpleMakeArray(baseValue).concat(simpleMakeArray(value)); + + if (DEBUG) { + // it is possible to use concatenatedProperties with strings (which cannot be frozen) + // only freeze objects... + if (typeof ret === 'object' && ret !== null) { + // prevent mutating `concatenatedProperties` array after it is applied + Object.freeze(ret); + } + } + + return ret; +} + +function applyMergedProperties( + key: string, + value: { [key: string]: any }, + values: { [key: string]: any } +): { [key: string]: any } { + let baseValue = values[key]; + + assert( + `You passed in \`${JSON.stringify( + value + )}\` as the value for \`${key}\` but \`${key}\` cannot be an Array`, + !isArray(value) + ); + + if (!baseValue) { + return value; + } + + let newBase = Object.assign({}, baseValue); + let hasFunction = false; + + let props = Object.keys(value); + + for (let prop of props) { + let propValue = value[prop]; + + if (typeof propValue === 'function') { + hasFunction = true; + newBase[prop] = giveMethodSuper(prop, propValue, baseValue, {}); + } else { + newBase[prop] = propValue; + } + } + + if (hasFunction) { + newBase._super = ROOT; + } + + return newBase; +} + +function mergeMixins( + mixins: MixinLike[], + meta: Meta, + descs: { [key: string]: object }, + values: { [key: string]: object }, + base: { [key: string]: object }, + keys: string[], + keysWithSuper: string[] +): void { + let currentMixin: MixinLike | undefined; + + for (let i = 0; i < mixins.length; i++) { + currentMixin = mixins[i]; + assert( + `Expected hash or Mixin instance, got ${Object.prototype.toString.call(currentMixin)}`, + typeof currentMixin === 'object' && + currentMixin !== null && + Object.prototype.toString.call(currentMixin) !== '[object Array]' + ); + + if (MIXINS.has(currentMixin)) { + if (meta.hasMixin(currentMixin)) { + continue; + } + meta.addMixin(currentMixin); + + let { properties, mixins } = currentMixin; + + if (properties !== undefined) { + mergeProps(meta, properties, descs, values, base, keys, keysWithSuper); + } else if (mixins !== undefined) { + mergeMixins(mixins, meta, descs, values, base, keys, keysWithSuper); + + if (currentMixin instanceof InternalMixin && currentMixin._without !== undefined) { + currentMixin._without.forEach((keyName: string) => { + // deleting the key means we won't process the value + let index = keys.indexOf(keyName); + + if (index !== -1) { + keys.splice(index, 1); + } + }); + } + } + } else { + mergeProps( + meta, + currentMixin as Record, + descs, + values, + base, + keys, + keysWithSuper + ); + } + } +} + +function mergeProps( + meta: Meta, + props: { [key: string]: unknown }, + descs: { [key: string]: unknown }, + values: { [key: string]: unknown }, + base: { [key: string]: unknown }, + keys: string[], + keysWithSuper: string[] +) { + let concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); + let mergings = concatenatedMixinProperties('mergedProperties', props, values, base); + + let propKeys = Object.keys(props); + + for (let key of propKeys) { + let value = props[key]; + + if (value === undefined) continue; + + if (keys.indexOf(key) === -1) { + keys.push(key); + + let desc = meta.peekDescriptors(key); + + if (desc === undefined) { + // If the value is a classic decorator, we don't want to actually + // access it, because that will execute the decorator while we're + // building the class. + if (!isClassicDecorator(value)) { + // The superclass did not have a CP, which means it may have + // observers or listeners on that property. + let prev = (values[key] = base[key]); + + if (typeof prev === 'function') { + updateObserversAndListeners(base, key, prev, false); + } + } + } else { + descs[key] = desc; + + // The super desc will be overwritten on descs, so save off the fact that + // there was a super so we know to Object.defineProperty when writing + // the value + keysWithSuper.push(key); + + desc.teardown(base, key, meta); + } + } + + let isFunction = typeof value === 'function'; + + if (isFunction) { + let desc: ComputedDescriptor | undefined | true = descriptorForDecorator(value as Function); + + if (desc !== undefined) { + // Wrap descriptor function to implement _super() if needed + descs[key] = giveDecoratorSuper( + key, + value as ComputedDecorator, + desc as ComputedProperty, + descs + ); + values[key] = undefined; + + continue; + } + } + + if ( + (concats && concats.indexOf(key) >= 0) || + key === 'concatenatedProperties' || + key === 'mergedProperties' + ) { + value = applyConcatenatedProperties(key, value, values); + } else if (mergings && mergings.indexOf(key) > -1) { + value = applyMergedProperties(key, value as object, values); + } else if (isFunction) { + value = giveMethodSuper(key, value as Function, values, descs); + } + + values[key] = value; + descs[key] = undefined; + } +} + +function updateObserversAndListeners(obj: object, key: string, fn: Function, add: boolean) { + let meta = observerListenerMetaFor(fn); + + if (meta === undefined) return; + + let { observers, listeners } = meta; + + if (observers !== undefined) { + let updateObserver = add ? addObserver : removeObserver; + + for (let path of observers.paths) { + updateObserver(obj, path, null, key, observers.sync); + } + } + + if (listeners !== undefined) { + let updateListener = add ? addListener : removeListener; + + for (let listener of listeners) { + updateListener(obj, listener, null, key); + } + } +} + +export function applyMixin( + obj: Record, + mixins: Array>, + _hideKeys = false +) { + let descs = Object.create(null); + let values = Object.create(null); + let meta = metaFor(obj); + let keys: string[] = []; + let keysWithSuper: string[] = []; + + (obj as any)._super = ROOT; + + // Go through all mixins and hashes passed in, and: + // + // * Handle concatenated properties + // * Handle merged properties + // * Set up _super wrapping if necessary + // * Set up computed property descriptors + // * Copying `toString` in broken browsers + mergeMixins(mixins, meta, descs, values, obj, keys, keysWithSuper); + + for (let key of keys) { + let value = values[key]; + let desc = descs[key]; + + if (value !== undefined) { + if (typeof value === 'function') { + updateObserversAndListeners(obj, key, value, true); + } + + defineValue(obj, key, value, keysWithSuper.indexOf(key) !== -1, !_hideKeys); + } else if (desc !== undefined) { + defineDecorator(obj, key, desc, meta); + } + } + + if (!meta.isPrototypeMeta(obj)) { + revalidateObservers(obj); + } + + return obj; +} + +/** + @method mixin + @param obj + @param mixins* + @return obj + @private +*/ +export function mixin(obj: object, ...args: any[]) { + applyMixin(obj, args); + return obj; +} + +const MIXINS = new WeakSet(); + +/** + The mixin machinery, without the deprecation that the public `Mixin` class + emits from `create`. Ember's own internals are built on mixins, so they build + them through this class; `Mixin` in `@ember/object/mixin` is the public, + deprecating subclass. + + @internal +*/ +export class InternalMixin { + /** @internal */ + declare static _disableDebugSeal?: boolean; + + /** @internal */ + mixins: InternalMixin[] | undefined; + + /** @internal */ + properties: { [key: string]: any } | undefined; + + /** @internal */ + ownerConstructor: any; + + /** @internal */ + _without: any[] | undefined; + + declare [INIT_FACTORY]?: null; + + /** @internal */ + constructor(mixins: InternalMixin[] | undefined, properties?: { [key: string]: any }) { + MIXINS.add(this); + this.properties = extractAccessors(properties); + this.mixins = buildMixinsArray(mixins); + this.ownerConstructor = undefined; + this._without = undefined; + + if (DEBUG) { + // Eagerly add INIT_FACTORY to avoid issues in DEBUG as a result of Object.seal(mixin) + this[INIT_FACTORY] = null; + + if (InternalMixin._disableDebugSeal !== true) { + Object.seal(this); + } + } + } + + /** @internal */ + static create(...args: any[]): InstanceType { + setUnprocessedMixins(); + let M = this; + return new M(args, undefined) as InstanceType; + } + + // returns the mixins currently applied to the specified object + // TODO: Make `mixin` + /** @internal */ + static mixins(obj: object): InternalMixin[] { + let meta = peekMeta(obj); + let ret: InternalMixin[] = []; + if (meta === null) { + return ret; + } + + meta.forEachMixins((currentMixin: InternalMixin) => { + // skip primitive mixins since these are always anonymous + if (!currentMixin.properties) { + ret.push(currentMixin); + } + }); + + return ret; + } + + /** + @method reopen + @param arguments* + @private + @internal + */ + reopen(...args: Array>): this { + if (args.length === 0) { + return this; + } + + if (this.properties) { + let currentMixin = new InternalMixin(undefined, this.properties); + this.properties = undefined; + this.mixins = [currentMixin]; + } else if (!this.mixins) { + this.mixins = []; + } + + this.mixins = this.mixins.concat(buildMixinsArray(args) as InternalMixin[]); + return this; + } + + /** + @method apply + @param obj + @return applied object + @private + @internal + */ + apply(obj: object, _hideKeys = false) { + // Ember.NativeArray is a normal Ember.Mixin that we mix into `Array.prototype` when prototype extensions are enabled + // mutating a native object prototype like this should _not_ result in enumerable properties being added (or we have significant + // issues with things like deep equality checks from test frameworks, or things like jQuery.extend(true, [], [])). + // + // _hideKeys disables enumerablity when applying the mixin. This is a hack, and we should stop mutating the array prototype by default 😫 + return applyMixin(obj, [this], _hideKeys); + } + + /** @internal */ + applyPartial(obj: object) { + return applyMixin(obj, [this]); + } + + /** + @method detect + @param obj + @return {Boolean} + @private + @internal + */ + detect(obj: any): boolean { + if (typeof obj !== 'object' || obj === null) { + return false; + } + if (MIXINS.has(obj)) { + return _detect(obj, this); + } + let meta = peekMeta(obj); + if (meta === null) { + return false; + } + return meta.hasMixin(this); + } + + /** @internal */ + without(...args: any[]) { + let ret = new InternalMixin([this]); + ret._without = args; + return ret; + } + + /** @internal */ + keys() { + let keys = _keys(this); + assert('[BUG] Missing keys for mixin!', keys); + return keys; + } + + /** @internal */ + toString() { + return '(unknown mixin)'; + } +} + +if (DEBUG) { + Object.defineProperty(InternalMixin, '_disableDebugSeal', { + configurable: true, + enumerable: false, + writable: true, + value: false, + }); +} + +function buildMixinsArray(mixins: MixinLike[] | undefined): InternalMixin[] | undefined { + let length = (mixins && mixins.length) || 0; + let m: InternalMixin[] | undefined = undefined; + + if (length > 0) { + m = new Array(length); + for (let i = 0; i < length; i++) { + let x = mixins![i]; + assert( + `Expected hash or Mixin instance, got ${Object.prototype.toString.call(x)}`, + typeof x === 'object' && + x !== null && + Object.prototype.toString.call(x) !== '[object Array]' + ); + + if (MIXINS.has(x)) { + m[i] = x as InternalMixin; + } else { + m[i] = new InternalMixin(undefined, x); + } + } + } + + return m; +} + +type MixinLike = InternalMixin | { [key: string]: any }; + +if (DEBUG) { + Object.seal(InternalMixin.prototype); +} + +function _detect(curMixin: InternalMixin, targetMixin: InternalMixin, seen = new Set()): boolean { + if (seen.has(curMixin)) { + return false; + } + seen.add(curMixin); + + if (curMixin === targetMixin) { + return true; + } + let mixins = curMixin.mixins; + if (mixins) { + return mixins.some((mixin) => _detect(mixin, targetMixin, seen)); + } + + return false; +} + +function _keys(mixin: InternalMixin, ret = new Set(), seen = new Set()) { + if (seen.has(mixin)) { + return; + } + seen.add(mixin); + + if (mixin.properties) { + let props = Object.keys(mixin.properties); + for (let prop of props) { + ret.add(prop); + } + } else if (mixin.mixins) { + mixin.mixins.forEach((x: any) => _keys(x, ret, seen)); + } + + return ret; +} + +/** + The public, deprecating face of a mixin that Ember also uses internally. + + RFC #1116 deprecates applying Ember's mixins, but Ember's own internals still + need them. Each such mixin therefore exists twice: an internal copy that + applies silently, and a public copy -- built with this class -- that wraps the + internal one and emits the deprecation from `init`. + + Because the two copies are distinct objects, `meta.hasMixin` would not match a + public mixin against an object that only ever had the internal copy applied. + `detect` is overridden here to look through to the wrapped copy, so that code + like `EmberArray.detect(someArrayProxy)` keeps working while the deprecation + is live. + + @internal +*/ +export class DeprecatedMixin extends InternalMixin { + detect(obj: any): boolean { + if (super.detect(obj)) { + return true; + } + + // The internal copy being wrapped is always the first constituent mixin, + // i.e. `DeprecatedMixin.create(internalCopy, { init() {} })`. + let internalMixin = this.mixins?.[0]; + return internalMixin !== undefined && internalMixin.detect(obj); + } +} diff --git a/packages/@ember/object/mixin.ts b/packages/@ember/object/mixin.ts index 29840c29956..d9c7d50b185 100644 --- a/packages/@ember/object/mixin.ts +++ b/packages/@ember/object/mixin.ts @@ -1,457 +1,10 @@ /** @module @ember/object/mixin */ -import { INIT_FACTORY } from '@ember/-internals/container/lib/container'; -import type { Meta } from '@ember/-internals/meta/lib/meta'; -import { meta as metaFor, peekMeta } from '@ember/-internals/meta/lib/meta'; -import { observerListenerMetaFor, ROOT, wrap } from '@ember/-internals/utils/lib/super'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import { assert } from '@ember/debug'; -import { DEBUG } from '@glimmer/env'; -import type { - ComputedDecorator, - ComputedPropertyGetter, - ComputedPropertyObj, - ComputedPropertySetter, -} from '@ember/-internals/metal/lib/computed'; -import { type ComputedDescriptor, isClassicDecorator } from '@ember/-internals/metal/lib/decorator'; -import { ComputedProperty } from '@ember/-internals/metal/lib/computed'; -import { - descriptorForDecorator, - makeComputedDecorator, - nativeDescDecorator, -} from '@ember/-internals/metal/lib/decorator'; -import { setUnprocessedMixins } from '@ember/-internals/metal/lib/namespace_search'; -import { - addObserver, - removeObserver, - revalidateObservers, -} from '@ember/-internals/metal/lib/observer'; -import { defineDecorator, defineValue } from '@ember/-internals/metal/lib/properties'; -import { addListener, removeListener } from '@ember/-internals/metal/lib/events'; +import { InternalMixin } from '@ember/object/mixin-internal'; import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; -const a_concat = Array.prototype.concat; -const { isArray } = Array; - -function extractAccessors(properties: { [key: string]: any } | undefined) { - if (properties !== undefined) { - for (let key of Object.keys(properties)) { - let desc = Object.getOwnPropertyDescriptor(properties, key)!; - - if (desc.get !== undefined || desc.set !== undefined) { - Object.defineProperty(properties, key, { value: nativeDescDecorator(desc) }); - } - } - } - - return properties; -} - -function concatenatedMixinProperties( - concatProp: string, - props: { [key: string]: any }, - values: { [key: string]: any }, - base: { [key: string]: any } -) { - // reset before adding each new mixin to pickup concats from previous - let concats = values[concatProp] || base[concatProp]; - if (props[concatProp]) { - concats = concats ? a_concat.call(concats, props[concatProp]) : props[concatProp]; - } - return concats; -} - -function giveDecoratorSuper( - key: string, - decorator: ComputedDecorator, - property: ComputedProperty | true, - descs: { [key: string]: any } -): ComputedDecorator { - if (property === true) { - return decorator; - } - - let originalGetter = property._getter; - - if (originalGetter === undefined) { - return decorator; - } - - let superDesc = descs[key]; - - // Check to see if the super property is a decorator first, if so load its descriptor - let superProperty: ComputedProperty | true | undefined = - typeof superDesc === 'function' ? descriptorForDecorator(superDesc) : superDesc; - - if (superProperty === undefined || superProperty === true) { - return decorator; - } - - let superGetter = superProperty._getter; - - if (superGetter === undefined) { - return decorator; - } - - let get = wrap(originalGetter, superGetter) as ComputedPropertyGetter; - let set; - let originalSetter = property._setter; - let superSetter = superProperty._setter; - - if (superSetter !== undefined) { - if (originalSetter !== undefined) { - set = wrap(originalSetter, superSetter) as ComputedPropertySetter; - } else { - // If the super property has a setter, we default to using it no matter what. - // This is clearly very broken and weird, but it's what was here so we have - // to keep it until the next major at least. - // - // TODO: Add a deprecation here. - set = superSetter; - } - } else { - set = originalSetter; - } - - // only create a new CP if we must - if (get !== originalGetter || set !== originalSetter) { - // Since multiple mixins may inherit from the same parent, we need - // to clone the computed property so that other mixins do not receive - // the wrapped version. - let dependentKeys = property._dependentKeys || []; - let newProperty = new ComputedProperty([ - ...dependentKeys, - { - get, - set, - } as ComputedPropertyObj, - ]); - - newProperty._readOnly = property._readOnly; - newProperty._meta = property._meta; - newProperty.enumerable = property.enumerable; - - // SAFETY: We passed in the impl for this class - return makeComputedDecorator(newProperty, ComputedProperty) as ComputedDecorator; - } - - return decorator; -} - -function giveMethodSuper( - key: string, - method: Function, - values: { [key: string]: any }, - descs: { [key: string]: any } -) { - // Methods overwrite computed properties, and do not call super to them. - if (descs[key] !== undefined) { - return method; - } - - // Find the original method in a parent mixin - let superMethod = values[key]; - - // Only wrap the new method if the original method was a function - if (typeof superMethod === 'function') { - return wrap(method, superMethod); - } - - return method; -} - -function simpleMakeArray(value: unknown) { - if (!value) { - return []; - } else if (!Array.isArray(value)) { - return [value]; - } else { - return value; - } -} - -function applyConcatenatedProperties(key: string, value: any, values: { [key: string]: any }) { - let baseValue = values[key]; - let ret = simpleMakeArray(baseValue).concat(simpleMakeArray(value)); - - if (DEBUG) { - // it is possible to use concatenatedProperties with strings (which cannot be frozen) - // only freeze objects... - if (typeof ret === 'object' && ret !== null) { - // prevent mutating `concatenatedProperties` array after it is applied - Object.freeze(ret); - } - } - - return ret; -} - -function applyMergedProperties( - key: string, - value: { [key: string]: any }, - values: { [key: string]: any } -): { [key: string]: any } { - let baseValue = values[key]; - - assert( - `You passed in \`${JSON.stringify( - value - )}\` as the value for \`${key}\` but \`${key}\` cannot be an Array`, - !isArray(value) - ); - - if (!baseValue) { - return value; - } - - let newBase = Object.assign({}, baseValue); - let hasFunction = false; - - let props = Object.keys(value); - - for (let prop of props) { - let propValue = value[prop]; - - if (typeof propValue === 'function') { - hasFunction = true; - newBase[prop] = giveMethodSuper(prop, propValue, baseValue, {}); - } else { - newBase[prop] = propValue; - } - } - - if (hasFunction) { - newBase._super = ROOT; - } - - return newBase; -} - -function mergeMixins( - mixins: MixinLike[], - meta: Meta, - descs: { [key: string]: object }, - values: { [key: string]: object }, - base: { [key: string]: object }, - keys: string[], - keysWithSuper: string[] -): void { - let currentMixin: MixinLike | undefined; - - for (let i = 0; i < mixins.length; i++) { - currentMixin = mixins[i]; - assert( - `Expected hash or Mixin instance, got ${Object.prototype.toString.call(currentMixin)}`, - typeof currentMixin === 'object' && - currentMixin !== null && - Object.prototype.toString.call(currentMixin) !== '[object Array]' - ); - - if (MIXINS.has(currentMixin)) { - if (meta.hasMixin(currentMixin)) { - continue; - } - meta.addMixin(currentMixin); - - let { properties, mixins } = currentMixin; - - if (properties !== undefined) { - mergeProps(meta, properties, descs, values, base, keys, keysWithSuper); - } else if (mixins !== undefined) { - mergeMixins(mixins, meta, descs, values, base, keys, keysWithSuper); - - if (currentMixin instanceof Mixin && currentMixin._without !== undefined) { - currentMixin._without.forEach((keyName: string) => { - // deleting the key means we won't process the value - let index = keys.indexOf(keyName); - - if (index !== -1) { - keys.splice(index, 1); - } - }); - } - } - } else { - mergeProps( - meta, - currentMixin as Record, - descs, - values, - base, - keys, - keysWithSuper - ); - } - } -} - -function mergeProps( - meta: Meta, - props: { [key: string]: unknown }, - descs: { [key: string]: unknown }, - values: { [key: string]: unknown }, - base: { [key: string]: unknown }, - keys: string[], - keysWithSuper: string[] -) { - let concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); - let mergings = concatenatedMixinProperties('mergedProperties', props, values, base); - - let propKeys = Object.keys(props); - - for (let key of propKeys) { - let value = props[key]; - - if (value === undefined) continue; - - if (keys.indexOf(key) === -1) { - keys.push(key); - - let desc = meta.peekDescriptors(key); - - if (desc === undefined) { - // If the value is a classic decorator, we don't want to actually - // access it, because that will execute the decorator while we're - // building the class. - if (!isClassicDecorator(value)) { - // The superclass did not have a CP, which means it may have - // observers or listeners on that property. - let prev = (values[key] = base[key]); - - if (typeof prev === 'function') { - updateObserversAndListeners(base, key, prev, false); - } - } - } else { - descs[key] = desc; - - // The super desc will be overwritten on descs, so save off the fact that - // there was a super so we know to Object.defineProperty when writing - // the value - keysWithSuper.push(key); - - desc.teardown(base, key, meta); - } - } - - let isFunction = typeof value === 'function'; - - if (isFunction) { - let desc: ComputedDescriptor | undefined | true = descriptorForDecorator(value as Function); - - if (desc !== undefined) { - // Wrap descriptor function to implement _super() if needed - descs[key] = giveDecoratorSuper( - key, - value as ComputedDecorator, - desc as ComputedProperty, - descs - ); - values[key] = undefined; - - continue; - } - } - - if ( - (concats && concats.indexOf(key) >= 0) || - key === 'concatenatedProperties' || - key === 'mergedProperties' - ) { - value = applyConcatenatedProperties(key, value, values); - } else if (mergings && mergings.indexOf(key) > -1) { - value = applyMergedProperties(key, value as object, values); - } else if (isFunction) { - value = giveMethodSuper(key, value as Function, values, descs); - } - - values[key] = value; - descs[key] = undefined; - } -} - -function updateObserversAndListeners(obj: object, key: string, fn: Function, add: boolean) { - let meta = observerListenerMetaFor(fn); - - if (meta === undefined) return; - - let { observers, listeners } = meta; - - if (observers !== undefined) { - let updateObserver = add ? addObserver : removeObserver; - - for (let path of observers.paths) { - updateObserver(obj, path, null, key, observers.sync); - } - } - - if (listeners !== undefined) { - let updateListener = add ? addListener : removeListener; - - for (let listener of listeners) { - updateListener(obj, listener, null, key); - } - } -} - -export function applyMixin( - obj: Record, - mixins: Array>, - _hideKeys = false -) { - let descs = Object.create(null); - let values = Object.create(null); - let meta = metaFor(obj); - let keys: string[] = []; - let keysWithSuper: string[] = []; - - (obj as any)._super = ROOT; - - // Go through all mixins and hashes passed in, and: - // - // * Handle concatenated properties - // * Handle merged properties - // * Set up _super wrapping if necessary - // * Set up computed property descriptors - // * Copying `toString` in broken browsers - mergeMixins(mixins, meta, descs, values, obj, keys, keysWithSuper); - - for (let key of keys) { - let value = values[key]; - let desc = descs[key]; - - if (value !== undefined) { - if (typeof value === 'function') { - updateObserversAndListeners(obj, key, value, true); - } - - defineValue(obj, key, value, keysWithSuper.indexOf(key) !== -1, !_hideKeys); - } else if (desc !== undefined) { - defineDecorator(obj, key, desc, meta); - } - } - - if (!meta.isPrototypeMeta(obj)) { - revalidateObservers(obj); - } - - return obj; -} - -/** - @method mixin - @param obj - @param mixins* - @return obj - @private -*/ -export function mixin(obj: object, ...args: any[]) { - applyMixin(obj, args); - return obj; -} - -const MIXINS = new WeakSet(); +export { applyMixin, mixin } from '@ember/object/mixin-internal'; /** The `Mixin` class allows you to create mixins, whose properties can be @@ -532,45 +85,11 @@ const MIXINS = new WeakSet(); }); ``` + @deprecated @class Mixin @public */ -export default class Mixin { - /** @internal */ - declare static _disableDebugSeal?: boolean; - - /** @internal */ - mixins: Mixin[] | undefined; - - /** @internal */ - properties: { [key: string]: any } | undefined; - - /** @internal */ - ownerConstructor: any; - - /** @internal */ - _without: any[] | undefined; - - declare [INIT_FACTORY]?: null; - - /** @internal */ - constructor(mixins: Mixin[] | undefined, properties?: { [key: string]: any }) { - MIXINS.add(this); - this.properties = extractAccessors(properties); - this.mixins = buildMixinsArray(mixins); - this.ownerConstructor = undefined; - this._without = undefined; - - if (DEBUG) { - // Eagerly add INIT_FACTORY to avoid issues in DEBUG as a result of Object.seal(mixin) - this[INIT_FACTORY] = null; - - if (Mixin._disableDebugSeal !== true) { - Object.seal(this); - } - } - } - +export default class Mixin extends InternalMixin { /** @method create @for @ember/object/mixin @@ -578,198 +97,11 @@ export default class Mixin { @param arguments* @public */ - static create(...args: any[]): InstanceType { + static override create(...args: any[]): InstanceType { deprecateUntil( `Using mixins is deprecated. Refactor to composition patterns or class decorators.`, DEPRECATIONS.DEPRECATE_MIXINS ); - return this[INTERNAL_MIXIN_CREATE](...args); - } - - /** @internal */ - static [INTERNAL_MIXIN_CREATE](...args: any[]): InstanceType { - setUnprocessedMixins(); - let M = this; - return new M(args, undefined) as InstanceType; + return super.create(...args); } - - // returns the mixins currently applied to the specified object - // TODO: Make `mixin` - /** @internal */ - static mixins(obj: object): Mixin[] { - let meta = peekMeta(obj); - let ret: Mixin[] = []; - if (meta === null) { - return ret; - } - - meta.forEachMixins((currentMixin: Mixin) => { - // skip primitive mixins since these are always anonymous - if (!currentMixin.properties) { - ret.push(currentMixin); - } - }); - - return ret; - } - - /** - @method reopen - @param arguments* - @private - @internal - */ - reopen(...args: Array>): this { - if (args.length === 0) { - return this; - } - - if (this.properties) { - let currentMixin = new Mixin(undefined, this.properties); - this.properties = undefined; - this.mixins = [currentMixin]; - } else if (!this.mixins) { - this.mixins = []; - } - - this.mixins = this.mixins.concat(buildMixinsArray(args) as Mixin[]); - return this; - } - - /** - @method apply - @param obj - @return applied object - @private - @internal - */ - apply(obj: object, _hideKeys = false) { - // Ember.NativeArray is a normal Ember.Mixin that we mix into `Array.prototype` when prototype extensions are enabled - // mutating a native object prototype like this should _not_ result in enumerable properties being added (or we have significant - // issues with things like deep equality checks from test frameworks, or things like jQuery.extend(true, [], [])). - // - // _hideKeys disables enumerablity when applying the mixin. This is a hack, and we should stop mutating the array prototype by default 😫 - return applyMixin(obj, [this], _hideKeys); - } - - /** @internal */ - applyPartial(obj: object) { - return applyMixin(obj, [this]); - } - - /** - @method detect - @param obj - @return {Boolean} - @private - @internal - */ - detect(obj: any): boolean { - if (typeof obj !== 'object' || obj === null) { - return false; - } - if (MIXINS.has(obj)) { - return _detect(obj, this); - } - let meta = peekMeta(obj); - if (meta === null) { - return false; - } - return meta.hasMixin(this); - } - - /** @internal */ - without(...args: any[]) { - let ret = new Mixin([this]); - ret._without = args; - return ret; - } - - /** @internal */ - keys() { - let keys = _keys(this); - assert('[BUG] Missing keys for mixin!', keys); - return keys; - } - - /** @internal */ - toString() { - return '(unknown mixin)'; - } -} - -if (DEBUG) { - Object.defineProperty(Mixin, '_disableDebugSeal', { - configurable: true, - enumerable: false, - writable: true, - value: false, - }); -} - -function buildMixinsArray(mixins: MixinLike[] | undefined): Mixin[] | undefined { - let length = (mixins && mixins.length) || 0; - let m: Mixin[] | undefined = undefined; - - if (length > 0) { - m = new Array(length); - for (let i = 0; i < length; i++) { - let x = mixins![i]; - assert( - `Expected hash or Mixin instance, got ${Object.prototype.toString.call(x)}`, - typeof x === 'object' && - x !== null && - Object.prototype.toString.call(x) !== '[object Array]' - ); - - if (MIXINS.has(x)) { - m[i] = x as Mixin; - } else { - m[i] = new Mixin(undefined, x); - } - } - } - - return m; -} - -type MixinLike = Mixin | { [key: string]: any }; - -if (DEBUG) { - Object.seal(Mixin.prototype); -} - -function _detect(curMixin: Mixin, targetMixin: Mixin, seen = new Set()): boolean { - if (seen.has(curMixin)) { - return false; - } - seen.add(curMixin); - - if (curMixin === targetMixin) { - return true; - } - let mixins = curMixin.mixins; - if (mixins) { - return mixins.some((mixin) => _detect(mixin, targetMixin, seen)); - } - - return false; -} - -function _keys(mixin: Mixin, ret = new Set(), seen = new Set()) { - if (seen.has(mixin)) { - return; - } - seen.add(mixin); - - if (mixin.properties) { - let props = Object.keys(mixin.properties); - for (let prop of props) { - ret.add(prop); - } - } else if (mixin.mixins) { - mixin.mixins.forEach((x: any) => _keys(x, ret, seen)); - } - - return ret; } diff --git a/packages/@ember/object/observable-internal.ts b/packages/@ember/object/observable-internal.ts new file mode 100644 index 00000000000..5a8da475e2f --- /dev/null +++ b/packages/@ember/object/observable-internal.ts @@ -0,0 +1,194 @@ +/** +@module @ember/object/observable +*/ + +import { peekMeta } from '@ember/-internals/meta/lib/meta'; +import { hasListeners } from '@ember/-internals/metal/lib/events'; +import { + beginPropertyChanges, + notifyPropertyChange, + endPropertyChanges, +} from '@ember/-internals/metal/lib/property_events'; +import { addObserver, removeObserver } from '@ember/-internals/metal/lib/observer'; +import { get } from '@ember/-internals/metal/lib/property_get'; +import { set } from '@ember/-internals/metal/lib/property_set'; +import getProperties from '@ember/-internals/metal/lib/get_properties'; +import setProperties from '@ember/-internals/metal/lib/set_properties'; + +import { InternalMixin } from '@ember/object/mixin-internal'; +import { assert } from '@ember/debug'; + +export type ObserverMethod = + | keyof Target + | ((this: Target, sender: Sender, key: string, value: any, rev: number) => void); + +/** + The internal counterpart to the public `Observable` mixin. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. The public API documentation lives on the public copy. + + @internal +*/ +interface InternalObservable { + get(key: K): this[K]; + get(key: string): unknown; + + getProperties>(list: L): { [Key in L[number]]: this[Key] }; + getProperties>(...list: L): { [Key in L[number]]: this[Key] }; + getProperties(list: L): { [Key in L[number]]: unknown }; + getProperties(...list: L): { [Key in L[number]]: unknown }; + + // NOT TYPE SAFE! + set(key: K, value: T): T; + set(key: string, value: T): T; + + // NOT TYPE SAFE! + setProperties(hash: P): P; + setProperties>(hash: T): T; + + notifyPropertyChange(keyName: string): this; + + addObserver(key: keyof this, target: Target, method: ObserverMethod): this; + addObserver(key: keyof this, method: ObserverMethod): this; + + removeObserver( + key: keyof this, + target: Target, + method: ObserverMethod + ): this; + removeObserver(key: keyof this, method: ObserverMethod): this; + + // NOT TYPE SAFE! + incrementProperty(keyName: keyof this, increment?: number): number; + + // NOT TYPE SAFE! + decrementProperty(keyName: keyof this, decrement?: number): number; + + // NOT TYPE SAFE! + toggleProperty(keyName: keyof this): boolean; + + cacheFor(key: K): unknown; +} +const InternalObservable = InternalMixin.create({ + get(keyName: string) { + return get(this, keyName); + }, + + getProperties(...args: string[]) { + return getProperties(this, ...args); + }, + + set(keyName: string, value: unknown) { + return set(this, keyName, value); + }, + + 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 + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call this + method at the beginning of the changes to begin deferring change + notifications. When you are done making changes, call + `endPropertyChanges()` to deliver the deferred change notifications and end + deferring. + + @method beginPropertyChanges + @return {Observable} + @private + */ + beginPropertyChanges() { + beginPropertyChanges(); + return this; + }, + + /** + Ends a grouping of property changes. + + You can use this method to group property changes so that notifications + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call + `beginPropertyChanges()` at the beginning of the changes to defer change + notifications. When you are done making changes, call this method to + deliver the deferred change notifications and end deferring. + + @method endPropertyChanges + @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; + }, + + /** + 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 + on the object. + + @method hasObserverFor + @param {String} key Key to check + @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) + ); + 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 InternalObservable; diff --git a/packages/@ember/object/observable.ts b/packages/@ember/object/observable.ts index b928183b051..1320fd97574 100644 --- a/packages/@ember/object/observable.ts +++ b/packages/@ember/object/observable.ts @@ -2,26 +2,12 @@ @module @ember/object/observable */ -import { peekMeta } from '@ember/-internals/meta/lib/meta'; -import { hasListeners } from '@ember/-internals/metal/lib/events'; -import { - beginPropertyChanges, - notifyPropertyChange, - endPropertyChanges, -} from '@ember/-internals/metal/lib/property_events'; -import { addObserver, removeObserver } from '@ember/-internals/metal/lib/observer'; -import { get } from '@ember/-internals/metal/lib/property_get'; -import { set } from '@ember/-internals/metal/lib/property_set'; -import getProperties from '@ember/-internals/metal/lib/get_properties'; -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 { assert } from '@ember/debug'; - -export type ObserverMethod = - | keyof Target - | ((this: Target, sender: Sender, key: string, value: any, rev: number) => void); +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalObservable from '@ember/object/observable-internal'; +import type { ObserverMethod } from '@ember/object/observable-internal'; + +export type { ObserverMethod } from '@ember/object/observable-internal'; /** ## Overview @@ -93,7 +79,7 @@ export type ObserverMethod = @class Observable @public */ -interface Observable { +interface Observable extends InternalObservable { /** Retrieves the value of a property from the object. @@ -421,125 +407,13 @@ interface Observable { */ cacheFor(key: K): unknown; } -const Observable = Mixin[INTERNAL_MIXIN_CREATE]({ - get(keyName: string) { - return get(this, keyName); - }, - - getProperties(...args: string[]) { - return getProperties(this, ...args); - }, - - set(keyName: string, value: unknown) { - return set(this, keyName, value); - }, - - 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 - will not be sent until the changes are finished. If you plan to make a - large number of changes to an object at one time, you should call this - method at the beginning of the changes to begin deferring change - notifications. When you are done making changes, call - `endPropertyChanges()` to deliver the deferred change notifications and end - deferring. - - @method beginPropertyChanges - @return {Observable} - @private - */ - beginPropertyChanges() { - beginPropertyChanges(); - return this; - }, - - /** - Ends a grouping of property changes. - - You can use this method to group property changes so that notifications - will not be sent until the changes are finished. If you plan to make a - large number of changes to an object at one time, you should call - `beginPropertyChanges()` at the beginning of the changes to defer change - notifications. When you are done making changes, call this method to - deliver the deferred change notifications and end deferring. - - @method endPropertyChanges - @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; - }, - - /** - 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 - on the object. - - @method hasObserverFor - @param {String} key Key to check - @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) +const Observable = DeprecatedMixin.create(InternalObservable, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `Observable` mixin is deprecated. Use native classes with tracked properties and native property access instead.', + DEPRECATIONS.DEPRECATE_OBSERVABLE_MIXIN ); - 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; }, }); diff --git a/packages/@ember/object/promise-proxy-mixin.ts b/packages/@ember/object/promise-proxy-mixin.ts index 977d645b68c..60e5bd72747 100644 --- a/packages/@ember/object/promise-proxy-mixin.ts +++ b/packages/@ember/object/promise-proxy-mixin.ts @@ -1,8 +1,7 @@ import { get } from '@ember/-internals/metal/lib/property_get'; 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 { InternalMixin } from '@ember/object/mixin-internal'; import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; import type { AnyFn, MethodNamesOf } from '@ember/-internals/utility-types'; import type RSVP from 'rsvp'; @@ -214,7 +213,7 @@ interface PromiseProxyMixin { */ finally: this['promise']['finally']; } -const PromiseProxyMixin = Mixin[INTERNAL_MIXIN_CREATE]({ +const PromiseProxyMixin = InternalMixin.create({ init() { this._super(...arguments); diff --git a/packages/@ember/object/proxy.ts b/packages/@ember/object/proxy.ts index be037bfc153..a4904485c0c 100644 --- a/packages/@ember/object/proxy.ts +++ b/packages/@ember/object/proxy.ts @@ -3,7 +3,8 @@ */ import { FrameworkObject } from '@ember/object/-internals'; -import _ProxyMixin from '@ember/-internals/runtime/lib/mixins/-proxy'; +import type _ProxyMixin from '@ember/-internals/runtime/lib/mixins/-proxy'; +import InternalProxyMixin from '@ember/-internals/runtime/lib/mixins/-proxy-internal'; import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; /** @@ -132,6 +133,6 @@ class ObjectProxy extends FrameworkObject { ); } } -ObjectProxy.PrototypeMixin.reopen(_ProxyMixin); +ObjectProxy.PrototypeMixin.reopen(InternalProxyMixin); export default ObjectProxy; diff --git a/packages/@ember/object/tests/evented_test.js b/packages/@ember/object/tests/evented_test.js index 6e2831d95da..51d4fe99283 100644 --- a/packages/@ember/object/tests/evented_test.js +++ b/packages/@ember/object/tests/evented_test.js @@ -31,11 +31,17 @@ moduleFor( DEPRECATIONS.DEPRECATE_EVENTED.isRemoved )} @test works properly on proxy-ish objects`](assert) { let eventedProxyObj; - eventedProxyObj = class extends CoreObject.extend(EventedMixin) { - unknownProperty() { - return true; - } - }.create(); + expectDeprecation( + () => { + eventedProxyObj = class extends CoreObject.extend(EventedMixin) { + unknownProperty() { + return true; + } + }.create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let noop = function () {}; diff --git a/packages/@ember/object/tests/events_test.js b/packages/@ember/object/tests/events_test.js index c9c600060c6..f8b1b34d6ad 100644 --- a/packages/@ember/object/tests/events_test.js +++ b/packages/@ember/object/tests/events_test.js @@ -15,7 +15,13 @@ moduleFor( }; let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); expectDeprecation( () => { @@ -56,7 +62,13 @@ moduleFor( }; let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); expectDeprecation( () => { @@ -93,7 +105,13 @@ moduleFor( let self, args; let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); expectDeprecation( () => { @@ -125,7 +143,13 @@ moduleFor( let count = 0; let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); expectDeprecation( () => { @@ -170,7 +194,13 @@ moduleFor( let self, args; let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let target = {}; expectDeprecation( @@ -208,7 +238,13 @@ moduleFor( }; let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); expectDeprecation( () => { @@ -243,9 +279,15 @@ moduleFor( DEPRECATIONS.DEPRECATE_EVENTED.isRemoved )} @test a listener registered with one can be removed with off`](assert) { let obj; - obj = class extends EmberObject.extend(Evented) { - F() {} - }.create(); + expectDeprecation( + () => { + obj = class extends EmberObject.extend(Evented) { + F() {} + }.create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let F = function () {}; expectDeprecation( @@ -305,7 +347,13 @@ moduleFor( DEPRECATIONS.DEPRECATE_EVENTED.isRemoved )} @test adding and removing listeners should be chainable`](assert) { let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let F = function () {}; let ret; diff --git a/packages/@ember/object/tests/mixin/deprecation_test.js b/packages/@ember/object/tests/mixin/deprecation_test.js index 9e1229a6530..3ca0380982a 100644 --- a/packages/@ember/object/tests/mixin/deprecation_test.js +++ b/packages/@ember/object/tests/mixin/deprecation_test.js @@ -1,6 +1,6 @@ import EmberObject from '@ember/object'; import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { InternalMixin } from '@ember/object/mixin-internal'; import { moduleFor, AbstractTestCase, @@ -42,7 +42,7 @@ moduleFor( let MixinA; expectNoDeprecation(() => { - MixinA = Mixin[INTERNAL_MIXIN_CREATE]({ + MixinA = InternalMixin.create({ foo: 'FOO', }); }); diff --git a/packages/@ember/object/tests/observable_test.js b/packages/@ember/object/tests/observable_test.js index ccb78081bc8..a6c3374cf73 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,7 @@ import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpe let object, objectA, objectB, objectC, objectD, objectE, objectF, lookup; -const ObservableObject = EmberObject.extend(Observable); +const ObservableObject = EmberObject.extend(); const originalLookup = context.lookup; class ObservableTestCase extends AbstractTestCase { @@ -58,7 +57,7 @@ moduleFor( 'object.get()', class extends ObservableTestCase { beforeEach() { - object = ObservableObject.extend(Observable, { + object = ObservableObject.extend({ computed: computed(function () { return 'value'; }), diff --git a/packages/@ember/routing/route.ts b/packages/@ember/routing/route.ts index d3078d53886..3390ca0e0de 100644 --- a/packages/@ember/routing/route.ts +++ b/packages/@ember/routing/route.ts @@ -21,7 +21,7 @@ import EmberObject from '@ember/object'; import Evented from '@ember/object/evented'; import { copyDefaultValue } from '@ember/-internals/routing/route-managers/classic/query-params'; import { meta as metaFor } from '@ember/-internals/meta/lib/meta'; -import ActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler'; +import InternalActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler-internal'; import { isProxy } from '@ember/-internals/utils/lib/is_proxy'; import lookupDescriptor from '@ember/-internals/utils/lib/lookup-descriptor'; import type { AnyFn } from '@ember/-internals/utility-types'; @@ -107,7 +107,7 @@ type MaybeReturnType = T extends AnyFn ? ReturnType : unknown; @public */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -interface Route extends ActionHandler { +interface Route extends InternalActionHandler { /** The `willTransition` action is fired at the beginning of any attempted transition with a `Transition` object as the sole @@ -281,7 +281,7 @@ interface Route extends ActionHandler { error?(error: Error, transition: Transition): boolean | void; } -class Route extends EmberObject.extend(ActionHandler) { +class Route extends EmberObject.extend(InternalActionHandler) { static { // The deprecated Evented mixin is no longer applied, but instances still // provide its methods, so `Evented.detect` must keep returning true. diff --git a/packages/ember/tests/service_injection_test.js b/packages/ember/tests/service_injection_test.js index 1e6033f9e1d..abcd39580fd 100644 --- a/packages/ember/tests/service_injection_test.js +++ b/packages/ember/tests/service_injection_test.js @@ -1,7 +1,7 @@ import { getOwner } from '@ember/-internals/owner'; import Controller from '@ember/controller'; import Service, { service } from '@ember/service'; -import { _ProxyMixin } from '@ember/-internals/runtime'; +import _ProxyMixin from '@ember/-internals/runtime/lib/mixins/-proxy-internal'; import { moduleFor, ApplicationTestCase } from 'internal-test-helpers'; import { computed } from '@ember/object'; import { precompileTemplate } from '@ember/template-compilation'; diff --git a/rollup.config.mjs b/rollup.config.mjs index b1ecfdb13b5..f74fdab472d 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -537,6 +537,16 @@ function pruneEmptyBundles() { }; } +// Mixins that Ember deprecates but still uses internally exist twice: a public +// copy that emits the deprecation, and an internal copy that applies silently. +// The internal copies are an implementation detail, so they are kept out of +// `renamed-modules` rather than being advertised as importable module paths. +// Matches `foo-internal.js` and `-internal.js`, but deliberately not the +// long-standing `-internals.js` modules, which stay listed. +function isInternalCopy(name) { + return /(^|\/|-)internal\.js$/.test(name); +} + function packageMeta() { return { name: 'package-meta', @@ -547,7 +557,8 @@ function packageMeta() { (name) => name.startsWith('packages/') && !name.startsWith('packages/shared-chunks/') && - name.endsWith('.js') + name.endsWith('.js') && + !isInternalCopy(name) ) .sort() .map((name) => { From d383a3697435c071535f1a5b8f3b43ac484a4039 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:46:45 -0400 Subject: [PATCH 02/19] Attempt to guard the isProxy guard in each-in --- .../@ember/-internals/glimmer/lib/helpers/each-in.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts b/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts index ed10579f268..630deff238d 100644 --- a/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts +++ b/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts @@ -9,6 +9,7 @@ import type { CapturedArguments } from '@glimmer/interfaces'; import { createComputeRef, valueForRef } from '@glimmer/reference/lib/reference'; import { consumeTag } from '@glimmer/validator/lib/tracking'; import { internalHelper } from './internal-helper'; +import { DEPRECATIONS } from '../../../deprecations'; /** The `{{#each}}` keyword loops over elements in a collection. It is an extension @@ -311,10 +312,12 @@ export default internalHelper(({ positional }: CapturedArguments) => { consumeTag(tagForObject(iterable)); - if (isProxy(iterable)) { - // this is because the each-in doesn't actually get(proxy, 'key') but bypasses it - // and the proxy's tag is lazy updated on access - iterable = _contentFor(iterable); + if (!DEPRECATIONS.DEPRECATE_PROXY_MIXIN.isRemoved) { + if (isProxy(iterable)) { + // this is because the each-in doesn't actually get(proxy, 'key') but bypasses it + // and the proxy's tag is lazy updated on access + iterable = _contentFor(iterable); + } } return new EachInWrapper(iterable); From 8185eca5a6d79f39e08bacd839e1da36f6fc515b Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:18:20 -0400 Subject: [PATCH 03/19] Add missing @deprecated --- packages/@ember/object/observable.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@ember/object/observable.ts b/packages/@ember/object/observable.ts index 1320fd97574..d4b6bb90eef 100644 --- a/packages/@ember/object/observable.ts +++ b/packages/@ember/object/observable.ts @@ -76,6 +76,7 @@ export type { ObserverMethod } from '@ember/object/observable-internal'; resulting value of the computed property is unchanged. This is necessary because computed properties are not computed until `get` is called. + @deprecated @class Observable @public */ From 3beac538aa661e80e768358647f5bd2ce20e92e5 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:01:38 -0400 Subject: [PATCH 04/19] Use the public mixins in tests, not the internal copies Tests imported the silent `-internal` copies of the deprecated mixins. Such a test stays after the deprecation is removed, and it keeps the internal copy in use. These tests now import the public mixin, expect its deprecation, and use `testUnless(...isRemoved)`. They go away with the deprecation. - `EmberArray` in `runtime/tests/mixins/array_test.js` - `MutableArray` in `glimmer/tests/integration/helpers/tracked-test.js` - `ContainerProxyMixin` in `container_proxy_test.js` - `TargetActionSupport` in `target_action_support_test.js` - `_ProxyMixin` in `ember/tests/service_injection_test.js` `TestArray#init` in `array_test.js` now calls `super.init`. Without that call, the deprecation of the mixin does not fire. Three tests in `array_test.js` do not use the mixin. They moved to their own module, so they still run after the removal. `mixin/deprecation_test.js` still imports `InternalMixin`. That test checks that the internal constructor does not fire the deprecation. Co-Authored-By: Claude Opus 5.5 --- .../tests/integration/helpers/tracked-test.js | 20 ++++- .../runtime/tests/mixins/array_test.js | 78 +++++++++++++++---- .../tests/mixins/container_proxy_test.js | 21 +++-- .../mixins/target_action_support_test.js | 29 ++++--- .../ember/tests/service_injection_test.js | 19 ++++- 5 files changed, 127 insertions(+), 40 deletions(-) diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js index 2aed389bace..2fd141bebd5 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js @@ -1,9 +1,16 @@ import EmberObject from '@ember/object'; import { A } from '@ember/array'; +import MutableArray from '@ember/array/mutable'; import { tracked, notifyPropertyChange } from '@ember/-internals/metal'; -import { InternalMutableArray as MutableArray } from '@ember/array/index-internal'; import Service, { service } from '@ember/service'; -import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers'; +import { + expectDeprecation, + moduleFor, + RenderingTestCase, + runTask, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; import { precompileTemplate } from '@ember/template-compilation'; import { setComponentTemplate } from '@glimmer/manager'; @@ -172,7 +179,14 @@ moduleFor( this.assertText('1, 2, 3, 4'); } - '@test custom ember array properties rerender when updated'() { + [`${testUnless( + DEPRECATIONS.DEPRECATE_MUTABLE_ARRAY_MIXIN.isRemoved + )} @test custom ember array properties rerender when updated`]() { + expectDeprecation( + /The `MutableArray` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_MUTABLE_ARRAY_MIXIN.isEnabled + ); + let CustomArray = class extends EmberObject.extend(MutableArray) { init() { super.init(...arguments); diff --git a/packages/@ember/-internals/runtime/tests/mixins/array_test.js b/packages/@ember/-internals/runtime/tests/mixins/array_test.js index 3ec1baed666..8e47fdd49a5 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/array_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/array_test.js @@ -7,9 +7,21 @@ import { arrayContentWillChange, } from '@ember/-internals/metal'; import EmberObject, { get, set, computed, observer as emberObserver } from '@ember/object'; -import { A as emberA } from '@ember/array'; -import { InternalEmberArray as EmberArray } from '@ember/array/index-internal'; -import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; +import EmberArray, { A as emberA } from '@ember/array'; +import { + expectDeprecation, + moduleFor, + AbstractTestCase, + runLoopSettled, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../../deprecations'; + +const MIXIN = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_MIXIN; + +function expectMixinDeprecation() { + expectDeprecation(/The `EmberArray` mixin is deprecated/, MIXIN.isEnabled); +} /* Implement a basic fake mutable array. This validates that any non-native @@ -19,6 +31,7 @@ const TestArray = class extends EmberObject.extend(EmberArray) { _content = null; init() { + super.init(...arguments); this._content = this._content || []; } @@ -50,7 +63,11 @@ const TestArray = class extends EmberObject.extend(EmberArray) { moduleFor( 'Ember.Array', class extends AbstractTestCase { - ['@test the return value of slice has Ember.Array applied'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test the return value of slice has Ember.Array applied`]( + assert + ) { + expectMixinDeprecation(); + let x = EmberObject.extend(EmberArray).create({ length: 0, }); @@ -58,7 +75,9 @@ moduleFor( assert.equal(EmberArray.detect(y), true, 'mixin should be applied'); } - ['@test slice supports negative index arguments'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test slice supports negative index arguments`](assert) { + expectMixinDeprecation(); + let testArray = TestArray.create({ _content: [1, 2, 3, 4] }); assert.deepEqual(testArray.slice(-2), [3, 4], 'slice(-2)'); @@ -97,7 +116,9 @@ let obj, observer; moduleFor( 'mixins/array/arrayContent[Will|Did]Change', class extends AbstractTestCase { - async ['@test should notify observers of []'](assert) { + async [`${testUnless(MIXIN.isRemoved)} @test should notify observers of []`](assert) { + expectMixinDeprecation(); + obj = DummyArray.extend({ enumerablePropertyDidChange: emberObserver('[]', function () { this._count++; @@ -130,6 +151,8 @@ moduleFor( 'notify observers of length', class extends AbstractTestCase { beforeEach(assert) { + expectMixinDeprecation(); + obj = DummyArray.extend({ lengthDidChange: emberObserver('length', function () { this._after++; @@ -146,7 +169,9 @@ moduleFor( obj = undefined; } - async ['@test should notify observers when call with no params'](assert) { + async [`${testUnless(MIXIN.isRemoved)} @test should notify observers when call with no params`]( + assert + ) { arrayContentWillChange(obj); await runLoopSettled(); @@ -159,7 +184,9 @@ moduleFor( } // API variation that included items only - async ['@test should not notify when passed lengths are same'](assert) { + async [`${testUnless(MIXIN.isRemoved)} @test should not notify when passed lengths are same`]( + assert + ) { arrayContentWillChange(obj, 0, 1, 1); await runLoopSettled(); @@ -171,7 +198,9 @@ moduleFor( assert.equal(obj._after, 0); } - async ['@test should notify when passed lengths are different'](assert) { + async [`${testUnless(MIXIN.isRemoved)} @test should notify when passed lengths are different`]( + assert + ) { arrayContentWillChange(obj, 0, 1, 2); await runLoopSettled(); @@ -193,6 +222,8 @@ moduleFor( 'notify array observers (internal)', class extends AbstractTestCase { beforeEach(assert) { + expectMixinDeprecation(); + obj = DummyArray.create(); observer = class extends EmberObject { @@ -220,7 +251,9 @@ moduleFor( obj = observer = null; } - ['@test should notify array observers when called with no params'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test should notify array observers when called with no params`]( + assert + ) { arrayContentWillChange(obj); assert.deepEqual(observer._before, [obj, 0, -1, -1]); @@ -229,7 +262,9 @@ moduleFor( } // API variation that included items only - ['@test should notify when called with same length items'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test should notify when called with same length items`]( + assert + ) { arrayContentWillChange(obj, 0, 1, 1); assert.deepEqual(observer._before, [obj, 0, 1, 1]); @@ -237,7 +272,9 @@ moduleFor( assert.deepEqual(observer._after, [obj, 0, 1, 1]); } - ['@test should notify when called with diff length items'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test should notify when called with diff length items`]( + assert + ) { arrayContentWillChange(obj, 0, 2, 1); assert.deepEqual(observer._before, [obj, 0, 2, 1]); @@ -245,7 +282,7 @@ moduleFor( assert.deepEqual(observer._after, [obj, 0, 2, 1]); } - ['@test removing array observer should disable'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test removing array observer should disable`](assert) { removeArrayObserver(obj, observer, { willChange: 'arrayWillChange', didChange: 'arrayDidChange', @@ -269,6 +306,8 @@ moduleFor( 'EmberArray.@each support', class extends AbstractTestCase { beforeEach() { + expectMixinDeprecation(); + ary = TestArray.create({ _content: [ { isDone: true, desc: 'Todo 1' }, @@ -284,7 +323,9 @@ moduleFor( ary = null; } - async ['@test adding an object should notify (@each.isDone)'](assert) { + async [`${testUnless(MIXIN.isRemoved)} @test adding an object should notify (@each.isDone)`]( + assert + ) { let called = 0; let observerObject = EmberObject.create({ @@ -306,7 +347,9 @@ moduleFor( assert.equal(called, 1, 'calls observer when object is pushed'); } - async ['@test using @each to observe arrays that does not return objects raise error'](assert) { + async [`${testUnless(MIXIN.isRemoved)} @test using @each to observe arrays that does not return objects raise error`]( + assert + ) { let called = 0; let observerObject = EmberObject.create({ @@ -333,7 +376,12 @@ moduleFor( await runLoopSettled(); assert.equal(called, 0, 'not calls observer when object is pushed'); } + } +); +moduleFor( + '@each support', + class extends AbstractTestCase { ['@test `objectAt` returns correct object'](assert) { let arr = ['first', 'second', 'third', 'fourth']; assert.equal(objectAt(arr, 2), 'third'); diff --git a/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js b/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js index 445e4eceedc..a8f3b97eec9 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js @@ -1,15 +1,20 @@ import { getOwner } from '@ember/-internals/owner'; import { Container, Registry } from '@ember/-internals/container'; -import ContainerProxy from '../../lib/mixins/container_proxy-internal'; +import ContainerProxy from '../../lib/mixins/container_proxy'; import EmberObject from '@ember/object'; import { run, schedule } from '@ember/runloop'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { expectDeprecation, moduleFor, AbstractTestCase, testUnless } from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../../deprecations'; import { destroy } from '@glimmer/destroyable'; +const MIXIN = DEPRECATIONS.DEPRECATE_CONTAINER_PROXY_MIXIN; + moduleFor( '@ember/-internals/runtime/mixins/container_proxy', class extends AbstractTestCase { beforeEach() { + expectDeprecation(/The `ContainerProxyMixin` mixin is deprecated/, MIXIN.isEnabled); + this.Owner = EmberObject.extend(ContainerProxy); this.instance = this.Owner.create(); @@ -20,14 +25,14 @@ moduleFor( }); } - ['@test provides ownerInjection helper method'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test provides ownerInjection helper method`](assert) { let result = this.instance.ownerInjection(); assert.equal(getOwner(result), this.instance, 'returns an object with an associated owner'); } - ['@test actions queue completes before destruction'](assert) { - assert.expect(1); + [`${testUnless(MIXIN.isRemoved)} @test actions queue completes before destruction`](assert) { + assert.expect(2); this.registry.register( 'service:auth', @@ -46,10 +51,12 @@ moduleFor( }); } - '@test being destroyed by @ember/destroyable properly destroys the container and created instances'( + [`${testUnless( + MIXIN.isRemoved + )} @test being destroyed by @ember/destroyable properly destroys the container and created instances`]( assert ) { - assert.expect(1); + assert.expect(2); this.registry.register( 'service:foo', diff --git a/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js b/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js index e2b205a93db..962635b1f05 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js @@ -1,6 +1,6 @@ import { context } from '@ember/-internals/environment'; import EmberObject from '@ember/object'; -import TargetActionSupport from '../../lib/mixins/target_action_support-internal'; +import TargetActionSupport from '../../lib/mixins/target_action_support'; import { expectDeprecation, moduleFor, AbstractTestCase, testUnless } from 'internal-test-helpers'; import { DEPRECATIONS } from '../../../deprecations'; @@ -12,6 +12,11 @@ moduleFor( class extends AbstractTestCase { beforeEach() { context.lookup = lookup = {}; + + expectDeprecation( + /The `TargetActionSupport` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled + ); } afterEach() { @@ -21,7 +26,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should return false if no target or action are specified`](assert) { - assert.expect(2); + assert.expect(3); expectDeprecation( /Calling `triggerAction` on/, @@ -36,7 +41,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should support actions specified as strings`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -59,7 +64,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should invoke the send() method on objects that implement it`](assert) { - assert.expect(4); + assert.expect(5); expectDeprecation( /Calling `triggerAction` on/, @@ -83,7 +88,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should find targets specified using a property path`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -110,7 +115,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use an actionContext object specified as a property on the object`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -136,7 +141,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should find an actionContext specified as a property path`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -163,7 +168,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use the target specified in the argument`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -188,7 +193,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use the action specified in the argument`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -212,7 +217,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use the actionContext specified in the argument`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -238,7 +243,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should allow multiple arguments from actionContext`](assert) { - assert.expect(4); + assert.expect(5); expectDeprecation( /Calling `triggerAction` on/, @@ -272,7 +277,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use a null value specified in the actionContext argument`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, diff --git a/packages/ember/tests/service_injection_test.js b/packages/ember/tests/service_injection_test.js index abcd39580fd..f30de7e8507 100644 --- a/packages/ember/tests/service_injection_test.js +++ b/packages/ember/tests/service_injection_test.js @@ -1,8 +1,14 @@ import { getOwner } from '@ember/-internals/owner'; import Controller from '@ember/controller'; import Service, { service } from '@ember/service'; -import _ProxyMixin from '@ember/-internals/runtime/lib/mixins/-proxy-internal'; -import { moduleFor, ApplicationTestCase } from 'internal-test-helpers'; +import { _ProxyMixin } from '@ember/-internals/runtime'; +import { + expectDeprecation, + moduleFor, + ApplicationTestCase, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; import { computed } from '@ember/object'; import { precompileTemplate } from '@ember/template-compilation'; @@ -27,7 +33,14 @@ moduleFor( assert.ok(controller.get('myService') instanceof MyService); } - async ['@test Service can be an object proxy and access owner in init GH#16484'](assert) { + async [`${testUnless( + DEPRECATIONS.DEPRECATE_PROXY_MIXIN.isRemoved + )} @test Service can be an object proxy and access owner in init GH#16484`](assert) { + expectDeprecation( + /The `ProxyMixin` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_PROXY_MIXIN.isEnabled + ); + let serviceOwner; this.add( From 8438f3e1aa7760e1dc8e806ca1a5e47d38d1c3fa Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:37:43 -0400 Subject: [PATCH 05/19] Fix the assertion count in the TargetActionSupport tests A production build runs one assertion for all deprecation expectations of a test. A development build runs one assertion for each expectation. Two expectations made `assert.expect` fail in the production build jobs. Each test now has one expectation. Its pattern matches the mixin deprecation and the `triggerAction` deprecation. The `assert.expect` counts are back to their values on the base branch. Co-Authored-By: Claude Opus 5.5 --- .../mixins/target_action_support_test.js | 86 ++++++------------- 1 file changed, 26 insertions(+), 60 deletions(-) diff --git a/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js b/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js index 962635b1f05..d1ea1cf96a0 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js @@ -4,6 +4,10 @@ import TargetActionSupport from '../../lib/mixins/target_action_support'; import { expectDeprecation, moduleFor, AbstractTestCase, testUnless } from 'internal-test-helpers'; import { DEPRECATIONS } from '../../../deprecations'; +// A production build runs one assertion for all deprecation expectations. +// One pattern keeps the assertion count equal in both builds. +const DEPRECATION = /The `TargetActionSupport` mixin is deprecated|Calling `triggerAction` on/; + let originalLookup = context.lookup; let lookup; @@ -12,11 +16,6 @@ moduleFor( class extends AbstractTestCase { beforeEach() { context.lookup = lookup = {}; - - expectDeprecation( - /The `TargetActionSupport` mixin is deprecated/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); } afterEach() { @@ -26,12 +25,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should return false if no target or action are specified`](assert) { - assert.expect(3); + assert.expect(2); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let obj = EmberObject.extend(TargetActionSupport).create(); @@ -41,12 +37,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should support actions specified as strings`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let obj = EmberObject.extend(TargetActionSupport).create({ target: EmberObject.create({ @@ -64,12 +57,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should invoke the send() method on objects that implement it`](assert) { - assert.expect(5); + assert.expect(4); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let obj = EmberObject.extend(TargetActionSupport).create({ target: EmberObject.create({ @@ -88,12 +78,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should find targets specified using a property path`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let Test = {}; lookup.Test = Test; @@ -115,12 +102,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use an actionContext object specified as a property on the object`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let obj = EmberObject.extend(TargetActionSupport).create({ action: 'anEvent', @@ -141,12 +125,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should find an actionContext specified as a property path`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let Test = {}; lookup.Test = Test; @@ -168,12 +149,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use the target specified in the argument`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let targetObj = EmberObject.create({ anEvent() { @@ -193,12 +171,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use the action specified in the argument`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let obj = EmberObject.extend(TargetActionSupport).create({ target: EmberObject.create({ @@ -217,12 +192,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use the actionContext specified in the argument`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let context = {}; let obj = EmberObject.extend(TargetActionSupport).create({ @@ -243,12 +215,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should allow multiple arguments from actionContext`](assert) { - assert.expect(5); + assert.expect(4); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let param1 = 'someParam'; let param2 = 'someOtherParam'; @@ -277,12 +246,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use a null value specified in the actionContext argument`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let obj = EmberObject.extend(TargetActionSupport).create({ target: EmberObject.create({ From 604f1d7313857d4adcb0f9ae0101a24739f7c588 Mon Sep 17 00:00:00 2001 From: Katie Gengler Date: Mon, 12 Oct 2026 12:39:05 -0400 Subject: [PATCH 06/19] Duplicate RegistryProxyMixin#register docs so they display as deprecated when on the mixin. No longer show that RegistryProxyMixin extends RegistryProxy so that the deprecated docs "win". --- .../runtime/lib/mixins/registry_proxy.ts | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts b/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts index ab2936d320a..ca83cbb4d1e 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts @@ -14,10 +14,84 @@ import InternalRegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/reg registry functionality. @class RegistryProxyMixin - @extends RegistryProxy @private @deprecated Use the owner API from `@ember/owner` instead. */ + +/* The following documentation is duplicated to properly show deprecated on + the instances on the RegistryProxyMixin + */ + +/** + Registers a factory that can be used for dependency injection (with + `inject`) or for service lookup. Each factory is registered with + a full name including two parts: `type:name`. + + A simple example: + + ```javascript + import Application from '@ember/application'; + import EmberObject from '@ember/object'; + + let App = Application.create(); + + App.Orange = EmberObject.extend(); + App.register('fruit:favorite', App.Orange); + ``` + + Ember will resolve factories from the `App` namespace automatically. + For example `App.CarsController` will be discovered and returned if + an application requests `controller:cars`. + + An example of registering a controller with a non-standard name: + + ```javascript + import Application from '@ember/application'; + import Controller from '@ember/controller'; + + let App = Application.create(); + let Session = Controller.extend(); + + App.register('controller:session', Session); + + // The Session controller can now be treated like a normal controller, + // despite its non-standard name. + App.ApplicationController = Controller.extend({ + needs: ['session'] + }); + ``` + + Registered factories are **instantiated** by having `create` + called on them. Additionally they are **singletons**, each time + they are looked up they return the same instance. + + Some examples modifying that default behavior: + + ```javascript + import Application from '@ember/application'; + import EmberObject from '@ember/object'; + + let App = Application.create(); + + App.Person = EmberObject.extend(); + App.Orange = EmberObject.extend(); + App.Email = EmberObject.extend(); + App.session = EmberObject.create(); + + App.register('model:user', App.Person, { singleton: false }); + App.register('fruit:favorite', App.Orange); + App.register('communication:main', App.Email, { singleton: false }); + App.register('session', App.session, { instantiate: false }); + ``` + + @method register + @param fullName {String} type:name (e.g., 'model:user') + @param factory {Factory|object} (e.g., App.Person) + @param options {Object} (optional) disable instantiation or singleton usage + @public + @deprecated Use the owner API from `@ember/owner` instead. + */ + interface RegistryProxyMixin extends RegistryProxy { /** @internal */ __registry__: Registry; From ab8707d9b275cbd18373813763cf8fa0fff3c296 Mon Sep 17 00:00:00 2001 From: Katie Gengler Date: Mon, 12 Oct 2026 19:17:39 -0400 Subject: [PATCH 07/19] Mark @ember/object/mixin#create as deprecated --- packages/@ember/object/mixin.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@ember/object/mixin.ts b/packages/@ember/object/mixin.ts index d9c7d50b185..e0f5dce1507 100644 --- a/packages/@ember/object/mixin.ts +++ b/packages/@ember/object/mixin.ts @@ -94,7 +94,8 @@ export default class Mixin extends InternalMixin { @method create @for @ember/object/mixin @static - @param arguments* + @param args + @deprecated @public */ static override create(...args: any[]): InstanceType { From 23778a0e2d859637010a11c6d9f3c6e70256ba7a Mon Sep 17 00:00:00 2001 From: Katie Gengler Date: Mon, 12 Oct 2026 19:18:54 -0400 Subject: [PATCH 08/19] [DOC] Mark Engine as extending RegistryProxy (rather than deprecated RegistryProxyMixin) so it displays the correct inherited non-deprecated method --- packages/@ember/engine/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/@ember/engine/index.ts b/packages/@ember/engine/index.ts index 5f244c81f01..6dc1e3282e5 100644 --- a/packages/@ember/engine/index.ts +++ b/packages/@ember/engine/index.ts @@ -51,8 +51,7 @@ export interface Initializer { Engines can spawn `EngineInstance` instances via `buildInstance()`. @class Engine - @extends Ember.Namespace - @uses RegistryProxyMixin + @extends RegistryProxy @public */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type From ffab91d4fae0dd887b223c61b9098558bc7b5d4a Mon Sep 17 00:00:00 2001 From: Katie Gengler Date: Mon, 12 Oct 2026 19:31:17 -0400 Subject: [PATCH 09/19] [DOC] Mark EngineInstance as using RegistryProxy, ContainerProxy rather than the mixins so that they properly show documentation of the public inherited methods. --- packages/@ember/engine/index.ts | 2 +- packages/@ember/engine/instance.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/@ember/engine/index.ts b/packages/@ember/engine/index.ts index 6dc1e3282e5..d8cc5a4f21a 100644 --- a/packages/@ember/engine/index.ts +++ b/packages/@ember/engine/index.ts @@ -51,7 +51,7 @@ export interface Initializer { Engines can spawn `EngineInstance` instances via `buildInstance()`. @class Engine - @extends RegistryProxy + @uses RegistryProxy @public */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type diff --git a/packages/@ember/engine/instance.ts b/packages/@ember/engine/instance.ts index 0bedeef2162..cd569861ddf 100644 --- a/packages/@ember/engine/instance.ts +++ b/packages/@ember/engine/instance.ts @@ -49,8 +49,7 @@ export interface EngineInstanceOptions { @public @class EngineInstance @extends EmberObject - @uses RegistryProxyMixin - @uses ContainerProxyMixin + @uses RegistryProxy, ContainerProxy */ // Note on types: since `EngineInstance` uses `RegistryProxyMixin` and From d771297343f95c559ce9feb91e8a85f5506feb5a Mon Sep 17 00:00:00 2001 From: Katie Gengler Date: Mon, 12 Oct 2026 19:44:57 -0400 Subject: [PATCH 10/19] [DOC] MutableEnumerable is empty and does not provide anything from a docs-standpoint --- packages/@ember/array/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@ember/array/index.ts b/packages/@ember/array/index.ts index 882e71fe473..735a2f4ce1b 100644 --- a/packages/@ember/array/index.ts +++ b/packages/@ember/array/index.ts @@ -1033,7 +1033,6 @@ const EmberArray = DeprecatedMixin.create(InternalEmberArrayMixin, { @class MutableArray @uses EmberArray - @uses MutableEnumerable @public */ interface MutableArray extends EmberArray, MutableEnumerable { From 947116266901a3e799f27abd5c5bd381c0a5982c Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:11:37 -0400 Subject: [PATCH 11/19] Delete private mixin: Enumerable --- lib/index.cjs | 2 - package.json | 2 - .../@ember/-internals/deprecations/index.ts | 14 ----- packages/@ember/-internals/package.json | 1 - packages/@ember/-internals/runtime/index.ts | 1 - .../tests/mixins/mutable_enumerable_test.js | 22 ------- .../tests/mutable-array/pushObjects-test.js | 2 +- packages/@ember/array/index-internal.ts | 6 +- packages/@ember/array/index.ts | 9 +-- packages/@ember/array/package.json | 1 - packages/@ember/debug/package.json | 1 - packages/@ember/enumerable/-internal.ts | 14 ----- packages/@ember/enumerable/index.ts | 31 --------- .../@ember/enumerable/mutable-internal.ts | 15 ----- packages/@ember/enumerable/mutable.ts | 32 ---------- packages/@ember/enumerable/package.json | 22 ------- .../enumerable/tests/enumerable_test.js | 22 ------- packages/@ember/object/package.json | 1 - packages/@ember/routing/package.json | 1 - packages/@ember/utils/package.json | 1 - packages/@ember/version/package.json | 1 - packages/ember-template-compiler/package.json | 1 - packages/ember/package.json | 1 - packages/internal-test-helpers/package.json | 1 - pnpm-lock.yaml | 63 ------------------- 25 files changed, 6 insertions(+), 261 deletions(-) delete mode 100644 packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js delete mode 100644 packages/@ember/enumerable/-internal.ts delete mode 100644 packages/@ember/enumerable/index.ts delete mode 100644 packages/@ember/enumerable/mutable-internal.ts delete mode 100644 packages/@ember/enumerable/mutable.ts delete mode 100644 packages/@ember/enumerable/package.json delete mode 100644 packages/@ember/enumerable/tests/enumerable_test.js diff --git a/lib/index.cjs b/lib/index.cjs index 1aab0ad0389..58d8db3e775 100644 --- a/lib/index.cjs +++ b/lib/index.cjs @@ -78,8 +78,6 @@ const shim = addonV1Shim(path.join(__dirname, '..'), { './dist/dev/packages/@ember/engine/index.js', './dist/dev/packages/@ember/engine/instance.js', './dist/dev/packages/@ember/engine/lib/engine-parent.js', - './dist/dev/packages/@ember/enumerable/index.js', - './dist/dev/packages/@ember/enumerable/mutable.js', './dist/dev/packages/@ember/helper/index.js', './dist/dev/packages/@ember/instrumentation/index.js', './dist/dev/packages/@ember/modifier/index.js', diff --git a/package.json b/package.json index 68a07542477..2474aeb2f52 100644 --- a/package.json +++ b/package.json @@ -273,8 +273,6 @@ "@ember/engine/lib/engine-parent.js": "ember-source/@ember/engine/lib/engine-parent.js", "@ember/engine/lib/strict-resolver.js": "ember-source/@ember/engine/lib/strict-resolver.js", "@ember/engine/parent.js": "ember-source/@ember/engine/parent.js", - "@ember/enumerable/index.js": "ember-source/@ember/enumerable/index.js", - "@ember/enumerable/mutable.js": "ember-source/@ember/enumerable/mutable.js", "@ember/helper/index.js": "ember-source/@ember/helper/index.js", "@ember/instrumentation/index.js": "ember-source/@ember/instrumentation/index.js", "@ember/modifier/index.js": "ember-source/@ember/modifier/index.js", diff --git a/packages/@ember/-internals/deprecations/index.ts b/packages/@ember/-internals/deprecations/index.ts index 51a5f91651c..80a73009664 100644 --- a/packages/@ember/-internals/deprecations/index.ts +++ b/packages/@ember/-internals/deprecations/index.ts @@ -182,20 +182,6 @@ export const DEPRECATIONS = { until: '8.0.0', url: 'https://deprecations.emberjs.com/id/deprecate-observable-mixin', }), - DEPRECATE_ENUMERABLE_MIXIN: deprecation({ - id: 'deprecate-enumerable-mixin', - for: 'ember-source', - since: { available: '7.5.0' }, - until: '7.9.0', - url: 'https://deprecations.emberjs.com/id/deprecate-enumerable-mixin', - }), - DEPRECATE_MUTABLE_ENUMERABLE_MIXIN: deprecation({ - id: 'deprecate-mutable-enumerable-mixin', - for: 'ember-source', - since: { available: '7.5.0' }, - until: '7.9.0', - url: 'https://deprecations.emberjs.com/id/deprecate-mutable-enumerable-mixin', - }), DEPRECATE_EMBER_ARRAY_MIXIN: deprecation({ id: 'deprecate-ember-array-mixin', for: 'ember-source', diff --git a/packages/@ember/-internals/package.json b/packages/@ember/-internals/package.json index 7f5f825722b..e6217f719b4 100644 --- a/packages/@ember/-internals/package.json +++ b/packages/@ember/-internals/package.json @@ -33,7 +33,6 @@ "@ember/debug": "workspace:*", "@ember/destroyable": "workspace:*", "@ember/engine": "workspace:*", - "@ember/enumerable": "workspace:*", "@ember/helper": "workspace:*", "@ember/instrumentation": "workspace:*", "@ember/modifier": "workspace:*", diff --git a/packages/@ember/-internals/runtime/index.ts b/packages/@ember/-internals/runtime/index.ts index ec028a7a2da..328605b3d52 100644 --- a/packages/@ember/-internals/runtime/index.ts +++ b/packages/@ember/-internals/runtime/index.ts @@ -3,7 +3,6 @@ export { default as ContainerProxyMixin } from './lib/mixins/container_proxy'; export { default as Comparable } from './lib/mixins/comparable'; export { default as ActionHandler } from './lib/mixins/action_handler'; export { default as _ProxyMixin, contentFor as _contentFor } from './lib/mixins/-proxy'; -export { default as MutableEnumerable } from '@ember/enumerable/mutable'; export { default as TargetActionSupport } from './lib/mixins/target_action_support'; export { default as RSVP, onerrorDefault } from './lib/ext/rsvp'; // just for side effect of extending Ember.RSVP diff --git a/packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js b/packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js deleted file mode 100644 index b08ad58d3fe..00000000000 --- a/packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js +++ /dev/null @@ -1,22 +0,0 @@ -import MutableEnumerable from '@ember/enumerable/mutable'; -import ArrayProxy from '@ember/array/proxy'; -import { A } from '@ember/array'; -import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; -import { DEPRECATIONS } from '@ember/-internals/deprecations'; - -moduleFor( - 'MutableEnumerable', - class extends AbstractTestCase { - ['@test should be mixed into A()'](assert) { - assert.ok(MutableEnumerable.detect(A())); - } - - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test should be mixed into ArrayProxy`]( - assert - ) { - expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled); - - assert.ok(MutableEnumerable.detect(ArrayProxy.create())); - } - } -); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/pushObjects-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/pushObjects-test.js index 55a67b4139e..7941647c545 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/pushObjects-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/pushObjects-test.js @@ -2,7 +2,7 @@ import { AbstractTestCase } from 'internal-test-helpers'; import { runArrayTests } from '../helpers/array'; class PushObjectsTests extends AbstractTestCase { - '@test should raise exception if not Ember.Enumerable is passed to pushObjects'() { + '@test should raise exception if not Iterable is passed to pushObjects'() { let obj = this.newObject([]); expectAssertion(() => obj.pushObjects('string')); diff --git a/packages/@ember/array/index-internal.ts b/packages/@ember/array/index-internal.ts index 2746d544132..4fc2dc1ac9b 100644 --- a/packages/@ember/array/index-internal.ts +++ b/packages/@ember/array/index-internal.ts @@ -12,8 +12,6 @@ import { get } from '@ember/-internals/metal/lib/property_get'; import { set } from '@ember/-internals/metal/lib/property_set'; import { InternalMixin } from '@ember/object/mixin-internal'; import { assert } from '@ember/debug'; -import InternalEnumerable from '@ember/enumerable/-internal'; -import InternalMutableEnumerable from '@ember/enumerable/mutable-internal'; import sortCompare from './lib/sort-compare'; import InternalObservable from '@ember/object/observable-internal'; import type { ComputedPropertyCallback } from '@ember/-internals/metal/lib/computed'; @@ -159,7 +157,7 @@ function mapBy(this: EmberArray, key: string) { @internal */ -const InternalEmberArray = InternalMixin.create(InternalEnumerable, { +const InternalEmberArray = InternalMixin.create({ init() { this._super(...arguments); setEmberArray(this); @@ -431,7 +429,7 @@ const InternalEmberArray = InternalMixin.create(InternalEnumerable, { }, }); -const InternalMutableArray = InternalMixin.create(InternalEmberArray, InternalMutableEnumerable, { +const InternalMutableArray = InternalMixin.create(InternalEmberArray, { clear() { let len = this.length; if (len === 0) { diff --git a/packages/@ember/array/index.ts b/packages/@ember/array/index.ts index 735a2f4ce1b..82bbbec8b52 100644 --- a/packages/@ember/array/index.ts +++ b/packages/@ember/array/index.ts @@ -2,8 +2,6 @@ @module @ember/array */ import { DeprecatedMixin } from '@ember/object/mixin-internal'; -import type Enumerable from '@ember/enumerable'; -import type MutableEnumerable from '@ember/enumerable/mutable'; import type Observable from '@ember/object/observable'; import type { MethodNamesOf, MethodParams, MethodReturns } from '@ember/-internals/utility-types'; import isArray from './lib/is-array'; @@ -49,11 +47,10 @@ export type EmberArrayLike = EmberArray | NativeArray; primitives to use it: `length()` and `objectAt()`. @class EmberArray - @uses Enumerable @since Ember 0.9.0 @public */ -interface EmberArray extends Enumerable { +interface EmberArray { /** __Required.__ You must implement this method to apply this mixin. @@ -1035,7 +1032,7 @@ const EmberArray = DeprecatedMixin.create(InternalEmberArrayMixin, { @uses EmberArray @public */ -interface MutableArray extends EmberArray, MutableEnumerable { +interface MutableArray extends EmberArray { /** __Required.__ You must implement this method to apply this mixin. @@ -1206,7 +1203,7 @@ interface MutableArray extends EmberArray, MutableEnumerable { ``` @method unshiftObjects - @param {Enumerable} objects the objects to add + @param {Iterable} objects the objects to add @return {EmberArray} receiver @public */ diff --git a/packages/@ember/array/package.json b/packages/@ember/array/package.json index 8585c775652..e3a78223804 100644 --- a/packages/@ember/array/package.json +++ b/packages/@ember/array/package.json @@ -14,7 +14,6 @@ "@ember/-internals": "workspace:*", "@ember/application": "workspace:*", "@ember/debug": "workspace:*", - "@ember/enumerable": "workspace:*", "@ember/object": "workspace:*", "@ember/runloop": "workspace:*", "@ember/utils": "workspace:*", diff --git a/packages/@ember/debug/package.json b/packages/@ember/debug/package.json index 0d271bb0f79..558eda637fa 100644 --- a/packages/@ember/debug/package.json +++ b/packages/@ember/debug/package.json @@ -13,7 +13,6 @@ "@ember/application": "workspace:*", "@ember/array": "workspace:*", "@ember/engine": "workspace:*", - "@ember/enumerable": "workspace:*", "@ember/object": "workspace:*", "@ember/owner": "workspace:*", "@ember/routing": "workspace:*", diff --git a/packages/@ember/enumerable/-internal.ts b/packages/@ember/enumerable/-internal.ts deleted file mode 100644 index 0a1c573248b..00000000000 --- a/packages/@ember/enumerable/-internal.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { InternalMixin } from '@ember/object/mixin-internal'; - -/** - The internal counterpart to the public `Enumerable` mixin. Ember's own - internals apply this so that they do not trigger the deprecation that the - public mixin emits. - - @internal -*/ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface InternalEnumerable {} -const InternalEnumerable = InternalMixin.create(); - -export default InternalEnumerable; diff --git a/packages/@ember/enumerable/index.ts b/packages/@ember/enumerable/index.ts deleted file mode 100644 index d3e5c2f3a30..00000000000 --- a/packages/@ember/enumerable/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { DeprecatedMixin } from '@ember/object/mixin-internal'; -import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; -import InternalEnumerable from '@ember/enumerable/-internal'; - -/** -@module @ember/enumerable -@private -*/ - -/** - The methods in this mixin have been moved to [MutableArray](/ember/release/classes/MutableArray). This mixin has - been intentionally preserved to avoid breaking Enumerable.detect checks - until the community migrates away from them. - - @class Enumerable - @private - @deprecated Use native arrays and array methods instead. -*/ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface Enumerable extends InternalEnumerable {} -const Enumerable = DeprecatedMixin.create(InternalEnumerable, { - init() { - this._super(...arguments); - deprecateUntil( - 'The `Enumerable` mixin is deprecated. Use native arrays and array methods instead.', - DEPRECATIONS.DEPRECATE_ENUMERABLE_MIXIN - ); - }, -}); - -export default Enumerable; diff --git a/packages/@ember/enumerable/mutable-internal.ts b/packages/@ember/enumerable/mutable-internal.ts deleted file mode 100644 index 96631833036..00000000000 --- a/packages/@ember/enumerable/mutable-internal.ts +++ /dev/null @@ -1,15 +0,0 @@ -import InternalEnumerable from '@ember/enumerable/-internal'; -import { InternalMixin } from '@ember/object/mixin-internal'; - -/** - The internal counterpart to the public `MutableEnumerable` mixin. Ember's own - internals apply this so that they do not trigger the deprecation that the - public mixin emits. - - @internal -*/ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface InternalMutableEnumerable extends InternalEnumerable {} -const InternalMutableEnumerable = InternalMixin.create(InternalEnumerable); - -export default InternalMutableEnumerable; diff --git a/packages/@ember/enumerable/mutable.ts b/packages/@ember/enumerable/mutable.ts deleted file mode 100644 index 38acfe00ff6..00000000000 --- a/packages/@ember/enumerable/mutable.ts +++ /dev/null @@ -1,32 +0,0 @@ -import InternalMutableEnumerable from '@ember/enumerable/mutable-internal'; -import { DeprecatedMixin } from '@ember/object/mixin-internal'; -import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; - -/** -@module ember -*/ - -/** - The methods in this mixin have been moved to MutableArray. This mixin has - been intentionally preserved to avoid breaking MutableEnumerable.detect - checks until the community migrates away from them. - - @class MutableEnumerable - @namespace Ember - @uses Enumerable - @private - @deprecated Use native arrays and array methods instead. -*/ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface MutableEnumerable extends InternalMutableEnumerable {} -const MutableEnumerable = DeprecatedMixin.create(InternalMutableEnumerable, { - init() { - this._super(...arguments); - deprecateUntil( - 'The `MutableEnumerable` mixin is deprecated. Use native arrays and array methods instead.', - DEPRECATIONS.DEPRECATE_MUTABLE_ENUMERABLE_MIXIN - ); - }, -}); - -export default MutableEnumerable; diff --git a/packages/@ember/enumerable/package.json b/packages/@ember/enumerable/package.json deleted file mode 100644 index a12c717454d..00000000000 --- a/packages/@ember/enumerable/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@ember/enumerable", - "private": true, - "type": "module", - "exports": { - ".": "./index.ts", - "./mutable": "./mutable.ts", - "./*": "./*.ts" - }, - "dependencies": { - "@ember/-internals": "workspace:*", - "@ember/array": "workspace:*", - "@ember/debug": "workspace:*", - "@ember/object": "workspace:*", - "@glimmer/destroyable": "workspace:*", - "@glimmer/env": "workspace:*", - "@glimmer/owner": "workspace:*", - "@glimmer/util": "workspace:*", - "@glimmer/validator": "workspace:*", - "internal-test-helpers": "workspace:*" - } -} diff --git a/packages/@ember/enumerable/tests/enumerable_test.js b/packages/@ember/enumerable/tests/enumerable_test.js deleted file mode 100644 index ee7e07e0139..00000000000 --- a/packages/@ember/enumerable/tests/enumerable_test.js +++ /dev/null @@ -1,22 +0,0 @@ -import Enumerable from '@ember/enumerable'; -import ArrayProxy from '@ember/array/proxy'; -import { A } from '@ember/array'; -import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; -import { DEPRECATIONS } from '@ember/-internals/deprecations'; - -moduleFor( - 'Enumerable', - class extends AbstractTestCase { - ['@test should be mixed into A()'](assert) { - assert.ok(Enumerable.detect(A())); - } - - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test should be mixed into ArrayProxy`]( - assert - ) { - expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled); - - assert.ok(Enumerable.detect(ArrayProxy.create())); - } - } -); diff --git a/packages/@ember/object/package.json b/packages/@ember/object/package.json index e0c4ce1ff36..1fdba733b9f 100644 --- a/packages/@ember/object/package.json +++ b/packages/@ember/object/package.json @@ -23,7 +23,6 @@ "@ember/application": "workspace:*", "@ember/array": "workspace:*", "@ember/debug": "workspace:*", - "@ember/enumerable": "workspace:*", "@ember/runloop": "workspace:*", "@ember/service": "workspace:*", "@ember/utils": "workspace:*", diff --git a/packages/@ember/routing/package.json b/packages/@ember/routing/package.json index 5a956191f63..b9c862884e7 100644 --- a/packages/@ember/routing/package.json +++ b/packages/@ember/routing/package.json @@ -21,7 +21,6 @@ "@ember/controller": "workspace:*", "@ember/debug": "workspace:*", "@ember/engine": "workspace:*", - "@ember/enumerable": "workspace:*", "@ember/object": "workspace:*", "@ember/owner": "workspace:*", "@ember/runloop": "workspace:*", diff --git a/packages/@ember/utils/package.json b/packages/@ember/utils/package.json index a26dab58aae..335153ebd97 100644 --- a/packages/@ember/utils/package.json +++ b/packages/@ember/utils/package.json @@ -11,7 +11,6 @@ "@ember/application": "workspace:*", "@ember/array": "workspace:*", "@ember/debug": "workspace:*", - "@ember/enumerable": "workspace:*", "@ember/object": "workspace:*", "@ember/runloop": "workspace:*", "@glimmer/destroyable": "workspace:*", diff --git a/packages/@ember/version/package.json b/packages/@ember/version/package.json index f61f55d3498..5245b961648 100644 --- a/packages/@ember/version/package.json +++ b/packages/@ember/version/package.json @@ -16,7 +16,6 @@ "@ember/debug": "workspace:*", "@ember/destroyable": "workspace:*", "@ember/engine": "workspace:*", - "@ember/enumerable": "workspace:*", "@ember/instrumentation": "workspace:*", "@ember/object": "workspace:*", "@ember/routing": "workspace:*", diff --git a/packages/ember-template-compiler/package.json b/packages/ember-template-compiler/package.json index 6bb122fbb56..d9721cc4610 100644 --- a/packages/ember-template-compiler/package.json +++ b/packages/ember-template-compiler/package.json @@ -17,7 +17,6 @@ "@ember/debug": "workspace:*", "@ember/destroyable": "workspace:*", "@ember/engine": "workspace:*", - "@ember/enumerable": "workspace:*", "@ember/instrumentation": "workspace:*", "@ember/object": "workspace:*", "@ember/routing": "workspace:*", diff --git a/packages/ember/package.json b/packages/ember/package.json index 03a84ccd1e7..e062a49a15a 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -15,7 +15,6 @@ "@ember/debug": "workspace:*", "@ember/destroyable": "workspace:*", "@ember/engine": "workspace:*", - "@ember/enumerable": "workspace:*", "@ember/helper": "workspace:*", "@ember/instrumentation": "workspace:*", "@ember/modifier": "workspace:*", diff --git a/packages/internal-test-helpers/package.json b/packages/internal-test-helpers/package.json index dff92d10e78..bff7e231fd4 100644 --- a/packages/internal-test-helpers/package.json +++ b/packages/internal-test-helpers/package.json @@ -17,7 +17,6 @@ "@ember/debug": "workspace:*", "@ember/destroyable": "workspace:*", "@ember/engine": "workspace:*", - "@ember/enumerable": "workspace:*", "@ember/instrumentation": "workspace:*", "@ember/object": "workspace:*", "@ember/owner": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3284a41422..762b657b46a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -298,9 +298,6 @@ importers: '@ember/engine': specifier: workspace:* version: link:../engine - '@ember/enumerable': - specifier: workspace:* - version: link:../enumerable '@ember/helper': specifier: workspace:* version: link:../helper @@ -509,9 +506,6 @@ importers: '@ember/debug': specifier: workspace:* version: link:../debug - '@ember/enumerable': - specifier: workspace:* - version: link:../enumerable '@ember/object': specifier: workspace:* version: link:../object @@ -643,9 +637,6 @@ importers: '@ember/engine': specifier: workspace:* version: link:../engine - '@ember/enumerable': - specifier: workspace:* - version: link:../enumerable '@ember/object': specifier: workspace:* version: link:../object @@ -766,39 +757,6 @@ importers: specifier: workspace:* version: link:../../router_js - packages/@ember/enumerable: - dependencies: - '@ember/-internals': - specifier: workspace:* - version: link:../-internals - '@ember/array': - specifier: workspace:* - version: link:../array - '@ember/debug': - specifier: workspace:* - version: link:../debug - '@ember/object': - specifier: workspace:* - version: link:../object - '@glimmer/destroyable': - specifier: workspace:* - version: link:../../@glimmer/destroyable - '@glimmer/env': - specifier: workspace:* - version: link:../../@glimmer/env - '@glimmer/owner': - specifier: workspace:* - version: link:../../@glimmer/owner - '@glimmer/util': - specifier: workspace:* - version: link:../../@glimmer/util - '@glimmer/validator': - specifier: workspace:* - version: link:../../@glimmer/validator - internal-test-helpers: - specifier: workspace:* - version: link:../../internal-test-helpers - packages/@ember/helper: dependencies: '@ember/-internals': @@ -888,9 +846,6 @@ importers: '@ember/debug': specifier: workspace:* version: link:../debug - '@ember/enumerable': - specifier: workspace:* - version: link:../enumerable '@ember/runloop': specifier: workspace:* version: link:../runloop @@ -997,9 +952,6 @@ importers: '@ember/engine': specifier: workspace:* version: link:../engine - '@ember/enumerable': - specifier: workspace:* - version: link:../enumerable '@ember/object': specifier: workspace:* version: link:../object @@ -1236,9 +1188,6 @@ importers: '@ember/debug': specifier: workspace:* version: link:../debug - '@ember/enumerable': - specifier: workspace:* - version: link:../enumerable '@ember/object': specifier: workspace:* version: link:../object @@ -1299,9 +1248,6 @@ importers: '@ember/engine': specifier: workspace:* version: link:../engine - '@ember/enumerable': - specifier: workspace:* - version: link:../enumerable '@ember/instrumentation': specifier: workspace:* version: link:../instrumentation @@ -2432,9 +2378,6 @@ importers: '@ember/engine': specifier: workspace:* version: link:../@ember/engine - '@ember/enumerable': - specifier: workspace:* - version: link:../@ember/enumerable '@ember/helper': specifier: workspace:* version: link:../@ember/helper @@ -2558,9 +2501,6 @@ importers: '@ember/engine': specifier: workspace:* version: link:../@ember/engine - '@ember/enumerable': - specifier: workspace:* - version: link:../@ember/enumerable '@ember/instrumentation': specifier: workspace:* version: link:../@ember/instrumentation @@ -2714,9 +2654,6 @@ importers: '@ember/engine': specifier: workspace:* version: link:../@ember/engine - '@ember/enumerable': - specifier: workspace:* - version: link:../@ember/enumerable '@ember/instrumentation': specifier: workspace:* version: link:../@ember/instrumentation From 40796a5ba4f937312244a6f9040ebd7c9d207e23 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:16:47 -0400 Subject: [PATCH 12/19] Revert "Delete private mixin: Enumerable" because @embroider/legacy-inspector-support is importing private APIs This reverts commit 947116266901a3e799f27abd5c5bd381c0a5982c. --- lib/index.cjs | 2 + package.json | 2 + .../@ember/-internals/deprecations/index.ts | 14 +++++ packages/@ember/-internals/package.json | 1 + packages/@ember/-internals/runtime/index.ts | 1 + .../tests/mixins/mutable_enumerable_test.js | 22 +++++++ .../tests/mutable-array/pushObjects-test.js | 2 +- packages/@ember/array/index-internal.ts | 6 +- packages/@ember/array/index.ts | 9 ++- packages/@ember/array/package.json | 1 + packages/@ember/debug/package.json | 1 + packages/@ember/enumerable/-internal.ts | 14 +++++ packages/@ember/enumerable/index.ts | 31 +++++++++ .../@ember/enumerable/mutable-internal.ts | 15 +++++ packages/@ember/enumerable/mutable.ts | 32 ++++++++++ packages/@ember/enumerable/package.json | 22 +++++++ .../enumerable/tests/enumerable_test.js | 22 +++++++ packages/@ember/object/package.json | 1 + packages/@ember/routing/package.json | 1 + packages/@ember/utils/package.json | 1 + packages/@ember/version/package.json | 1 + packages/ember-template-compiler/package.json | 1 + packages/ember/package.json | 1 + packages/internal-test-helpers/package.json | 1 + pnpm-lock.yaml | 63 +++++++++++++++++++ 25 files changed, 261 insertions(+), 6 deletions(-) create mode 100644 packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js create mode 100644 packages/@ember/enumerable/-internal.ts create mode 100644 packages/@ember/enumerable/index.ts create mode 100644 packages/@ember/enumerable/mutable-internal.ts create mode 100644 packages/@ember/enumerable/mutable.ts create mode 100644 packages/@ember/enumerable/package.json create mode 100644 packages/@ember/enumerable/tests/enumerable_test.js diff --git a/lib/index.cjs b/lib/index.cjs index 58d8db3e775..1aab0ad0389 100644 --- a/lib/index.cjs +++ b/lib/index.cjs @@ -78,6 +78,8 @@ const shim = addonV1Shim(path.join(__dirname, '..'), { './dist/dev/packages/@ember/engine/index.js', './dist/dev/packages/@ember/engine/instance.js', './dist/dev/packages/@ember/engine/lib/engine-parent.js', + './dist/dev/packages/@ember/enumerable/index.js', + './dist/dev/packages/@ember/enumerable/mutable.js', './dist/dev/packages/@ember/helper/index.js', './dist/dev/packages/@ember/instrumentation/index.js', './dist/dev/packages/@ember/modifier/index.js', diff --git a/package.json b/package.json index 2474aeb2f52..68a07542477 100644 --- a/package.json +++ b/package.json @@ -273,6 +273,8 @@ "@ember/engine/lib/engine-parent.js": "ember-source/@ember/engine/lib/engine-parent.js", "@ember/engine/lib/strict-resolver.js": "ember-source/@ember/engine/lib/strict-resolver.js", "@ember/engine/parent.js": "ember-source/@ember/engine/parent.js", + "@ember/enumerable/index.js": "ember-source/@ember/enumerable/index.js", + "@ember/enumerable/mutable.js": "ember-source/@ember/enumerable/mutable.js", "@ember/helper/index.js": "ember-source/@ember/helper/index.js", "@ember/instrumentation/index.js": "ember-source/@ember/instrumentation/index.js", "@ember/modifier/index.js": "ember-source/@ember/modifier/index.js", diff --git a/packages/@ember/-internals/deprecations/index.ts b/packages/@ember/-internals/deprecations/index.ts index 80a73009664..51a5f91651c 100644 --- a/packages/@ember/-internals/deprecations/index.ts +++ b/packages/@ember/-internals/deprecations/index.ts @@ -182,6 +182,20 @@ export const DEPRECATIONS = { until: '8.0.0', url: 'https://deprecations.emberjs.com/id/deprecate-observable-mixin', }), + DEPRECATE_ENUMERABLE_MIXIN: deprecation({ + id: 'deprecate-enumerable-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-enumerable-mixin', + }), + DEPRECATE_MUTABLE_ENUMERABLE_MIXIN: deprecation({ + id: 'deprecate-mutable-enumerable-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-mutable-enumerable-mixin', + }), DEPRECATE_EMBER_ARRAY_MIXIN: deprecation({ id: 'deprecate-ember-array-mixin', for: 'ember-source', diff --git a/packages/@ember/-internals/package.json b/packages/@ember/-internals/package.json index e6217f719b4..7f5f825722b 100644 --- a/packages/@ember/-internals/package.json +++ b/packages/@ember/-internals/package.json @@ -33,6 +33,7 @@ "@ember/debug": "workspace:*", "@ember/destroyable": "workspace:*", "@ember/engine": "workspace:*", + "@ember/enumerable": "workspace:*", "@ember/helper": "workspace:*", "@ember/instrumentation": "workspace:*", "@ember/modifier": "workspace:*", diff --git a/packages/@ember/-internals/runtime/index.ts b/packages/@ember/-internals/runtime/index.ts index 328605b3d52..ec028a7a2da 100644 --- a/packages/@ember/-internals/runtime/index.ts +++ b/packages/@ember/-internals/runtime/index.ts @@ -3,6 +3,7 @@ export { default as ContainerProxyMixin } from './lib/mixins/container_proxy'; export { default as Comparable } from './lib/mixins/comparable'; export { default as ActionHandler } from './lib/mixins/action_handler'; export { default as _ProxyMixin, contentFor as _contentFor } from './lib/mixins/-proxy'; +export { default as MutableEnumerable } from '@ember/enumerable/mutable'; export { default as TargetActionSupport } from './lib/mixins/target_action_support'; export { default as RSVP, onerrorDefault } from './lib/ext/rsvp'; // just for side effect of extending Ember.RSVP diff --git a/packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js b/packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js new file mode 100644 index 00000000000..b08ad58d3fe --- /dev/null +++ b/packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js @@ -0,0 +1,22 @@ +import MutableEnumerable from '@ember/enumerable/mutable'; +import ArrayProxy from '@ember/array/proxy'; +import { A } from '@ember/array'; +import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; + +moduleFor( + 'MutableEnumerable', + class extends AbstractTestCase { + ['@test should be mixed into A()'](assert) { + assert.ok(MutableEnumerable.detect(A())); + } + + [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test should be mixed into ArrayProxy`]( + assert + ) { + expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled); + + assert.ok(MutableEnumerable.detect(ArrayProxy.create())); + } + } +); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/pushObjects-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/pushObjects-test.js index 7941647c545..55a67b4139e 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/pushObjects-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/pushObjects-test.js @@ -2,7 +2,7 @@ import { AbstractTestCase } from 'internal-test-helpers'; import { runArrayTests } from '../helpers/array'; class PushObjectsTests extends AbstractTestCase { - '@test should raise exception if not Iterable is passed to pushObjects'() { + '@test should raise exception if not Ember.Enumerable is passed to pushObjects'() { let obj = this.newObject([]); expectAssertion(() => obj.pushObjects('string')); diff --git a/packages/@ember/array/index-internal.ts b/packages/@ember/array/index-internal.ts index 4fc2dc1ac9b..2746d544132 100644 --- a/packages/@ember/array/index-internal.ts +++ b/packages/@ember/array/index-internal.ts @@ -12,6 +12,8 @@ import { get } from '@ember/-internals/metal/lib/property_get'; import { set } from '@ember/-internals/metal/lib/property_set'; import { InternalMixin } from '@ember/object/mixin-internal'; import { assert } from '@ember/debug'; +import InternalEnumerable from '@ember/enumerable/-internal'; +import InternalMutableEnumerable from '@ember/enumerable/mutable-internal'; import sortCompare from './lib/sort-compare'; import InternalObservable from '@ember/object/observable-internal'; import type { ComputedPropertyCallback } from '@ember/-internals/metal/lib/computed'; @@ -157,7 +159,7 @@ function mapBy(this: EmberArray, key: string) { @internal */ -const InternalEmberArray = InternalMixin.create({ +const InternalEmberArray = InternalMixin.create(InternalEnumerable, { init() { this._super(...arguments); setEmberArray(this); @@ -429,7 +431,7 @@ const InternalEmberArray = InternalMixin.create({ }, }); -const InternalMutableArray = InternalMixin.create(InternalEmberArray, { +const InternalMutableArray = InternalMixin.create(InternalEmberArray, InternalMutableEnumerable, { clear() { let len = this.length; if (len === 0) { diff --git a/packages/@ember/array/index.ts b/packages/@ember/array/index.ts index 82bbbec8b52..735a2f4ce1b 100644 --- a/packages/@ember/array/index.ts +++ b/packages/@ember/array/index.ts @@ -2,6 +2,8 @@ @module @ember/array */ import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import type Enumerable from '@ember/enumerable'; +import type MutableEnumerable from '@ember/enumerable/mutable'; import type Observable from '@ember/object/observable'; import type { MethodNamesOf, MethodParams, MethodReturns } from '@ember/-internals/utility-types'; import isArray from './lib/is-array'; @@ -47,10 +49,11 @@ export type EmberArrayLike = EmberArray | NativeArray; primitives to use it: `length()` and `objectAt()`. @class EmberArray + @uses Enumerable @since Ember 0.9.0 @public */ -interface EmberArray { +interface EmberArray extends Enumerable { /** __Required.__ You must implement this method to apply this mixin. @@ -1032,7 +1035,7 @@ const EmberArray = DeprecatedMixin.create(InternalEmberArrayMixin, { @uses EmberArray @public */ -interface MutableArray extends EmberArray { +interface MutableArray extends EmberArray, MutableEnumerable { /** __Required.__ You must implement this method to apply this mixin. @@ -1203,7 +1206,7 @@ interface MutableArray extends EmberArray { ``` @method unshiftObjects - @param {Iterable} objects the objects to add + @param {Enumerable} objects the objects to add @return {EmberArray} receiver @public */ diff --git a/packages/@ember/array/package.json b/packages/@ember/array/package.json index e3a78223804..8585c775652 100644 --- a/packages/@ember/array/package.json +++ b/packages/@ember/array/package.json @@ -14,6 +14,7 @@ "@ember/-internals": "workspace:*", "@ember/application": "workspace:*", "@ember/debug": "workspace:*", + "@ember/enumerable": "workspace:*", "@ember/object": "workspace:*", "@ember/runloop": "workspace:*", "@ember/utils": "workspace:*", diff --git a/packages/@ember/debug/package.json b/packages/@ember/debug/package.json index 558eda637fa..0d271bb0f79 100644 --- a/packages/@ember/debug/package.json +++ b/packages/@ember/debug/package.json @@ -13,6 +13,7 @@ "@ember/application": "workspace:*", "@ember/array": "workspace:*", "@ember/engine": "workspace:*", + "@ember/enumerable": "workspace:*", "@ember/object": "workspace:*", "@ember/owner": "workspace:*", "@ember/routing": "workspace:*", diff --git a/packages/@ember/enumerable/-internal.ts b/packages/@ember/enumerable/-internal.ts new file mode 100644 index 00000000000..0a1c573248b --- /dev/null +++ b/packages/@ember/enumerable/-internal.ts @@ -0,0 +1,14 @@ +import { InternalMixin } from '@ember/object/mixin-internal'; + +/** + The internal counterpart to the public `Enumerable` mixin. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. + + @internal +*/ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface InternalEnumerable {} +const InternalEnumerable = InternalMixin.create(); + +export default InternalEnumerable; diff --git a/packages/@ember/enumerable/index.ts b/packages/@ember/enumerable/index.ts new file mode 100644 index 00000000000..d3e5c2f3a30 --- /dev/null +++ b/packages/@ember/enumerable/index.ts @@ -0,0 +1,31 @@ +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalEnumerable from '@ember/enumerable/-internal'; + +/** +@module @ember/enumerable +@private +*/ + +/** + The methods in this mixin have been moved to [MutableArray](/ember/release/classes/MutableArray). This mixin has + been intentionally preserved to avoid breaking Enumerable.detect checks + until the community migrates away from them. + + @class Enumerable + @private + @deprecated Use native arrays and array methods instead. +*/ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface Enumerable extends InternalEnumerable {} +const Enumerable = DeprecatedMixin.create(InternalEnumerable, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `Enumerable` mixin is deprecated. Use native arrays and array methods instead.', + DEPRECATIONS.DEPRECATE_ENUMERABLE_MIXIN + ); + }, +}); + +export default Enumerable; diff --git a/packages/@ember/enumerable/mutable-internal.ts b/packages/@ember/enumerable/mutable-internal.ts new file mode 100644 index 00000000000..96631833036 --- /dev/null +++ b/packages/@ember/enumerable/mutable-internal.ts @@ -0,0 +1,15 @@ +import InternalEnumerable from '@ember/enumerable/-internal'; +import { InternalMixin } from '@ember/object/mixin-internal'; + +/** + The internal counterpart to the public `MutableEnumerable` mixin. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. + + @internal +*/ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface InternalMutableEnumerable extends InternalEnumerable {} +const InternalMutableEnumerable = InternalMixin.create(InternalEnumerable); + +export default InternalMutableEnumerable; diff --git a/packages/@ember/enumerable/mutable.ts b/packages/@ember/enumerable/mutable.ts new file mode 100644 index 00000000000..38acfe00ff6 --- /dev/null +++ b/packages/@ember/enumerable/mutable.ts @@ -0,0 +1,32 @@ +import InternalMutableEnumerable from '@ember/enumerable/mutable-internal'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; + +/** +@module ember +*/ + +/** + The methods in this mixin have been moved to MutableArray. This mixin has + been intentionally preserved to avoid breaking MutableEnumerable.detect + checks until the community migrates away from them. + + @class MutableEnumerable + @namespace Ember + @uses Enumerable + @private + @deprecated Use native arrays and array methods instead. +*/ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface MutableEnumerable extends InternalMutableEnumerable {} +const MutableEnumerable = DeprecatedMixin.create(InternalMutableEnumerable, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `MutableEnumerable` mixin is deprecated. Use native arrays and array methods instead.', + DEPRECATIONS.DEPRECATE_MUTABLE_ENUMERABLE_MIXIN + ); + }, +}); + +export default MutableEnumerable; diff --git a/packages/@ember/enumerable/package.json b/packages/@ember/enumerable/package.json new file mode 100644 index 00000000000..a12c717454d --- /dev/null +++ b/packages/@ember/enumerable/package.json @@ -0,0 +1,22 @@ +{ + "name": "@ember/enumerable", + "private": true, + "type": "module", + "exports": { + ".": "./index.ts", + "./mutable": "./mutable.ts", + "./*": "./*.ts" + }, + "dependencies": { + "@ember/-internals": "workspace:*", + "@ember/array": "workspace:*", + "@ember/debug": "workspace:*", + "@ember/object": "workspace:*", + "@glimmer/destroyable": "workspace:*", + "@glimmer/env": "workspace:*", + "@glimmer/owner": "workspace:*", + "@glimmer/util": "workspace:*", + "@glimmer/validator": "workspace:*", + "internal-test-helpers": "workspace:*" + } +} diff --git a/packages/@ember/enumerable/tests/enumerable_test.js b/packages/@ember/enumerable/tests/enumerable_test.js new file mode 100644 index 00000000000..ee7e07e0139 --- /dev/null +++ b/packages/@ember/enumerable/tests/enumerable_test.js @@ -0,0 +1,22 @@ +import Enumerable from '@ember/enumerable'; +import ArrayProxy from '@ember/array/proxy'; +import { A } from '@ember/array'; +import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; + +moduleFor( + 'Enumerable', + class extends AbstractTestCase { + ['@test should be mixed into A()'](assert) { + assert.ok(Enumerable.detect(A())); + } + + [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test should be mixed into ArrayProxy`]( + assert + ) { + expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled); + + assert.ok(Enumerable.detect(ArrayProxy.create())); + } + } +); diff --git a/packages/@ember/object/package.json b/packages/@ember/object/package.json index 1fdba733b9f..e0c4ce1ff36 100644 --- a/packages/@ember/object/package.json +++ b/packages/@ember/object/package.json @@ -23,6 +23,7 @@ "@ember/application": "workspace:*", "@ember/array": "workspace:*", "@ember/debug": "workspace:*", + "@ember/enumerable": "workspace:*", "@ember/runloop": "workspace:*", "@ember/service": "workspace:*", "@ember/utils": "workspace:*", diff --git a/packages/@ember/routing/package.json b/packages/@ember/routing/package.json index b9c862884e7..5a956191f63 100644 --- a/packages/@ember/routing/package.json +++ b/packages/@ember/routing/package.json @@ -21,6 +21,7 @@ "@ember/controller": "workspace:*", "@ember/debug": "workspace:*", "@ember/engine": "workspace:*", + "@ember/enumerable": "workspace:*", "@ember/object": "workspace:*", "@ember/owner": "workspace:*", "@ember/runloop": "workspace:*", diff --git a/packages/@ember/utils/package.json b/packages/@ember/utils/package.json index 335153ebd97..a26dab58aae 100644 --- a/packages/@ember/utils/package.json +++ b/packages/@ember/utils/package.json @@ -11,6 +11,7 @@ "@ember/application": "workspace:*", "@ember/array": "workspace:*", "@ember/debug": "workspace:*", + "@ember/enumerable": "workspace:*", "@ember/object": "workspace:*", "@ember/runloop": "workspace:*", "@glimmer/destroyable": "workspace:*", diff --git a/packages/@ember/version/package.json b/packages/@ember/version/package.json index 5245b961648..f61f55d3498 100644 --- a/packages/@ember/version/package.json +++ b/packages/@ember/version/package.json @@ -16,6 +16,7 @@ "@ember/debug": "workspace:*", "@ember/destroyable": "workspace:*", "@ember/engine": "workspace:*", + "@ember/enumerable": "workspace:*", "@ember/instrumentation": "workspace:*", "@ember/object": "workspace:*", "@ember/routing": "workspace:*", diff --git a/packages/ember-template-compiler/package.json b/packages/ember-template-compiler/package.json index d9721cc4610..6bb122fbb56 100644 --- a/packages/ember-template-compiler/package.json +++ b/packages/ember-template-compiler/package.json @@ -17,6 +17,7 @@ "@ember/debug": "workspace:*", "@ember/destroyable": "workspace:*", "@ember/engine": "workspace:*", + "@ember/enumerable": "workspace:*", "@ember/instrumentation": "workspace:*", "@ember/object": "workspace:*", "@ember/routing": "workspace:*", diff --git a/packages/ember/package.json b/packages/ember/package.json index e062a49a15a..03a84ccd1e7 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -15,6 +15,7 @@ "@ember/debug": "workspace:*", "@ember/destroyable": "workspace:*", "@ember/engine": "workspace:*", + "@ember/enumerable": "workspace:*", "@ember/helper": "workspace:*", "@ember/instrumentation": "workspace:*", "@ember/modifier": "workspace:*", diff --git a/packages/internal-test-helpers/package.json b/packages/internal-test-helpers/package.json index bff7e231fd4..dff92d10e78 100644 --- a/packages/internal-test-helpers/package.json +++ b/packages/internal-test-helpers/package.json @@ -17,6 +17,7 @@ "@ember/debug": "workspace:*", "@ember/destroyable": "workspace:*", "@ember/engine": "workspace:*", + "@ember/enumerable": "workspace:*", "@ember/instrumentation": "workspace:*", "@ember/object": "workspace:*", "@ember/owner": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 762b657b46a..e3284a41422 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -298,6 +298,9 @@ importers: '@ember/engine': specifier: workspace:* version: link:../engine + '@ember/enumerable': + specifier: workspace:* + version: link:../enumerable '@ember/helper': specifier: workspace:* version: link:../helper @@ -506,6 +509,9 @@ importers: '@ember/debug': specifier: workspace:* version: link:../debug + '@ember/enumerable': + specifier: workspace:* + version: link:../enumerable '@ember/object': specifier: workspace:* version: link:../object @@ -637,6 +643,9 @@ importers: '@ember/engine': specifier: workspace:* version: link:../engine + '@ember/enumerable': + specifier: workspace:* + version: link:../enumerable '@ember/object': specifier: workspace:* version: link:../object @@ -757,6 +766,39 @@ importers: specifier: workspace:* version: link:../../router_js + packages/@ember/enumerable: + dependencies: + '@ember/-internals': + specifier: workspace:* + version: link:../-internals + '@ember/array': + specifier: workspace:* + version: link:../array + '@ember/debug': + specifier: workspace:* + version: link:../debug + '@ember/object': + specifier: workspace:* + version: link:../object + '@glimmer/destroyable': + specifier: workspace:* + version: link:../../@glimmer/destroyable + '@glimmer/env': + specifier: workspace:* + version: link:../../@glimmer/env + '@glimmer/owner': + specifier: workspace:* + version: link:../../@glimmer/owner + '@glimmer/util': + specifier: workspace:* + version: link:../../@glimmer/util + '@glimmer/validator': + specifier: workspace:* + version: link:../../@glimmer/validator + internal-test-helpers: + specifier: workspace:* + version: link:../../internal-test-helpers + packages/@ember/helper: dependencies: '@ember/-internals': @@ -846,6 +888,9 @@ importers: '@ember/debug': specifier: workspace:* version: link:../debug + '@ember/enumerable': + specifier: workspace:* + version: link:../enumerable '@ember/runloop': specifier: workspace:* version: link:../runloop @@ -952,6 +997,9 @@ importers: '@ember/engine': specifier: workspace:* version: link:../engine + '@ember/enumerable': + specifier: workspace:* + version: link:../enumerable '@ember/object': specifier: workspace:* version: link:../object @@ -1188,6 +1236,9 @@ importers: '@ember/debug': specifier: workspace:* version: link:../debug + '@ember/enumerable': + specifier: workspace:* + version: link:../enumerable '@ember/object': specifier: workspace:* version: link:../object @@ -1248,6 +1299,9 @@ importers: '@ember/engine': specifier: workspace:* version: link:../engine + '@ember/enumerable': + specifier: workspace:* + version: link:../enumerable '@ember/instrumentation': specifier: workspace:* version: link:../instrumentation @@ -2378,6 +2432,9 @@ importers: '@ember/engine': specifier: workspace:* version: link:../@ember/engine + '@ember/enumerable': + specifier: workspace:* + version: link:../@ember/enumerable '@ember/helper': specifier: workspace:* version: link:../@ember/helper @@ -2501,6 +2558,9 @@ importers: '@ember/engine': specifier: workspace:* version: link:../@ember/engine + '@ember/enumerable': + specifier: workspace:* + version: link:../@ember/enumerable '@ember/instrumentation': specifier: workspace:* version: link:../@ember/instrumentation @@ -2654,6 +2714,9 @@ importers: '@ember/engine': specifier: workspace:* version: link:../@ember/engine + '@ember/enumerable': + specifier: workspace:* + version: link:../@ember/enumerable '@ember/instrumentation': specifier: workspace:* version: link:../@ember/instrumentation From c6e4a8c8e858e9ef220ffad0f8884c9d311bbf0d Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:31:43 -0400 Subject: [PATCH 13/19] Delete internal Enumerables --- packages/@ember/array/index-internal.ts | 8 ++++---- packages/@ember/enumerable/-internal.ts | 14 -------------- packages/@ember/enumerable/index.ts | 7 +++---- packages/@ember/enumerable/mutable-internal.ts | 15 --------------- packages/@ember/enumerable/mutable.ts | 8 ++++---- 5 files changed, 11 insertions(+), 41 deletions(-) delete mode 100644 packages/@ember/enumerable/-internal.ts delete mode 100644 packages/@ember/enumerable/mutable-internal.ts diff --git a/packages/@ember/array/index-internal.ts b/packages/@ember/array/index-internal.ts index 2746d544132..2cdc603665b 100644 --- a/packages/@ember/array/index-internal.ts +++ b/packages/@ember/array/index-internal.ts @@ -12,8 +12,8 @@ import { get } from '@ember/-internals/metal/lib/property_get'; import { set } from '@ember/-internals/metal/lib/property_set'; import { InternalMixin } from '@ember/object/mixin-internal'; import { assert } from '@ember/debug'; -import InternalEnumerable from '@ember/enumerable/-internal'; -import InternalMutableEnumerable from '@ember/enumerable/mutable-internal'; +import Enumerable from '@ember/enumerable'; +import MutableEnumerable from '@ember/enumerable/mutable'; import sortCompare from './lib/sort-compare'; import InternalObservable from '@ember/object/observable-internal'; import type { ComputedPropertyCallback } from '@ember/-internals/metal/lib/computed'; @@ -159,7 +159,7 @@ function mapBy(this: EmberArray, key: string) { @internal */ -const InternalEmberArray = InternalMixin.create(InternalEnumerable, { +const InternalEmberArray = InternalMixin.create(Enumerable, { init() { this._super(...arguments); setEmberArray(this); @@ -431,7 +431,7 @@ const InternalEmberArray = InternalMixin.create(InternalEnumerable, { }, }); -const InternalMutableArray = InternalMixin.create(InternalEmberArray, InternalMutableEnumerable, { +const InternalMutableArray = InternalMixin.create(InternalEmberArray, MutableEnumerable, { clear() { let len = this.length; if (len === 0) { diff --git a/packages/@ember/enumerable/-internal.ts b/packages/@ember/enumerable/-internal.ts deleted file mode 100644 index 0a1c573248b..00000000000 --- a/packages/@ember/enumerable/-internal.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { InternalMixin } from '@ember/object/mixin-internal'; - -/** - The internal counterpart to the public `Enumerable` mixin. Ember's own - internals apply this so that they do not trigger the deprecation that the - public mixin emits. - - @internal -*/ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface InternalEnumerable {} -const InternalEnumerable = InternalMixin.create(); - -export default InternalEnumerable; diff --git a/packages/@ember/enumerable/index.ts b/packages/@ember/enumerable/index.ts index d3e5c2f3a30..8bbf0da674e 100644 --- a/packages/@ember/enumerable/index.ts +++ b/packages/@ember/enumerable/index.ts @@ -1,6 +1,5 @@ -import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { InternalMixin } from '@ember/object/mixin-internal'; import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; -import InternalEnumerable from '@ember/enumerable/-internal'; /** @module @ember/enumerable @@ -17,8 +16,8 @@ import InternalEnumerable from '@ember/enumerable/-internal'; @deprecated Use native arrays and array methods instead. */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface Enumerable extends InternalEnumerable {} -const Enumerable = DeprecatedMixin.create(InternalEnumerable, { +interface Enumerable {} +const Enumerable = InternalMixin.create({ init() { this._super(...arguments); deprecateUntil( diff --git a/packages/@ember/enumerable/mutable-internal.ts b/packages/@ember/enumerable/mutable-internal.ts deleted file mode 100644 index 96631833036..00000000000 --- a/packages/@ember/enumerable/mutable-internal.ts +++ /dev/null @@ -1,15 +0,0 @@ -import InternalEnumerable from '@ember/enumerable/-internal'; -import { InternalMixin } from '@ember/object/mixin-internal'; - -/** - The internal counterpart to the public `MutableEnumerable` mixin. Ember's own - internals apply this so that they do not trigger the deprecation that the - public mixin emits. - - @internal -*/ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface InternalMutableEnumerable extends InternalEnumerable {} -const InternalMutableEnumerable = InternalMixin.create(InternalEnumerable); - -export default InternalMutableEnumerable; diff --git a/packages/@ember/enumerable/mutable.ts b/packages/@ember/enumerable/mutable.ts index 38acfe00ff6..6f89bc09d30 100644 --- a/packages/@ember/enumerable/mutable.ts +++ b/packages/@ember/enumerable/mutable.ts @@ -1,5 +1,5 @@ -import InternalMutableEnumerable from '@ember/enumerable/mutable-internal'; -import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import Enumerable from '@ember/enumerable'; +import { InternalMixin } from '@ember/object/mixin-internal'; import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; /** @@ -18,8 +18,8 @@ import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; @deprecated Use native arrays and array methods instead. */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface MutableEnumerable extends InternalMutableEnumerable {} -const MutableEnumerable = DeprecatedMixin.create(InternalMutableEnumerable, { +interface MutableEnumerable extends Enumerable {} +const MutableEnumerable = InternalMixin.create(Enumerable, { init() { this._super(...arguments); deprecateUntil( From 16abfb4c8a678f6224a5c92543e24c8c6dfa3da4 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:50:45 -0400 Subject: [PATCH 14/19] Arrays extending Enumerables no longer --- packages/@ember/array/index-internal.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/@ember/array/index-internal.ts b/packages/@ember/array/index-internal.ts index 2cdc603665b..4fc2dc1ac9b 100644 --- a/packages/@ember/array/index-internal.ts +++ b/packages/@ember/array/index-internal.ts @@ -12,8 +12,6 @@ import { get } from '@ember/-internals/metal/lib/property_get'; import { set } from '@ember/-internals/metal/lib/property_set'; import { InternalMixin } from '@ember/object/mixin-internal'; import { assert } from '@ember/debug'; -import Enumerable from '@ember/enumerable'; -import MutableEnumerable from '@ember/enumerable/mutable'; import sortCompare from './lib/sort-compare'; import InternalObservable from '@ember/object/observable-internal'; import type { ComputedPropertyCallback } from '@ember/-internals/metal/lib/computed'; @@ -159,7 +157,7 @@ function mapBy(this: EmberArray, key: string) { @internal */ -const InternalEmberArray = InternalMixin.create(Enumerable, { +const InternalEmberArray = InternalMixin.create({ init() { this._super(...arguments); setEmberArray(this); @@ -431,7 +429,7 @@ const InternalEmberArray = InternalMixin.create(Enumerable, { }, }); -const InternalMutableArray = InternalMixin.create(InternalEmberArray, MutableEnumerable, { +const InternalMutableArray = InternalMixin.create(InternalEmberArray, { clear() { let len = this.length; if (len === 0) { From b00f9d604d25d12062fa1df5a82d1faf6f0146c9 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:30:34 -0400 Subject: [PATCH 15/19] Cleanup improper Enumerable usage (which does change public API Docs --- .../tests/mixins/mutable_enumerable_test.js | 22 ------------------- .../tests/mutable-array/pushObjects-test.js | 2 +- packages/@ember/array/index.ts | 9 +++----- .../enumerable/tests/enumerable_test.js | 22 ------------------- 4 files changed, 4 insertions(+), 51 deletions(-) delete mode 100644 packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js delete mode 100644 packages/@ember/enumerable/tests/enumerable_test.js diff --git a/packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js b/packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js deleted file mode 100644 index b08ad58d3fe..00000000000 --- a/packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js +++ /dev/null @@ -1,22 +0,0 @@ -import MutableEnumerable from '@ember/enumerable/mutable'; -import ArrayProxy from '@ember/array/proxy'; -import { A } from '@ember/array'; -import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; -import { DEPRECATIONS } from '@ember/-internals/deprecations'; - -moduleFor( - 'MutableEnumerable', - class extends AbstractTestCase { - ['@test should be mixed into A()'](assert) { - assert.ok(MutableEnumerable.detect(A())); - } - - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test should be mixed into ArrayProxy`]( - assert - ) { - expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled); - - assert.ok(MutableEnumerable.detect(ArrayProxy.create())); - } - } -); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/pushObjects-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/pushObjects-test.js index 55a67b4139e..7941647c545 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/pushObjects-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/pushObjects-test.js @@ -2,7 +2,7 @@ import { AbstractTestCase } from 'internal-test-helpers'; import { runArrayTests } from '../helpers/array'; class PushObjectsTests extends AbstractTestCase { - '@test should raise exception if not Ember.Enumerable is passed to pushObjects'() { + '@test should raise exception if not Iterable is passed to pushObjects'() { let obj = this.newObject([]); expectAssertion(() => obj.pushObjects('string')); diff --git a/packages/@ember/array/index.ts b/packages/@ember/array/index.ts index 735a2f4ce1b..82bbbec8b52 100644 --- a/packages/@ember/array/index.ts +++ b/packages/@ember/array/index.ts @@ -2,8 +2,6 @@ @module @ember/array */ import { DeprecatedMixin } from '@ember/object/mixin-internal'; -import type Enumerable from '@ember/enumerable'; -import type MutableEnumerable from '@ember/enumerable/mutable'; import type Observable from '@ember/object/observable'; import type { MethodNamesOf, MethodParams, MethodReturns } from '@ember/-internals/utility-types'; import isArray from './lib/is-array'; @@ -49,11 +47,10 @@ export type EmberArrayLike = EmberArray | NativeArray; primitives to use it: `length()` and `objectAt()`. @class EmberArray - @uses Enumerable @since Ember 0.9.0 @public */ -interface EmberArray extends Enumerable { +interface EmberArray { /** __Required.__ You must implement this method to apply this mixin. @@ -1035,7 +1032,7 @@ const EmberArray = DeprecatedMixin.create(InternalEmberArrayMixin, { @uses EmberArray @public */ -interface MutableArray extends EmberArray, MutableEnumerable { +interface MutableArray extends EmberArray { /** __Required.__ You must implement this method to apply this mixin. @@ -1206,7 +1203,7 @@ interface MutableArray extends EmberArray, MutableEnumerable { ``` @method unshiftObjects - @param {Enumerable} objects the objects to add + @param {Iterable} objects the objects to add @return {EmberArray} receiver @public */ diff --git a/packages/@ember/enumerable/tests/enumerable_test.js b/packages/@ember/enumerable/tests/enumerable_test.js deleted file mode 100644 index ee7e07e0139..00000000000 --- a/packages/@ember/enumerable/tests/enumerable_test.js +++ /dev/null @@ -1,22 +0,0 @@ -import Enumerable from '@ember/enumerable'; -import ArrayProxy from '@ember/array/proxy'; -import { A } from '@ember/array'; -import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; -import { DEPRECATIONS } from '@ember/-internals/deprecations'; - -moduleFor( - 'Enumerable', - class extends AbstractTestCase { - ['@test should be mixed into A()'](assert) { - assert.ok(Enumerable.detect(A())); - } - - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test should be mixed into ArrayProxy`]( - assert - ) { - expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled); - - assert.ok(Enumerable.detect(ArrayProxy.create())); - } - } -); From ac387157dbdd644b257245aa88297180ba81355b Mon Sep 17 00:00:00 2001 From: Katie Gengler Date: Mon, 12 Oct 2026 21:22:25 -0400 Subject: [PATCH 16/19] [DOC] Mark array mixins as deprecated --- packages/@ember/array/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/@ember/array/index.ts b/packages/@ember/array/index.ts index 82bbbec8b52..25393ff6ef4 100644 --- a/packages/@ember/array/index.ts +++ b/packages/@ember/array/index.ts @@ -49,6 +49,7 @@ export type EmberArrayLike = EmberArray | NativeArray; @class EmberArray @since Ember 0.9.0 @public + @deprecated Use Native array methods instead. */ interface EmberArray { /** @@ -1031,6 +1032,7 @@ const EmberArray = DeprecatedMixin.create(InternalEmberArrayMixin, { @class MutableArray @uses EmberArray @public + @deprecated Use Native array methods instead. */ interface MutableArray extends EmberArray { /** @@ -1468,6 +1470,7 @@ interface MutableArrayWithoutNative extends Omit< @uses MutableArray @uses Observable @public + @deprecated Use native array methods */ interface NativeArray extends Array, Observable, MutableArrayWithoutNative {} From 2f5651567b0e59409fda1f2214eaac1d62c16f6a Mon Sep 17 00:00:00 2001 From: Katie Gengler Date: Mon, 12 Oct 2026 21:22:58 -0400 Subject: [PATCH 17/19] [DOC] Remove @uses Enumerable from MutableEnumerable, Enumerable was an empty mixin --- packages/@ember/enumerable/mutable.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@ember/enumerable/mutable.ts b/packages/@ember/enumerable/mutable.ts index 6f89bc09d30..38e35f14ed0 100644 --- a/packages/@ember/enumerable/mutable.ts +++ b/packages/@ember/enumerable/mutable.ts @@ -13,7 +13,6 @@ import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; @class MutableEnumerable @namespace Ember - @uses Enumerable @private @deprecated Use native arrays and array methods instead. */ From e5886fbf74015fa09ec223391cfc14460164fc6f Mon Sep 17 00:00:00 2001 From: Katie Gengler Date: Mon, 12 Oct 2026 22:28:16 -0400 Subject: [PATCH 18/19] [DOC] Mark the Enumerable method as deprecated --- packages/@ember/enumerable/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@ember/enumerable/index.ts b/packages/@ember/enumerable/index.ts index 8bbf0da674e..b7ff3d59064 100644 --- a/packages/@ember/enumerable/index.ts +++ b/packages/@ember/enumerable/index.ts @@ -4,6 +4,7 @@ import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; /** @module @ember/enumerable @private +@deprecated */ /** From 0e11d300abe0a72c0781c0c9b016833b673a343f Mon Sep 17 00:00:00 2001 From: Katie Gengler Date: Mon, 12 Oct 2026 22:29:08 -0400 Subject: [PATCH 19/19] [DOC] Add a note to Observerable mixin that it is deprecated but its methods are not deprecated where they are included. I'd prefer to put the documentation on InternalObservable and have the documentation show that way, but I believe the filtering is keeping that from ending up in the docs. --- packages/@ember/object/observable.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/@ember/object/observable.ts b/packages/@ember/object/observable.ts index d4b6bb90eef..c7f2c565d78 100644 --- a/packages/@ember/object/observable.ts +++ b/packages/@ember/object/observable.ts @@ -14,6 +14,9 @@ export type { ObserverMethod } from '@ember/object/observable-internal'; This mixin provides properties and property observing functionality, core features of the Ember object model. + + NOTE: Using this mixin directly is deprecated. The methods provided by it + are not deprecated when they are included in framework objects. Properties and observers allow one object to observe changes to a property on another object. This is one of the fundamental ways that