@dsivd/prestations-ng 19.0.6 → 19.0.8-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -4
- package/CONTRIBUTING.md +3 -2
- package/ESLINT_PLUGIN.md +170 -35
- package/UPGRADING_V19.md +22 -145
- package/dsivd-prestations-ng-19.0.8-beta.1.tgz +0 -0
- package/eslint/component-path.mjs +15 -0
- package/eslint/configs/template-base.mjs +2 -2
- package/eslint/configs/template-recommended.mjs +15 -6
- package/eslint/configs/template-rules.mjs +17 -0
- package/eslint/configs/ts-base.mjs +2 -2
- package/eslint/configs/ts-recommended.mjs +133 -3
- package/eslint/configs/ts-rules.mjs +14 -0
- package/eslint/index.mjs +10 -4
- package/eslint/rules/no-direct-signal-mutation.mjs +370 -142
- package/eslint/rules/no-uninvoked-signal-in-template.mjs +1 -12
- package/eslint/signal-kinds.mjs +57 -0
- package/eslint/signal-names.mjs +43 -49
- package/fesm2022/dsivd-prestations-ng.mjs +487 -487
- package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
- package/package.json +1 -1
- package/src/eslint/component-path.mjs +15 -0
- package/src/eslint/configs/__tests__/configs.test.mjs +113 -48
- package/src/eslint/configs/template-base.mjs +2 -2
- package/src/eslint/configs/template-recommended.mjs +15 -6
- package/src/eslint/configs/template-rules.mjs +17 -0
- package/src/eslint/configs/ts-base.mjs +2 -2
- package/src/eslint/configs/ts-recommended.mjs +133 -3
- package/src/eslint/configs/ts-rules.mjs +14 -0
- package/src/eslint/index.mjs +10 -4
- package/src/eslint/rules/__tests__/no-direct-signal-mutation.test.mjs +365 -128
- package/src/eslint/rules/no-direct-signal-mutation.mjs +370 -142
- package/src/eslint/rules/no-uninvoked-signal-in-template.mjs +1 -12
- package/src/eslint/signal-kinds.mjs +57 -0
- package/src/eslint/signal-names.mjs +43 -49
- package/types/dsivd-prestations-ng.d.ts +0 -1
- package/dsivd-prestations-ng-19.0.6.tgz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,14 +6,48 @@
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## [
|
|
9
|
+
## [19.0.8]
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- ESLint rule [`no-direct-signal-mutation`](ESLINT_PLUGIN.md#no-direct-signal-mutation) now
|
|
14
|
+
covers much more than the plain assignment it started with. 🚨 Expect new errors on
|
|
15
|
+
`npm update`; each can be turned off from your own `rules` block.
|
|
16
|
+
- **in-place mutators** are reported as well, since they notify nothing either: those of
|
|
17
|
+
`Array` (`splice`, `push`, `pop`, `shift`, `unshift`, `sort`, `reverse`, `fill`,
|
|
18
|
+
`copyWithin`) and of `Map` / `Set` (`set`, `add`, `delete`, `clear`)
|
|
19
|
+
- so are `delete model().users[0]` and `Object.assign(model(), …)`, and
|
|
20
|
+
`items()["push"](x)` counts as `items().push(x)`
|
|
21
|
+
- the value is **followed through the bindings that can be resolved exactly**: a
|
|
22
|
+
variable written once, the element parameter of an iteration callback, a `@for` item
|
|
23
|
+
and a `@let`. `@for (user of model().users)` then `user.done = true` is reported
|
|
24
|
+
- 🚨 **no signal is mutable**, whatever its kind: `signal()`, `model()`,
|
|
25
|
+
`linkedSignal()`, `computed()`, `input()` and `toSignal()` are all reported, each with
|
|
26
|
+
the fix that applies to it. A **view query** is the single exception —
|
|
27
|
+
`viewChild()`, `contentChildren()` and their `.required` variants hand out a DOM
|
|
28
|
+
element or a component instance, which is there to be mutated
|
|
29
|
+
- only the signals the class really declares are inspected, resolved through the whole
|
|
30
|
+
inheritance chain, so a plain method (`this.getConfig().items.push(x)`) is never
|
|
31
|
+
reported
|
|
32
|
+
- a signal inherited from a published package and known by its type only (`Signal<T>`)
|
|
33
|
+
is left alone: a declaration file no longer says whether it was a `computed()` or a
|
|
34
|
+
view query
|
|
35
|
+
|
|
36
|
+
## [19.0.7]
|
|
10
37
|
|
|
11
38
|
### Added
|
|
12
39
|
|
|
13
40
|
- ESLint plugin: **shareable configs**, documented in [ESLINT_PLUGIN.md](ESLINT_PLUGIN.md)
|
|
14
41
|
- add `configs.tsRecommended` and `configs.templateRecommended` to the `extends` of your
|
|
15
|
-
`**/*.ts` and `**/*.html` blocks — see [UPGRADING_V19.md](UPGRADING_V19.md).
|
|
16
|
-
|
|
42
|
+
`**/*.ts` and `**/*.html` blocks — see [UPGRADING_V19.md](UPGRADING_V19.md). They carry
|
|
43
|
+
the whole shared setup: the `eslint`, `typescript-eslint` and `angular-eslint`
|
|
44
|
+
baselines, the house conventions, and the rules below. Nothing else is needed: no
|
|
45
|
+
`plugins` declaration, no rule list.
|
|
46
|
+
- `configs.tsRules` and `configs.templateRules` enable only the rules written here, for a
|
|
47
|
+
project that supplies its own style
|
|
48
|
+
- 🚨 do **not** also spread `angular.configs.tsRecommended` or
|
|
49
|
+
`angular.configs.templateRecommended`: they are already included, and declaring the
|
|
50
|
+
same plugin twice makes ESLint fail outright
|
|
17
51
|
- 🚨 **Prerequisite**: `lintFilePatterns` in your `angular.json` must include
|
|
18
52
|
`src/**/*.html`, otherwise the template rules never run
|
|
19
53
|
- 🚨 **A rule added to these configs makes your lint fail on `npm update`.** This is
|
|
@@ -37,7 +71,6 @@
|
|
|
37
71
|
- [recaptcha.service.ts](projects/prestations-ng/src/sdk-recaptcha/recaptcha.service.ts)
|
|
38
72
|
- fixed recaptcha called infinitely when backend says that it must be revalidated
|
|
39
73
|
|
|
40
|
-
|
|
41
74
|
## [19.0.0] - should be aligned with prestations-be 19.0.x
|
|
42
75
|
|
|
43
76
|
### Added
|
package/CONTRIBUTING.md
CHANGED
|
@@ -131,8 +131,9 @@ below mandatory:
|
|
|
131
131
|
|
|
132
132
|
1. Write the rule in `projects/prestations-ng/src/eslint/rules/<rule-name>.mjs`
|
|
133
133
|
2. Register it in `rules/index.mjs`, keyed by its public kebab-case name
|
|
134
|
-
3. Enable it in `configs/ts-
|
|
135
|
-
|
|
134
|
+
3. Enable it in `configs/ts-rules.mjs`, `configs/template-rules.mjs`, or both depending on
|
|
135
|
+
what it inspects — the `*-recommended.mjs` presets pick those layers up. A change to a
|
|
136
|
+
shared convention rather than to a rule of ours goes into `configs/ts-recommended.mjs`.
|
|
136
137
|
4. Test it in `rules/__tests__/<rule-name>.test.mjs` — use the `@angular-eslint/template-parser`
|
|
137
138
|
for a template rule
|
|
138
139
|
5. Run `npm run test:eslint`, then `npm run lint:js` on the whole repository: a rule that
|
package/ESLINT_PLUGIN.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# ESLint plugin
|
|
2
2
|
|
|
3
3
|
`@dsivd/prestations-ng` ships an ESLint plugin with rules that catch signal misuses the
|
|
4
|
-
Angular compiler lets through
|
|
5
|
-
follow the version of the
|
|
4
|
+
Angular compiler lets through, plus the lint setup shared by every project built on the
|
|
5
|
+
library. It is published as a subpath of the library, so both follow the version of the
|
|
6
|
+
library.
|
|
6
7
|
|
|
7
8
|
## Setup
|
|
8
9
|
|
|
@@ -17,18 +18,18 @@ import prestationsNg from '@dsivd/prestations-ng/eslint';
|
|
|
17
18
|
export default tseslint.config(
|
|
18
19
|
{
|
|
19
20
|
files: ['**/*.ts'],
|
|
20
|
-
extends: [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
extends: [...prestationsNg.configs.tsRecommended],
|
|
22
|
+
languageOptions: {
|
|
23
|
+
parserOptions: {
|
|
24
|
+
projectService: true,
|
|
25
|
+
tsconfigRootDir: import.meta.dirname,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
24
28
|
processor: angular.processInlineTemplates,
|
|
25
29
|
},
|
|
26
30
|
{
|
|
27
31
|
files: ['**/*.html'],
|
|
28
|
-
extends: [
|
|
29
|
-
...angular.configs.templateRecommended,
|
|
30
|
-
...prestationsNg.configs.templateRecommended,
|
|
31
|
-
],
|
|
32
|
+
extends: [...prestationsNg.configs.templateRecommended],
|
|
32
33
|
},
|
|
33
34
|
);
|
|
34
35
|
```
|
|
@@ -37,17 +38,38 @@ That is all. You do **not** declare `plugins`, and you do **not** list the rules
|
|
|
37
38
|
from the config. Inline templates written in a `@Component` are covered too, through
|
|
38
39
|
`angular.processInlineTemplates`.
|
|
39
40
|
|
|
41
|
+
> 🚨 **Do not spread `angular.configs.tsRecommended` or `angular.configs.templateRecommended`
|
|
42
|
+
> alongside these configs.** They already include them. Declaring the same plugin twice from
|
|
43
|
+
> two different copies makes ESLint fail outright, before linting anything.
|
|
44
|
+
|
|
40
45
|
> 🚨 **Prerequisite**: `lintFilePatterns` in your `angular.json` must include
|
|
41
46
|
> `src/**/*.html`. Without it `ng lint` never opens your templates, and the template rules
|
|
42
47
|
> silently never run.
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
## The configs
|
|
50
|
+
|
|
51
|
+
| Config | Target | Contents |
|
|
45
52
|
| ----------------------------- | ----------- | -------------------------------------------------------------- |
|
|
46
|
-
| `configs.tsRecommended` | `**/*.ts` | `
|
|
47
|
-
| `configs.templateRecommended` | `**/*.html` |
|
|
53
|
+
| `configs.tsRecommended` | `**/*.ts` | the baselines, the house style, and `tsRules` |
|
|
54
|
+
| `configs.templateRecommended` | `**/*.html` | the baselines, the house style, and `templateRules` |
|
|
55
|
+
| `configs.tsRules` | `**/*.ts` | `no-direct-signal-mutation` |
|
|
56
|
+
| `configs.templateRules` | `**/*.html` | `no-direct-signal-mutation`, `no-uninvoked-signal-in-template` |
|
|
57
|
+
|
|
58
|
+
The `*Recommended` presets are what a project normally extends. On top of the rules written
|
|
59
|
+
here they bring:
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
- the baselines — `eslint.configs.recommended`, `typescript-eslint` recommended and
|
|
62
|
+
stylistic, `angular-eslint` recommended, and for templates its accessibility set;
|
|
63
|
+
- the house style — naming and selector conventions, import ordering
|
|
64
|
+
(`simple-import-sort`, `import-x`), rxjs conventions, and the signal rules of
|
|
65
|
+
`angular-eslint` (`no-uncalled-signals`, `prefer-signals`, `prefer-signal-model`).
|
|
66
|
+
|
|
67
|
+
Use `tsRules` / `templateRules` instead when a project supplies its own style and only wants
|
|
68
|
+
the rules of this library. They register the plugin and nothing else, so you keep control of
|
|
69
|
+
the baselines — and stay responsible for keeping them aligned.
|
|
70
|
+
|
|
71
|
+
`no-direct-signal-mutation` appears in both targets: the rule carries one visitor for
|
|
72
|
+
TypeScript and one for the Angular template AST, and each fires on its own kind of file.
|
|
51
73
|
|
|
52
74
|
## What happens when you update the library
|
|
53
75
|
|
|
@@ -62,10 +84,7 @@ your own `rules` block. It is applied after the shared config, so it wins:
|
|
|
62
84
|
```ts
|
|
63
85
|
{
|
|
64
86
|
files: ['**/*.html'],
|
|
65
|
-
extends: [
|
|
66
|
-
...angular.configs.templateRecommended,
|
|
67
|
-
...prestationsNg.configs.templateRecommended,
|
|
68
|
-
],
|
|
87
|
+
extends: [...prestationsNg.configs.templateRecommended],
|
|
69
88
|
rules: {
|
|
70
89
|
// decided by this project, survives library updates
|
|
71
90
|
'@dsivd/prestations-ng/no-direct-signal-mutation': ['off'],
|
|
@@ -77,8 +96,9 @@ your own `rules` block. It is applied after the shared config, so it wins:
|
|
|
77
96
|
|
|
78
97
|
### `no-direct-signal-mutation`
|
|
79
98
|
|
|
80
|
-
Reports
|
|
81
|
-
|
|
99
|
+
Reports a mutation of the value held by a signal, in either of its two forms: an
|
|
100
|
+
assignment into one of its members, or a call to an in-place mutator. Mutating that value
|
|
101
|
+
does not notify the signal's dependents, so the view is not refreshed.
|
|
82
102
|
|
|
83
103
|
Applies to **TypeScript files and templates**, including the case where the mutation hides
|
|
84
104
|
in a two-way binding.
|
|
@@ -86,14 +106,20 @@ in a two-way binding.
|
|
|
86
106
|
```ts
|
|
87
107
|
// ✗ mutates the object inside the signal, no notification
|
|
88
108
|
this.model().property = value;
|
|
109
|
+
this.model().items.splice(index, 1);
|
|
89
110
|
|
|
90
111
|
// ✓
|
|
91
112
|
this.model.update((current) => ({ ...current, property: value }));
|
|
113
|
+
this.model.update((current) => ({
|
|
114
|
+
...current,
|
|
115
|
+
items: current.items.filter((_, i) => i !== index),
|
|
116
|
+
}));
|
|
92
117
|
```
|
|
93
118
|
|
|
94
119
|
```html
|
|
95
120
|
<!-- ✗ -->
|
|
96
121
|
<button (click)="model().property = value">…</button>
|
|
122
|
+
<button (click)="model().items.splice(index, 1)">…</button>
|
|
97
123
|
<input [(ngModel)]="model().property" />
|
|
98
124
|
|
|
99
125
|
<!-- ✓ -->
|
|
@@ -103,23 +129,130 @@ this.model.update((current) => ({ ...current, property: value }));
|
|
|
103
129
|
All the assignment operators accepted by the Angular expression parser are covered: `=`,
|
|
104
130
|
`+=`, `-=`, `*=`, `/=`, `%=`, `**=`, `&&=`, `||=`, `??=`.
|
|
105
131
|
|
|
106
|
-
|
|
132
|
+
The in-place mutators covered are those of `Array` — `push`, `pop`, `shift`, `unshift`,
|
|
133
|
+
`splice`, `sort`, `reverse`, `fill`, `copyWithin` — and of `Map` / `Set` — `set`, `add`,
|
|
134
|
+
`delete`, `clear`. Their non-mutating counterparts, `toSorted`, `toReversed`, `filter`,
|
|
135
|
+
`concat`, `with`, are never reported: they return a new value. `items()["push"](x)` is the
|
|
136
|
+
same call as `items().push(x)` and is reported alike.
|
|
107
137
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
138
|
+
Two other ways of writing into the value are covered, in TypeScript — the Angular template
|
|
139
|
+
parser accepts neither:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
delete this.model().users[0];
|
|
143
|
+
Object.assign(this.model(), { property: value });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### Names bound to the value
|
|
147
|
+
|
|
148
|
+
A mutation is rarely written on the signal read itself. The rule follows the value through
|
|
149
|
+
the bindings it can resolve exactly, and only those:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// ✗ a variable written once
|
|
153
|
+
const users = this.model().users;
|
|
154
|
+
users.push(user);
|
|
155
|
+
|
|
156
|
+
// ✗ the element parameter of an iteration callback
|
|
157
|
+
this.model().users.forEach((user) => (user.done = true));
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```angular-html
|
|
161
|
+
<!-- ✗ a @for item, bound only inside its own block -->
|
|
162
|
+
@for (user of model().users; track user.id) {
|
|
163
|
+
<input [(ngModel)]="user.done" />
|
|
164
|
+
<button (click)="user.tags.push(tag)">…</button>
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
<!-- ✗ a @let -->
|
|
168
|
+
@let users = model().users;
|
|
169
|
+
<button (click)="users.push(user)">…</button>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
In TypeScript these come from ESLint's scope analysis, in templates from the block that
|
|
173
|
+
introduces the name — nothing is inferred from a naming convention. A variable that is
|
|
174
|
+
**reassigned** is dropped: the name may hold something else by the time it is mutated. Same
|
|
175
|
+
for `reduce`, whose first parameter is the accumulator and not an element.
|
|
176
|
+
|
|
177
|
+
**What it cannot follow** is a value crossing a function boundary — handed to a method,
|
|
178
|
+
stored in a class field, returned and mutated elsewhere:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// not reported: the rule cannot know what `sortInto` does
|
|
182
|
+
this.sortInto(this.model().users);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
That needs interprocedural analysis, which ESLint does not provide; a full type checker
|
|
186
|
+
would be required. It is the one remaining gap in detection.
|
|
187
|
+
|
|
188
|
+
#### Only real signals are inspected
|
|
189
|
+
|
|
190
|
+
The rule does not guess from the shape of a call. It reads the declaring class — the linted
|
|
191
|
+
file itself, or the component a template belongs to — and resolves its signal members
|
|
192
|
+
through the whole inheritance chain, the way TypeScript would: relative paths,
|
|
193
|
+
`node_modules`, `paths` mappings, and the flattened `.d.ts` of a published library. Nothing
|
|
194
|
+
has to be declared by hand.
|
|
195
|
+
|
|
196
|
+
**No signal's value is to be mutated, whatever its kind** — a mutation notifies none of its
|
|
197
|
+
dependents. The kinds differ only in the advice the message carries, since none but the
|
|
198
|
+
first has a `.set()`:
|
|
199
|
+
|
|
200
|
+
| Declared as | On a mutation of its value |
|
|
201
|
+
| --------------------------------------- | ----------------------------------------------------------- |
|
|
202
|
+
| `signal()`, `model()`, `linkedSignal()` | use `.set()` / `.update()` |
|
|
203
|
+
| `computed()` | the value is derived: mutate the signal it is computed from |
|
|
204
|
+
| `input()` | the value belongs to the parent: ask through an `output()` |
|
|
205
|
+
| `toSignal()` | the value comes from the observable: change it upstream |
|
|
206
|
+
| `viewChild()`, `contentChildren()`, … | **the one exception**, left alone: see below |
|
|
207
|
+
|
|
208
|
+
A view query is the exception because it hands out a DOM element or a component instance,
|
|
209
|
+
and mutating that is the reason for asking for it: `rows().nativeElement.hidden = true` is
|
|
210
|
+
the normal way to use one. `.required` variants included.
|
|
211
|
+
|
|
212
|
+
Anything that is not a declared signal is left alone as well:
|
|
213
|
+
|
|
214
|
+
| Expression | Why |
|
|
215
|
+
| ------------------------------------ | ------------------------------------------------------------ |
|
|
216
|
+
| `this.getConfig().items.push(item)` | `getConfig()` is a plain method, not a signal |
|
|
217
|
+
| `map.get(key).files.push(file)` | the call takes arguments, so it is not a signal read |
|
|
218
|
+
| `component.model().items.push(item)` | reached through a foreign receiver, not the component itself |
|
|
219
|
+
|
|
220
|
+
Because the rule knows a real signal from an ordinary method, `set` / `add` / `delete` are
|
|
221
|
+
safe to report: `byId().set(k, v)` mutates the `Map` inside the signal, while `byId.set(m)`
|
|
222
|
+
replaces the signal's value and is correct.
|
|
223
|
+
|
|
224
|
+
**One accepted false positive.** A `computed()` may legitimately hand out a live mutable
|
|
225
|
+
object, and no type information is available to tell that apart from a derived value:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
readonly items = computed(() => this.form.get('items') as FormArray);
|
|
229
|
+
this.items().push(control); // reported, though this is the FormArray API
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**One accepted gap, and it is forced.** A published declaration file spells a `computed()`,
|
|
233
|
+
a `toSignal()` and a view query alike, `Signal<T>` — the factory name is gone. A signal
|
|
234
|
+
**inherited from a package** and known by its type only is therefore left alone:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
// in the published .d.ts, indistinguishable from one another
|
|
238
|
+
readonly fieldId: Signal<string>; // was a computed()
|
|
239
|
+
readonly inputElement: Signal<ElementRef>; // was a viewChild()
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Reporting that group would break the lint of every project extending a component of this
|
|
243
|
+
library: `prestations-ng` publishes 54 view queries, `AbstractPageComponent` and
|
|
244
|
+
`FoehnInputComponent` among them, and touching the element they queried is normal code. So
|
|
245
|
+
a mutation of an inherited `computed()` goes unreported — the narrower harm of the two.
|
|
246
|
+
Signals declared in your own project, base classes included, are unaffected: their factory
|
|
247
|
+
call is right there to be read.
|
|
114
248
|
|
|
115
249
|
The rule has no autofix: the correct rewrite depends on what you meant.
|
|
116
250
|
|
|
117
|
-
**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
which is read-only as well.
|
|
251
|
+
**When the declaring class cannot be read** — an unsaved buffer, a template with no twin
|
|
252
|
+
`.ts` — its signals are unknown, and the rule falls back to treating any argument-less
|
|
253
|
+
accessor on the component as a signal. It may then report a plain method. That direction is
|
|
254
|
+
deliberate: a false positive costs one `eslint-disable-next-line`, a missed mutation is a
|
|
255
|
+
silent bug.
|
|
123
256
|
|
|
124
257
|
### `no-uninvoked-signal-in-template`
|
|
125
258
|
|
|
@@ -162,13 +295,15 @@ found without declaring anything.
|
|
|
162
295
|
| `[(value)]="mySignal"` | a writable signal is two-way bound **without** parentheses, Angular calls `.set()` itself |
|
|
163
296
|
| `#ref`, `@for` items, `@if … as`, `@let`, `let-x`, `$index`… | template-local names shadow the class members |
|
|
164
297
|
|
|
165
|
-
##
|
|
298
|
+
## The TypeScript counterpart
|
|
166
299
|
|
|
167
300
|
`@angular-eslint/no-uncalled-signals` covers the same class of bug **in TypeScript**, using
|
|
168
301
|
the real type checker — far more reliable than what a template rule can do. It cannot run on
|
|
169
302
|
templates: it requires type information, which a `.html` file has no program for. The two
|
|
170
303
|
are complementary, not redundant.
|
|
171
304
|
|
|
305
|
+
`configs.tsRecommended` already enables it. With `configs.tsRules` you enable it yourself:
|
|
306
|
+
|
|
172
307
|
```ts
|
|
173
308
|
'@angular-eslint/no-uncalled-signals': 'error',
|
|
174
309
|
```
|
package/UPGRADING_V19.md
CHANGED
|
@@ -1085,13 +1085,10 @@ rm .eslintrc.json
|
|
|
1085
1085
|
|
|
1086
1086
|
```ts
|
|
1087
1087
|
// @ts-check
|
|
1088
|
-
import
|
|
1088
|
+
import prettier from 'eslint-config-prettier';
|
|
1089
1089
|
import tseslint from 'typescript-eslint';
|
|
1090
1090
|
import angular from 'angular-eslint';
|
|
1091
|
-
|
|
1092
|
-
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
|
1093
|
-
import prettier from 'eslint-config-prettier';
|
|
1094
|
-
import importX from 'eslint-plugin-import-x';
|
|
1091
|
+
|
|
1095
1092
|
import prestationsNg from '@dsivd/prestations-ng/eslint';
|
|
1096
1093
|
|
|
1097
1094
|
export default tseslint.config(
|
|
@@ -1102,152 +1099,20 @@ export default tseslint.config(
|
|
|
1102
1099
|
// ─── TypeScript ────────────────────────────────────────────────────────────
|
|
1103
1100
|
{
|
|
1104
1101
|
files: ['**/*.ts'],
|
|
1105
|
-
extends: [
|
|
1106
|
-
eslint.configs.recommended,
|
|
1107
|
-
...tseslint.configs.recommended, // covers: no-explicit-any, ban-ts-comment,
|
|
1108
|
-
// no-unused-expressions, no-var, no-unused-vars…
|
|
1109
|
-
...tseslint.configs.stylistic, // covers: array-type, consistent-type-assertions,
|
|
1110
|
-
// dot-notation, no-inferrable-types (default options),
|
|
1111
|
-
// no-empty-function (error), prefer-for-of,
|
|
1112
|
-
// prefer-function-type…
|
|
1113
|
-
...angular.configs.tsRecommended, // covers: prefer-inject, no-empty-lifecycle-method,
|
|
1114
|
-
// use-lifecycle-interface…
|
|
1115
|
-
...prestationsNg.configs.tsRecommended, // covers: no-direct-signal-mutation
|
|
1116
|
-
],
|
|
1102
|
+
extends: [...prestationsNg.configs.tsRecommended],
|
|
1117
1103
|
languageOptions: {
|
|
1118
1104
|
parserOptions: {
|
|
1119
1105
|
projectService: true,
|
|
1120
1106
|
tsconfigRootDir: import.meta.dirname,
|
|
1121
1107
|
},
|
|
1122
1108
|
},
|
|
1123
|
-
plugins: {
|
|
1124
|
-
rxjs,
|
|
1125
|
-
'simple-import-sort': simpleImportSort,
|
|
1126
|
-
'import-x': importX,
|
|
1127
|
-
},
|
|
1128
1109
|
processor: angular.processInlineTemplates,
|
|
1129
|
-
rules: {
|
|
1130
|
-
// ── @angular-eslint ──────────────────────────────────────────────
|
|
1131
|
-
'@angular-eslint/component-selector': [
|
|
1132
|
-
'error',
|
|
1133
|
-
{
|
|
1134
|
-
type: 'element',
|
|
1135
|
-
prefix: 'app',
|
|
1136
|
-
style: 'kebab-case',
|
|
1137
|
-
},
|
|
1138
|
-
],
|
|
1139
|
-
'@angular-eslint/directive-selector': [
|
|
1140
|
-
'error',
|
|
1141
|
-
{
|
|
1142
|
-
type: 'attribute',
|
|
1143
|
-
prefix: 'app',
|
|
1144
|
-
style: 'camelCase',
|
|
1145
|
-
},
|
|
1146
|
-
],
|
|
1147
|
-
'@angular-eslint/no-uncalled-signals': ['error'],
|
|
1148
|
-
'@angular-eslint/prefer-signal-model': ['error'],
|
|
1149
|
-
'@angular-eslint/prefer-signals': ['error'],
|
|
1150
|
-
|
|
1151
|
-
// ── @typescript-eslint ───────────────────────────────────────────
|
|
1152
|
-
'@typescript-eslint/explicit-function-return-type': [
|
|
1153
|
-
'error',
|
|
1154
|
-
{
|
|
1155
|
-
allowExpressions: true,
|
|
1156
|
-
allowTypedFunctionExpressions: true,
|
|
1157
|
-
allowHigherOrderFunctions: true,
|
|
1158
|
-
allowDirectConstAssertionInArrowFunctions: true,
|
|
1159
|
-
},
|
|
1160
|
-
],
|
|
1161
|
-
'@typescript-eslint/explicit-member-accessibility': [
|
|
1162
|
-
'error',
|
|
1163
|
-
{ accessibility: 'no-public' },
|
|
1164
|
-
],
|
|
1165
|
-
'@typescript-eslint/member-delimiter-style': 'error',
|
|
1166
|
-
'@typescript-eslint/member-ordering': 'error',
|
|
1167
|
-
'@typescript-eslint/no-base-to-string': 'warn',
|
|
1168
|
-
// stylistic enables no-inferrable-types with default options (ignoreParameters: false);
|
|
1169
|
-
// override here to allow typed parameters explicitly
|
|
1170
|
-
'@typescript-eslint/no-inferrable-types': [
|
|
1171
|
-
'error',
|
|
1172
|
-
{ ignoreParameters: true },
|
|
1173
|
-
],
|
|
1174
|
-
// stylistic sets no-empty-function to error; downgrade to warn
|
|
1175
|
-
'@typescript-eslint/no-empty-function': 'warn',
|
|
1176
|
-
'@typescript-eslint/prefer-includes': 'warn',
|
|
1177
|
-
'@typescript-eslint/return-await': ['error', 'never'],
|
|
1178
|
-
'@typescript-eslint/typedef': ['error', { parameter: true }],
|
|
1179
|
-
'@typescript-eslint/unified-signatures': 'error',
|
|
1180
|
-
// off here — @typescript-eslint/no-shadow handles it correctly for TS
|
|
1181
|
-
'no-shadow': 'off',
|
|
1182
|
-
'@typescript-eslint/no-shadow': ['error', { hoist: 'all' }],
|
|
1183
|
-
'@typescript-eslint/no-unused-vars': [
|
|
1184
|
-
'error',
|
|
1185
|
-
{
|
|
1186
|
-
varsIgnorePattern: '^_',
|
|
1187
|
-
argsIgnorePattern: '^_',
|
|
1188
|
-
caughtErrorsIgnorePattern: '^_',
|
|
1189
|
-
},
|
|
1190
|
-
],
|
|
1191
|
-
|
|
1192
|
-
// ── eslint core ──────────────────────────────────────────────────
|
|
1193
|
-
'arrow-body-style': 'error',
|
|
1194
|
-
'arrow-parens': ['error', 'as-needed'],
|
|
1195
|
-
curly: 'error',
|
|
1196
|
-
eqeqeq: ['error', 'always'],
|
|
1197
|
-
'guard-for-in': 'error',
|
|
1198
|
-
'no-bitwise': 'error',
|
|
1199
|
-
'no-caller': 'error',
|
|
1200
|
-
'no-console': ['error', { allow: ['log', 'warn', 'error'] }],
|
|
1201
|
-
'no-eval': 'error',
|
|
1202
|
-
'no-extra-boolean-cast': 'off',
|
|
1203
|
-
'no-multiple-empty-lines': 'error',
|
|
1204
|
-
'no-new-wrappers': 'error',
|
|
1205
|
-
'no-restricted-imports': [
|
|
1206
|
-
'error',
|
|
1207
|
-
{ paths: ['rxjs/Rx', 'primeng/primeng', 'primeng'] },
|
|
1208
|
-
],
|
|
1209
|
-
'no-return-assign': 'error',
|
|
1210
|
-
'no-throw-literal': 'error',
|
|
1211
|
-
'no-undef-init': 'error',
|
|
1212
|
-
'no-useless-concat': 'error',
|
|
1213
|
-
'object-shorthand': ['error', 'always', { avoidQuotes: true }],
|
|
1214
|
-
'one-var': ['error', 'never'],
|
|
1215
|
-
'prefer-arrow-callback': 'error',
|
|
1216
|
-
'prefer-template': 'error',
|
|
1217
|
-
radix: 'error',
|
|
1218
|
-
|
|
1219
|
-
// ── rxjs ─────────────────────────────────────────────────────────
|
|
1220
|
-
'rxjs/no-implicit-any-catch': ['error', { allowExplicitAny: true }],
|
|
1221
|
-
'rxjs/no-sharereplay': 'off',
|
|
1222
|
-
|
|
1223
|
-
// ── import-x ─────────────────────────────────────────────────────
|
|
1224
|
-
'import-x/no-cycle': [
|
|
1225
|
-
'warn',
|
|
1226
|
-
{ maxDepth: 3, ignoreExternal: true },
|
|
1227
|
-
],
|
|
1228
|
-
'import-x/no-deprecated': 'warn',
|
|
1229
|
-
'import-x/first': 'error',
|
|
1230
|
-
'import-x/newline-after-import': 'error',
|
|
1231
|
-
'import-x/no-duplicates': 'error',
|
|
1232
|
-
|
|
1233
|
-
// ── simple-import-sort ───────────────────────────────────────────
|
|
1234
|
-
'simple-import-sort/imports': 'error',
|
|
1235
|
-
'simple-import-sort/exports': 'error',
|
|
1236
|
-
},
|
|
1237
1110
|
},
|
|
1238
1111
|
|
|
1239
1112
|
// ─── HTML templates ────────────────────────────────────────────────────────
|
|
1240
1113
|
{
|
|
1241
1114
|
files: ['**/*.html'],
|
|
1242
|
-
extends: [
|
|
1243
|
-
...angular.configs.templateRecommended,
|
|
1244
|
-
...angular.configs.templateAccessibility,
|
|
1245
|
-
...prestationsNg.configs.templateRecommended, // covers no-direct-signal-mutation, no-uninvoked-signal-in-template
|
|
1246
|
-
],
|
|
1247
|
-
rules: {
|
|
1248
|
-
'@angular-eslint/template/no-negated-async': 'off',
|
|
1249
|
-
'@angular-eslint/template/button-has-type': 'error',
|
|
1250
|
-
},
|
|
1115
|
+
extends: [...prestationsNg.configs.templateRecommended],
|
|
1251
1116
|
},
|
|
1252
1117
|
|
|
1253
1118
|
// ─── Prettier (must be last — disables formatting rules) ───────────────────
|
|
@@ -1255,19 +1120,31 @@ export default tseslint.config(
|
|
|
1255
1120
|
);
|
|
1256
1121
|
```
|
|
1257
1122
|
|
|
1123
|
+
That is the whole file. The two `*Recommended` configs come from the library and carry
|
|
1124
|
+
everything: the `eslint`, `typescript-eslint` and `angular-eslint` baselines, the template
|
|
1125
|
+
accessibility set, the naming and import conventions, and the rules written by the library
|
|
1126
|
+
itself. You declare no `plugins` and list no rule.
|
|
1127
|
+
|
|
1128
|
+
The packages installed above are still required — the library's config imports them from your
|
|
1129
|
+
`node_modules` rather than shipping its own copies. For the same reason, do **not** spread
|
|
1130
|
+
`angular.configs.tsRecommended` or `angular.configs.templateRecommended` yourself: they are
|
|
1131
|
+
already included, and declaring the same plugin twice from two different copies makes ESLint
|
|
1132
|
+
fail outright.
|
|
1133
|
+
|
|
1258
1134
|
#### prestations-ng ESLint rules
|
|
1259
1135
|
|
|
1260
1136
|
The library ships **shareable custom configs** that are auto-updatable with the library version. Two
|
|
1261
1137
|
rules are enabled today — `no-direct-signal-mutation` and `no-uninvoked-signal-in-template` — and both
|
|
1262
|
-
are documented in [the plugin guide](ESLINT_PLUGIN.md)
|
|
1138
|
+
are documented in [the plugin guide](ESLINT_PLUGIN.md), alongside the leaner `tsRules` / `templateRules`
|
|
1139
|
+
configs for a project that supplies its own style.
|
|
1263
1140
|
|
|
1264
1141
|
One consequence to keep in mind:
|
|
1265
1142
|
|
|
1266
|
-
- A rule added in a later version of the library lands in the config, so
|
|
1267
|
-
make your lint fail** on the offending code. That is the point — it is
|
|
1268
|
-
rolled out to every project. Each addition is listed in the
|
|
1269
|
-
a rule you cannot fix right away can be turned off in your
|
|
1270
|
-
applied after the shared config:
|
|
1143
|
+
- A rule added — or tightened — in a later version of the library lands in the config, so
|
|
1144
|
+
`npm update` **will make your lint fail** on the offending code. That is the point — it is
|
|
1145
|
+
how a practice is rolled out to every project. Each addition is listed in the
|
|
1146
|
+
[CHANGELOG](CHANGELOG.md), and a rule you cannot fix right away can be turned off in your
|
|
1147
|
+
own `rules` block, which is applied after the shared config:
|
|
1271
1148
|
|
|
1272
1149
|
```ts
|
|
1273
1150
|
rules: {
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Path of the component a linted template belongs to, `undefined` when the file is not a
|
|
2
|
+
// template.
|
|
3
|
+
//
|
|
4
|
+
// An inline template is a virtual block whose path is derived from the `.ts`, and whose
|
|
5
|
+
// name ends with `.html` too: `foo.component.ts/1_inline-template-….component.html`.
|
|
6
|
+
// It has to be matched BEFORE the external template, otherwise it resolves to garbage.
|
|
7
|
+
export function resolveComponentPath(filename) {
|
|
8
|
+
const inlineMatch = filename.match(/^(.*\.ts)(?:[/\\]|$)/);
|
|
9
|
+
if (inlineMatch) {
|
|
10
|
+
return inlineMatch[1];
|
|
11
|
+
}
|
|
12
|
+
return filename.endsWith(".html")
|
|
13
|
+
? `${filename.slice(0, -".html".length)}.ts`
|
|
14
|
+
: undefined;
|
|
15
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Registers the plugin without enabling any rule. Meant to be used through
|
|
2
|
-
// `templateRecommended`, inside the `extends` of a block that
|
|
3
|
-
// `**/*.html` and sets the Angular template parser.
|
|
2
|
+
// `templateRules` or `templateRecommended`, inside the `extends` of a block that
|
|
3
|
+
// already targets `**/*.html` and sets the Angular template parser.
|
|
4
4
|
const templateBase = (plugin) => ({
|
|
5
5
|
name: 'prestations-ng/template-base',
|
|
6
6
|
plugins: {
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import
|
|
1
|
+
import angular from 'angular-eslint';
|
|
2
2
|
|
|
3
|
+
import templateRules from './template-rules.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The house Angular template preset: the recommended and accessibility baselines, this
|
|
7
|
+
* library's own rules, and the conventions every prestations-ng project follows.
|
|
8
|
+
*
|
|
9
|
+
* Meant for the `extends` of a `**\/*.html` block. It brings its own parser and plugins,
|
|
10
|
+
* so do not spread `angular.configs.templateRecommended` alongside it.
|
|
11
|
+
*/
|
|
3
12
|
const templateRecommended = (plugin) => [
|
|
4
|
-
|
|
13
|
+
...angular.configs.templateRecommended,
|
|
14
|
+
...angular.configs.templateAccessibility,
|
|
15
|
+
...templateRules(plugin),
|
|
5
16
|
{
|
|
6
17
|
name: 'prestations-ng/template-recommended',
|
|
7
18
|
rules: {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'@dsivd/prestations-ng/no-direct-signal-mutation': 'error',
|
|
11
|
-
'@dsivd/prestations-ng/no-uninvoked-signal-in-template': 'error',
|
|
19
|
+
'@angular-eslint/template/no-negated-async': 'off',
|
|
20
|
+
'@angular-eslint/template/button-has-type': 'error',
|
|
12
21
|
},
|
|
13
22
|
},
|
|
14
23
|
];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import templateBase from './template-base.mjs';
|
|
2
|
+
|
|
3
|
+
// Only the rules written by this library. `templateRecommended` includes it, alongside
|
|
4
|
+
// the house style; use this layer directly to opt out of the latter.
|
|
5
|
+
const templateRules = (plugin) => [
|
|
6
|
+
templateBase(plugin),
|
|
7
|
+
{
|
|
8
|
+
name: 'prestations-ng/template-rules',
|
|
9
|
+
rules: {
|
|
10
|
+
// Also in `tsRules`: the rule carries both an ESTree and an Angular
|
|
11
|
+
// template visitor, and each fires on its own kind of file.
|
|
12
|
+
'@dsivd/prestations-ng/no-direct-signal-mutation': 'error',
|
|
13
|
+
'@dsivd/prestations-ng/no-uninvoked-signal-in-template': 'error',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
export default templateRules;
|