Skip to content

Commit e2e2e1f

Browse files
Add no-tracked-built-ins rule to autofix tracked-built-ins imports to @ember/reactive
Co-authored-by: NullVoxPopuli <[email protected]>
1 parent 1800bc7 commit e2e2e1f

8 files changed

Lines changed: 392 additions & 3 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ npm-debug.log
1414

1515
# eslint-remote-tester
1616
eslint-remote-tester-results
17+
18+
# Lock file (project uses pnpm)
19+
package-lock.json

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ rules in templates can be disabled with eslint directives with mustache or html
336336
| [no-classic-classes](docs/rules/no-classic-classes.md) | disallow "classic" classes in favor of native JS classes || | |
337337
| [no-ember-super-in-es-classes](docs/rules/no-ember-super-in-es-classes.md) | disallow use of `this._super` in ES class methods || 🔧 | |
338338
| [no-empty-glimmer-component-classes](docs/rules/no-empty-glimmer-component-classes.md) | disallow empty backing classes for Glimmer components || | |
339+
| [no-tracked-built-ins](docs/rules/no-tracked-built-ins.md) | enforce usage of `@ember/reactive` imports instead of `tracked-built-ins` | | 🔧 | |
339340
| [no-tracked-properties-from-args](docs/rules/no-tracked-properties-from-args.md) | disallow creating @tracked properties from this.args || | |
340341
| [template-indent](docs/rules/template-indent.md) | enforce consistent indentation for gts/gjs templates | | 🔧 | |
341342
| [template-no-deprecated](docs/rules/template-no-deprecated.md) | disallow using deprecated Glimmer components, helpers, and modifiers in templates | | | |

