Skip to content

Commit a43f2c8

Browse files
NullVoxPopuliclaude
andcommitted
Extract @ember/object's action decorator to its own sub-path
The `action` decorator lived inline in `@ember/object/index.ts`, which also imports `CoreObject` and `Observable` at module top — so any component that pulled `import { action } from '@ember/object'` (Input, Textarea, AbstractInput, LinkTo) dragged the full EmberObject / Observable / Mixin graph along with it. Move `action` (plus its `setupAction` helper, `BINDINGS_MAP`, and `hasProto`) to `@ember/object/action.ts`. `index.ts` re-exports it via `export { action } from './action'` so the no-barrel-imports lint autofix rewrites internal call sites to the deep path. `@ember/object/index.ts` itself loses its references to `isElementDescriptor` / `setClassicDecorator` / `ElementDescriptor` / `ExtendedMethodDecorator`, since those now live in `action.ts`. Hello-world: 134.12 KB / 42.94 KB → 133.42 KB / 42.69 KB gzip. Verified with `pnpm lint` (clean), `pnpm type-check:internals`, hello-world build, classic v2-app and v1 app smoke-test `pnpm test` (1/1 each). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 7e64bb1 commit a43f2c8

7 files changed

Lines changed: 126 additions & 123 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@
267267
"@ember/modifier/index.js": "ember-source/@ember/modifier/index.js",
268268
"@ember/modifier/on.js": "ember-source/@ember/modifier/on.js",
269269
"@ember/object/-internals.js": "ember-source/@ember/object/-internals.js",
270+
"@ember/object/action.js": "ember-source/@ember/object/action.js",
270271
"@ember/object/compat.js": "ember-source/@ember/object/compat.js",
271272
"@ember/object/computed.js": "ember-source/@ember/object/computed.js",
272273
"@ember/object/core.js": "ember-source/@ember/object/core.js",

