From a92fe4fa58d1a52f8581929e32ef4572b3aa799c Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:45:03 -0400 Subject: [PATCH 1/3] Spike: deprecate EmberObject (RFC 1234) Add the `deprecate-ember-object` deprecation and the `EmberENV._NO_EMBER_OBJECT` flag. The flag is a preview of the removal: each deprecated API throws. A class that extends `EmberObject` or `CoreObject` directly reports the deprecation on each construction. The classes that Ember builds on `EmberObject` have a private symbol mark, so `Route`, `Controller`, `Service` and their subclasses stay usable. The mark replaces the internal copy that the RFC describes, because a copy makes `service instanceof EmberObject` false. The object model methods (`get`, `set`, `getProperties`, `setProperties`, `incrementProperty`, `decrementProperty`, `toggleProperty`, `notifyPropertyChange`) report on all classes. An `init` on a subclass of a framework class reports too. The public `computed` and the macros from `@ember/object/computed` are wrappers that report. Ember code imports the implementations, and calls the get and set functions in place of the methods. The test page does not load with all deprecations on, or with the flag on, because test files use `EmberObject` and `computed` at module scope. Co-Authored-By: Claude Opus 5.5 --- index.html | 4 + .../@ember/-internals/deprecations/index.ts | 28 +++ .../@ember/-internals/environment/lib/env.ts | 16 ++ .../glimmer/lib/component-managers/curly.ts | 3 +- .../glimmer/lib/component-managers/mount.ts | 3 +- .../-internals/glimmer/lib/component.ts | 5 +- .../@ember/-internals/glimmer/lib/helper.ts | 3 + .../-internals/runtime/lib/mixins/-proxy.ts | 2 +- .../utils/lib/internal-framework-class.ts | 31 +++ .../views/lib/system/event_dispatcher.ts | 3 + .../-internals/views/lib/views/core_view.ts | 3 + packages/@ember/application/index.ts | 3 + packages/@ember/application/instance.ts | 3 + packages/@ember/application/namespace.ts | 3 + packages/@ember/array/proxy.ts | 5 +- packages/@ember/controller/index.ts | 3 + .../@ember/debug/container-debug-adapter.ts | 3 + packages/@ember/debug/data-adapter.ts | 3 + packages/@ember/engine/index.ts | 3 + packages/@ember/engine/instance.ts | 3 + packages/@ember/object/-internals.ts | 3 + packages/@ember/object/computed.ts | 86 ++++--- packages/@ember/object/core.ts | 44 +++- packages/@ember/object/index.ts | 12 +- packages/@ember/object/observable.ts | 9 + packages/@ember/object/proxy.ts | 3 + .../tests/deprecate_ember_object_test.js | 216 ++++++++++++++++++ packages/@ember/routing/hash-location.ts | 3 + packages/@ember/routing/history-location.ts | 3 + .../@ember/routing/lib/routing-service.ts | 2 +- packages/@ember/routing/none-location.ts | 3 + packages/@ember/routing/route.ts | 3 + packages/@ember/routing/router-service.ts | 2 +- packages/@ember/routing/router.ts | 16 +- packages/@ember/service/index.ts | 3 + .../ember-testing/lib/adapters/adapter.ts | 3 + testem.cjs | 4 + tests/docs/expected.cjs | 1 + 38 files changed, 495 insertions(+), 51 deletions(-) create mode 100644 packages/@ember/-internals/utils/lib/internal-framework-class.ts create mode 100644 packages/@ember/object/tests/deprecate_ember_object_test.js diff --git a/index.html b/index.html index d67e07a7834..2c31a761e38 100644 --- a/index.html +++ b/index.html @@ -33,6 +33,10 @@ EmberENV['_ALL_DEPRECATIONS_ENABLED'] = true; } + if (QUnit.urlParams.NO_EMBER_OBJECT) { + EmberENV['_NO_EMBER_OBJECT'] = true; + } + if (QUnit.urlParams.OVERRIDE_DEPRECATION_VERSION) { EmberENV['_OVERRIDE_DEPRECATION_VERSION'] = QUnit.urlParams.OVERRIDE_DEPRECATION_VERSION; } diff --git a/packages/@ember/-internals/deprecations/index.ts b/packages/@ember/-internals/deprecations/index.ts index b8a83a7054d..affba7dadf7 100644 --- a/packages/@ember/-internals/deprecations/index.ts +++ b/packages/@ember/-internals/deprecations/index.ts @@ -168,6 +168,13 @@ export const DEPRECATIONS = { until: '8.0.0', url: 'https://deprecations.emberjs.com/id/deprecate-ember-utils', }), + DEPRECATE_EMBER_OBJECT: deprecation({ + id: 'deprecate-ember-object', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '9.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-ember-object', + }), }; export function deprecateUntil(message: string, deprecation: DeprecationObject) { @@ -183,3 +190,24 @@ export function deprecateUntil(message: string, deprecation: DeprecationObject) } deprecate(message, deprecation.test, options); } + +/** + Reports the use of an API that RFC 1234 removes with `EmberObject`. + + The `no-ember-object` optional feature is a preview of the removal: + when it is on, the API throws. + + @private +*/ +export function deprecateEmberObject(api: string) { + if (ENV._NO_EMBER_OBJECT) { + throw new Error( + `${api} is not available, because the \`no-ember-object\` optional feature is on. Please see ${DEPRECATIONS.DEPRECATE_EMBER_OBJECT.options.url} for more details.` + ); + } + + deprecateUntil( + `${api} is deprecated. Use native classes with tracked properties instead.`, + DEPRECATIONS.DEPRECATE_EMBER_OBJECT + ); +} diff --git a/packages/@ember/-internals/environment/lib/env.ts b/packages/@ember/-internals/environment/lib/env.ts index 4c3e37fe4a8..92139441391 100644 --- a/packages/@ember/-internals/environment/lib/env.ts +++ b/packages/@ember/-internals/environment/lib/env.ts @@ -125,6 +125,22 @@ export const ENV = { */ _DEFAULT_ASYNC_OBSERVERS: false, + /** + Whether `EmberObject` and the APIs of the Ember object model throw. + + This is a preview of the removal of `EmberObject`. + + This is not intended to be set directly, as the implementation may change in + the future. Use `@ember/optional-features` instead. + + @property _NO_EMBER_OBJECT + @for EmberENV + @type Boolean + @default false + @private + */ + _NO_EMBER_OBJECT: false, + /** Controls the maximum number of scheduled rerenders without "settling". In general, applications should not need to modify this environment variable, but please diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts index c7e1027ca35..9eebe5a9212 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts @@ -5,6 +5,7 @@ import { setOwner, } from '@ember/-internals/owner'; import { guidFor } from '@ember/-internals/utils/lib/guid'; +import setProperties from '@ember/-internals/metal/lib/set_properties'; import { addChildView, setElementView, @@ -454,7 +455,7 @@ export default class CurlyComponentManager bucket.argsRevision = valueForTag(argsTag); component[IS_DISPATCHING_ATTRS] = true; - component.setProperties(props); + setProperties(component, props); component[IS_DISPATCHING_ATTRS] = false; sendCoreViewEvent(component, 'didUpdateAttrs'); diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/mount.ts b/packages/@ember/-internals/glimmer/lib/component-managers/mount.ts index 538377dcea2..9ad7bb7b06a 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/mount.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/mount.ts @@ -1,6 +1,7 @@ import type { InternalOwner } from '@ember/-internals/owner'; import { generateControllerFactory } from '@ember/routing/lib/generate_controller'; import { assert } from '@ember/debug'; +import { set } from '@ember/-internals/metal/lib/property_set'; import type EngineInstance from '@ember/engine/instance'; import { associateDestroyableChild } from '@glimmer/destroyable'; import type { @@ -161,7 +162,7 @@ class MountManager let { controller, modelRef } = bucket; if (modelRef !== undefined) { - controller.set('model', valueForRef(modelRef)); + set(controller, 'model', valueForRef(modelRef)); } } } diff --git a/packages/@ember/-internals/glimmer/lib/component.ts b/packages/@ember/-internals/glimmer/lib/component.ts index 5c0735cd780..7f9883987b7 100644 --- a/packages/@ember/-internals/glimmer/lib/component.ts +++ b/packages/@ember/-internals/glimmer/lib/component.ts @@ -32,6 +32,7 @@ import { getComponentCapturedArgs, } from './component-managers/curly'; import hasDOM from '@ember/-internals/browser-environment/lib/has-dom'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; // Keep track of which component classes have already been processed for lazy event setup. let lazyEventsProcessed = new WeakMap>(); @@ -1051,7 +1052,7 @@ class Component getAttr(key: string) { // TODO Intimate API should be deprecated - return this.get(key); + return get(this, key); } /** @@ -1693,4 +1694,6 @@ Component.reopenClass({ setInternalComponentManager(CURLY_COMPONENT_MANAGER, Component); +setFrameworkClass(Component); + export default Component; diff --git a/packages/@ember/-internals/glimmer/lib/helper.ts b/packages/@ember/-internals/glimmer/lib/helper.ts index 81e59cca7e0..571e86dfe09 100644 --- a/packages/@ember/-internals/glimmer/lib/helper.ts +++ b/packages/@ember/-internals/glimmer/lib/helper.ts @@ -16,6 +16,7 @@ import type { DirtyableTag } from '@glimmer/interfaces'; import { consumeTag } from '@glimmer/validator/lib/tracking'; import { createTag, DIRTY_TAG as dirtyTag } from '@glimmer/validator/lib/validators'; import { IS_CLASSIC_HELPER } from './helper-brand'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; export { isClassicHelper } from './helper-brand'; @@ -408,3 +409,5 @@ export function helper( // `helper()` invocation in favor of using plain functions. return new Wrapper(helperFn) as unknown as FunctionBasedHelper; } + +setFrameworkClass(Helper); diff --git a/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts b/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts index cf3e23234e6..ca147af0651 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts @@ -110,7 +110,7 @@ const ProxyMixin = /*@__PURE__*/ Mixin[INTERNAL_MIXIN_CREATE]({ }, willDestroy() { - this.set('content', null); + set(this, 'content', null); this._super(...arguments); }, diff --git a/packages/@ember/-internals/utils/lib/internal-framework-class.ts b/packages/@ember/-internals/utils/lib/internal-framework-class.ts new file mode 100644 index 00000000000..6b04e703163 --- /dev/null +++ b/packages/@ember/-internals/utils/lib/internal-framework-class.ts @@ -0,0 +1,31 @@ +/** + Marks the classes that Ember itself builds on `EmberObject`, such as + `Route`, `Controller` and `Service`. + + The `EmberObject` deprecation applies to classes that extend `EmberObject` + directly. A class that has this mark, and each subclass of it, stays + usable. + + This is a Symbol so that application code cannot set the mark by name. + + @private +*/ +const FRAMEWORK_CLASS = Symbol('__internal__framework__class__'); + +export function setFrameworkClass(Class: object): void { + (Class as Record)[FRAMEWORK_CLASS] = true; +} + +/** + True for a marked class and for each subclass of it. +*/ +export function isFrameworkClass(Class: object): boolean { + return (Class as Record)[FRAMEWORK_CLASS] === true; +} + +/** + True only for the class that Ember marked, not for its subclasses. +*/ +export function isOwnFrameworkClass(Class: object): boolean { + return Object.prototype.hasOwnProperty.call(Class, FRAMEWORK_CLASS); +} diff --git a/packages/@ember/-internals/views/lib/system/event_dispatcher.ts b/packages/@ember/-internals/views/lib/system/event_dispatcher.ts index 1c1357cc3bd..62312ec34f0 100644 --- a/packages/@ember/-internals/views/lib/system/event_dispatcher.ts +++ b/packages/@ember/-internals/views/lib/system/event_dispatcher.ts @@ -6,6 +6,7 @@ import EmberObject from '@ember/object'; import { getElementView } from './utils'; import type { BootEnvironment } from '@ember/engine/instance'; import type Component from '@ember/-internals/glimmer/lib/component'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** @module ember @@ -316,3 +317,5 @@ export default class EventDispatcher extends EmberObject { return '(EventDispatcher)'; } } + +setFrameworkClass(EventDispatcher); diff --git a/packages/@ember/-internals/views/lib/views/core_view.ts b/packages/@ember/-internals/views/lib/views/core_view.ts index c1a7d1c0a15..f8df30d3208 100644 --- a/packages/@ember/-internals/views/lib/views/core_view.ts +++ b/packages/@ember/-internals/views/lib/views/core_view.ts @@ -13,6 +13,7 @@ import Evented from '@ember/object/evented'; import { FrameworkObject } from '@ember/object/-internals'; import type { ViewState } from './states'; import states from './states'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** `CoreView` is an abstract class that exists to give view-like behavior @@ -135,4 +136,6 @@ class CoreView extends FrameworkObject.extend(ActionHandler) { // Declare on the prototype to have a single shared value. CoreView.prototype._states = states; +setFrameworkClass(CoreView); + export default CoreView; diff --git a/packages/@ember/application/index.ts b/packages/@ember/application/index.ts index e4ac76f8de3..93240716b3e 100644 --- a/packages/@ember/application/index.ts +++ b/packages/@ember/application/index.ts @@ -29,6 +29,7 @@ import { setupApplicationRegistry } from '@ember/-internals/glimmer/lib/setup-re import RouterService from '@ember/routing/router-service'; import type { EngineInstanceOptions } from '@ember/engine/instance'; import type { SimpleDocument, SimpleElement } from '@simple-dom/interface'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** * @deprecated Use `import { getOwner } from '@ember/owner';` instead. @@ -1133,4 +1134,6 @@ function commonSetupRegistry(registry: Registry) { registry.register('service:router', RouterService); } +setFrameworkClass(Application); + export { Application as default }; diff --git a/packages/@ember/application/instance.ts b/packages/@ember/application/instance.ts index f006714131c..17335100204 100644 --- a/packages/@ember/application/instance.ts +++ b/packages/@ember/application/instance.ts @@ -20,6 +20,7 @@ import Router from '@ember/routing/router'; import EventDispatcher from '@ember/-internals/views/lib/system/event_dispatcher'; import type Registry from '@ember/-internals/container/lib/registry'; import type { SimpleElement } from '@simple-dom/interface'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** The `ApplicationInstance` encapsulates all of the stateful aspects of a @@ -503,4 +504,6 @@ class _BootOptions { } } +setFrameworkClass(ApplicationInstance); + export default ApplicationInstance; diff --git a/packages/@ember/application/namespace.ts b/packages/@ember/application/namespace.ts index 6b3277f24be..b1e7b636bab 100644 --- a/packages/@ember/application/namespace.ts +++ b/packages/@ember/application/namespace.ts @@ -18,6 +18,7 @@ import { getName, setName } from '@ember/-internals/utils/lib/name'; import { guidFor } from '@ember/-internals/utils/lib/guid'; import { assert } from '@ember/debug'; import EmberObject from '@ember/object'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** A Namespace is an object usually used to contain other objects or methods @@ -78,4 +79,6 @@ class Namespace extends EmberObject { // Declare on the prototype to have a single shared value. Namespace.prototype.isNamespace = true; +setFrameworkClass(Namespace); + export default Namespace; diff --git a/packages/@ember/array/proxy.ts b/packages/@ember/array/proxy.ts index 192c55215ee..de7f2937663 100644 --- a/packages/@ember/array/proxy.ts +++ b/packages/@ember/array/proxy.ts @@ -29,6 +29,7 @@ import { import { consumeTag } from '@glimmer/validator/lib/tracking'; import { tagFor } from '@glimmer/validator/lib/meta'; import type { Tag } from '@glimmer/interfaces'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; function isMutable(obj: T[] | EmberArray): obj is T[] | MutableArray { return Array.isArray(obj) || typeof (obj as MutableArray).replace === 'function'; @@ -387,7 +388,7 @@ class ArrayProxy extends EmberObject implements PropertyDidChange { this._arrangedContentTag === null || !validateTag(this._arrangedContentTag, this._arrangedContentRevision!) ) { - let arrangedContent = this.get('arrangedContent'); + let arrangedContent = get(this, 'arrangedContent'); if (this._arrangedContentTag === null) { // This is the first time the proxy has been setup, only add the observer @@ -416,4 +417,6 @@ ArrayProxy.reopen(MutableArray, { arrangedContent: alias('content'), }); +setFrameworkClass(ArrayProxy); + export default ArrayProxy; diff --git a/packages/@ember/controller/index.ts b/packages/@ember/controller/index.ts index b83b4e6ae98..d195b4bdb2a 100644 --- a/packages/@ember/controller/index.ts +++ b/packages/@ember/controller/index.ts @@ -13,6 +13,7 @@ import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixi import type { RouteArgs } from '@ember/routing/-internals'; import ActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler'; import type { Transition } from 'router_js'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; export type ControllerQueryParamType = 'boolean' | 'number' | 'array' | 'string'; export type ControllerQueryParam = @@ -375,6 +376,8 @@ export function inject( return metalInject('controller', ...args); } +setFrameworkClass(Controller); + export { Controller as default, ControllerMixin }; /** diff --git a/packages/@ember/debug/container-debug-adapter.ts b/packages/@ember/debug/container-debug-adapter.ts index dec462e13a0..6831bdf9b5d 100644 --- a/packages/@ember/debug/container-debug-adapter.ts +++ b/packages/@ember/debug/container-debug-adapter.ts @@ -5,6 +5,7 @@ import type Owner from '@ember/owner'; import { getOwner } from '@ember/-internals/owner'; import type { Resolver } from '@ember/-internals/owner'; import Namespace from '@ember/application/namespace'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** @module @ember/debug/container-debug-adapter @@ -107,3 +108,5 @@ export default class ContainerDebugAdapter extends EmberObject { return types; } } + +setFrameworkClass(ContainerDebugAdapter); diff --git a/packages/@ember/debug/data-adapter.ts b/packages/@ember/debug/data-adapter.ts index 4b37faaff4b..13f6abbffea 100644 --- a/packages/@ember/debug/data-adapter.ts +++ b/packages/@ember/debug/data-adapter.ts @@ -12,6 +12,7 @@ import { consumeTag, createCache, getValue, untrack } from '@glimmer/validator/l import { tagFor } from '@glimmer/validator/lib/meta'; import type ContainerDebugAdapter from '@ember/debug/container-debug-adapter'; import { assert } from '.'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** @module @ember/debug/data-adapter @@ -685,3 +686,5 @@ export default class DataAdapter extends EmberObject { return null; } } + +setFrameworkClass(DataAdapter); diff --git a/packages/@ember/engine/index.ts b/packages/@ember/engine/index.ts index 478de9e9b13..de210c4fc7f 100644 --- a/packages/@ember/engine/index.ts +++ b/packages/@ember/engine/index.ts @@ -16,6 +16,7 @@ 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 { StrictResolver } from './lib/strict-resolver'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; function props(obj: object) { let properties = []; @@ -532,4 +533,6 @@ function commonSetupRegistry(registry: Registry) { registry.register('container-debug-adapter:main', ContainerDebugAdapter); } +setFrameworkClass(Engine); + export default Engine; diff --git a/packages/@ember/engine/instance.ts b/packages/@ember/engine/instance.ts index b49d1280121..34d62207c1a 100644 --- a/packages/@ember/engine/instance.ts +++ b/packages/@ember/engine/instance.ts @@ -16,6 +16,7 @@ import { type FullName, isFactory } from '@ember/-internals/owner'; import type Engine from '@ember/engine'; import type Application from '@ember/application'; import type { SimpleElement } from '@simple-dom/interface'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; export interface BootOptions { isBrowser?: boolean; @@ -265,4 +266,6 @@ class EngineInstance extends EmberObject.extend(RegistryProxyMixin, ContainerPro } } +setFrameworkClass(EngineInstance); + export default EngineInstance; diff --git a/packages/@ember/object/-internals.ts b/packages/@ember/object/-internals.ts index 9021a8dd03d..5d50e471ad3 100644 --- a/packages/@ember/object/-internals.ts +++ b/packages/@ember/object/-internals.ts @@ -5,6 +5,7 @@ import { addListener } from '@ember/-internals/metal/lib/events'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import EmberObject from '.'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; // Here we have runtime shenanigans to add debug-only errors to the class in dev // builds. Those runtime shenanigans produce the need for type-level shenanigans @@ -47,4 +48,6 @@ if (DEBUG) { addListener(FrameworkObject.prototype, 'init', null, ASSERT_INIT_WAS_CALLED); } +setFrameworkClass(FrameworkObject); + export { FrameworkObject }; diff --git a/packages/@ember/object/computed.ts b/packages/@ember/object/computed.ts index 5e4d41bc33b..aa6ba2a3f59 100644 --- a/packages/@ember/object/computed.ts +++ b/packages/@ember/object/computed.ts @@ -1,40 +1,54 @@ +import { deprecateEmberObject } from '@ember/-internals/deprecations'; +import type { AnyFn } from '@ember/-internals/utility-types'; +import internalAlias from '@ember/-internals/metal/lib/alias'; +import * as macros from './lib/computed/computed_macros'; +import * as reduceMacros from './lib/computed/reduce_computed_macros'; + export { ComputedProperty as default } from '@ember/-internals/metal/lib/computed'; export { default as expandProperties } from '@ember/-internals/metal/lib/expand_properties'; -export { default as alias } from '@ember/-internals/metal/lib/alias'; -export { - empty, - notEmpty, - none, - not, - bool, - match, - equal, - gt, - gte, - lt, - lte, - oneWay, - oneWay as reads, - readOnly, - deprecatingAlias, - and, - or, -} from './lib/computed/computed_macros'; +/** + Makes the public version of a macro. Ember's own code imports the macros + from `./lib/computed`, which do not report the deprecation. +*/ +function deprecated(name: string, macro: T): T { + return function (this: unknown, ...args: unknown[]) { + deprecateEmberObject(`The \`${name}\` macro from \`@ember/object/computed\``); + return macro.apply(this, args); + } as T; +} + +export const alias = deprecated('alias', internalAlias); + +export const empty = deprecated('empty', macros.empty); +export const notEmpty = deprecated('notEmpty', macros.notEmpty); +export const none = deprecated('none', macros.none); +export const not = deprecated('not', macros.not); +export const bool = deprecated('bool', macros.bool); +export const match = deprecated('match', macros.match); +export const equal = deprecated('equal', macros.equal); +export const gt = deprecated('gt', macros.gt); +export const gte = deprecated('gte', macros.gte); +export const lt = deprecated('lt', macros.lt); +export const lte = deprecated('lte', macros.lte); +export const oneWay = deprecated('oneWay', macros.oneWay); +export const readOnly = deprecated('readOnly', macros.readOnly); +export const deprecatingAlias = deprecated('deprecatingAlias', macros.deprecatingAlias); +export const and = deprecated('and', macros.and); +export const or = deprecated('or', macros.or); +export const reads = deprecated('reads', macros.oneWay); -export { - sum, - min, - max, - map, - sort, - setDiff, - mapBy, - filter, - filterBy, - uniq, - uniqBy, - union, - intersect, - collect, -} from './lib/computed/reduce_computed_macros'; +export const sum = deprecated('sum', reduceMacros.sum); +export const min = deprecated('min', reduceMacros.min); +export const max = deprecated('max', reduceMacros.max); +export const map = deprecated('map', reduceMacros.map); +export const sort = deprecated('sort', reduceMacros.sort); +export const setDiff = deprecated('setDiff', reduceMacros.setDiff); +export const mapBy = deprecated('mapBy', reduceMacros.mapBy); +export const filter = deprecated('filter', reduceMacros.filter); +export const filterBy = deprecated('filterBy', reduceMacros.filterBy); +export const uniq = deprecated('uniq', reduceMacros.uniq); +export const uniqBy = deprecated('uniqBy', reduceMacros.uniqBy); +export const union = deprecated('union', reduceMacros.union); +export const intersect = deprecated('intersect', reduceMacros.intersect); +export const collect = deprecated('collect', reduceMacros.collect); diff --git a/packages/@ember/object/core.ts b/packages/@ember/object/core.ts index 2dcec80ec1c..3b21efbeb82 100644 --- a/packages/@ember/object/core.ts +++ b/packages/@ember/object/core.ts @@ -20,6 +20,11 @@ import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixi import ActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler'; import makeArray from '@ember/array/make'; import { assert } from '@ember/debug'; +import { deprecateEmberObject } from '@ember/-internals/deprecations'; +import { + isFrameworkClass, + isOwnFrameworkClass, +} from '@ember/-internals/utils/lib/internal-framework-class'; import { DEBUG } from '@glimmer/env'; import { destroy, isDestroying, isDestroyed, registerDestructor } from '@glimmer/destroyable'; import { OWNER } from '@glimmer/owner'; @@ -56,6 +61,35 @@ function hasToStringExtension(val: unknown): val is HasToStringExtension { const reopen = Mixin.prototype.reopen; const wasApplied = new WeakSet(); +const INIT_OVERRIDES = new WeakMap(); + +/** + Checks whether a subclass of a framework class, such as a `Route`, + defines its own `init`. The search stops at the class that Ember owns. + + Call this after `proto()`, which puts the `init` of a classic class on + the prototype. +*/ +function overridesInit(Class: typeof CoreObject): boolean { + let result = INIT_OVERRIDES.get(Class); + + if (result === undefined) { + result = false; + + let proto = Class.prototype; + while (proto !== null && proto !== undefined && !isOwnFrameworkClass(proto.constructor)) { + if (Object.prototype.hasOwnProperty.call(proto, 'init')) { + result = true; + break; + } + proto = Object.getPrototypeOf(proto); + } + + INIT_OVERRIDES.set(Class, result); + } + + return result; +} const prototypeMixinMap = new WeakMap(); const initCalled = DEBUG ? new WeakSet() : undefined; // only used in debug builds to enable the proxy trap @@ -242,8 +276,16 @@ class CoreObject { constructor(owner?: Owner) { this[OWNER] = owner; + let Class = this.constructor as typeof CoreObject; + // prepare prototype... - (this.constructor as typeof CoreObject).proto(); + Class.proto(); + + if (!isFrameworkClass(Class)) { + deprecateEmberObject('`EmberObject`'); + } else if (overridesInit(Class)) { + deprecateEmberObject('The `init` method'); + } let self; if (DEBUG && hasUnknownProperty(this)) { diff --git a/packages/@ember/object/index.ts b/packages/@ember/object/index.ts index aff353ba9ce..c760aa56628 100644 --- a/packages/@ember/object/index.ts +++ b/packages/@ember/object/index.ts @@ -9,6 +9,8 @@ import expandProperties from '@ember/-internals/metal/lib/expand_properties'; import { getFactoryFor } from '@ember/-internals/container/lib/container'; import { setObservers } from '@ember/-internals/utils/lib/super'; import type { AnyFn } from '@ember/-internals/utility-types'; +import internalComputed from '@ember/-internals/metal/lib/computed'; +import { deprecateEmberObject } from '@ember/-internals/deprecations'; import CoreObject from '@ember/object/core'; import Observable from '@ember/object/observable'; import { @@ -24,12 +26,20 @@ export { get } from '@ember/-internals/metal/lib/property_get'; export { set, trySet } from '@ember/-internals/metal/lib/property_set'; export { default as getProperties } from '@ember/-internals/metal/lib/get_properties'; export { default as setProperties } from '@ember/-internals/metal/lib/set_properties'; -export { default as computed } from '@ember/-internals/metal/lib/computed'; /** @module @ember/object */ +/** + The public `computed`. Ember's own code imports the implementation, which + does not report the deprecation. +*/ +export const computed = function computed(this: unknown, ...args: unknown[]) { + deprecateEmberObject('`computed`'); + return (internalComputed as AnyFn).apply(this, args); +} as typeof internalComputed; + /** `EmberObject` is the main base class for all Ember objects. It is a subclass of `CoreObject` with the `Observable` mixin applied. For details, diff --git a/packages/@ember/object/observable.ts b/packages/@ember/object/observable.ts index b928183b051..6ea8d8e82f2 100644 --- a/packages/@ember/object/observable.ts +++ b/packages/@ember/object/observable.ts @@ -18,6 +18,7 @@ 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'; +import { deprecateEmberObject } from '@ember/-internals/deprecations'; export type ObserverMethod = | keyof Target @@ -423,18 +424,22 @@ interface Observable { } const Observable = Mixin[INTERNAL_MIXIN_CREATE]({ get(keyName: string) { + deprecateEmberObject('The `get` method'); return get(this, keyName); }, getProperties(...args: string[]) { + deprecateEmberObject('The `getProperties` method'); return getProperties(this, ...args); }, set(keyName: string, value: unknown) { + deprecateEmberObject('The `set` method'); return set(this, keyName, value); }, setProperties(hash: object) { + deprecateEmberObject('The `setProperties` method'); return setProperties(this, hash); }, @@ -478,6 +483,7 @@ const Observable = Mixin[INTERNAL_MIXIN_CREATE]({ }, notifyPropertyChange(keyName: string) { + deprecateEmberObject('The `notifyPropertyChange` method'); notifyPropertyChange(this, keyName); return this; }, @@ -518,6 +524,7 @@ const Observable = Mixin[INTERNAL_MIXIN_CREATE]({ }, incrementProperty(keyName: string, increment = 1) { + deprecateEmberObject('The `incrementProperty` method'); assert( 'Must pass a numeric value to incrementProperty', !isNaN(parseFloat(String(increment))) && isFinite(increment) @@ -526,6 +533,7 @@ const Observable = Mixin[INTERNAL_MIXIN_CREATE]({ }, decrementProperty(keyName: string, decrement = 1) { + deprecateEmberObject('The `decrementProperty` method'); assert( 'Must pass a numeric value to decrementProperty', (typeof decrement === 'number' || !isNaN(parseFloat(decrement))) && isFinite(decrement) @@ -534,6 +542,7 @@ const Observable = Mixin[INTERNAL_MIXIN_CREATE]({ }, toggleProperty(keyName: string) { + deprecateEmberObject('The `toggleProperty` method'); return set(this, keyName, !get(this, keyName)); }, diff --git a/packages/@ember/object/proxy.ts b/packages/@ember/object/proxy.ts index be037bfc153..b2f8c703c7c 100644 --- a/packages/@ember/object/proxy.ts +++ b/packages/@ember/object/proxy.ts @@ -5,6 +5,7 @@ import { FrameworkObject } from '@ember/object/-internals'; import _ProxyMixin from '@ember/-internals/runtime/lib/mixins/-proxy'; import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** `ObjectProxy` forwards all properties not defined by the proxy itself @@ -134,4 +135,6 @@ class ObjectProxy extends FrameworkObject { } ObjectProxy.PrototypeMixin.reopen(_ProxyMixin); +setFrameworkClass(ObjectProxy); + export default ObjectProxy; diff --git a/packages/@ember/object/tests/deprecate_ember_object_test.js b/packages/@ember/object/tests/deprecate_ember_object_test.js new file mode 100644 index 00000000000..7fd5083e7a0 --- /dev/null +++ b/packages/@ember/object/tests/deprecate_ember_object_test.js @@ -0,0 +1,216 @@ +import EmberObject, { computed, get, set } from '@ember/object'; +import CoreObject from '@ember/object/core'; +import { alias, readOnly, sort } from '@ember/object/computed'; +import Service from '@ember/service'; +import Controller from '@ember/controller'; +import Route from '@ember/routing/route'; +import { tracked } from '@glimmer/tracking'; +import { ENV } from '@ember/-internals/environment/lib/env'; +import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../-internals/deprecations'; + +const { isEnabled, isRemoved } = DEPRECATIONS.DEPRECATE_EMBER_OBJECT; + +function expectEmberObjectDeprecation(callback, message) { + expectDeprecation(callback, message, isEnabled); +} + +moduleFor( + 'EmberObject deprecation', + class extends AbstractTestCase { + [`${testUnless(isRemoved)} @test a class that extends EmberObject is deprecated`](assert) { + class Cart extends EmberObject {} + + expectEmberObjectDeprecation(() => { + let cart = Cart.create({ currency: 'USD' }); + assert.strictEqual(cart.currency, 'USD'); + }, /`EmberObject` is deprecated/); + + expectEmberObjectDeprecation(() => EmberObject.create(), /`EmberObject` is deprecated/); + expectEmberObjectDeprecation(() => CoreObject.create(), /`EmberObject` is deprecated/); + } + + ['@test framework classes and their subclasses are not deprecated'](assert) { + class Session extends Service { + @tracked user = null; + + get isLoggedIn() { + return Boolean(this.user); + } + } + + class Posts extends Route {} + class PostsController extends Controller {} + + let session = Session.create(); + session.user = 'zoey'; + assert.true(session.isLoggedIn); + + let instances = [session, Service.create(), Posts.create(), PostsController.create()]; + assert.true(instances.every((instance) => instance instanceof EmberObject)); + } + + [`${testUnless(isRemoved)} @test the object model methods are deprecated on framework classes`]( + assert + ) { + let service = Service.create(); + + expectEmberObjectDeprecation(() => service.set('count', 1), /The `set` method is deprecated/); + expectEmberObjectDeprecation(() => { + assert.strictEqual(service.get('count'), 1); + }, /The `get` method is deprecated/); + expectEmberObjectDeprecation( + () => service.setProperties({ name: 'zoey' }), + /The `setProperties` method is deprecated/ + ); + expectEmberObjectDeprecation(() => { + assert.deepEqual(service.getProperties('name', 'count'), { name: 'zoey', count: 1 }); + }, /The `getProperties` method is deprecated/); + expectEmberObjectDeprecation( + () => service.incrementProperty('count'), + /The `incrementProperty` method is deprecated/ + ); + expectEmberObjectDeprecation( + () => service.decrementProperty('count'), + /The `decrementProperty` method is deprecated/ + ); + expectEmberObjectDeprecation( + () => service.toggleProperty('isOpen'), + /The `toggleProperty` method is deprecated/ + ); + expectEmberObjectDeprecation( + () => service.notifyPropertyChange('count'), + /The `notifyPropertyChange` method is deprecated/ + ); + + assert.strictEqual(service.count, 1); + assert.true(service.isOpen); + } + + ['@test the get and set functions are not deprecated'](assert) { + let service = Service.create(); + + set(service, 'count', 1); + + assert.strictEqual(get(service, 'count'), 1); + } + + [`${testUnless(isRemoved)} @test an init method on a subclass of a framework class is deprecated`]( + assert + ) { + class Session extends Service { + init() { + super.init(...arguments); + assert.step('init'); + } + } + + class Child extends Session {} + + expectEmberObjectDeprecation(() => Session.create(), /The `init` method is deprecated/); + expectEmberObjectDeprecation(() => Child.create(), /The `init` method is deprecated/); + + assert.verifySteps(['init', 'init']); + } + + [`${testUnless(isRemoved)} @test computed and the computed macros are deprecated`](assert) { + expectEmberObjectDeprecation(() => { + class Session extends Service { + @tracked user = 'zoey'; + + @computed('user') + get name() { + return this.user; + } + } + + assert.strictEqual(Session.create().name, 'zoey'); + }, /`computed` is deprecated/); + + expectEmberObjectDeprecation( + () => alias('user.name'), + /The `alias` macro from `@ember\/object\/computed` is deprecated/ + ); + expectEmberObjectDeprecation( + () => readOnly('user.name'), + /The `readOnly` macro from `@ember\/object\/computed` is deprecated/ + ); + expectEmberObjectDeprecation( + () => sort('items', 'sortKeys'), + /The `sort` macro from `@ember\/object\/computed` is deprecated/ + ); + } + } +); + +moduleFor( + 'EmberObject deprecation: no-ember-object optional feature', + class extends AbstractTestCase { + constructor() { + super(...arguments); + this.originalFlag = ENV._NO_EMBER_OBJECT; + ENV._NO_EMBER_OBJECT = true; + } + + teardown() { + ENV._NO_EMBER_OBJECT = this.originalFlag; + } + + ['@test a class that extends EmberObject throws'](assert) { + class Cart extends EmberObject {} + + assert.throws(() => Cart.create(), /`EmberObject` is not available/); + assert.throws(() => EmberObject.create(), /`EmberObject` is not available/); + assert.throws(() => CoreObject.create(), /`EmberObject` is not available/); + } + + ['@test framework classes stay usable with native class features'](assert) { + class Session extends Service { + @tracked user = null; + + get isLoggedIn() { + return Boolean(this.user); + } + } + + let session = Session.create(); + session.user = 'zoey'; + + assert.true(session.isLoggedIn); + + set(session, 'user', null); + + assert.false(get(session, 'isLoggedIn')); + } + + ['@test the object model methods throw'](assert) { + let service = Service.create(); + + assert.throws(() => service.get('count'), /The `get` method is not available/); + assert.throws(() => service.set('count', 1), /The `set` method is not available/); + assert.throws( + () => service.setProperties({ count: 1 }), + /The `setProperties` method is not available/ + ); + assert.throws( + () => service.notifyPropertyChange('count'), + /The `notifyPropertyChange` method is not available/ + ); + } + + ['@test an init method on a subclass of a framework class throws'](assert) { + class Session extends Service { + init() { + super.init(...arguments); + } + } + + assert.throws(() => Session.create(), /The `init` method is not available/); + } + + ['@test computed and the computed macros throw'](assert) { + assert.throws(() => computed('user', function () {}), /`computed` is not available/); + assert.throws(() => alias('user.name'), /The `alias` macro .* is not available/); + } + } +); diff --git a/packages/@ember/routing/hash-location.ts b/packages/@ember/routing/hash-location.ts index 2dfd8f31b10..c318bef91cb 100644 --- a/packages/@ember/routing/hash-location.ts +++ b/packages/@ember/routing/hash-location.ts @@ -2,6 +2,7 @@ import EmberObject from '@ember/object'; import { bind } from '@ember/runloop'; import type { default as EmberLocation, UpdateCallback } from '@ember/routing/location'; import { getHash } from './lib/location-utils'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** @module @ember/routing/hash-location @@ -170,3 +171,5 @@ export default class HashLocation extends EmberObject implements EmberLocation { } } } + +setFrameworkClass(HashLocation); diff --git a/packages/@ember/routing/history-location.ts b/packages/@ember/routing/history-location.ts index 786a8fab83d..446f6e2fe57 100644 --- a/packages/@ember/routing/history-location.ts +++ b/packages/@ember/routing/history-location.ts @@ -2,6 +2,7 @@ import EmberObject from '@ember/object'; import { assert } from '@ember/debug'; import type { default as EmberLocation, UpdateCallback } from '@ember/routing/location'; import { escapeRegExp, getHash } from './lib/location-utils'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** @module @ember/routing/history-location @@ -265,3 +266,5 @@ export default class HistoryLocation extends EmberObject implements EmberLocatio } } } + +setFrameworkClass(HistoryLocation); diff --git a/packages/@ember/routing/lib/routing-service.ts b/packages/@ember/routing/lib/routing-service.ts index b44e2daf70c..e91bc686d38 100644 --- a/packages/@ember/routing/lib/routing-service.ts +++ b/packages/@ember/routing/lib/routing-service.ts @@ -4,7 +4,7 @@ import { getOwner } from '@ember/-internals/owner'; import { assert } from '@ember/debug'; -import { readOnly } from '@ember/object/computed'; +import { readOnly } from '@ember/object/lib/computed/computed_macros'; import Service from '@ember/service'; import type Route from '@ember/routing/route'; import type { ModelFor } from '@ember/routing/route'; diff --git a/packages/@ember/routing/none-location.ts b/packages/@ember/routing/none-location.ts index e396d8ef335..35f4d6c603b 100644 --- a/packages/@ember/routing/none-location.ts +++ b/packages/@ember/routing/none-location.ts @@ -2,6 +2,7 @@ import EmberObject from '@ember/object'; import { assert } from '@ember/debug'; import type { default as EmberLocation, UpdateCallback } from '@ember/routing/location'; import { escapeRegExp } from './lib/location-utils'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** @module @ember/routing/none-location @@ -131,3 +132,5 @@ NoneLocation.reopen({ path: '', rootURL: '/', }); + +setFrameworkClass(NoneLocation); diff --git a/packages/@ember/routing/route.ts b/packages/@ember/routing/route.ts index d3078d53886..132fcf99116 100644 --- a/packages/@ember/routing/route.ts +++ b/packages/@ember/routing/route.ts @@ -48,6 +48,7 @@ import { queryParamType, stashParamNames, } from './lib/utils'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; export interface ExtendedInternalRouteInfo extends InternalRouteInfo> { _names?: unknown[]; @@ -2083,4 +2084,6 @@ Route.reopen({ setRouteManager((owner) => new ClassicRouteManager(owner), Route); +setFrameworkClass(Route); + export default Route; diff --git a/packages/@ember/routing/router-service.ts b/packages/@ember/routing/router-service.ts index 0e4b6cae977..ba9c411d7bd 100644 --- a/packages/@ember/routing/router-service.ts +++ b/packages/@ember/routing/router-service.ts @@ -3,7 +3,7 @@ */ import { getOwner } from '@ember/-internals/owner'; import { assert } from '@ember/debug'; -import { readOnly } from '@ember/object/computed'; +import { readOnly } from '@ember/object/lib/computed/computed_macros'; import Service from '@ember/service'; import { consumeTag } from '@glimmer/validator/lib/tracking'; import { tagFor } from '@glimmer/validator/lib/meta'; diff --git a/packages/@ember/routing/router.ts b/packages/@ember/routing/router.ts index 3f6a356eaa1..1780a2617cc 100644 --- a/packages/@ember/routing/router.ts +++ b/packages/@ember/routing/router.ts @@ -4,6 +4,7 @@ import { OutletState } from '@ember/-internals/routing/route-managers/outlet-sta import computed from '@ember/-internals/metal/lib/computed'; import { get } from '@ember/-internals/metal/lib/property_get'; import { set } from '@ember/-internals/metal/lib/property_set'; +import { notifyPropertyChange } from '@ember/-internals/metal/lib/property_events'; import type Owner from '@ember/owner'; import { getOwner } from '@ember/owner'; import { getRouteManager } from '@ember/-internals/routing/route-managers/registry'; @@ -70,6 +71,7 @@ import { eventedOff, eventedHas, } from '@ember/-internals/metal/lib/evented-methods'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** @module @ember/routing/router @@ -78,8 +80,8 @@ import { function defaultDidTransition(this: EmberRouter, infos: InternalRouteInfo[]) { updatePaths(this); - this.notifyPropertyChange('url'); - this.set('currentState', this.targetState); + notifyPropertyChange(this, 'url'); + set(this, 'currentState', this.targetState); if (DEBUG) { // @ts-expect-error namespace isn't public @@ -548,12 +550,12 @@ class EmberRouter extends EmberObject { // to make router.currentRoute.name consistent with router.currentRouteName // see https://github.com/emberjs/ember.js/issues/19449 if (transition.isIntermediate) { - router.set('currentRoute', transition.to); + set(router, 'currentRoute', transition.to); } } routeDidChange(transition: Transition) { - router.set('currentRoute', transition.to); + set(router, 'currentRoute', transition.to); once(() => { sendEvent(router, 'routeDidChange', [transition]); @@ -1782,9 +1784,9 @@ function didBeginTransition(transition: Transition, router: EmberRouter) { let routerState = new RouterState(router, router._routerMicrolib, transition[STATE_SYMBOL]!); if (!router.currentState) { - router.set('currentState', routerState); + set(router, 'currentState', routerState); } - router.set('targetState', routerState); + set(router, 'targetState', routerState); transition.promise = transition.catch((error: any) => { if (router._isErrorHandled(error)) { @@ -1847,4 +1849,6 @@ EmberRouter.reopen({ }), }); +setFrameworkClass(EmberRouter); + export default EmberRouter; diff --git a/packages/@ember/service/index.ts b/packages/@ember/service/index.ts index 6eafac4ebf7..b41dc5a112a 100644 --- a/packages/@ember/service/index.ts +++ b/packages/@ember/service/index.ts @@ -6,6 +6,7 @@ import type { } from '@ember/-internals/metal/lib/decorator'; import metalInject from '@ember/-internals/metal/lib/injected_property'; import type { InjectionDecorator } from '@ember/-internals/metal/lib/decorator-util'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** @module @ember/service @@ -144,3 +145,5 @@ export default class Service extends FrameworkObject { // registered services which have no base class. // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface Registry extends Record {} + +setFrameworkClass(Service); diff --git a/packages/ember-testing/lib/adapters/adapter.ts b/packages/ember-testing/lib/adapters/adapter.ts index 26876da9d42..94ca56f6dc7 100644 --- a/packages/ember-testing/lib/adapters/adapter.ts +++ b/packages/ember-testing/lib/adapters/adapter.ts @@ -1,4 +1,5 @@ import EmberObject from '@ember/object'; +import { setFrameworkClass } from '@ember/-internals/utils/lib/internal-framework-class'; /** @module @ember/test @@ -58,4 +59,6 @@ const Adapter = EmberObject.extend({ }, }); +setFrameworkClass(Adapter); + export default Adapter; diff --git a/testem.cjs b/testem.cjs index 201511b862a..b683ec2fef5 100644 --- a/testem.cjs +++ b/testem.cjs @@ -17,6 +17,10 @@ const variants = [ // Throw on unexpected deprecations. Defaults to true if not set explicitly. 'RAISE_ON_DEPRECATION', + + // Turns on the `no-ember-object` optional feature, which is a preview of the + // removal of `EmberObject`: the deprecated APIs throw. + 'NO_EMBER_OBJECT', ]; let queryString = ''; diff --git a/tests/docs/expected.cjs b/tests/docs/expected.cjs index 2f945d871c8..b8e39dcc219 100644 --- a/tests/docs/expected.cjs +++ b/tests/docs/expected.cjs @@ -9,6 +9,7 @@ module.exports = { '[]', '_DEBUG_RENDER_TREE', '_DEFAULT_ASYNC_OBSERVERS', + '_NO_EMBER_OBJECT', '_RERENDER_LOOP_LIMIT', '_ALL_DEPRECATIONS_ENABLED', '_OVERRIDE_DEPRECATION_VERSION', From e6406e1e38c7a24e79e5cde4d22172b567bc45a5 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:44:46 -0400 Subject: [PATCH 2/3] Read the EmberObject preview flag from import.meta.env Replace `EmberENV._NO_EMBER_OBJECT` with the build-time flag `import.meta.env?.EMBER_DROP_EMBER_OBJECT`, as RFC 1247 (build-time feature flags) describes for sveltable deprecations. The bundler of the app replaces the expression with a literal, so the published build keeps the expression as it is. The test build passes `EMBER_DROP_*` variables through the Vite `envPrefix`. A build has one value for the flag, so the test file registers either the deprecation module or the flag module. Co-Authored-By: Claude Opus 5.5 --- .../@ember/-internals/deprecations/index.ts | 14 +- .../@ember/-internals/environment/lib/env.ts | 16 - .../tests/deprecate_ember_object_test.js | 328 +++++++++--------- testem.cjs | 4 - tests/docs/expected.cjs | 1 - vite.config.mjs | 2 +- 6 files changed, 176 insertions(+), 189 deletions(-) diff --git a/packages/@ember/-internals/deprecations/index.ts b/packages/@ember/-internals/deprecations/index.ts index affba7dadf7..5ba4c6e7486 100644 --- a/packages/@ember/-internals/deprecations/index.ts +++ b/packages/@ember/-internals/deprecations/index.ts @@ -191,18 +191,24 @@ export function deprecateUntil(message: string, deprecation: DeprecationObject) deprecate(message, deprecation.test, options); } +declare global { + interface ImportMetaEnv { + EMBER_DROP_EMBER_OBJECT?: boolean | string; + } +} + /** Reports the use of an API that RFC 1234 removes with `EmberObject`. - The `no-ember-object` optional feature is a preview of the removal: - when it is on, the API throws. + The `EMBER_DROP_EMBER_OBJECT` build flag is a preview of the removal: + when it is set, the API throws. @private */ export function deprecateEmberObject(api: string) { - if (ENV._NO_EMBER_OBJECT) { + if (import.meta.env?.EMBER_DROP_EMBER_OBJECT) { throw new Error( - `${api} is not available, because the \`no-ember-object\` optional feature is on. Please see ${DEPRECATIONS.DEPRECATE_EMBER_OBJECT.options.url} for more details.` + `${api} is not available, because the \`EMBER_DROP_EMBER_OBJECT\` build flag is set. Please see ${DEPRECATIONS.DEPRECATE_EMBER_OBJECT.options.url} for more details.` ); } diff --git a/packages/@ember/-internals/environment/lib/env.ts b/packages/@ember/-internals/environment/lib/env.ts index 92139441391..4c3e37fe4a8 100644 --- a/packages/@ember/-internals/environment/lib/env.ts +++ b/packages/@ember/-internals/environment/lib/env.ts @@ -125,22 +125,6 @@ export const ENV = { */ _DEFAULT_ASYNC_OBSERVERS: false, - /** - Whether `EmberObject` and the APIs of the Ember object model throw. - - This is a preview of the removal of `EmberObject`. - - This is not intended to be set directly, as the implementation may change in - the future. Use `@ember/optional-features` instead. - - @property _NO_EMBER_OBJECT - @for EmberENV - @type Boolean - @default false - @private - */ - _NO_EMBER_OBJECT: false, - /** Controls the maximum number of scheduled rerenders without "settling". In general, applications should not need to modify this environment variable, but please diff --git a/packages/@ember/object/tests/deprecate_ember_object_test.js b/packages/@ember/object/tests/deprecate_ember_object_test.js index 7fd5083e7a0..f0c154a3a78 100644 --- a/packages/@ember/object/tests/deprecate_ember_object_test.js +++ b/packages/@ember/object/tests/deprecate_ember_object_test.js @@ -5,212 +5,214 @@ import Service from '@ember/service'; import Controller from '@ember/controller'; import Route from '@ember/routing/route'; import { tracked } from '@glimmer/tracking'; -import { ENV } from '@ember/-internals/environment/lib/env'; import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; import { DEPRECATIONS } from '../../-internals/deprecations'; const { isEnabled, isRemoved } = DEPRECATIONS.DEPRECATE_EMBER_OBJECT; +/* + The flag is a build-time value, so one build runs only one of the two + modules below. +*/ +const isDropped = Boolean(import.meta.env?.EMBER_DROP_EMBER_OBJECT); + function expectEmberObjectDeprecation(callback, message) { expectDeprecation(callback, message, isEnabled); } -moduleFor( - 'EmberObject deprecation', - class extends AbstractTestCase { - [`${testUnless(isRemoved)} @test a class that extends EmberObject is deprecated`](assert) { - class Cart extends EmberObject {} +if (!isDropped) { + moduleFor( + 'EmberObject deprecation', + class extends AbstractTestCase { + [`${testUnless(isRemoved)} @test a class that extends EmberObject is deprecated`](assert) { + class Cart extends EmberObject {} - expectEmberObjectDeprecation(() => { - let cart = Cart.create({ currency: 'USD' }); - assert.strictEqual(cart.currency, 'USD'); - }, /`EmberObject` is deprecated/); + expectEmberObjectDeprecation(() => { + let cart = Cart.create({ currency: 'USD' }); + assert.strictEqual(cart.currency, 'USD'); + }, /`EmberObject` is deprecated/); - expectEmberObjectDeprecation(() => EmberObject.create(), /`EmberObject` is deprecated/); - expectEmberObjectDeprecation(() => CoreObject.create(), /`EmberObject` is deprecated/); - } + expectEmberObjectDeprecation(() => EmberObject.create(), /`EmberObject` is deprecated/); + expectEmberObjectDeprecation(() => CoreObject.create(), /`EmberObject` is deprecated/); + } - ['@test framework classes and their subclasses are not deprecated'](assert) { - class Session extends Service { - @tracked user = null; + ['@test framework classes and their subclasses are not deprecated'](assert) { + class Session extends Service { + @tracked user = null; - get isLoggedIn() { - return Boolean(this.user); + get isLoggedIn() { + return Boolean(this.user); + } } - } - class Posts extends Route {} - class PostsController extends Controller {} + class Posts extends Route {} + class PostsController extends Controller {} - let session = Session.create(); - session.user = 'zoey'; - assert.true(session.isLoggedIn); + let session = Session.create(); + session.user = 'zoey'; + assert.true(session.isLoggedIn); - let instances = [session, Service.create(), Posts.create(), PostsController.create()]; - assert.true(instances.every((instance) => instance instanceof EmberObject)); - } + let instances = [session, Service.create(), Posts.create(), PostsController.create()]; + assert.true(instances.every((instance) => instance instanceof EmberObject)); + } - [`${testUnless(isRemoved)} @test the object model methods are deprecated on framework classes`]( - assert - ) { - let service = Service.create(); - - expectEmberObjectDeprecation(() => service.set('count', 1), /The `set` method is deprecated/); - expectEmberObjectDeprecation(() => { - assert.strictEqual(service.get('count'), 1); - }, /The `get` method is deprecated/); - expectEmberObjectDeprecation( - () => service.setProperties({ name: 'zoey' }), - /The `setProperties` method is deprecated/ - ); - expectEmberObjectDeprecation(() => { - assert.deepEqual(service.getProperties('name', 'count'), { name: 'zoey', count: 1 }); - }, /The `getProperties` method is deprecated/); - expectEmberObjectDeprecation( - () => service.incrementProperty('count'), - /The `incrementProperty` method is deprecated/ - ); - expectEmberObjectDeprecation( - () => service.decrementProperty('count'), - /The `decrementProperty` method is deprecated/ - ); - expectEmberObjectDeprecation( - () => service.toggleProperty('isOpen'), - /The `toggleProperty` method is deprecated/ - ); - expectEmberObjectDeprecation( - () => service.notifyPropertyChange('count'), - /The `notifyPropertyChange` method is deprecated/ - ); - - assert.strictEqual(service.count, 1); - assert.true(service.isOpen); - } + [`${testUnless(isRemoved)} @test the object model methods are deprecated on framework classes`]( + assert + ) { + let service = Service.create(); + + expectEmberObjectDeprecation( + () => service.set('count', 1), + /The `set` method is deprecated/ + ); + expectEmberObjectDeprecation(() => { + assert.strictEqual(service.get('count'), 1); + }, /The `get` method is deprecated/); + expectEmberObjectDeprecation( + () => service.setProperties({ name: 'zoey' }), + /The `setProperties` method is deprecated/ + ); + expectEmberObjectDeprecation(() => { + assert.deepEqual(service.getProperties('name', 'count'), { name: 'zoey', count: 1 }); + }, /The `getProperties` method is deprecated/); + expectEmberObjectDeprecation( + () => service.incrementProperty('count'), + /The `incrementProperty` method is deprecated/ + ); + expectEmberObjectDeprecation( + () => service.decrementProperty('count'), + /The `decrementProperty` method is deprecated/ + ); + expectEmberObjectDeprecation( + () => service.toggleProperty('isOpen'), + /The `toggleProperty` method is deprecated/ + ); + expectEmberObjectDeprecation( + () => service.notifyPropertyChange('count'), + /The `notifyPropertyChange` method is deprecated/ + ); + + assert.strictEqual(service.count, 1); + assert.true(service.isOpen); + } - ['@test the get and set functions are not deprecated'](assert) { - let service = Service.create(); + ['@test the get and set functions are not deprecated'](assert) { + let service = Service.create(); - set(service, 'count', 1); + set(service, 'count', 1); - assert.strictEqual(get(service, 'count'), 1); - } + assert.strictEqual(get(service, 'count'), 1); + } - [`${testUnless(isRemoved)} @test an init method on a subclass of a framework class is deprecated`]( - assert - ) { - class Session extends Service { - init() { - super.init(...arguments); - assert.step('init'); + [`${testUnless(isRemoved)} @test an init method on a subclass of a framework class is deprecated`]( + assert + ) { + class Session extends Service { + init() { + super.init(...arguments); + assert.step('init'); + } } - } - class Child extends Session {} + class Child extends Session {} - expectEmberObjectDeprecation(() => Session.create(), /The `init` method is deprecated/); - expectEmberObjectDeprecation(() => Child.create(), /The `init` method is deprecated/); + expectEmberObjectDeprecation(() => Session.create(), /The `init` method is deprecated/); + expectEmberObjectDeprecation(() => Child.create(), /The `init` method is deprecated/); - assert.verifySteps(['init', 'init']); - } + assert.verifySteps(['init', 'init']); + } - [`${testUnless(isRemoved)} @test computed and the computed macros are deprecated`](assert) { - expectEmberObjectDeprecation(() => { - class Session extends Service { - @tracked user = 'zoey'; + [`${testUnless(isRemoved)} @test computed and the computed macros are deprecated`](assert) { + expectEmberObjectDeprecation(() => { + class Session extends Service { + @tracked user = 'zoey'; - @computed('user') - get name() { - return this.user; + @computed('user') + get name() { + return this.user; + } } - } - - assert.strictEqual(Session.create().name, 'zoey'); - }, /`computed` is deprecated/); - - expectEmberObjectDeprecation( - () => alias('user.name'), - /The `alias` macro from `@ember\/object\/computed` is deprecated/ - ); - expectEmberObjectDeprecation( - () => readOnly('user.name'), - /The `readOnly` macro from `@ember\/object\/computed` is deprecated/ - ); - expectEmberObjectDeprecation( - () => sort('items', 'sortKeys'), - /The `sort` macro from `@ember\/object\/computed` is deprecated/ - ); - } - } -); - -moduleFor( - 'EmberObject deprecation: no-ember-object optional feature', - class extends AbstractTestCase { - constructor() { - super(...arguments); - this.originalFlag = ENV._NO_EMBER_OBJECT; - ENV._NO_EMBER_OBJECT = true; - } - teardown() { - ENV._NO_EMBER_OBJECT = this.originalFlag; + assert.strictEqual(Session.create().name, 'zoey'); + }, /`computed` is deprecated/); + + expectEmberObjectDeprecation( + () => alias('user.name'), + /The `alias` macro from `@ember\/object\/computed` is deprecated/ + ); + expectEmberObjectDeprecation( + () => readOnly('user.name'), + /The `readOnly` macro from `@ember\/object\/computed` is deprecated/ + ); + expectEmberObjectDeprecation( + () => sort('items', 'sortKeys'), + /The `sort` macro from `@ember\/object\/computed` is deprecated/ + ); + } } + ); +} - ['@test a class that extends EmberObject throws'](assert) { - class Cart extends EmberObject {} +if (isDropped) { + moduleFor( + 'EmberObject deprecation: EMBER_DROP_EMBER_OBJECT build flag', + class extends AbstractTestCase { + ['@test a class that extends EmberObject throws'](assert) { + class Cart extends EmberObject {} - assert.throws(() => Cart.create(), /`EmberObject` is not available/); - assert.throws(() => EmberObject.create(), /`EmberObject` is not available/); - assert.throws(() => CoreObject.create(), /`EmberObject` is not available/); - } + assert.throws(() => Cart.create(), /`EmberObject` is not available/); + assert.throws(() => EmberObject.create(), /`EmberObject` is not available/); + assert.throws(() => CoreObject.create(), /`EmberObject` is not available/); + } - ['@test framework classes stay usable with native class features'](assert) { - class Session extends Service { - @tracked user = null; + ['@test framework classes stay usable with native class features'](assert) { + class Session extends Service { + @tracked user = null; - get isLoggedIn() { - return Boolean(this.user); + get isLoggedIn() { + return Boolean(this.user); + } } - } - let session = Session.create(); - session.user = 'zoey'; + let session = Session.create(); + session.user = 'zoey'; - assert.true(session.isLoggedIn); + assert.true(session.isLoggedIn); - set(session, 'user', null); + set(session, 'user', null); - assert.false(get(session, 'isLoggedIn')); - } + assert.false(get(session, 'isLoggedIn')); + } - ['@test the object model methods throw'](assert) { - let service = Service.create(); - - assert.throws(() => service.get('count'), /The `get` method is not available/); - assert.throws(() => service.set('count', 1), /The `set` method is not available/); - assert.throws( - () => service.setProperties({ count: 1 }), - /The `setProperties` method is not available/ - ); - assert.throws( - () => service.notifyPropertyChange('count'), - /The `notifyPropertyChange` method is not available/ - ); - } + ['@test the object model methods throw'](assert) { + let service = Service.create(); + + assert.throws(() => service.get('count'), /The `get` method is not available/); + assert.throws(() => service.set('count', 1), /The `set` method is not available/); + assert.throws( + () => service.setProperties({ count: 1 }), + /The `setProperties` method is not available/ + ); + assert.throws( + () => service.notifyPropertyChange('count'), + /The `notifyPropertyChange` method is not available/ + ); + } - ['@test an init method on a subclass of a framework class throws'](assert) { - class Session extends Service { - init() { - super.init(...arguments); + ['@test an init method on a subclass of a framework class throws'](assert) { + class Session extends Service { + init() { + super.init(...arguments); + } } - } - assert.throws(() => Session.create(), /The `init` method is not available/); - } + assert.throws(() => Session.create(), /The `init` method is not available/); + } - ['@test computed and the computed macros throw'](assert) { - assert.throws(() => computed('user', function () {}), /`computed` is not available/); - assert.throws(() => alias('user.name'), /The `alias` macro .* is not available/); + ['@test computed and the computed macros throw'](assert) { + assert.throws(() => computed('user', function () {}), /`computed` is not available/); + assert.throws(() => alias('user.name'), /The `alias` macro .* is not available/); + } } - } -); + ); +} diff --git a/testem.cjs b/testem.cjs index b683ec2fef5..201511b862a 100644 --- a/testem.cjs +++ b/testem.cjs @@ -17,10 +17,6 @@ const variants = [ // Throw on unexpected deprecations. Defaults to true if not set explicitly. 'RAISE_ON_DEPRECATION', - - // Turns on the `no-ember-object` optional feature, which is a preview of the - // removal of `EmberObject`: the deprecated APIs throw. - 'NO_EMBER_OBJECT', ]; let queryString = ''; diff --git a/tests/docs/expected.cjs b/tests/docs/expected.cjs index b8e39dcc219..2f945d871c8 100644 --- a/tests/docs/expected.cjs +++ b/tests/docs/expected.cjs @@ -9,7 +9,6 @@ module.exports = { '[]', '_DEBUG_RENDER_TREE', '_DEFAULT_ASYNC_OBSERVERS', - '_NO_EMBER_OBJECT', '_RERENDER_LOOP_LIMIT', '_ALL_DEPRECATIONS_ENABLED', '_OVERRIDE_DEPRECATION_VERSION', diff --git a/vite.config.mjs b/vite.config.mjs index 768765fc057..e35a56ca8ee 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -74,7 +74,7 @@ export default defineConfig(({ mode }) => { // the stock esbuild support for typescript is horribly broken. For example, // it will simply remove your decorators. esbuild: false, - envPrefix: ['VM_', 'VITE_'], + envPrefix: ['VM_', 'VITE_', 'EMBER_DROP_'], }; }); From c611b9e84d8c4787c1d75d2f33c6fa352b3d07a1 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:52:21 -0400 Subject: [PATCH 3/3] Rename the preview flag to EMBER_DROP_CLASSIC_FEATURES One flag covers the classic features as a group, so that later changes can put more of them behind the same flag. Co-Authored-By: Claude Opus 5.5 --- packages/@ember/-internals/deprecations/index.ts | 8 ++++---- .../@ember/object/tests/deprecate_ember_object_test.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/@ember/-internals/deprecations/index.ts b/packages/@ember/-internals/deprecations/index.ts index 5ba4c6e7486..f28aad7de22 100644 --- a/packages/@ember/-internals/deprecations/index.ts +++ b/packages/@ember/-internals/deprecations/index.ts @@ -193,22 +193,22 @@ export function deprecateUntil(message: string, deprecation: DeprecationObject) declare global { interface ImportMetaEnv { - EMBER_DROP_EMBER_OBJECT?: boolean | string; + EMBER_DROP_CLASSIC_FEATURES?: boolean | string; } } /** Reports the use of an API that RFC 1234 removes with `EmberObject`. - The `EMBER_DROP_EMBER_OBJECT` build flag is a preview of the removal: + The `EMBER_DROP_CLASSIC_FEATURES` build flag is a preview of the removal: when it is set, the API throws. @private */ export function deprecateEmberObject(api: string) { - if (import.meta.env?.EMBER_DROP_EMBER_OBJECT) { + if (import.meta.env?.EMBER_DROP_CLASSIC_FEATURES) { throw new Error( - `${api} is not available, because the \`EMBER_DROP_EMBER_OBJECT\` build flag is set. Please see ${DEPRECATIONS.DEPRECATE_EMBER_OBJECT.options.url} for more details.` + `${api} is not available, because the \`EMBER_DROP_CLASSIC_FEATURES\` build flag is set. Please see ${DEPRECATIONS.DEPRECATE_EMBER_OBJECT.options.url} for more details.` ); } diff --git a/packages/@ember/object/tests/deprecate_ember_object_test.js b/packages/@ember/object/tests/deprecate_ember_object_test.js index f0c154a3a78..d6bd4f67ce5 100644 --- a/packages/@ember/object/tests/deprecate_ember_object_test.js +++ b/packages/@ember/object/tests/deprecate_ember_object_test.js @@ -14,7 +14,7 @@ const { isEnabled, isRemoved } = DEPRECATIONS.DEPRECATE_EMBER_OBJECT; The flag is a build-time value, so one build runs only one of the two modules below. */ -const isDropped = Boolean(import.meta.env?.EMBER_DROP_EMBER_OBJECT); +const isDropped = Boolean(import.meta.env?.EMBER_DROP_CLASSIC_FEATURES); function expectEmberObjectDeprecation(callback, message) { expectDeprecation(callback, message, isEnabled); @@ -155,7 +155,7 @@ if (!isDropped) { if (isDropped) { moduleFor( - 'EmberObject deprecation: EMBER_DROP_EMBER_OBJECT build flag', + 'EmberObject deprecation: EMBER_DROP_CLASSIC_FEATURES build flag', class extends AbstractTestCase { ['@test a class that extends EmberObject throws'](assert) { class Cart extends EmberObject {}