@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +37 -4
  2. package/CONTRIBUTING.md +3 -2
  3. package/ESLINT_PLUGIN.md +170 -35
  4. package/UPGRADING_V19.md +22 -145
  5. package/dsivd-prestations-ng-19.0.8-beta.1.tgz +0 -0
  6. package/eslint/component-path.mjs +15 -0
  7. package/eslint/configs/template-base.mjs +2 -2
  8. package/eslint/configs/template-recommended.mjs +15 -6
  9. package/eslint/configs/template-rules.mjs +17 -0
  10. package/eslint/configs/ts-base.mjs +2 -2
  11. package/eslint/configs/ts-recommended.mjs +133 -3
  12. package/eslint/configs/ts-rules.mjs +14 -0
  13. package/eslint/index.mjs +10 -4
  14. package/eslint/rules/no-direct-signal-mutation.mjs +370 -142
  15. package/eslint/rules/no-uninvoked-signal-in-template.mjs +1 -12
  16. package/eslint/signal-kinds.mjs +57 -0
  17. package/eslint/signal-names.mjs +43 -49
  18. package/fesm2022/dsivd-prestations-ng.mjs +487 -487
  19. package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
  20. package/package.json +1 -1
  21. package/src/eslint/component-path.mjs +15 -0
  22. package/src/eslint/configs/__tests__/configs.test.mjs +113 -48
  23. package/src/eslint/configs/template-base.mjs +2 -2
  24. package/src/eslint/configs/template-recommended.mjs +15 -6
  25. package/src/eslint/configs/template-rules.mjs +17 -0
  26. package/src/eslint/configs/ts-base.mjs +2 -2
  27. package/src/eslint/configs/ts-recommended.mjs +133 -3
  28. package/src/eslint/configs/ts-rules.mjs +14 -0
  29. package/src/eslint/index.mjs +10 -4
  30. package/src/eslint/rules/__tests__/no-direct-signal-mutation.test.mjs +365 -128
  31. package/src/eslint/rules/no-direct-signal-mutation.mjs +370 -142
  32. package/src/eslint/rules/no-uninvoked-signal-in-template.mjs +1 -12
  33. package/src/eslint/signal-kinds.mjs +57 -0
  34. package/src/eslint/signal-names.mjs +43 -49
  35. package/types/dsivd-prestations-ng.d.ts +0 -1
  36. 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
- ## [Unreleased]
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). Nothing
16
- else is needed: no `plugins` declaration, no rule list.
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-recommended.mjs`, `configs/template-recommended.mjs`, or both
135
- depending on what it inspects
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. It is published as a subpath of the library, so the rules
5
- follow the version of the library.
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
- ...angular.configs.tsRecommended,
22
- ...prestationsNg.configs.tsRecommended,
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
- | Config | Target | Rules |
49
+ ## The configs
50
+
51
+ | Config | Target | Contents |
45
52
  | ----------------------------- | ----------- | -------------------------------------------------------------- |
46
- | `configs.tsRecommended` | `**/*.ts` | `no-direct-signal-mutation` |
47
- | `configs.templateRecommended` | `**/*.html` | `no-direct-signal-mutation`, `no-uninvoked-signal-in-template` |
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
- `no-direct-signal-mutation` appears in both: the rule carries one visitor for TypeScript
50
- and one for the Angular template AST, and each fires on its own kind of file.
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 an assignment into a member of a signal's value. Mutating the object held by a
81
- signal does not notify its dependents, so the view is not refreshed.
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
- **Not reported**, because the call is not assumed to be a signal read:
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
- | Expression | Why |
109
- | --------------------------------------------------- | ----------------------------------------------------------------- |
110
- | `map.get(key).files = []` | the call takes arguments |
111
- | `component.inputElement().nativeElement.value = ''` | reached through a foreign receiver, not `this` |
112
- | `obj.getter().property = value` | idem |
113
- | `viewChild(…)` / `viewChild.required()` accessors | a view query signal is read-only, there is no `.set()` to suggest |
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
- **Known limitation.** The view query exclusion needs the component class, which a template
118
- AST does not carry. A mutation reached through a view query **from a template** is therefore
119
- still reported, with a message suggesting a `.update()` that does not exist on a read-only
120
- signal. It is rare mutating the DOM from a template expression but if you hit it, turn
121
- the rule off for that file. The same imprecision applies to `computed()` in TypeScript,
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
- ## Also worth enabling
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 eslint from '@eslint/js';
1088
+ import prettier from 'eslint-config-prettier';
1089
1089
  import tseslint from 'typescript-eslint';
1090
1090
  import angular from 'angular-eslint';
1091
- import rxjs from '@smarttools/eslint-plugin-rxjs';
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 `npm update` **will
1267
- make your lint fail** on the offending code. That is the point — it is how a practice is
1268
- rolled out to every project. Each addition is listed in the [CHANGELOG](CHANGELOG.md), and
1269
- a rule you cannot fix right away can be turned off in your own `rules` block, which is
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: {
@@ -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 already targets
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 templateBase from './template-base.mjs';
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
- templateBase(plugin),
13
+ ...angular.configs.templateRecommended,
14
+ ...angular.configs.templateAccessibility,
15
+ ...templateRules(plugin),
5
16
  {
6
17
  name: 'prestations-ng/template-recommended',
7
18
  rules: {
8
- // Also in `tsRecommended`: the rule carries both an ESTree and an Angular
9
- // template visitor, and each fires on its own kind of file.
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;