@dsivd/prestations-ng 19.0.6-beta.2 → 19.0.6

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 (35) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/CONTRIBUTING.md +24 -0
  3. package/ESLINT_PLUGIN.md +177 -0
  4. package/README.md +3 -0
  5. package/UPGRADING_V19.md +24 -6
  6. package/dsivd-prestations-ng-19.0.6.tgz +0 -0
  7. package/eslint/configs/template-base.mjs +10 -0
  8. package/eslint/configs/template-recommended.mjs +15 -0
  9. package/eslint/configs/ts-base.mjs +10 -0
  10. package/eslint/configs/ts-recommended.mjs +12 -0
  11. package/eslint/index.mjs +14 -5
  12. package/eslint/rules/index.mjs +7 -1
  13. package/eslint/rules/no-direct-signal-mutation.mjs +240 -136
  14. package/eslint/rules/no-uninvoked-signal-in-template.mjs +170 -0
  15. package/eslint/signal-names.mjs +232 -0
  16. package/eslint/template-ast.mjs +26 -0
  17. package/fesm2022/dsivd-prestations-ng.mjs +7 -9
  18. package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
  19. package/package.json +1 -1
  20. package/src/eslint/configs/__tests__/configs.test.mjs +70 -0
  21. package/src/eslint/configs/template-base.mjs +10 -0
  22. package/src/eslint/configs/template-recommended.mjs +15 -0
  23. package/src/eslint/configs/ts-base.mjs +10 -0
  24. package/src/eslint/configs/ts-recommended.mjs +12 -0
  25. package/src/eslint/index.mjs +14 -5
  26. package/src/eslint/rules/__tests__/no-direct-signal-mutation.test.mjs +86 -4
  27. package/src/eslint/rules/__tests__/no-uninvoked-signal-in-template.test.mjs +291 -0
  28. package/src/eslint/rules/index.mjs +7 -1
  29. package/src/eslint/rules/no-direct-signal-mutation.mjs +240 -136
  30. package/src/eslint/rules/no-uninvoked-signal-in-template.mjs +170 -0
  31. package/src/eslint/signal-names.mjs +232 -0
  32. package/src/eslint/template-ast.mjs +26 -0
  33. package/types/dsivd-prestations-ng.d.ts +0 -2
  34. package/dsivd-prestations-ng-19.0.6-beta.2.tgz +0 -0
  35. package/eslint/rules/__tests__/no-direct-signal-mutation.test.mjs +0 -98
package/CHANGELOG.md CHANGED
@@ -6,6 +6,30 @@
6
6
 
7
7
  ---
8
8
 
