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..f28aad7de22 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,30 @@ export function deprecateUntil(message: string, deprecation: DeprecationObject) } deprecate(message, deprecation.test, options); } + +declare global { + interface ImportMetaEnv { + EMBER_DROP_CLASSIC_FEATURES?: boolean | string; + } +} + +/** + Reports the use of an API that RFC 1234 removes with `EmberObject`. + + 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_CLASSIC_FEATURES) { + throw new Error( + `${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.` + ); + } + + deprecateUntil( + `${api} is deprecated. Use native classes with tracked properties instead.`, + DEPRECATIONS.DEPRECATE_EMBER_OBJECT + ); +} 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..d6bd4f67ce5 --- /dev/null +++ b/packages/@ember/object/tests/deprecate_ember_object_test.js @@ -0,0 +1,218 @@ +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 { 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_CLASSIC_FEATURES); + +function expectEmberObjectDeprecation(callback, message) { + expectDeprecation(callback, message, isEnabled); +} + +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(() => 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/ + ); + } + } + ); +} + +if (isDropped) { + moduleFor( + 'EmberObject deprecation: EMBER_DROP_CLASSIC_FEATURES 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/); + } + + ['@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/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_'], }; });