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

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 (39) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CONTRIBUTING.md +25 -0
  3. package/ESLINT_PLUGIN.md +198 -0
  4. package/README.md +3 -0
  5. package/UPGRADING_V19.md +38 -143
  6. package/dsivd-prestations-ng-19.0.7.tgz +0 -0
  7. package/eslint/configs/template-base.mjs +10 -0
  8. package/eslint/configs/template-recommended.mjs +24 -0
  9. package/eslint/configs/template-rules.mjs +17 -0
  10. package/eslint/configs/ts-base.mjs +10 -0
  11. package/eslint/configs/ts-recommended.mjs +142 -0
  12. package/eslint/configs/ts-rules.mjs +14 -0
  13. package/eslint/index.mjs +20 -5
  14. package/eslint/rules/index.mjs +7 -1
  15. package/eslint/rules/no-direct-signal-mutation.mjs +240 -136
  16. package/eslint/rules/no-uninvoked-signal-in-template.mjs +170 -0
  17. package/eslint/signal-names.mjs +232 -0
  18. package/eslint/template-ast.mjs +26 -0
  19. package/fesm2022/dsivd-prestations-ng.mjs +7 -9
  20. package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
  21. package/package.json +1 -1
  22. package/src/eslint/configs/__tests__/configs.test.mjs +135 -0
  23. package/src/eslint/configs/template-base.mjs +10 -0
  24. package/src/eslint/configs/template-recommended.mjs +24 -0
  25. package/src/eslint/configs/template-rules.mjs +17 -0
  26. package/src/eslint/configs/ts-base.mjs +10 -0
  27. package/src/eslint/configs/ts-recommended.mjs +142 -0
  28. package/src/eslint/configs/ts-rules.mjs +14 -0
  29. package/src/eslint/index.mjs +20 -5
  30. package/src/eslint/rules/__tests__/no-direct-signal-mutation.test.mjs +86 -4
  31. package/src/eslint/rules/__tests__/no-uninvoked-signal-in-template.test.mjs +291 -0
  32. package/src/eslint/rules/index.mjs +7 -1
  33. package/src/eslint/rules/no-direct-signal-mutation.mjs +240 -136
  34. package/src/eslint/rules/no-uninvoked-signal-in-template.mjs +170 -0
  35. package/src/eslint/signal-names.mjs +232 -0
  36. package/src/eslint/template-ast.mjs +26 -0
  37. package/types/dsivd-prestations-ng.d.ts +0 -2
  38. package/dsivd-prestations-ng-19.0.6-beta.2.tgz +0 -0
  39. package/eslint/rules/__tests__/no-direct-signal-mutation.test.mjs +0 -98
