From 66886f4249dbf10bcaa256cf00bc53bac745e377 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:02:22 -0400 Subject: [PATCH 1/3] Deprecate the computed macros (RFC 1234) Add the `deprecate-computed-macros` deprecation for the macros of `@ember/object/computed` that had no deprecation yet: `alias`, `and`, `bool`, `deprecatingAlias`, `equal`, `gt`, `gte`, `lt`, `lte`, `match`, `not`, `oneWay`, `or`, `readOnly` and `reads`. Each macro reports after its decorator-misuse assertion. The public `alias` is now a function in `@ember/object/computed` that reports and then calls the implementation in `@ember/-internals/metal`. `RouterService` and the private routing service used the public `readOnly`. They now build the same read-only alias from the implementation, so their behavior does not change. A getter with `@dependentKeyCompat` is not a replacement here: a second reader of the getter can hide an invalidation from a classic computed property that depends on it. A new test covers a computed property that depends on `router.currentURL`. Tests of the macros are skipped when the deprecation is removed, and expect the deprecation when it is enabled. Tests of other features no longer use the macros. Co-Authored-By: Claude Opus 5.5 --- .../@ember/-internals/deprecations/index.ts | 7 + .../components/curly-components-test.js | 10 +- .../glimmer/tests/integration/content-test.js | 7 +- .../tests/system/array_proxy/length_test.js | 15 +- packages/@ember/object/computed.ts | 17 +- .../object/lib/computed/computed_macros.ts | 45 +++++ .../tests/computed/computed_macros_test.js | 154 +++++++++++++++--- packages/@ember/object/tests/computed_test.js | 28 +++- packages/@ember/object/tests/create_test.js | 9 +- .../@ember/routing/lib/routing-service.ts | 6 +- packages/@ember/routing/router-service.ts | 6 +- .../currenturl_lifecycle_test.js | 44 ++++- 12 files changed, 306 insertions(+), 42 deletions(-) diff --git a/packages/@ember/-internals/deprecations/index.ts b/packages/@ember/-internals/deprecations/index.ts index b8a83a7054d..7c18ab9cda3 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_COMPUTED_MACROS: deprecation({ + id: 'deprecate-computed-macros', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '9.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-computed-macros', + }), }; export function deprecateUntil(message: string, deprecation: DeprecationObject) { diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js index 07a2e782a3d..c74db332bf0 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js @@ -6,7 +6,10 @@ import { equalsElement, runTask, defineSimpleModifier, + expectDeprecation, + testUnless, } from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; import { tracked } from '@ember/-internals/metal'; import { alias } from '@ember/object/computed'; @@ -1571,7 +1574,12 @@ moduleFor( this.assertText('3'); } - ['@test ensure aliases are watched properly [GH#17243]']() { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test ensure aliases are watched properly [GH#17243]`]() { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let fooInstance, barInstance; let FooComponent = class extends GlimmerComponent { diff --git a/packages/@ember/-internals/glimmer/tests/integration/content-test.js b/packages/@ember/-internals/glimmer/tests/integration/content-test.js index 2825d162a18..db996125b83 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/content-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/content-test.js @@ -640,7 +640,12 @@ class DynamicContentTest extends RenderingTestCase { this.assertInvariants(); } - ['@test it can render a readOnly property of a path']() { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test it can render a readOnly property of a path`]() { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let Messenger = class extends EmberObject { @readOnly('a.b.c') message; diff --git a/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js b/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js index 13408d36ee7..80b642e21d9 100644 --- a/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js +++ b/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js @@ -1,6 +1,6 @@ import ArrayProxy from '@ember/array/proxy'; -import EmberObject, { observer } from '@ember/object'; -import { oneWay as reads, not } from '@ember/object/computed'; +import EmberObject, { computed, observer } from '@ember/object'; +import { oneWay as reads } from '@ember/object/computed'; import { A as a } from '@ember/array'; import { moduleFor, @@ -39,8 +39,10 @@ moduleFor( assert ) { let proxy = class extends ArrayProxy { - @not('length') - isEmpty; + @computed('length') + get isEmpty() { + return !this.length; + } }.create({ content: a([1, 2, 3]), }); @@ -186,6 +188,11 @@ moduleFor( async [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test array proxy + aliasedProperty complex test`]( assert ) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let aCalled, bCalled, cCalled, dCalled, eCalled; aCalled = bCalled = cCalled = dCalled = eCalled = 0; diff --git a/packages/@ember/object/computed.ts b/packages/@ember/object/computed.ts index 5e4d41bc33b..a00e2f120a3 100644 --- a/packages/@ember/object/computed.ts +++ b/packages/@ember/object/computed.ts @@ -1,6 +1,21 @@ 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'; + +import { assert } from '@ember/debug'; +import internalAlias, { type AliasDecorator } from '@ember/-internals/metal/lib/alias'; +import { isDecoratorCall } from '@ember/-internals/metal/lib/decorator'; +import { deprecateComputedMacro } from './lib/computed/computed_macros'; + +export function alias(altKey: string): AliasDecorator { + assert( + 'You attempted to use @alias as a decorator directly, but it requires a `altKey` parameter', + !isDecoratorCall(Array.prototype.slice.call(arguments)) + ); + + deprecateComputedMacro('alias'); + + return internalAlias(altKey); +} export { empty, diff --git a/packages/@ember/object/lib/computed/computed_macros.ts b/packages/@ember/object/lib/computed/computed_macros.ts index 9d7a2f8d7c3..7638de8c5a0 100644 --- a/packages/@ember/object/lib/computed/computed_macros.ts +++ b/packages/@ember/object/lib/computed/computed_macros.ts @@ -15,6 +15,13 @@ import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; @module @ember/object */ +export function deprecateComputedMacro(name: string) { + deprecateUntil( + `\`${name}\` from \`@ember/object/computed\` is deprecated. Use a getter instead.`, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS + ); +} + function expandPropertiesToArray(predicateName: string, properties: string[]) { let expandedProperties: string[] = []; @@ -43,6 +50,8 @@ function generateComputedWithPredicate(name: string, predicate: (value: unknown) !isDecoratorCall(properties) ); + deprecateComputedMacro(name); + let dependentKeys = expandPropertiesToArray(name, properties); let computedFunc = computed(...dependentKeys, function () { @@ -250,6 +259,7 @@ export function none(dependentKey: string) { @return {ComputedProperty} computed property which returns inverse of the original value for property @public + @deprecated Use a getter instead. */ export function not(dependentKey: string) { assert( @@ -257,6 +267,8 @@ export function not(dependentKey: string) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateComputedMacro('not'); + return computed(dependentKey, function () { return !get(this, dependentKey); }); @@ -298,6 +310,7 @@ export function not(dependentKey: string) { @return {ComputedProperty} computed property which converts to boolean the original value for property @public + @deprecated Use a getter instead. */ export function bool(dependentKey: string) { assert( @@ -305,6 +318,8 @@ export function bool(dependentKey: string) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateComputedMacro('bool'); + return computed(dependentKey, function () { return Boolean(get(this, dependentKey)); }); @@ -344,6 +359,7 @@ export function bool(dependentKey: string) { @return {ComputedProperty} computed property which match the original value for property against a given RegExp @public + @deprecated Use a getter instead. */ export function match(dependentKey: string, regexp: RegExp) { assert( @@ -351,6 +367,8 @@ export function match(dependentKey: string, regexp: RegExp) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateComputedMacro('match'); + return computed(dependentKey, function () { let value = get(this, dependentKey); return regexp.test(value); @@ -390,6 +408,7 @@ export function match(dependentKey: string, regexp: RegExp) { @return {ComputedProperty} computed property which returns true if the original value for property is equal to the given value. @public + @deprecated Use a getter instead. */ export function equal(dependentKey: string, value: unknown) { assert( @@ -397,6 +416,8 @@ export function equal(dependentKey: string, value: unknown) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateComputedMacro('equal'); + return computed(dependentKey, function () { return get(this, dependentKey) === value; }); @@ -435,6 +456,7 @@ export function equal(dependentKey: string, value: unknown) { @return {ComputedProperty} computed property which returns true if the original value for property is greater than given value. @public + @deprecated Use a getter instead. */ export function gt(dependentKey: string, value: number) { assert( @@ -442,6 +464,8 @@ export function gt(dependentKey: string, value: number) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateComputedMacro('gt'); + return computed(dependentKey, function () { return get(this, dependentKey) > value; }); @@ -480,6 +504,7 @@ export function gt(dependentKey: string, value: number) { @return {ComputedProperty} computed property which returns true if the original value for property is greater or equal then given value. @public + @deprecated Use a getter instead. */ export function gte(dependentKey: string, value: number) { assert( @@ -487,6 +512,8 @@ export function gte(dependentKey: string, value: number) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateComputedMacro('gte'); + return computed(dependentKey, function () { return get(this, dependentKey) >= value; }); @@ -525,6 +552,7 @@ export function gte(dependentKey: string, value: number) { @return {ComputedProperty} computed property which returns true if the original value for property is less then given value. @public + @deprecated Use a getter instead. */ export function lt(dependentKey: string, value: number) { assert( @@ -532,6 +560,8 @@ export function lt(dependentKey: string, value: number) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateComputedMacro('lt'); + return computed(dependentKey, function () { return get(this, dependentKey) < value; }); @@ -570,6 +600,7 @@ export function lt(dependentKey: string, value: number) { @return {ComputedProperty} computed property which returns true if the original value for property is less or equal than given value. @public + @deprecated Use a getter instead. */ export function lte(dependentKey: string, value: number) { assert( @@ -577,6 +608,8 @@ export function lte(dependentKey: string, value: number) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateComputedMacro('lte'); + return computed(dependentKey, function () { return get(this, dependentKey) <= value; }); @@ -625,6 +658,7 @@ export function lte(dependentKey: string, value: number) { @return {ComputedProperty} computed property which performs a logical `and` on the values of all the original values for properties. @public + @deprecated Use a getter instead. */ export const and = generateComputedWithPredicate('and', (value) => value); @@ -668,6 +702,7 @@ export const and = generateComputedWithPredicate('and', (value) => value); @return {ComputedProperty} computed property which performs a logical `or` on the values of all the original values for properties. @public + @deprecated Use a getter instead. */ export const or = generateComputedWithPredicate('or', (value) => !value); @@ -704,6 +739,7 @@ export const or = generateComputedWithPredicate('or', (value) => !value); @return {ComputedProperty} computed property which creates an alias to the original value for property. @public + @deprecated Use a getter instead. */ /** @@ -744,6 +780,7 @@ export const or = generateComputedWithPredicate('or', (value) => !value); @return {ComputedProperty} computed property which creates a one way computed property to the original value for property. @public + @deprecated Use a getter instead. */ export function oneWay(dependentKey: string) { assert( @@ -751,6 +788,8 @@ export function oneWay(dependentKey: string) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateComputedMacro('oneWay'); + return alias(dependentKey).oneWay() as FieldDecorator; } @@ -808,6 +847,7 @@ export function oneWay(dependentKey: string) { property to the original value for property. @since 1.5.0 @public + @deprecated Use a getter instead. */ export function readOnly(dependentKey: string) { assert( @@ -815,6 +855,8 @@ export function readOnly(dependentKey: string) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateComputedMacro('readOnly'); + return alias(dependentKey).readOnly() as FieldDecorator; } @@ -852,6 +894,7 @@ export function readOnly(dependentKey: string) { deprecation to the original value for property. @since 1.7.0 @public + @deprecated Use a getter instead. */ export function deprecatingAlias(dependentKey: string, options: DeprecationOptions) { assert( @@ -859,6 +902,8 @@ export function deprecatingAlias(dependentKey: string, options: DeprecationOptio !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateComputedMacro('deprecatingAlias'); + return computed(dependentKey, { get(key) { deprecate( diff --git a/packages/@ember/object/tests/computed/computed_macros_test.js b/packages/@ember/object/tests/computed/computed_macros_test.js index c127c6aa945..ba51b84fa99 100644 --- a/packages/@ember/object/tests/computed/computed_macros_test.js +++ b/packages/@ember/object/tests/computed/computed_macros_test.js @@ -75,7 +75,12 @@ moduleFor( assert.equal(get(obj, 'LannistersKnown'), true, 'empty respects array mutations'); } - ['@test not'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test not`](assert) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { foo: true }; defineProperty(obj, 'notFoo', not('foo')); assert.equal(get(obj, 'notFoo'), false); @@ -105,7 +110,12 @@ moduleFor( assert.equal(get(obj, 'quzEmpty'), false); } - ['@test bool'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test bool`](assert) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { foo() {}, bar: 'asdf', baz: null, quz: false }; defineProperty(obj, 'fooBool', bool('foo')); defineProperty(obj, 'barBool', bool('bar')); @@ -117,7 +127,12 @@ moduleFor( assert.equal(get(obj, 'quzBool'), false); } - ['@test alias'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test alias`](assert) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { bar: 'asdf', baz: null, quz: false }; defineProperty( obj, @@ -150,7 +165,12 @@ moduleFor( assert.equal(get(obj, 'quz'), null); } - ['@test alias set'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test alias set`](assert) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = {}; let constantValue = 'always `a`'; @@ -177,7 +197,12 @@ moduleFor( assert.equal(get(obj, 'aliased'), constantValue); } - ['@test match'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test match`](assert) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { name: 'Paul' }; defineProperty(obj, 'isPaul', match('name', /Paul/)); @@ -220,7 +245,12 @@ moduleFor( assert.true(get(obj, 'isHungry'), 'undefined is none'); } - ['@test equal'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test equal`](assert) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { name: 'Paul' }; defineProperty(obj, 'isPaul', computedEqual('name', 'Paul')); @@ -231,7 +261,12 @@ moduleFor( assert.equal(get(obj, 'isPaul'), false, 'is not Paul anymore'); } - ['@test gt'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test gt`](assert) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { number: 2 }; defineProperty(obj, 'isGreaterThenOne', gt('number', 1)); @@ -246,7 +281,12 @@ moduleFor( assert.equal(get(obj, 'isGreaterThenOne'), false, 'is not gt'); } - ['@test gte'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test gte`](assert) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { number: 2 }; defineProperty(obj, 'isGreaterOrEqualThenOne', gte('number', 1)); @@ -261,7 +301,12 @@ moduleFor( assert.equal(get(obj, 'isGreaterOrEqualThenOne'), false, 'is not gte'); } - ['@test lt'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test lt`](assert) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { number: 0 }; defineProperty(obj, 'isLesserThenOne', lt('number', 1)); @@ -276,7 +321,12 @@ moduleFor( assert.equal(get(obj, 'isLesserThenOne'), false, 'is not lt'); } - ['@test lte'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test lte`](assert) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { number: 0 }; defineProperty(obj, 'isLesserOrEqualThenOne', lte('number', 1)); @@ -291,7 +341,14 @@ moduleFor( assert.equal(get(obj, 'isLesserOrEqualThenOne'), false, 'is not lte'); } - ['@test and, with two properties'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test and, with two properties`]( + assert + ) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { one: true, two: true }; defineProperty(obj, 'oneAndTwo', and('one', 'two')); @@ -312,7 +369,14 @@ moduleFor( assert.equal(get(obj, 'oneAndTwo'), 2, 'returns truthy value as in &&'); } - ['@test and, with three properties'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test and, with three properties`]( + assert + ) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { one: true, two: true, three: true }; defineProperty(obj, 'oneTwoThree', and('one', 'two', 'three')); @@ -329,7 +393,14 @@ moduleFor( assert.equal(get(obj, 'oneTwoThree'), 3, 'returns truthy value as in &&'); } - ['@test and, with expand properties'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test and, with expand properties`]( + assert + ) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { one: true, two: true, three: true }; defineProperty(obj, 'oneTwoThree', and('{one,two,three}')); @@ -346,7 +417,14 @@ moduleFor( assert.equal(get(obj, 'oneTwoThree'), 3, 'returns truthy value as in &&'); } - ['@test or, with two properties'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test or, with two properties`]( + assert + ) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { one: true, two: true }; defineProperty(obj, 'oneOrTwo', or('one', 'two')); @@ -373,7 +451,14 @@ moduleFor( assert.equal(get(obj, 'oneOrTwo'), 1, 'returns truthy value as in ||'); } - ['@test or, with three properties'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test or, with three properties`]( + assert + ) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { one: true, two: true, three: true }; defineProperty(obj, 'oneTwoThree', or('one', 'two', 'three')); @@ -404,7 +489,14 @@ moduleFor( assert.equal(get(obj, 'oneTwoThree'), 1, 'returns truthy value as in ||'); } - ['@test or, with expand properties'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test or, with expand properties`]( + assert + ) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { one: true, two: true, three: true }; defineProperty(obj, 'oneTwoThree', or('{one,two,three}')); @@ -435,7 +527,12 @@ moduleFor( assert.equal(get(obj, 'oneTwoThree'), 1, 'returns truthy value as in ||'); } - ['@test or and and, warn about dependent keys with spaces']() { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test or and and, warn about dependent keys with spaces`]() { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { one: true, two: true }; expectAssertion(function () { defineProperty(obj, 'oneOrTwo', or('one', 'two three')); @@ -446,7 +543,12 @@ moduleFor( }, /Dependent keys passed to `and` computed macro can't have spaces\./); } - ['@test oneWay'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test oneWay`](assert) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { firstName: 'Teddy', lastName: 'Zeenny', @@ -470,7 +572,12 @@ moduleFor( assert.equal(get(obj, 'nickName'), 'TeddyBear'); } - ['@test readOnly'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test readOnly`](assert) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { firstName: 'Teddy', lastName: 'Zeenny', @@ -496,7 +603,14 @@ moduleFor( assert.equal(get(obj, 'nickName'), 'TEDDDDDDDDYYY'); } - ['@test deprecatingAlias'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test deprecatingAlias`]( + assert + ) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let obj = { bar: 'asdf', baz: null, quz: false }; defineProperty( obj, diff --git a/packages/@ember/object/tests/computed_test.js b/packages/@ember/object/tests/computed_test.js index 59d294d421f..7274897300f 100644 --- a/packages/@ember/object/tests/computed_test.js +++ b/packages/@ember/object/tests/computed_test.js @@ -2,7 +2,8 @@ import { notifyPropertyChange } from '@ember/-internals/metal'; import { alias, oneWay as reads } from '@ember/object/computed'; import { A as emberA, isArray } from '@ember/array'; import EmberObject, { defineProperty, get, set, computed, observer } from '@ember/object'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; function K() { return this; @@ -201,7 +202,14 @@ moduleFor( ); } - ['@test can iterate over a list of computed properties for a class'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test can iterate over a list of computed properties for a class`]( + assert + ) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let MyClass = EmberObject.extend({ foo: computed(function () {}), @@ -353,9 +361,14 @@ moduleFor( assert.strictEqual(macroCalls, 1, "the macro's getter is used"); } - ['@test observing prop installed with computed macro reads and overriding it in create() works']( + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test observing prop installed with computed macro reads and overriding it in create() works`]( assert ) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let Obj = EmberObject.extend({ name: reads('model.name'), nameDidChange: observer('name', function () {}), @@ -511,7 +524,14 @@ moduleFor( assert.deepEqual(n.normalized, ['bar']); } - ['@test lazy computation cannot cause infinite cycles'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test lazy computation cannot cause infinite cycles`]( + assert + ) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + // This is based off a real world bug found in ember-cp-validations: // https://github.com/offirgolan/ember-cp-validations/issues/659 let CycleObject = class extends EmberObject { diff --git a/packages/@ember/object/tests/create_test.js b/packages/@ember/object/tests/create_test.js index 266ac20072b..c4ab1498e4d 100644 --- a/packages/@ember/object/tests/create_test.js +++ b/packages/@ember/object/tests/create_test.js @@ -112,7 +112,14 @@ moduleFor( } } - ['@test sets up mandatory setters for simple properties watched with aliases'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test sets up mandatory setters for simple properties watched with aliases`]( + assert + ) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + if (DEBUG) { let MyClass = class extends EmberObject { foo = null; diff --git a/packages/@ember/routing/lib/routing-service.ts b/packages/@ember/routing/lib/routing-service.ts index b44e2daf70c..37595a516ca 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 alias from '@ember/-internals/metal/lib/alias'; import Service from '@ember/service'; import type Route from '@ember/routing/route'; import type { ModelFor } from '@ember/routing/route'; @@ -12,6 +12,10 @@ import EmberRouter from '@ember/routing/router'; import type RouterState from './router_state'; import { ROUTER } from '@ember/routing/router-service'; +function readOnly(dependentKey: string) { + return alias(dependentKey).readOnly(); +} + /** The Routing service is used by LinkTo, and provides facilities for the component/view layer to interact with the router. diff --git a/packages/@ember/routing/router-service.ts b/packages/@ember/routing/router-service.ts index 0e4b6cae977..4495b83735c 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 alias from '@ember/-internals/metal/lib/alias'; import Service from '@ember/service'; import { consumeTag } from '@glimmer/validator/lib/tracking'; import { tagFor } from '@glimmer/validator/lib/meta'; @@ -21,6 +21,10 @@ import { sendEvent, } from '@ember/-internals/metal/lib/events'; +function readOnly(dependentKey: string) { + return alias(dependentKey).readOnly(); +} + export const ROUTER = Symbol('ROUTER'); function cleanURL(url: string, rootURL: string) { diff --git a/packages/ember/tests/routing/router_service_test/currenturl_lifecycle_test.js b/packages/ember/tests/routing/router_service_test/currenturl_lifecycle_test.js index 05f80311eb4..71c55426b1e 100644 --- a/packages/ember/tests/routing/router_service_test/currenturl_lifecycle_test.js +++ b/packages/ember/tests/routing/router_service_test/currenturl_lifecycle_test.js @@ -1,6 +1,5 @@ import { service } from '@ember/service'; -import { action } from '@ember/object'; -import { readOnly } from '@ember/object/computed'; +import { action, computed } from '@ember/object'; import Component from '@glimmer/component'; import Route from '@ember/routing/route'; import { get } from '@ember/object'; @@ -131,12 +130,18 @@ moduleFor( let CurrenURLComponent = class extends Component { @service('router') routerService; - @readOnly('routerService.currentURL') - currentURL; - @readOnly('routerService.currentRouteName') - currentRouteName; - @readOnly('routerService.currentRoute') - currentRoute; + + get currentURL() { + return this.routerService.currentURL; + } + + get currentRouteName() { + return this.routerService.currentRouteName; + } + + get currentRoute() { + return this.routerService.currentRoute; + } }; this.add( @@ -294,6 +299,29 @@ moduleFor( }); } + async ['@test a computed property can depend on RouterService#currentURL'](assert) { + await this.visit('/'); + + let routerService = this.routerService; + + class Watcher { + routerService = routerService; + + @computed('routerService.currentURL') + get currentURL() { + return this.routerService.currentURL; + } + } + + let watcher = new Watcher(); + + assert.strictEqual(watcher.currentURL, '/'); + + await this.visit('/child'); + + assert.strictEqual(watcher.currentURL, '/child'); + } + ['@test RouterService#currentURL is correctly set with component after consecutive visits']( assert ) { From a7fa708d307bc49997b193262f353fd872721bd4 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:03:19 -0400 Subject: [PATCH 2/3] Expect the alias deprecation only in debug builds The test body runs only in debug builds and expects zero assertions in production. The deprecation expectation adds an assertion, so it now sits in the debug branch. Co-Authored-By: Claude Opus 5.5 --- packages/@ember/object/tests/create_test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@ember/object/tests/create_test.js b/packages/@ember/object/tests/create_test.js index c4ab1498e4d..1da8049f676 100644 --- a/packages/@ember/object/tests/create_test.js +++ b/packages/@ember/object/tests/create_test.js @@ -115,12 +115,12 @@ moduleFor( [`${testUnless(DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isRemoved)} @test sets up mandatory setters for simple properties watched with aliases`]( assert ) { - expectDeprecation( - /from `@ember\/object\/computed` is deprecated/, - DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled - ); - if (DEBUG) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated/, + DEPRECATIONS.DEPRECATE_COMPUTED_MACROS.isEnabled + ); + let MyClass = class extends EmberObject { foo = null; bar = null; From f6f874acc2efe282cc9cb7622271358b4884cc0b Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:48:08 -0400 Subject: [PATCH 3/3] Use getters in the router services `RouterService` and the private routing service used the `readOnly` macro for the properties that they read from the router. Each property is a native getter with a plain read now. The router properties are tracked, so the reads are reactive. The getters of `RouterService` have `@dependentKeyCompat`, so that a classic computed property or observer can depend on them, for example with the key `router.currentURL`. `LinkTo` consumed the `currentState` tag of the routing service. It now consumes the tag of the router. Co-Authored-By: Claude Opus 5.5 --- .../glimmer/lib/components/link-to.ts | 4 +-- .../@ember/routing/lib/routing-service.ts | 33 +++++++++-------- packages/@ember/routing/router-service.ts | 36 +++++++++++-------- 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/components/link-to.ts b/packages/@ember/-internals/glimmer/lib/components/link-to.ts index d47ddd8b7ea..7bb8258f46e 100644 --- a/packages/@ember/-internals/glimmer/lib/components/link-to.ts +++ b/packages/@ember/-internals/glimmer/lib/components/link-to.ts @@ -328,7 +328,7 @@ class _LinkTo extends InternalComponent { // consume the current router state so we invalidate when QP changes // TODO: can we narrow this down to QP changes only? - consumeTag(tagFor(routing, 'currentState')); + consumeTag(tagFor(routing.router, 'currentState')); if (DEBUG) { try { @@ -417,7 +417,7 @@ class _LinkTo extends InternalComponent { // GH #17963 private currentRouteCache = createCache>(() => { - consumeTag(tagFor(this.routing, 'currentState')); + consumeTag(tagFor(this.routing.router, 'currentState')); return untrack(() => this.routing.currentRouteName); }); diff --git a/packages/@ember/routing/lib/routing-service.ts b/packages/@ember/routing/lib/routing-service.ts index 37595a516ca..44424525a11 100644 --- a/packages/@ember/routing/lib/routing-service.ts +++ b/packages/@ember/routing/lib/routing-service.ts @@ -4,7 +4,6 @@ import { getOwner } from '@ember/-internals/owner'; import { assert } from '@ember/debug'; -import alias from '@ember/-internals/metal/lib/alias'; import Service from '@ember/service'; import type Route from '@ember/routing/route'; import type { ModelFor } from '@ember/routing/route'; @@ -12,10 +11,6 @@ import EmberRouter from '@ember/routing/router'; import type RouterState from './router_state'; import { ROUTER } from '@ember/routing/router-service'; -function readOnly(dependentKey: string) { - return alias(dependentKey).readOnly(); -} - /** The Routing service is used by LinkTo, and provides facilities for the component/view layer to interact with the router. @@ -27,13 +22,24 @@ function readOnly(dependentKey: string) { @class RoutingService */ export default class RoutingService extends Service { - declare targetState: EmberRouter['targetState']; - declare currentState: EmberRouter['currentState']; - declare currentRouteName: EmberRouter['currentRouteName']; - declare currentPath: EmberRouter['currentPath']; - [ROUTER]?: EmberRouter; + get targetState(): EmberRouter['targetState'] { + return this.router.targetState; + } + + get currentState(): EmberRouter['currentState'] { + return this.router.currentState; + } + + get currentRouteName(): EmberRouter['currentRouteName'] { + return this.router.currentRouteName; + } + + get currentPath(): EmberRouter['currentPath'] { + return this.router.currentPath; + } + get router(): EmberRouter { let router = this[ROUTER]; if (router !== undefined) { @@ -132,13 +138,6 @@ export default class RoutingService extends Service { } } -RoutingService.reopen({ - targetState: readOnly('router.targetState'), - currentState: readOnly('router.currentState'), - currentRouteName: readOnly('router.currentRouteName'), - currentPath: readOnly('router.currentPath'), -}); - function numberOfContextsAcceptedByHandler(handlerName: string, handlerInfos: any[]) { let req = 0; for (let i = 0; i < handlerInfos.length; i++) { diff --git a/packages/@ember/routing/router-service.ts b/packages/@ember/routing/router-service.ts index 4495b83735c..17f4d29e480 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 alias from '@ember/-internals/metal/lib/alias'; +import { dependentKeyCompat } from '@ember/object/compat'; import Service from '@ember/service'; import { consumeTag } from '@glimmer/validator/lib/tracking'; import { tagFor } from '@glimmer/validator/lib/meta'; @@ -21,10 +21,6 @@ import { sendEvent, } from '@ember/-internals/metal/lib/events'; -function readOnly(dependentKey: string) { - return alias(dependentKey).readOnly(); -} - export const ROUTER = Symbol('ROUTER'); function cleanURL(url: string, rootURL: string) { @@ -730,8 +726,10 @@ class RouterService extends Service { @type {String | null} @public */ - @readOnly('_router.currentRouteName') - readonly currentRouteName!: this['_router']['currentRouteName']; + @dependentKeyCompat + get currentRouteName(): this['_router']['currentRouteName'] { + return this._router.currentRouteName; + } /** Current URL for the application. @@ -759,8 +757,10 @@ class RouterService extends Service { @type String @public */ - @readOnly('_router.currentURL') - readonly currentURL!: this['_router']['currentURL']; + @dependentKeyCompat + get currentURL(): this['_router']['currentURL'] { + return this._router.currentURL; + } /** The `location` property returns what implementation of the `location` API @@ -798,8 +798,10 @@ class RouterService extends Service { @see {Location} @public */ - @readOnly('_router.location') - readonly location!: this['_router']['location']; + @dependentKeyCompat + get location(): this['_router']['location'] { + return this._router.location; + } /** The `rootURL` property represents the URL of the root of @@ -828,8 +830,10 @@ class RouterService extends Service { @default '/' @public */ - @readOnly('_router.rootURL') - readonly rootURL!: this['_router']['rootURL']; + @dependentKeyCompat + get rootURL(): this['_router']['rootURL'] { + return this._router.rootURL; + } /** The `currentRoute` property contains metadata about the current leaf route. @@ -860,8 +864,10 @@ class RouterService extends Service { @type RouteInfo @public */ - @readOnly('_router.currentRoute') - readonly currentRoute!: this['_router']['currentRoute']; + @dependentKeyCompat + get currentRoute(): this['_router']['currentRoute'] { + return this._router.currentRoute; + } } export { RouterService as default, type RouteInfo, type RouteInfoWithAttributes };