docs/rules/no-tracked-built-ins.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# ember/no-tracked-built-ins
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Enforce usage of `@ember/reactive` imports instead of `tracked-built-ins`.
8+
9+
## Context
10+
11+
Per [RFC #1068](https://github.com/emberjs/rfcs/pull/1068), the tracked collection utilities from the `tracked-built-ins` package are being moved into the framework as `@ember/reactive`. The new API also changes from class constructors (`new TrackedArray(...)`) to factory functions (`trackedArray(...)`).
12+
13+
## Rule Details
14+
15+
This rule detects imports from `tracked-built-ins` and provides an autofix to convert them to `@ember/reactive` with the new function-based API.
16+
17+
The following mappings are applied:
18+
19+
| Old (`tracked-built-ins`) | New (`@ember/reactive`) |
20+
|--------------------------|------------------------|
21+
| `TrackedArray` | `trackedArray` |
22+
| `TrackedObject` | `trackedObject` |
23+
| `TrackedMap` | `trackedMap` |
24+
| `TrackedSet` | `trackedSet` |
25+
| `TrackedWeakMap` | `trackedWeakMap` |
26+
| `TrackedWeakSet` | `trackedWeakSet` |
27+
28+
Additionally, `new` expressions using these imports are automatically converted to direct function calls.
29+
30+
## Examples
31+
32+
Examples of **incorrect** code for this rule:
33+
34+
```js
35+
import { TrackedArray } from 'tracked-built-ins';
36+
37+
const arr = new TrackedArray([1, 2, 3]);
38+
```
39+
40+
```js
41+
import { TrackedObject, TrackedMap } from 'tracked-built-ins';
42+
43+
const obj = new TrackedObject({ a: 1 });
44+
const map = new TrackedMap();
45+
```
46+
47+
Examples of **correct** code for this rule:
48+
49+
```js
50+
import { trackedArray } from '@ember/reactive';
51+
52+
const arr = trackedArray([1, 2, 3]);
53+
```
54+
55+
```js
56+
import { trackedObject, trackedMap } from '@ember/reactive';
57+
58+
const obj = trackedObject({ a: 1 });
59+
const map = trackedMap();
60+
```
61+
62+
## Migration
63+
64+
This rule provides automatic fixes via `--fix`. Running ESLint with the `--fix` flag will:
65+
66+
1. Replace `import { TrackedArray } from 'tracked-built-ins'` with `import { trackedArray } from '@ember/reactive'`
67+
2. Replace `new TrackedArray(...)` with `trackedArray(...)`
68+
69+
## References
70+
71+
- [RFC #1068: Built in tracking utilities for common collections](https://github.com/emberjs/rfcs/pull/1068)
72+
- [`tracked-built-ins` package](https://github.com/tracked-tools/tracked-built-ins)

lib/recommended-rules-gjs.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
* definitions, execute "npm run update"
66
*/
77
module.exports = {
8-
'ember/template-no-let-reference': 'error',
8+
"ember/template-no-let-reference": "error"
99
};

lib/recommended-rules-gts.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
* definitions, execute "npm run update"
66
*/
77
module.exports = {
8-
'ember/template-no-let-reference': 'error',
8+
"ember/template-no-let-reference": "error"
99
};

lib/recommended-rules.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,4 @@ module.exports = {
7575
"ember/routes-segments-snake-case": "error",
7676
"ember/use-brace-expansion": "error",
7777
"ember/use-ember-data-rfc-395-imports": "error"
78-
}
78+
};

lib/rules/no-tracked-built-ins.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
'use strict';
2+
3+
//------------------------------------------------------------------------------
4+
// Mapping from tracked-built-ins exports to @ember/reactive exports
5+
//------------------------------------------------------------------------------
6+
7+
const TRACKED_BUILT_INS_MAPPING = {
8+
TrackedArray: 'trackedArray',
9+
TrackedObject: 'trackedObject',
10+
TrackedMap: 'trackedMap',
11+
TrackedWeakMap: 'trackedWeakMap',
12+
TrackedSet: 'trackedSet',
13+
TrackedWeakSet: 'trackedWeakSet',
14+
};
15+
16+
const TRACKED_BUILT_INS_MODULE = 'tracked-built-ins';
17+
const EMBER_REACTIVE_MODULE = '@ember/reactive';
18+
19+
const ERROR_MESSAGE_IMPORT =
20+
'Use imports from `@ember/reactive` instead of `tracked-built-ins`.';
21+
22+
//------------------------------------------------------------------------------
23+
// Rule Definition
24+
//------------------------------------------------------------------------------
25+
26+
/** @type {import('eslint').Rule.RuleModule} */
27+
module.exports = {
28+
meta: {
29+
type: 'suggestion',
30+
docs: {
31+
description:
32+
'enforce usage of `@ember/reactive` imports instead of `tracked-built-ins`',
33+
category: 'Ember Octane',
34+
recommended: false,
35+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-tracked-built-ins.md',
36+
},
37+
fixable: 'code',
38+
schema: [],
39+
messages: {
40+
import: ERROR_MESSAGE_IMPORT,
41+
newExpression:
42+
'Use `{{newName}}(...)` instead of `new {{oldName}}(...)`. The `@ember/reactive` utilities do not use `new`.',
43+
},
44+
},
45+
46+
ERROR_MESSAGE_IMPORT,
47+
48+
create(context) {
49+
// Track which imported identifiers map to tracked-built-ins classes
50+
// so we can fix `new TrackedArray(...)` → `trackedArray(...)`
51+
const trackedIdentifiers = new Map();
52+
53+
return {
54+
ImportDeclaration(node) {
55+
if (node.source.value !== TRACKED_BUILT_INS_MODULE) {
56+
return;
57+
}
58+
59+
context.report({
60+
node,
61+
messageId: 'import',
62+
fix(fixer) {
63+
const specifiers = node.specifiers;
64+
65+
// Only autofix named imports we know how to map
66+
const namedSpecifiers = specifiers.filter(
67+
(s) =>
68+
s.type === 'ImportSpecifier' &&
69+
s.imported.name in TRACKED_BUILT_INS_MAPPING
70+
);
71+
72+
// If there's a default import or unknown named imports, we can't fully autofix
73+
const hasDefault = specifiers.some(
74+
(s) => s.type === 'ImportDefaultSpecifier'
75+
);
76+
const unknownNamed = specifiers.filter(
77+
(s) =>
78+
s.type === 'ImportSpecifier' &&
79+
!(s.imported.name in TRACKED_BUILT_INS_MAPPING)
80+
);
81+
82+
if (hasDefault || unknownNamed.length > 0 || namedSpecifiers.length === 0) {
83+
return null;
84+
}
85+
86+
const newSpecifiers = namedSpecifiers.map((s) => {
87+
const newName = TRACKED_BUILT_INS_MAPPING[s.imported.name];
88+
if (s.local.name !== s.imported.name) {
89+
// Has alias: `import { TrackedArray as TA }` → `import { trackedArray as TA }`
90+
return `${newName} as ${s.local.name}`;
91+
}
92+
return newName;
93+
});
94+
95+
const newImport = `import { ${newSpecifiers.join(', ')} } from '${EMBER_REACTIVE_MODULE}';`;
96+
return fixer.replaceText(node, newImport);
97+
},
98+
});
99+
100+
// Register the local names for NewExpression tracking
101+
for (const specifier of node.specifiers) {
102+
if (
103+
specifier.type === 'ImportSpecifier' &&
104+
specifier.imported.name in TRACKED_BUILT_INS_MAPPING
105+
) {
106+
const isAliased = specifier.local.name !== specifier.imported.name;
107+
trackedIdentifiers.set(specifier.local.name, {
108+
newName: TRACKED_BUILT_INS_MAPPING[specifier.imported.name],
109+
isAliased,
110+
});
111+
}
112+
}
113+
},
114+
115+
NewExpression(node) {
116+
if (
117+
node.callee.type === 'Identifier' &&
118+
trackedIdentifiers.has(node.callee.name)
119+
) {
120+
const oldName = node.callee.name;
121+
const { newName, isAliased } = trackedIdentifiers.get(oldName);
122+
123+
context.report({
124+
node,
125+
messageId: 'newExpression',
126+
data: { oldName, newName },
127+
fix(fixer) {
128+
const sourceCode = context.getSourceCode();
129+
const newKeyword = sourceCode.getFirstToken(node);
130+
const fixes = [
131+
// Remove the `new` keyword and the space after it
132+
fixer.removeRange([newKeyword.range[0], newKeyword.range[1] + 1]),
133+
];
134+
// Only rename the callee if it's not aliased
135+
if (!isAliased) {
136+
fixes.push(fixer.replaceText(node.callee, newName));
137+
}
138+
return fixes;
139+
},
140+
});
141+
}
142+
},
143+
};
144+
},
145+
};

0 commit comments

Comments
 (0)