@@ -0,0 +1,291 @@
1
+ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import * as templateParser from '@angular-eslint/template-parser';
5
+ import { RuleTester } from 'eslint';
6
+ import { beforeAll, describe, it } from 'vitest';
7
+ import rule from '../no-uninvoked-signal-in-template.mjs';
8
+
9
+ describe('no-uninvoked-signal-in-template', () => {
10
+ const ruleTester = new RuleTester({
11
+ languageOptions: { parser: templateParser },
12
+ });
13
+
14
+ // The rule reads the twin `.ts` of the template and walks its inheritance chain, so
15
+ // the fixtures have to exist on disk. They are laid out like a real project — the
16
+ // base class package under `node_modules` — so that module resolution is exercised.
17
+ let templatePath;
18
+ let inlineTemplatePath;
19
+ let heirTemplatePath;
20
+
21
+ const write = (path, content) => {
22
+ mkdirSync(join(path, '..'), { recursive: true });
23
+ writeFileSync(path, content, 'utf8');
24
+ };
25
+
26
+ beforeAll(() => {
27
+ const root = mkdtempSync(join(tmpdir(), 'no-uninvoked-signal-'));
28
+
29
+ // A flattened declaration bundle, as published by ng-packagr: several classes in a
30
+ // single file, signal types qualified by the namespace they were imported under.
31
+ write(
32
+ join(root, 'node_modules', '@fake', 'kit', 'package.json'),
33
+ JSON.stringify({ name: '@fake/kit', types: 'index.d.ts' }),
34
+ );
35
+ write(
36
+ join(root, 'node_modules', '@fake', 'kit', 'index.d.ts'),
37
+ `import * as _angular_core from '@angular/core';
38
+ declare class KitInputComponent {
39
+ readonly label: _angular_core.InputSignal<string>;
40
+ readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
41
+ readonly value: _angular_core.ModelSignal<string>;
42
+ plainInherited: string;
43
+ reset(): void;
44
+ }
45
+ declare class KitFieldComponent extends KitInputComponent {
46
+ readonly fieldId: _angular_core.Signal<string>;
47
+ }
48
+ declare class UnrelatedComponent {
49
+ readonly unrelated: _angular_core.InputSignal<string>;
50
+ }
51
+ export { KitFieldComponent, KitInputComponent, UnrelatedComponent };`,
52
+ );
53
+
54
+ // A base class of the project itself, reached through a relative import.
55
+ write(
56
+ join(root, 'src', 'abstract-page.component.ts'),
57
+ `import { KitFieldComponent } from '@fake/kit';
58
+ export abstract class AbstractPageComponent extends KitFieldComponent {
59
+ readonly foehnFormComponent = viewChild(FoehnFormComponent);
60
+ protected readonly step = signal(0);
61
+ }`,
62
+ );
63
+
64
+ write(
65
+ join(root, 'src', 'sample.component.ts'),
66
+ `import { Component, computed, input, model, signal } from '@angular/core';
67
+ import { AbstractPageComponent } from './abstract-page.component';
68
+
69
+ class LocalBaseComponent extends AbstractPageComponent {
70
+ readonly fromLocalBase = input<string>('');
71
+ }
72
+
73
+ @Component({ selector: 'app-sample', templateUrl: './sample.component.html' })
74
+ export class SampleComponent extends LocalBaseComponent {
75
+ readonly count = signal(0);
76
+ readonly title = input.required<string>();
77
+ readonly comment = model<string>('');
78
+ readonly double = computed(() => this.count() * 2);
79
+ readonly items = computed<string[]>(() => []);
80
+ readonly fromType: Signal<number> = this.double;
81
+ plainField = 'plain';
82
+ helper(): string { return ''; }
83
+ }`,
84
+ );
85
+
86
+ templatePath = join(root, 'src', 'sample.component.html');
87
+ // Virtual block name produced by `angular.processInlineTemplates`.
88
+ inlineTemplatePath = join(
89
+ root,
90
+ 'src',
91
+ 'sample.component.ts',
92
+ '1_inline-template-sample.component.ts-1.component.html',
93
+ );
94
+
95
+ // A component whose only signals come from its base classes.
96
+ write(
97
+ join(root, 'src', 'heir.component.ts'),
98
+ `import { AbstractPageComponent } from './abstract-page.component';
99
+ export class HeirComponent extends AbstractPageComponent {}`,
100
+ );
101
+ heirTemplatePath = join(root, 'src', 'heir.component.html');
102
+ });
103
+
104
+ const template = (code) => ({ code, filename: templatePath });
105
+ const invalidTemplate = (code, output, ...names) => ({
106
+ ...template(code),
107
+ output,
108
+ errors: names.map((name) => ({ messageId: 'uninvoked', data: { name } })),
109
+ });
110
+
111
+ it('should validate uninvoked signal rule', () => {
112
+ ruleTester.run('no-uninvoked-signal-in-template', rule, {
113
+ valid: [
114
+ // Invoked signals
115
+ template('{{ count() }}'),
116
+ template('{{ this.count() }}'),
117
+ template('{{ double() + count() }}'),
118
+ template('<div [hidden]="!count()"></div>'),
119
+ template('@if (double()) { {{ title() }} }'),
120
+
121
+ // Safe call
122
+ template('{{ count?.() }}'),
123
+
124
+ // Members of the signal API target the signal itself
125
+ template('<button (click)="count.set(1)"></button>'),
126
+ template('<button (click)="comment.update((v) => v + \'!\')"></button>'),
127
+ template('<app-child [value]="comment.asReadonly()"></app-child>'),
128
+
129
+ // Non-signal members
130
+ template('{{ plainField }}'),
131
+ template('{{ helper() }}'),
132
+
133
+ // Written position: Angular calls `.set()` itself
134
+ template('<button (click)="comment = \'x\'"></button>'),
135
+
136
+ // Template-local names shadow the class members
137
+ template('<input #count />{{ count.value }}'),
138
+ template('@for (title of items(); track title) { {{ title }} }'),
139
+ template('@if (comment(); as count) { {{ count }} }'),
140
+ template('@let count = 1; {{ count }}'),
141
+ template('<ng-template let-count>{{ count }}</ng-template>'),
142
+ template('@for (item of items(); track item) { {{ $index }} }'),
143
+
144
+ // No twin `.ts`: nothing to resolve
145
+ { code: '{{ count }}', filename: 'unknown.component.html' },
146
+ ],
147
+
148
+ invalid: [
149
+ invalidTemplate('{{ count }}', '{{ count() }}', 'count'),
150
+ invalidTemplate('{{ this.count }}', '{{ this.count() }}', 'count'),
151
+ invalidTemplate(
152
+ '<div [hidden]="!count"></div>',
153
+ '<div [hidden]="!count()"></div>',
154
+ 'count',
155
+ ),
156
+ invalidTemplate(
157
+ '{{ title + \'x\' }}',
158
+ '{{ title() + \'x\' }}',
159
+ 'title',
160
+ ),
161
+ invalidTemplate(
162
+ '@if (double) { {{ count }} }',
163
+ '@if (double()) { {{ count() }} }',
164
+ 'double',
165
+ 'count',
166
+ ),
167
+ invalidTemplate(
168
+ '<div>{{ items.length }}</div>',
169
+ '<div>{{ items().length }}</div>',
170
+ 'items',
171
+ ),
172
+ // Declared by its type only, without a signal factory
173
+ invalidTemplate('{{ fromType }}', '{{ fromType() }}', 'fromType'),
174
+ invalidTemplate(
175
+ '@for (item of items; track item) { {{ item }} }',
176
+ '@for (item of items(); track item) { {{ item }} }',
177
+ 'items',
178
+ ),
179
+ invalidTemplate(
180
+ '<button (click)="doSomething(count)"></button>',
181
+ '<button (click)="doSomething(count())"></button>',
182
+ 'count',
183
+ ),
184
+ ],
185
+ });
186
+ });
187
+
188
+ // A writable signal is two-way bound WITHOUT parentheses: Angular reads it and calls
189
+ // `.set()` itself. Adding `()` there is a compilation error, so the autofix must not
190
+ // touch the target of a `[(…)]` binding.
191
+ it('should not require parentheses on a two-way bound writable signal', () => {
192
+ ruleTester.run('no-uninvoked-signal-in-template', rule, {
193
+ valid: [
194
+ template('<app-child [(value)]="comment"></app-child>'),
195
+ template('<app-child [(value)]="this.comment"></app-child>'),
196
+ template('<input [(ngModel)]="comment" />'),
197
+ // Inherited writable signal as the target
198
+ template('<app-child [(value)]="value"></app-child>'),
199
+ // Two-way binding next to a correctly invoked signal
200
+ template('<app-child [(value)]="comment" [label]="title()"></app-child>'),
201
+ ],
202
+
203
+ invalid: [
204
+ // The exemption is scoped to the bound position, not to the signal name
205
+ invalidTemplate(
206
+ '<app-child [(value)]="comment"></app-child>{{ comment }}',
207
+ '<app-child [(value)]="comment"></app-child>{{ comment() }}',
208
+ 'comment',
209
+ ),
210
+ // A one-way binding on the same element is still reported, exactly once
211
+ invalidTemplate(
212
+ '<app-child [(value)]="comment" [label]="title"></app-child>',
213
+ '<app-child [(value)]="comment" [label]="title()"></app-child>',
214
+ 'title',
215
+ ),
216
+ // `[value]` alone is not a two-way binding
217
+ invalidTemplate(
218
+ '<app-child [value]="comment"></app-child>',
219
+ '<app-child [value]="comment()"></app-child>',
220
+ 'comment',
221
+ ),
222
+ ],
223
+ });
224
+ });
225
+
226
+ // No list of base-class members is configured anywhere: everything below is resolved
227
+ // by following the `extends` clauses across files and packages.
228
+ it('should resolve inherited signals automatically', () => {
229
+ const heir = (code) => ({ code, filename: heirTemplatePath });
230
+ const invalidHeir = (code, output, name) => ({
231
+ ...heir(code),
232
+ output,
233
+ errors: [{ messageId: 'uninvoked', data: { name } }],
234
+ });
235
+
236
+ ruleTester.run('no-uninvoked-signal-in-template', rule, {
237
+ valid: [
238
+ // Relative import, then a package, up the whole chain
239
+ heir('{{ foehnFormComponent()?.id }}'),
240
+ heir('{{ fieldId() }}'),
241
+ heir('<div [class.off]="disabled()">{{ label() }}</div>'),
242
+ template('{{ fromLocalBase() }}'),
243
+
244
+ // Not a signal, even though it sits on the same base class
245
+ heir('{{ plainInherited }}'),
246
+ heir('<button (click)="reset()"></button>'),
247
+
248
+ // A class of the same declaration bundle that is NOT in the chain
249
+ heir('{{ unrelated }}'),
250
+ ],
251
+
252
+ invalid: [
253
+ // Declared in the base class of the twin `.ts`
254
+ invalidHeir(
255
+ '{{ foehnFormComponent }}',
256
+ '{{ foehnFormComponent() }}',
257
+ 'foehnFormComponent',
258
+ ),
259
+ // Declared in a package, reached through two levels of `extends`
260
+ invalidHeir('{{ label }}', '{{ label() }}', 'label'),
261
+ invalidHeir('{{ fieldId }}', '{{ fieldId() }}', 'fieldId'),
262
+ // `InputSignalWithTransform`
263
+ invalidHeir(
264
+ '<div [class.off]="disabled"></div>',
265
+ '<div [class.off]="disabled()"></div>',
266
+ 'disabled',
267
+ ),
268
+ // Base class declared in the component file itself
269
+ invalidTemplate(
270
+ '{{ fromLocalBase }}',
271
+ '{{ fromLocalBase() }}',
272
+ 'fromLocalBase',
273
+ ),
274
+ ],
275
+ });
276
+ });
277
+
278
+ it('should resolve the signals of an inline template', () => {
279
+ ruleTester.run('no-uninvoked-signal-in-template', rule, {
280
+ valid: [{ code: '{{ count() }}', filename: inlineTemplatePath }],
281
+ invalid: [
282
+ {
283
+ code: '{{ count }}',
284
+ filename: inlineTemplatePath,
285
+ output: '{{ count() }}',
286
+ errors: [{ messageId: 'uninvoked', data: { name: 'count' } }],
287
+ },
288
+ ],
289
+ });
290
+ });
291
+ });
@@ -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
+ };