@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.
- package/CHANGELOG.md +31 -0
- package/CONTRIBUTING.md +25 -0
- package/ESLINT_PLUGIN.md +198 -0
- package/README.md +3 -0
- package/UPGRADING_V19.md +38 -143
- package/dsivd-prestations-ng-19.0.7.tgz +0 -0
- package/eslint/configs/template-base.mjs +10 -0
- package/eslint/configs/template-recommended.mjs +24 -0
- package/eslint/configs/template-rules.mjs +17 -0
- package/eslint/configs/ts-base.mjs +10 -0
- package/eslint/configs/ts-recommended.mjs +142 -0
- package/eslint/configs/ts-rules.mjs +14 -0
- package/eslint/index.mjs +20 -5
- package/eslint/rules/index.mjs +7 -1
- package/eslint/rules/no-direct-signal-mutation.mjs +240 -136
- package/eslint/rules/no-uninvoked-signal-in-template.mjs +170 -0
- package/eslint/signal-names.mjs +232 -0
- package/eslint/template-ast.mjs +26 -0
- package/fesm2022/dsivd-prestations-ng.mjs +7 -9
- package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
- package/package.json +1 -1
- package/src/eslint/configs/__tests__/configs.test.mjs +135 -0
- package/src/eslint/configs/template-base.mjs +10 -0
- package/src/eslint/configs/template-recommended.mjs +24 -0
- package/src/eslint/configs/template-rules.mjs +17 -0
- package/src/eslint/configs/ts-base.mjs +10 -0
- package/src/eslint/configs/ts-recommended.mjs +142 -0
- package/src/eslint/configs/ts-rules.mjs +14 -0
- package/src/eslint/index.mjs +20 -5
- package/src/eslint/rules/__tests__/no-direct-signal-mutation.test.mjs +86 -4
- package/src/eslint/rules/__tests__/no-uninvoked-signal-in-template.test.mjs +291 -0
- package/src/eslint/rules/index.mjs +7 -1
- package/src/eslint/rules/no-direct-signal-mutation.mjs +240 -136
- package/src/eslint/rules/no-uninvoked-signal-in-template.mjs +170 -0
- package/src/eslint/signal-names.mjs +232 -0
- package/src/eslint/template-ast.mjs +26 -0
- package/types/dsivd-prestations-ng.d.ts +0 -2
- package/dsivd-prestations-ng-19.0.6-beta.2.tgz +0 -0
- 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
|
-
|
|
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
|
+
};
|