From 8ed880ef3dff7af64902ed6773214e37e4684f46 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] 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 | 79 +- .../-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, 2500 insertions(+), 1920 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 e06f32f3651..a6a334b106f 100644 --- a/packages/@ember/-internals/deprecations/index.ts +++ b/packages/@ember/-internals/deprecations/index.ts @@ -123,7 +123,7 @@ export const DEPRECATIONS = { for: 'ember-source', id: 'deprecate-target-action-support', since: { available: '7.3.0', enabled: '7.5.0' }, - until: '8.0.0', + until: '7.9.0', url: 'https://deprecations.emberjs.com/id/deprecate-target-action-support', }), DEPRECATE_EVENTED: deprecation({ @@ -161,6 +161,83 @@ export const DEPRECATIONS = { until: '8.0.0', url: 'https://deprecations.emberjs.com/id/deprecate-promise-proxy-mixin', }), + 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..fb708eeefc6 --- /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 compare from '@ember/utils/lib/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 = compare(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 0710bff2f09..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 compare from '@ember/utils/lib/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 = compare(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 8b6ce290204..33f63a40743 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 cb3ec22442c..c7dfcdf3bd2 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 typeOf from '@ember/utils/lib/type-of'; import { isProxy } from '@ember/-internals/utils/lib/is_proxy'; import lookupDescriptor from '@ember/-internals/utils/lib/lookup-descriptor'; @@ -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 bd803d41166..2cdf8a0bc6f 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -529,6 +529,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', @@ -539,7 +549,8 @@ function packageMeta() { (name) => name.startsWith('packages/') && !name.startsWith('packages/shared-chunks/') && - name.endsWith('.js') + name.endsWith('.js') && + !isInternalCopy(name) ) .sort() .map((name) => {