9
+ ## [Unreleased]
10
+
11
+ ### Added
12
+
13
+ - ESLint plugin: **shareable configs**, documented in [ESLINT_PLUGIN.md](ESLINT_PLUGIN.md)
14
+ - 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.
17
+ - 🚨 **Prerequisite**: `lintFilePatterns` in your `angular.json` must include
18
+ `src/**/*.html`, otherwise the template rules never run
19
+ - 🚨 **A rule added to these configs makes your lint fail on `npm update`.** This is
20
+ intentional. Every addition will be listed here, and a rule can be turned off from your
21
+ own `rules` block.
22
+ - ESLint rule [`no-direct-signal-mutation`](ESLINT_PLUGIN.md#no-direct-signal-mutation)
23
+ - reports an assignment into a member of a signal's value
24
+ (`model().property = value`), which mutates the object without notifying the signal.
25
+ Covers TypeScript files and templates, including the mutation hidden in a two-way
26
+ binding (`[(ngModel)]="model().property"`)
27
+ - ESLint rule [`no-uninvoked-signal-in-template`](ESLINT_PLUGIN.md#no-uninvoked-signal-in-template)
28
+ - reports a signal referenced in a template without being called (`{{ mySignal }}`,
29
+ `!mySignal`, `mySignal + 'x'`), which the Angular compiler only partially catches.
30
+ Autofixable. No configuration: inherited signals are resolved by walking the `extends`
31
+ chain of the component
32
+
9
33
  ## [19.0.2]
10
34
 
11
35
  ### Fixed
package/CONTRIBUTING.md CHANGED
@@ -122,6 +122,30 @@ You can auto-format the code on save with IntelliJ. More informations
122
122
 
123
123
  Please make sure you have followed the **Before committing** section so you will see formating/linting errors before you commit.
124
124
 
125
+ ### Adding a rule to the ESLint plugin
126
+
127
+ The library ships its own ESLint plugin (see [ESLINT_PLUGIN.md](ESLINT_PLUGIN.md)). Its rules
128
+ reach every consuming project through shareable configs, so **adding a rule breaks the lint of
129
+ those projects on their next `npm update`**. That is the intent, but it makes the checklist
130
+ below mandatory:
131
+
132
+ 1. Write the rule in `projects/prestations-ng/src/eslint/rules/<rule-name>.mjs`
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
136
+ 4. Test it in `rules/__tests__/<rule-name>.test.mjs` — use the `@angular-eslint/template-parser`
137
+ for a template rule
138
+ 5. Run `npm run test:eslint`, then `npm run lint:js` on the whole repository: a rule that
139
+ reports on the library's own code is a rule that will report on every project
140
+ 6. Document it in `ESLINT_PLUGIN.md`: what it catches, ✗/✓ examples, whether it autofixes, and
141
+ its known limitations
142
+ 7. Announce it in `CHANGELOG.md` **as a breaking change for consumers**, with the line needed
143
+ to turn it off
144
+
145
+ Step 5 matters more than it looks: the config tests in `configs/__tests__/configs.test.mjs`
146
+ guard against a config that references a rule which does not exist — a mistake that would
147
+ break every consuming project at once.
148
+
125
149
  ## Publishing a new version of the package
126
150
 
127
151
  Please use the dedicated Jenkins build:
@@ -0,0 +1,177 @@
1
+ # ESLint plugin
2
+
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.
6
+
7
+ ## Setup
8
+
9
+ The plugin exposes **shareable configs**, in the same spirit as `angular-eslint`. Add them to
10
+ the `extends` of the `**/*.ts` and `**/*.html` blocks of your `eslint.config.mjs`:
11
+
12
+ ```ts
13
+ import angular from 'angular-eslint';
14
+ import tseslint from 'typescript-eslint';
15
+ import prestationsNg from '@dsivd/prestations-ng/eslint';
16
+
17
+ export default tseslint.config(
18
+ {
19
+ files: ['**/*.ts'],
20
+ extends: [
21
+ ...angular.configs.tsRecommended,
22
+ ...prestationsNg.configs.tsRecommended,
23
+ ],
24
+ processor: angular.processInlineTemplates,
25
+ },
26
+ {
27
+ files: ['**/*.html'],
28
+ extends: [
29
+ ...angular.configs.templateRecommended,
30
+ ...prestationsNg.configs.templateRecommended,
31
+ ],
32
+ },
33
+ );
34
+ ```
35
+
36
+ That is all. You do **not** declare `plugins`, and you do **not** list the rules — both come
37
+ from the config. Inline templates written in a `@Component` are covered too, through
38
+ `angular.processInlineTemplates`.
39
+
40
+ > 🚨 **Prerequisite**: `lintFilePatterns` in your `angular.json` must include
41
+ > `src/**/*.html`. Without it `ng lint` never opens your templates, and the template rules
42
+ > silently never run.
43
+
44
+ | Config | Target | Rules |
45
+ | ----------------------------- | ----------- | -------------------------------------------------------------- |
46
+ | `configs.tsRecommended` | `**/*.ts` | `no-direct-signal-mutation` |
47
+ | `configs.templateRecommended` | `**/*.html` | `no-direct-signal-mutation`, `no-uninvoked-signal-in-template` |
48
+
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.
51
+
52
+ ## What happens when you update the library
53
+
54
+ A new rule added to the library lands in the config, so `npm update @dsivd/prestations-ng`
55
+ **will make your lint fail** on the code that violates it. This is intentional: it is how a
56
+ new practice gets rolled out across every project using the library. Every added rule is
57
+ listed in the [CHANGELOG](CHANGELOG.md).
58
+
59
+ If you cannot fix the violations right away, turn the rule off — or lower its severity — in
60
+ your own `rules` block. It is applied after the shared config, so it wins:
61
+
62
+ ```ts
63
+ {
64
+ files: ['**/*.html'],
65
+ extends: [
66
+ ...angular.configs.templateRecommended,
67
+ ...prestationsNg.configs.templateRecommended,
68
+ ],
69
+ rules: {
70
+ // decided by this project, survives library updates
71
+ '@dsivd/prestations-ng/no-direct-signal-mutation': ['off'],
72
+ },
73
+ },
74
+ ```
75
+
76
+ ## Rules
77
+
78
+ ### `no-direct-signal-mutation`
79
+
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.
82
+
83
+ Applies to **TypeScript files and templates**, including the case where the mutation hides
84
+ in a two-way binding.
85
+
86
+ ```ts
87
+ // ✗ mutates the object inside the signal, no notification
88
+ this.model().property = value;
89
+
90
+ // ✓
91
+ this.model.update((current) => ({ ...current, property: value }));
92
+ ```
93
+
94
+ ```html
95
+ <!-- ✗ -->
96
+ <button (click)="model().property = value">…</button>
97
+ <input [(ngModel)]="model().property" />
98
+
99
+ <!-- ✓ -->
100
+ <button (click)="model.update((v) => ({ ...v, property: value }))">…</button>
101
+ ```
102
+
103
+ All the assignment operators accepted by the Angular expression parser are covered: `=`,
104
+ `+=`, `-=`, `*=`, `/=`, `%=`, `**=`, `&&=`, `||=`, `??=`.
105
+
106
+ **Not reported**, because the call is not assumed to be a signal read:
107
+
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 |
114
+
115
+ The rule has no autofix: the correct rewrite depends on what you meant.
116
+
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.
123
+
124
+ ### `no-uninvoked-signal-in-template`
125
+
126
+ Reports a signal referenced in a template without being called. A bare reference evaluates
127
+ to the function itself: always truthy, and serialized as source code when interpolated.
128
+
129
+ Applies to **templates only** (external and inline).
130
+
131
+ ```html
132
+ <!-- ✗ -->
133
+ <p>{{ userName }}</p>
134
+ <p>{{ userName + '!' }}</p>
135
+ <div [hidden]="!isLoading">…</div>
136
+
137
+ <!-- ✓ -->
138
+ <p>{{ userName() }}</p>
139
+ <p>{{ userName() + '!' }}</p>
140
+ <div [hidden]="!isLoading()">…</div>
141
+ ```
142
+
143
+ A control flow condition counts too: `@if (isLoading)` must be `@if (isLoading())`.
144
+
145
+ The Angular compiler only covers part of these cases (`NG8109` / `NG8117`): it lets the
146
+ negation and the concatenation through.
147
+
148
+ The rule is **autofixable** — `npm run lint` with `--fix` adds the missing parentheses.
149
+
150
+ Signal names are resolved automatically, with no configuration: the rule reads the twin
151
+ `.ts` of the template and walks its whole inheritance chain, resolving each `extends` the
152
+ way TypeScript would — relative paths, `node_modules`, `paths` mappings. Signals inherited
153
+ from `FoehnInputComponent`, `AbstractPageComponent` or from one of your own base classes are
154
+ found without declaring anything.
155
+
156
+ **Not reported:**
157
+
158
+ | Expression | Why |
159
+ | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
160
+ | `mySignal()`, `mySignal?.()` | already called |
161
+ | `mySignal.set(…)`, `.update(…)`, `.asReadonly()` | targets the signal, not its value |
162
+ | `[(value)]="mySignal"` | a writable signal is two-way bound **without** parentheses, Angular calls `.set()` itself |
163
+ | `#ref`, `@for` items, `@if … as`, `@let`, `let-x`, `$index`… | template-local names shadow the class members |
164
+
165
+ ## Also worth enabling
166
+
167
+ `@angular-eslint/no-uncalled-signals` covers the same class of bug **in TypeScript**, using
168
+ the real type checker — far more reliable than what a template rule can do. It cannot run on
169
+ templates: it requires type information, which a `.html` file has no program for. The two
170
+ are complementary, not redundant.
171
+
172
+ ```ts
173
+ '@angular-eslint/no-uncalled-signals': 'error',
174
+ ```
175
+
176
+ It only reports signals used in logic positions (`if`, `!x`, `&&`, `===`, `.length`…), not a
177
+ bare `const x = mySignal`.
package/README.md CHANGED
@@ -20,6 +20,9 @@ The documentation is available online at [https://dsi-vd.github.io/prestations-n
20
20
  - If you're using **Angular 6 or 7**, use `@dsivd/prestations-ng` at version `^8`.
21
21
  - If you're using **Angular 5**, use `prestations-ng` at version `^7`.
22
22
 
23
+ The library also ships an ESLint plugin whose rules apply to your project: see
24
+ [the plugin guide](ESLINT_PLUGIN.md).
25
+
23
26
  Migration guides are available to provide an upgrade path from one major version to another.
24
27
 
25
28
  - If you need to upgrade to version `19+`, please refer to [the V19 upgrade guide](UPGRADING_V19.md).
package/UPGRADING_V19.md CHANGED
@@ -381,7 +381,7 @@ ng update ngx-matomo-client@9
381
381
 
382
382
  - In your `.eslintrc.json`, remove `"/src/main.ts",` from `ignorePatterns`.
383
383
  - In `main.ts`, replace `platformBrowserDynamic` by `platformBrowser`.
384
- - When using `<foehn-list-summary` component, replace `list` by `model`.
384
+ - When using `<foehn-list-summary` component, replace `[list]` by `[(model)]` (with double binding to handle item being removed).
385
385
 
386
386
  ### Now fix your project before continuing
387
387
 
@@ -1092,7 +1092,7 @@ import rxjs from '@smarttools/eslint-plugin-rxjs';
1092
1092
  import simpleImportSort from 'eslint-plugin-simple-import-sort';
1093
1093
  import prettier from 'eslint-config-prettier';
1094
1094
  import importX from 'eslint-plugin-import-x';
1095
- import prestationsNgEslint from '@dsivd/prestations-ng/eslint';
1095
+ import prestationsNg from '@dsivd/prestations-ng/eslint';
1096
1096
 
1097
1097
  export default tseslint.config(
1098
1098
  {
@@ -1112,6 +1112,7 @@ export default tseslint.config(
1112
1112
  // prefer-function-type…
1113
1113
  ...angular.configs.tsRecommended, // covers: prefer-inject, no-empty-lifecycle-method,
1114
1114
  // use-lifecycle-interface…
1115
+ ...prestationsNg.configs.tsRecommended, // covers: no-direct-signal-mutation
1115
1116
  ],
1116
1117
  languageOptions: {
1117
1118
  parserOptions: {
@@ -1123,7 +1124,6 @@ export default tseslint.config(
1123
1124
  rxjs,
1124
1125
  'simple-import-sort': simpleImportSort,
1125
1126
  'import-x': importX,
1126
- '@dsivd/prestations-ng': prestationsNgEslint,
1127
1127
  },
1128
1128
  processor: angular.processInlineTemplates,
1129
1129
  rules: {
@@ -1233,9 +1233,6 @@ export default tseslint.config(
1233
1233
  // ── simple-import-sort ───────────────────────────────────────────
1234
1234
  'simple-import-sort/imports': 'error',
1235
1235
  'simple-import-sort/exports': 'error',
1236
-
1237
- // ── prestations-ng ──────────────────────────────────────────────
1238
- '@dsivd/prestations-ng/no-direct-signal-mutation': 'error',
1239
1236
  },
1240
1237
  },
1241
1238
 
@@ -1245,6 +1242,7 @@ export default tseslint.config(
1245
1242
  extends: [
1246
1243
  ...angular.configs.templateRecommended,
1247
1244
  ...angular.configs.templateAccessibility,
1245
+ ...prestationsNg.configs.templateRecommended, // covers no-direct-signal-mutation, no-uninvoked-signal-in-template
1248
1246
  ],
1249
1247
  rules: {
1250
1248
  '@angular-eslint/template/no-negated-async': 'off',
@@ -1257,6 +1255,26 @@ export default tseslint.config(
1257
1255
  );
1258
1256
  ```
1259
1257
 
1258
+ #### prestations-ng ESLint rules
1259
+
1260
+ The library ships **shareable custom configs** that are auto-updatable with the library version. Two
1261
+ 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).
1263
+
1264
+ One consequence to keep in mind:
1265
+
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:
1271
+
1272
+ ```ts
1273
+ rules: {
1274
+ '@dsivd/prestations-ng/no-direct-signal-mutation': ['off'],
1275
+ },
1276
+ ```
1277
+
1260
1278
  #### Delete `typings.d.ts`
1261
1279
 
1262
1280
  In `front/src/`, remove the file `typings.d.ts` if it is still there.
Binary file
@@ -0,0 +1,10 @@
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.
4
+ const templateBase = (plugin) => ({
5
+ name: 'prestations-ng/template-base',
6
+ plugins: {
7
+ '@dsivd/prestations-ng': plugin,
8
+ },
9
+ });
10
+ export default templateBase;
@@ -0,0 +1,15 @@
1
+ import templateBase from './template-base.mjs';
2
+
3
+ const templateRecommended = (plugin) => [
4
+ templateBase(plugin),
5
+ {
6
+ name: 'prestations-ng/template-recommended',
7
+ 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',
12
+ },
13
+ },
14
+ ];
15
+ export default templateRecommended;
@@ -0,0 +1,10 @@
1
+ // Registers the plugin without enabling any rule. Meant to be used through
2
+ // `tsRecommended`, inside the `extends` of a block that already targets `**/*.ts`
3
+ // and sets the TypeScript parser.
4
+ const tsBase = (plugin) => ({
5
+ name: 'prestations-ng/ts-base',
6
+ plugins: {
7
+ '@dsivd/prestations-ng': plugin,
8
+ },
9
+ });
10
+ export default tsBase;
@@ -0,0 +1,12 @@
1
+ import tsBase from './ts-base.mjs';
2
+
3
+ const tsRecommended = (plugin) => [
4
+ tsBase(plugin),
5
+ {
6
+ name: 'prestations-ng/ts-recommended',
7
+ rules: {
8
+ '@dsivd/prestations-ng/no-direct-signal-mutation': 'error',
9
+ },
10
+ },
11
+ ];
12
+ export default tsRecommended;
package/eslint/index.mjs CHANGED
@@ -1,7 +1,16 @@
1
- import { noDirectSignalMutation } from './rules/index.mjs';
1
+ import templateRecommended from './configs/template-recommended.mjs';
2
+ import tsRecommended from './configs/ts-recommended.mjs';
3
+ import rules from './rules/index.mjs';
2
4
 
3
- export default {
4
- rules: {
5
- 'no-direct-signal-mutation': noDirectSignalMutation,
6
- },
5
+ const plugin = {
6
+ meta: { name: '@dsivd/prestations-ng' },
7
+ rules,
7
8
  };
9
+
10
+ const configs = {
11
+ tsRecommended: tsRecommended(plugin),
12
+ templateRecommended: templateRecommended(plugin),
13
+ };
14
+
15
+ export { configs, plugin, rules };
16
+ export default { configs, plugin, rules };
@@ -1 +1,7 @@
1
- export { default as noDirectSignalMutation } from './no-direct-signal-mutation.mjs';
1
+ import noDirectSignalMutation from './no-direct-signal-mutation.mjs';
2
+ import noUninvokedSignalInTemplate from './no-uninvoked-signal-in-template.mjs';
3
+
4
+ export default {
5
+ 'no-direct-signal-mutation': noDirectSignalMutation,
6
+ 'no-uninvoked-signal-in-template': noUninvokedSignalInTemplate,
7
+ };