packages/@ember/-internals/glimmer/lib/components/abstract-input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { tracked } from '@ember/-internals/metal/lib/tracked';
22
import { assert } from '@ember/debug';
3-
import { action } from '@ember/object';
3+
import { action } from '@ember/object/action';
44
import type { Reference } from '@glimmer/reference/lib/reference';
55
import {
66
isConstRef,

packages/@ember/-internals/glimmer/lib/components/input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import hasDOM from '@ember/-internals/browser-environment/lib/has-dom';
55
import { type Opaque } from '@ember/-internals/utility-types';
66
import { assert, warn } from '@ember/debug';
7-
import { action } from '@ember/object';
7+
import { action } from '@ember/object/action';
88
import { valueForRef } from '@glimmer/reference/lib/reference';
99
import { untrack } from '@glimmer/validator/lib/tracking';
1010
import InputTemplate from '../templates/input';

packages/@ember/-internals/glimmer/lib/components/link-to.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { assert, debugFreeze, warn } from '@ember/debug';
66
import { getEngineParent } from '@ember/engine/parent';
77
import type EngineInstance from '@ember/engine/instance';
88
import { flaggedInstrument } from '@ember/instrumentation/lib/internal-instrument';
9-
import { action } from '@ember/object';
9+
import { action } from '@ember/object/action';
1010
import { service } from '@ember/service';
1111
import { DEBUG } from '@glimmer/env';
1212
import type { Maybe } from '@glimmer/interfaces';

packages/@ember/-internals/glimmer/lib/components/textarea.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
@module @ember/component
33
*/
44
import { type Opaque } from '@ember/-internals/utility-types';
5-
import { action } from '@ember/object';
5+
import { action } from '@ember/object/action';
66
import TextareaTemplate from '../templates/textarea';
77
import AbstractInput from './abstract-input';
88
import { type OpaqueInternalComponentConstructor, opaquify } from './internal';

packages/@ember/object/action.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { assert } from '@ember/debug';
2+
import type {
3+
ElementDescriptor,
4+
ExtendedMethodDecorator,
5+
} from '@ember/-internals/metal/lib/decorator';
6+
import { isElementDescriptor, setClassicDecorator } from '@ember/-internals/metal/lib/decorator';
7+
8+
const BINDINGS_MAP = new WeakMap();
9+
10+
interface HasProto {
11+
constructor: {
12+
proto(): void;
13+
};
14+
}
15+
16+
function hasProto(obj: unknown): obj is HasProto {
17+
return (
18+
obj != null &&
19+
(obj as any).constructor !== undefined &&
20+
typeof ((obj as any).constructor as any).proto === 'function'
21+
);
22+
}
23+
24+
interface HasActions {
25+
actions: Record<string | symbol, unknown>;
26+
}
27+
28+
function setupAction(
29+
target: Partial<HasActions>,
30+
key: string | symbol,
31+
actionFn: Function
32+
): TypedPropertyDescriptor<unknown> {
33+
if (hasProto(target)) {
34+
target.constructor.proto();
35+
}
36+
37+
if (!Object.prototype.hasOwnProperty.call(target, 'actions')) {
38+
let parentActions = target.actions;
39+
// we need to assign because of the way mixins copy actions down when inheriting
40+
target.actions = parentActions ? Object.assign({}, parentActions) : {};
41+
}
42+
43+
assert("[BUG] Somehow the target doesn't have actions!", target.actions != null);
44+
45+
target.actions[key] = actionFn;
46+
47+
return {
48+
get() {
49+
let bindings = BINDINGS_MAP.get(this);
50+
51+
if (bindings === undefined) {
52+
bindings = new Map();
53+
BINDINGS_MAP.set(this, bindings);
54+
}
55+
56+
let fn = bindings.get(actionFn);
57+
58+
if (fn === undefined) {
59+
fn = actionFn.bind(this);
60+
bindings.set(actionFn, fn);
61+
}
62+
63+
return fn;
64+
},
65+
};
66+
}
67+
68+
export function action(
69+
target: ElementDescriptor[0],
70+
key: ElementDescriptor[1],
71+
desc: ElementDescriptor[2]
72+
): PropertyDescriptor;
73+
export function action(desc: PropertyDescriptor): ExtendedMethodDecorator;
74+
export function action(
75+
...args: ElementDescriptor | [PropertyDescriptor]
76+
): PropertyDescriptor | ExtendedMethodDecorator {
77+
let actionFn: object | Function;
78+
79+
if (!isElementDescriptor(args)) {
80+
actionFn = args[0];
81+
82+
let decorator: ExtendedMethodDecorator = function (
83+
target,
84+
key,
85+
_desc,
86+
_meta,
87+
isClassicDecorator
88+
) {
89+
assert(
90+
'The @action decorator may only be passed a method when used in classic classes. You should decorate methods directly in native classes',
91+
isClassicDecorator
92+
);
93+
94+
assert(
95+
'The action() decorator must be passed a method when used in classic classes',
96+
typeof actionFn === 'function'
97+
);
98+
99+
return setupAction(target, key, actionFn);
100+
};
101+
102+
setClassicDecorator(decorator);
103+
104+
return decorator;
105+
}
106+
107+
let [target, key, desc] = args;
108+
109+
actionFn = desc?.value;
110+
111+
assert(
112+
'The @action decorator must be applied to methods when used in native classes',
113+
typeof actionFn === 'function'
114+
);
115+
116+
// SAFETY: TS types are weird with decorators. This should work.
117+
return setupAction(target, key, actionFn);
118+
}
119+
120+
setClassicDecorator(action as ExtendedMethodDecorator);

packages/@ember/object/index.ts

Lines changed: 1 addition & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { assert } from '@ember/debug';
22
import { ENV } from '@ember/-internals/environment/lib/env';
3-
import type {
4-
ElementDescriptor,
5-
ExtendedMethodDecorator,
6-
} from '@ember/-internals/metal/lib/decorator';
7-
import { isElementDescriptor, setClassicDecorator } from '@ember/-internals/metal/lib/decorator';
83
import expandProperties from '@ember/-internals/metal/lib/expand_properties';
94
import { getFactoryFor } from '@ember/-internals/container/lib/container';
105
import { setObservers } from '@ember/-internals/utils/lib/super';
@@ -109,120 +104,7 @@ export default EmberObject;
109104
@return {PropertyDecorator} property decorator instance
110105
*/
111106

112-
const BINDINGS_MAP = new WeakMap();
113-
114-
interface HasProto {
115-
constructor: {
116-
proto(): void;
117-
};
118-
}
119-
120-
function hasProto(obj: unknown): obj is HasProto {
121-
return (
122-
obj != null &&
123-
(obj as any).constructor !== undefined &&
124-
typeof ((obj as any).constructor as any).proto === 'function'
125-
);
126-
}
127-
128-
interface HasActions {
129-
actions: Record<string | symbol, unknown>;
130-
}
131-
132-
function setupAction(
133-
target: Partial<HasActions>,
134-
key: string | symbol,
135-
actionFn: Function
136-
): TypedPropertyDescriptor<unknown> {
137-
if (hasProto(target)) {
138-
target.constructor.proto();
139-
}
140-
141-
if (!Object.prototype.hasOwnProperty.call(target, 'actions')) {
142-
let parentActions = target.actions;
143-
// we need to assign because of the way mixins copy actions down when inheriting
144-
target.actions = parentActions ? Object.assign({}, parentActions) : {};
145-
}
146-
147-
assert("[BUG] Somehow the target doesn't have actions!", target.actions != null);
148-
149-
target.actions[key] = actionFn;
150-
151-
return {
152-
get() {
153-
let bindings = BINDINGS_MAP.get(this);
154-
155-
if (bindings === undefined) {
156-
bindings = new Map();
157-
BINDINGS_MAP.set(this, bindings);
158-
}
159-
160-
let fn = bindings.get(actionFn);
161-
162-
if (fn === undefined) {
163-
fn = actionFn.bind(this);
164-
bindings.set(actionFn, fn);
165-
}
166-
167-
return fn;
168-
},
169-
};
170-
}
171-
172-
export function action(
173-
target: ElementDescriptor[0],
174-
key: ElementDescriptor[1],
175-
desc: ElementDescriptor[2]
176-
): PropertyDescriptor;
177-
export function action(desc: PropertyDescriptor): ExtendedMethodDecorator;
178-
export function action(
179-
...args: ElementDescriptor | [PropertyDescriptor]
180-
): PropertyDescriptor | ExtendedMethodDecorator {
181-
let actionFn: object | Function;
182-
183-
if (!isElementDescriptor(args)) {
184-
actionFn = args[0];
185-
186-
let decorator: ExtendedMethodDecorator = function (
187-
target,
188-
key,
189-
_desc,
190-
_meta,
191-
isClassicDecorator
192-
) {
193-
assert(
194-
'The @action decorator may only be passed a method when used in classic classes. You should decorate methods directly in native classes',
195-
isClassicDecorator
196-
);
197-
198-
assert(
199-
'The action() decorator must be passed a method when used in classic classes',
200-
typeof actionFn === 'function'
201-
);
202-
203-
return setupAction(target, key, actionFn);
204-
};
205-
206-
setClassicDecorator(decorator);
207-
208-
return decorator;
209-
}
210-
211-
let [target, key, desc] = args;
212-
213-
actionFn = desc?.value;
214-
215-
assert(
216-
'The @action decorator must be applied to methods when used in native classes',
217-
typeof actionFn === 'function'
218-
);
219-
220-
// SAFETY: TS types are weird with decorators. This should work.
221-
return setupAction(target, key, actionFn);
222-
}
223-
224-
// SAFETY: TS types are weird with decorators. This should work.
225-
setClassicDecorator(action as ExtendedMethodDecorator);
107+
export { action } from './action';
226108

227109
// ..........................................................
228110
// OBSERVER HELPER

0 commit comments

Comments
 (0)