@dsivd/prestations-ng 19.1.2 → 19.2.0-beta.10
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 +186 -7
- package/CONTRIBUTING.md +43 -3
- package/GOOD_PRACTICES.md +329 -124
- package/UPGRADING_V18.md +1 -0
- package/UPGRADING_V19.md +224 -3
- package/dsivd-prestations-ng-19.2.0-beta.10.tgz +0 -0
- package/eslint/configs/spec-ts-recommended.mjs +34 -0
- package/eslint/configs/template-recommended.mjs +2 -0
- package/eslint/configs/template-rules.mjs +6 -2
- package/eslint/configs/ts-recommended.mjs +2 -0
- package/eslint/configs/ts-rules.mjs +6 -2
- package/eslint/index.mjs +17 -0
- package/eslint/rules/no-direct-signal-mutation.mjs +1 -1
- package/eslint/signal-names.mjs +66 -35
- package/fesm2022/dsivd-prestations-ng.mjs +5013 -4196
- package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
- package/package.json +1 -2
- package/schematics/ng-add/index.js +2 -4
- package/schematics/ng-update/migrations/update-13_2_0/index.js +7 -2
- package/schematics/utility/add-allow-synthetic-default-imports.js +2 -2
- package/schematics/utility/add-allowed-common-js-dependencies.js +39 -29
- package/schematics/utility/foehn2-bootstrap5-migration.js +20 -20
- package/schematics/utility/migrate-to-karma-coverage.js +2 -2
- package/schematics/utility/remove-emit-decorator-metadata.js +8 -9
- package/src/eslint/__tests__/signal-names.test.mjs +129 -0
- package/src/eslint/configs/__tests__/configs.test.mjs +46 -2
- package/src/eslint/configs/spec-ts-recommended.mjs +34 -0
- package/src/eslint/configs/template-recommended.mjs +2 -0
- package/src/eslint/configs/template-rules.mjs +6 -2
- package/src/eslint/configs/ts-recommended.mjs +2 -0
- package/src/eslint/configs/ts-rules.mjs +6 -2
- package/src/eslint/index.mjs +17 -0
- package/src/eslint/rules/no-direct-signal-mutation.mjs +1 -1
- package/src/eslint/signal-names.mjs +66 -35
- package/types/dsivd-prestations-ng.d.ts +1184 -998
- package/dsivd-prestations-ng-19.1.2.tgz +0 -0
package/UPGRADING_V18.md
CHANGED
|
@@ -458,6 +458,7 @@ npm install @angular/cli@19 -g
|
|
|
458
458
|
ng update typescript@5.5
|
|
459
459
|
ng update @dsivd/prestations-ng@18
|
|
460
460
|
```
|
|
461
|
+
|
|
461
462
|
NB: You might need to use the --force option.
|
|
462
463
|
|
|
463
464
|
When asked the question "Select the migrations that you'd like to run", accept "provide-initializer" by hitting "Space" key then "Enter".
|
package/UPGRADING_V19.md
CHANGED
|
@@ -11,6 +11,9 @@ From here on, we are in the root folder of your project, for example ~/git/cybsd
|
|
|
11
11
|
|
|
12
12
|
### Spring Boot 4
|
|
13
13
|
|
|
14
|
+
NB : if the following instructions (jdk25 + latest parent + openrewrite) are not working fine, you can also try to
|
|
15
|
+
do in the reverse order : in jdk21 do the rewrite commands, then switch to jdk25 and latest parent.
|
|
16
|
+
|
|
14
17
|
You can switch to Java 25 `jdk25` + change in IntelliJ: right-click on your project > Open Module Settings > Project > set "SDK" to `jdk25` from devtools and "Language
|
|
15
18
|
level" to `25` in "Modules"
|
|
16
19
|
|
|
@@ -78,6 +81,17 @@ if you use ldap, you should replace `spring-ldap-core` by `spring-boot-starter-l
|
|
|
78
81
|
</dependency>
|
|
79
82
|
```
|
|
80
83
|
|
|
84
|
+
### ORACLE
|
|
85
|
+
|
|
86
|
+
if you use oracle, you should replace `ojdbc10` by `ojdbc17` in your `pom.xml` and let the spring boot parent set the version
|
|
87
|
+
|
|
88
|
+
```xml
|
|
89
|
+
<dependency>
|
|
90
|
+
<groupId>com.oracle.database.jdbc</groupId>
|
|
91
|
+
<artifactId>ojdbc17</artifactId>
|
|
92
|
+
</dependency>
|
|
93
|
+
```
|
|
94
|
+
|
|
81
95
|
### Jackson migration
|
|
82
96
|
|
|
83
97
|
With Spring Boot 4, Jackson 3 is now used instead of Jackson 2, so we need to replace the previous Jackson 2 classes.
|
|
@@ -857,7 +871,7 @@ ng generate @angular/core:signal-input-migration --insert-todos
|
|
|
857
871
|
[Angular migration guide: migrating to Vitest](https://angular.dev/guide/testing/migrating-to-vitest)
|
|
858
872
|
|
|
859
873
|
```bash
|
|
860
|
-
npm install --save-dev vitest jsdom
|
|
874
|
+
npm install --save-dev vitest@4.0 jsdom
|
|
861
875
|
```
|
|
862
876
|
|
|
863
877
|
In your `angular.json` file, remove `"zone.js/dist/zone-error"` from `"allowedCommonJsDependencies"`
|
|
@@ -1050,6 +1064,8 @@ npm install --save-dev eslint-plugin-simple-import-sort@13
|
|
|
1050
1064
|
npm install --save-dev eslint-config-prettier@10
|
|
1051
1065
|
npm install --save-dev eslint-plugin-import-x@4
|
|
1052
1066
|
npm install --save-dev prettier
|
|
1067
|
+
# only if your project has Vitest specs (see "Migrate to vitest")
|
|
1068
|
+
npm install --save-dev @vitest/eslint-plugin@1
|
|
1053
1069
|
```
|
|
1054
1070
|
|
|
1055
1071
|
In your `front` folder, add a `.prettierrc` file with the following content:
|
|
@@ -1126,6 +1142,12 @@ export default tseslint.config(
|
|
|
1126
1142
|
extends: [...prestationsNg.configs.templateRecommended],
|
|
1127
1143
|
},
|
|
1128
1144
|
|
|
1145
|
+
// ─── Vitest specs (only if your project has Vitest specs) ─────────────────
|
|
1146
|
+
{
|
|
1147
|
+
files: ['**/*.spec.ts'],
|
|
1148
|
+
extends: [...prestationsNg.configs.specTsRecommended],
|
|
1149
|
+
},
|
|
1150
|
+
|
|
1129
1151
|
// ─── Prettier (must be last — disables formatting rules) ───────────────────
|
|
1130
1152
|
prettier,
|
|
1131
1153
|
);
|
|
@@ -1136,6 +1158,11 @@ everything: the `eslint`, `typescript-eslint` and `angular-eslint` baselines, th
|
|
|
1136
1158
|
accessibility set, the naming and import conventions, and the rules written by the library
|
|
1137
1159
|
itself. You declare no `plugins` and list no rule.
|
|
1138
1160
|
|
|
1161
|
+
`specTsRecommended` is the `recommended` config of `@vitest/eslint-plugin`, which also counts the assertions
|
|
1162
|
+
made by the `HttpTestingController` (`expectOne`, `expectNone`, `verify`) and by helpers named `expect*`,
|
|
1163
|
+
`assert*` or `test*`. The `**/*.spec.ts` pattern does not reach the Cypress `*.spec.cy.ts` files. It needs
|
|
1164
|
+
`@vitest/eslint-plugin` only when you use it: the other configs work without it.
|
|
1165
|
+
|
|
1139
1166
|
The packages installed above are still required — the library's config imports them from your
|
|
1140
1167
|
`node_modules` rather than shipping its own copies. For the same reason, do **not** spread
|
|
1141
1168
|
`angular.configs.tsRecommended` or `angular.configs.templateRecommended` yourself: they are
|
|
@@ -1396,7 +1423,7 @@ npm uninstall @angular/animations
|
|
|
1396
1423
|
|
|
1397
1424
|
### Add missing `^` or `~` in `package.json` devDependencies
|
|
1398
1425
|
|
|
1399
|
-
Check you devDependencies in your `package.json` file and add `^` in front of every version number if missing excepted for `typescript` where you should add `~`. Then run:
|
|
1426
|
+
Check you devDependencies in your `package.json` file and add `^` in front of every version number if missing excepted for `typescript` and `vitest` where you should add `~`. Then run:
|
|
1400
1427
|
|
|
1401
1428
|
```bash
|
|
1402
1429
|
npm install
|
|
@@ -1416,6 +1443,191 @@ When using `IntelliJ`, you can run code inspection on your project to fix deprec
|
|
|
1416
1443
|
- IntelliJ window menu, go to: `Code > Analyze code > Run inspection by name...` (or **Ctrl+Alt+Maj+i**) and type `Deprecated symbol used`.
|
|
1417
1444
|
- Or select your project in your project panel and `right click` on it then `Analyze > Run inspection by name...` (or **Ctrl+Alt+Maj+i**) and type `Deprecated symbol used`.
|
|
1418
1445
|
|
|
1446
|
+
### Strict mode (optional when migrating large projects but recommended)
|
|
1447
|
+
|
|
1448
|
+
- In your `tsconfig.json` file, add `"strict": true` in `compilerOptions` and remove `alwaysStrict`, `noImplicitThis`, `strictBindCallApply` and `strictFunctionTypes` as they are already included in `strict` mode.
|
|
1449
|
+
- In your `tsconfig.json` file, remove `useDefineForClassFields` from `compilerOptions`.
|
|
1450
|
+
|
|
1451
|
+
Now fix your project until no more errors:
|
|
1452
|
+
|
|
1453
|
+
```bash
|
|
1454
|
+
npx tsc -b --noEmit
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
and finally run the following commands to build, test and start your application:
|
|
1458
|
+
|
|
1459
|
+
```bash
|
|
1460
|
+
npm run build
|
|
1461
|
+
npm run test
|
|
1462
|
+
npm start
|
|
1463
|
+
```
|
|
1464
|
+
|
|
1465
|
+
### `foehn-list-summary` is now generic (since 19.2.0)
|
|
1466
|
+
|
|
1467
|
+
`FoehnListSummaryComponent` is now `FoehnListSummaryComponent<T extends FoehnListItem>` and `FoehnListItemDescription` now takes a type parameter: `FoehnListItemDescription<T>`.
|
|
1468
|
+
Using `FoehnListItemDescription` without its type parameter no longer compiles.
|
|
1469
|
+
|
|
1470
|
+
Replace every `FoehnListItemDescription` with `FoehnListItemDescription<MyItem>`, where `MyItem` is your class extending `FoehnListItem`. You can then drop the casts (or `any`) in `getFormattedValue` and `hideIfEmpty`:
|
|
1471
|
+
|
|
1472
|
+
Before:
|
|
1473
|
+
|
|
1474
|
+
```typescript
|
|
1475
|
+
getListItemsDescription(): FoehnListItemDescription[] {
|
|
1476
|
+
return [
|
|
1477
|
+
{
|
|
1478
|
+
label: 'Name',
|
|
1479
|
+
getFormattedValue: (item: FoehnListItem) =>
|
|
1480
|
+
(item as MyItem).name ?? '',
|
|
1481
|
+
},
|
|
1482
|
+
];
|
|
1483
|
+
}
|
|
1484
|
+
```
|
|
1485
|
+
|
|
1486
|
+
After:
|
|
1487
|
+
|
|
1488
|
+
```typescript
|
|
1489
|
+
getListItemsDescription(): FoehnListItemDescription<MyItem>[] {
|
|
1490
|
+
return [
|
|
1491
|
+
{
|
|
1492
|
+
label: 'Name',
|
|
1493
|
+
getFormattedValue: (item: MyItem) => item.name ?? '',
|
|
1494
|
+
},
|
|
1495
|
+
];
|
|
1496
|
+
}
|
|
1497
|
+
```
|
|
1498
|
+
|
|
1499
|
+
The same applies to the function passed to `getListItemTitle`, which now receives a `MyItem`.
|
|
1500
|
+
|
|
1501
|
+
### `core-js` is no longer needed (since 19.2.0)
|
|
1502
|
+
|
|
1503
|
+
prestations-ng no longer imports `core-js` and no longer has it as a peer dependency. If your project does not
|
|
1504
|
+
import `core-js` itself, uninstall it:
|
|
1505
|
+
|
|
1506
|
+
```bash
|
|
1507
|
+
npm uninstall core-js
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1510
|
+
Then, in your `angular.json` file, remove `"core-js/modules/es.array.includes"` from `"allowedCommonJsDependencies"`.
|
|
1511
|
+
The CommonJS dependencies of prestations-ng are now `"iban"` and `"dayjs"` (which also covers `dayjs/plugin/*` and
|
|
1512
|
+
`dayjs/locale/*`):
|
|
1513
|
+
|
|
1514
|
+
```json
|
|
1515
|
+
"allowedCommonJsDependencies": ["iban", "dayjs"]
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
### Zoneless (optional but recommended)
|
|
1519
|
+
|
|
1520
|
+
Since v19.2, prestations-ng does not need zone.js anymore: the state of its components is held by signals, which
|
|
1521
|
+
tell Angular when a component has to be refreshed. prestations-ng keeps working in an application using zone.js,
|
|
1522
|
+
so this step is optional, but a zoneless application is lighter, faster and easier to debug. See the
|
|
1523
|
+
[Angular zoneless guide](https://angular.dev/guide/zoneless).
|
|
1524
|
+
|
|
1525
|
+
In any case, if your code reads some fields of the prestations-ng components (e.g. through `viewChild()` or in
|
|
1526
|
+
your tests), check the fields that are now signals in the [CHANGELOG](CHANGELOG.md#1920---should-be-aligned-with-prestations-be-192x).
|
|
1527
|
+
|
|
1528
|
+
#### Enable the zoneless change detection
|
|
1529
|
+
|
|
1530
|
+
Zoneless is the default since Angular 21: `ng update` added `provideZoneChangeDetection()` to keep zone.js in your
|
|
1531
|
+
application. Replace it in your `app.config.ts` (`providePrestationsNgCore()` already provides
|
|
1532
|
+
`provideBrowserGlobalErrorListeners()`, which reports to your `ErrorHandler` the errors zone.js used to catch):
|
|
1533
|
+
|
|
1534
|
+
```diff
|
|
1535
|
+
export const appConfig: ApplicationConfig = {
|
|
1536
|
+
providers: [
|
|
1537
|
+
- provideZoneChangeDetection({ eventCoalescing: true }),
|
|
1538
|
+
+ provideZonelessChangeDetection(),
|
|
1539
|
+
providePrestationsNgCore(),
|
|
1540
|
+
...
|
|
1541
|
+
],
|
|
1542
|
+
};
|
|
1543
|
+
```
|
|
1544
|
+
|
|
1545
|
+
Remove `zone.js` and `zone.js/testing` from the `polyfills` of the `build` and `test` targets in your
|
|
1546
|
+
`angular.json` (and from your `polyfills.ts` file if you still have one): without them, the `TestBed` is zoneless
|
|
1547
|
+
too.
|
|
1548
|
+
|
|
1549
|
+
```diff
|
|
1550
|
+
"polyfills": [
|
|
1551
|
+
- "zone.js"
|
|
1552
|
+
],
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
then uninstall zone.js:
|
|
1556
|
+
|
|
1557
|
+
```bash
|
|
1558
|
+
npm uninstall zone.js
|
|
1559
|
+
```
|
|
1560
|
+
|
|
1561
|
+
#### Check your components
|
|
1562
|
+
|
|
1563
|
+
Without zone.js, Angular refreshes a component only when it is notified that something changed:
|
|
1564
|
+
|
|
1565
|
+
- a listener of its template has been called (`(click)`, `(modelChange)`, `(userInput)`...), including the
|
|
1566
|
+
`[(model)]` two-way bindings
|
|
1567
|
+
- a signal read by its template has changed (`signal()`, `computed()`, `input()`, `model()`, `toSignal()`...)
|
|
1568
|
+
- `ChangeDetectorRef.markForCheck()` has been called, which is what the `async` pipe does when an observable emits
|
|
1569
|
+
|
|
1570
|
+
A field modified elsewhere (in a `subscribe()`, a `setTimeout()`, a `Promise`, a callback...) is not displayed
|
|
1571
|
+
anymore until something else refreshes the component. Hold it in a signal (see
|
|
1572
|
+
[INTRODUCTION_ANGULAR_SIGNALS.md](INTRODUCTION_ANGULAR_SIGNALS.md)):
|
|
1573
|
+
|
|
1574
|
+
```diff
|
|
1575
|
+
- isLoading = true;
|
|
1576
|
+
+ readonly isLoading = signal(true);
|
|
1577
|
+
|
|
1578
|
+
ngOnInit(): void {
|
|
1579
|
+
this.myService.load().subscribe(() => {
|
|
1580
|
+
- this.isLoading = false;
|
|
1581
|
+
+ this.isLoading.set(false);
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
```
|
|
1585
|
+
|
|
1586
|
+
```diff
|
|
1587
|
+
- @if (isLoading) {
|
|
1588
|
+
+ @if (isLoading()) {
|
|
1589
|
+
```
|
|
1590
|
+
|
|
1591
|
+
Pay attention to:
|
|
1592
|
+
|
|
1593
|
+
- your pages (`AbstractPageComponent` and its subclasses): prestations-ng refreshes them when it sets `form` and
|
|
1594
|
+
`reference`. But if you modify the form yourself in an asynchronous callback, e.g. the one given to
|
|
1595
|
+
`SessionInfoWithApplicationService.prefillForm()` or a `subscribe()` of your own service, call
|
|
1596
|
+
`markForCheck()` afterwards:
|
|
1597
|
+
|
|
1598
|
+
```ts
|
|
1599
|
+
private readonly changeDetectorRef = inject(ChangeDetectorRef);
|
|
1600
|
+
|
|
1601
|
+
ngOnInit(): void {
|
|
1602
|
+
super.ngOnInit();
|
|
1603
|
+
this.sessionInfoWithApplicationService.prefillForm((info) => {
|
|
1604
|
+
this.form.email = info?.email ?? null;
|
|
1605
|
+
this.changeDetectorRef.markForCheck();
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
```
|
|
1609
|
+
|
|
1610
|
+
- `NgZone`: `onStable`, `onMicrotaskEmpty` and `onUnstable` never emit without zone.js, and `run()` or
|
|
1611
|
+
`runOutsideAngular()` are useless. To act on the rendered DOM (focus, scroll...), replace them, as well as the
|
|
1612
|
+
`setTimeout()` waiting for the view, by `afterNextRender()`. `AbstractMenuPageComponent.ngZone` is deprecated.
|
|
1613
|
+
|
|
1614
|
+
```diff
|
|
1615
|
+
- this.ngZone.onStable.pipe(first()).subscribe(() => this.focusFirstField());
|
|
1616
|
+
+ afterNextRender(() => this.focusFirstField(), { injector: this.injector });
|
|
1617
|
+
```
|
|
1618
|
+
|
|
1619
|
+
#### Check your tests
|
|
1620
|
+
|
|
1621
|
+
- `fakeAsync()`, `tick()`, `flush()` and `waitForAsync()` need zone.js: write `async` tests, with
|
|
1622
|
+
`vi.useFakeTimers()` and `await vi.advanceTimersByTimeAsync(ms)` to control the timers (see
|
|
1623
|
+
[Migrate to vitest](#migrate-to-vitest)).
|
|
1624
|
+
- `fixture.detectChanges()` only refreshes the views which have been notified, then checks that nothing changed
|
|
1625
|
+
meanwhile: a field modified without notification makes the test fail with the error `NG0100`
|
|
1626
|
+
(`ExpressionChangedAfterItHasBeenCheckedError`). Fix the component (signal, `markForCheck()`) rather than the
|
|
1627
|
+
test.
|
|
1628
|
+
- `await fixture.whenStable()` waits for the change detection and for the pending tasks of the application
|
|
1629
|
+
(e.g. the debounced `userInput` of the prestations-ng inputs), not for the timers.
|
|
1630
|
+
|
|
1419
1631
|
### Last but not least, check if your application is working!!!
|
|
1420
1632
|
|
|
1421
1633
|
#### Be sure your application gets a fresh start
|
|
@@ -1455,7 +1667,8 @@ See [getImtPortailApplications](https://dsigit.etat-de-vaud.ch/outils/git/projec
|
|
|
1455
1667
|
'project': 'cybersdk',
|
|
1456
1668
|
'name' : 'skeleton',
|
|
1457
1669
|
'gitUrl' : 'ssh://git@bitbucket.etat-de-vaud.ch/cybsdk/skeleton.git',
|
|
1458
|
-
|
|
1670
|
+
- 'jdk' : '21',
|
|
1671
|
+
+ 'jdk' : '25',
|
|
1459
1672
|
- 'node' : '20'
|
|
1460
1673
|
+ 'node' : '22'
|
|
1461
1674
|
],
|
|
@@ -1465,3 +1678,11 @@ See [getImtPortailApplications](https://dsigit.etat-de-vaud.ch/outils/git/projec
|
|
|
1465
1678
|
|
|
1466
1679
|
Enroll your project in Renovate bot to automatically update your dependencies.
|
|
1467
1680
|
See [Mend Renovate CE](https://dgnsiwiki.etat-de-vaud.ch/outils/dgnsiwiki/x/7QFDgg) for more information.
|
|
1681
|
+
|
|
1682
|
+
update renovate.json :
|
|
1683
|
+
|
|
1684
|
+
```json
|
|
1685
|
+
{
|
|
1686
|
+
"extends": ["local>PEE/renovate-config"]
|
|
1687
|
+
}
|
|
1688
|
+
```
|
|
Binary file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The house preset of the Vitest specs: the recommended rules of `@vitest/eslint-plugin`, with the
|
|
3
|
+
* assertions also made by the helpers of the specs and by the `HttpTestingController`.
|
|
4
|
+
*
|
|
5
|
+
* Meant for the `extends` of a `**\/*.spec.ts` block (the Cypress `*.spec.cy.ts` files are not
|
|
6
|
+
* Vitest specs).
|
|
7
|
+
*
|
|
8
|
+
* @param {import('@vitest/eslint-plugin').default} vitest the `@vitest/eslint-plugin` module
|
|
9
|
+
* @returns {import('typescript-eslint').ConfigArray}
|
|
10
|
+
*/
|
|
11
|
+
const specTsRecommended = (vitest) => [
|
|
12
|
+
vitest.configs.recommended,
|
|
13
|
+
{
|
|
14
|
+
name: 'prestations-ng/spec-ts-recommended',
|
|
15
|
+
rules: {
|
|
16
|
+
'vitest/expect-expect': [
|
|
17
|
+
'error',
|
|
18
|
+
{
|
|
19
|
+
assertFunctionNames: [
|
|
20
|
+
'expect',
|
|
21
|
+
'expect*',
|
|
22
|
+
'assert*',
|
|
23
|
+
'test*',
|
|
24
|
+
'*.expectOne',
|
|
25
|
+
'*.expectNone',
|
|
26
|
+
'*.verify',
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export default specTsRecommended;
|
|
@@ -8,6 +8,8 @@ import templateRules from './template-rules.mjs';
|
|
|
8
8
|
*
|
|
9
9
|
* Meant for the `extends` of a `**\/*.html` block. It brings its own parser and plugins,
|
|
10
10
|
* so do not spread `angular.configs.templateRecommended` alongside it.
|
|
11
|
+
*
|
|
12
|
+
* @returns {import('typescript-eslint').ConfigArray}
|
|
11
13
|
*/
|
|
12
14
|
const templateRecommended = (plugin) => [
|
|
13
15
|
...angular.configs.templateRecommended,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import templateBase from './template-base.mjs';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Only the rules written by this library. `templateRecommended` includes it, alongside
|
|
5
|
+
* the house style; use this layer directly to opt out of the latter.
|
|
6
|
+
*
|
|
7
|
+
* @returns {import('typescript-eslint').ConfigArray}
|
|
8
|
+
*/
|
|
5
9
|
const templateRules = (plugin) => [
|
|
6
10
|
templateBase(plugin),
|
|
7
11
|
{
|
|
@@ -14,6 +14,8 @@ import tsRules from './ts-rules.mjs';
|
|
|
14
14
|
* Meant for the `extends` of a `**\/*.ts` block. It brings its own parser and plugins,
|
|
15
15
|
* so do not spread `angular.configs.tsRecommended` alongside it — declaring the same
|
|
16
16
|
* plugin twice from two different copies makes ESLint fail outright.
|
|
17
|
+
*
|
|
18
|
+
* @returns {import('typescript-eslint').ConfigArray}
|
|
17
19
|
*/
|
|
18
20
|
const tsRecommended = (plugin) => [
|
|
19
21
|
eslint.configs.recommended,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import tsBase from './ts-base.mjs';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Only the rules written by this library. `tsRecommended` includes it, alongside the
|
|
5
|
+
* house style; use this layer directly to opt out of the latter.
|
|
6
|
+
*
|
|
7
|
+
* @returns {import('typescript-eslint').ConfigArray}
|
|
8
|
+
*/
|
|
5
9
|
const tsRules = (plugin) => [
|
|
6
10
|
tsBase(plugin),
|
|
7
11
|
{
|
package/eslint/index.mjs
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import specTsRecommended from './configs/spec-ts-recommended.mjs';
|
|
1
2
|
import templateRecommended from './configs/template-recommended.mjs';
|
|
2
3
|
import templateRules from './configs/template-rules.mjs';
|
|
3
4
|
import tsRecommended from './configs/ts-recommended.mjs';
|
|
4
5
|
import tsRules from './configs/ts-rules.mjs';
|
|
5
6
|
import rules from './rules/index.mjs';
|
|
6
7
|
|
|
8
|
+
// Only needed by the projects linting Vitest specs: the other configs can be used without it.
|
|
9
|
+
const vitest = await import('@vitest/eslint-plugin').then(
|
|
10
|
+
(module) => module.default,
|
|
11
|
+
() => null,
|
|
12
|
+
);
|
|
13
|
+
const specTsRecommendedConfig = vitest ? specTsRecommended(vitest) : null;
|
|
14
|
+
|
|
7
15
|
const plugin = {
|
|
8
16
|
meta: { name: '@dsivd/prestations-ng' },
|
|
9
17
|
rules,
|
|
@@ -16,6 +24,15 @@ const configs = {
|
|
|
16
24
|
// Only this library's own rules, for a project that supplies its own style.
|
|
17
25
|
tsRules: tsRules(plugin),
|
|
18
26
|
templateRules: templateRules(plugin),
|
|
27
|
+
// The preset of the Vitest specs (needs @vitest/eslint-plugin)
|
|
28
|
+
get specTsRecommended() {
|
|
29
|
+
if (!specTsRecommendedConfig) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'prestationsNg.configs.specTsRecommended needs @vitest/eslint-plugin: npm install --save-dev @vitest/eslint-plugin@1',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return specTsRecommendedConfig;
|
|
35
|
+
},
|
|
19
36
|
};
|
|
20
37
|
|
|
21
38
|
export { configs, plugin, rules };
|
|
@@ -241,7 +241,7 @@ function boundNameSignalKind(identifier, scopeContext, seen) {
|
|
|
241
241
|
scopeContext.sourceCode.getScope(identifier),
|
|
242
242
|
identifier.name,
|
|
243
243
|
);
|
|
244
|
-
if (
|
|
244
|
+
if (variable?.defs.length !== 1 || seen.has(variable)) {
|
|
245
245
|
return undefined;
|
|
246
246
|
}
|
|
247
247
|
seen.add(variable);
|
package/eslint/signal-names.mjs
CHANGED
|
@@ -153,6 +153,67 @@ const reExportedModulesOf = (sourceFile) =>
|
|
|
153
153
|
.filter((statement) => ts.isExportDeclaration(statement) && statement.moduleSpecifier)
|
|
154
154
|
.map((statement) => statement.moduleSpecifier.text);
|
|
155
155
|
|
|
156
|
+
const classDeclarationsOf = (sourceFile, className) =>
|
|
157
|
+
className
|
|
158
|
+
? [findClass(sourceFile, className)].filter(Boolean)
|
|
159
|
+
: sourceFile.statements.filter(ts.isClassDeclaration);
|
|
160
|
+
|
|
161
|
+
const isBarrelMiss = (className, classDeclarations) =>
|
|
162
|
+
Boolean(className) && classDeclarations.length === 0;
|
|
163
|
+
|
|
164
|
+
const collectFromReExports = (sourceFile, path, className, kinds, visited) => {
|
|
165
|
+
for (const specifier of reExportedModulesOf(sourceFile)) {
|
|
166
|
+
const target = resolveModule(specifier, path);
|
|
167
|
+
if (target) {
|
|
168
|
+
collect(target, className, kinds, visited);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const addMemberKindIfEligible = (member, kinds) => {
|
|
174
|
+
if (!member.name || !ts.isIdentifier(member.name)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const kind = kindOfMember(member);
|
|
178
|
+
// A subclass overriding a member wins: it is collected first.
|
|
179
|
+
if (kind && !kinds.has(member.name.text)) {
|
|
180
|
+
kinds.set(member.name.text, kind);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const collectMemberKinds = (classDeclaration, kinds) => {
|
|
185
|
+
for (const member of classDeclaration.members) {
|
|
186
|
+
addMemberKindIfEligible(member, kinds);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const baseTargetOf = (sourceFile, path, classDeclaration) => {
|
|
191
|
+
const baseName = extendedClassNameOf(classDeclaration);
|
|
192
|
+
if (!baseName) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Declaration bundles are flattened: the base class often sits in the same file.
|
|
197
|
+
if (findClass(sourceFile, baseName)) {
|
|
198
|
+
return { path, className: baseName };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const imported = importOf(sourceFile, baseName);
|
|
202
|
+
if (!imported) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const target = resolveModule(imported.specifier, path);
|
|
207
|
+
return target ? { path: target, className: imported.exportedName } : undefined;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const collectBase = (sourceFile, path, classDeclaration, kinds, visited) => {
|
|
211
|
+
const target = baseTargetOf(sourceFile, path, classDeclaration);
|
|
212
|
+
if (target) {
|
|
213
|
+
collect(target.path, target.className, kinds, visited);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
156
217
|
const collect = (path, className, kinds, visited) => {
|
|
157
218
|
const key = `${path}::${className ?? "*"}`;
|
|
158
219
|
if (visited.has(key)) {
|
|
@@ -165,47 +226,17 @@ const collect = (path, className, kinds, visited) => {
|
|
|
165
226
|
return;
|
|
166
227
|
}
|
|
167
228
|
|
|
168
|
-
const classDeclarations = className
|
|
169
|
-
? [findClass(sourceFile, className)].filter(Boolean)
|
|
170
|
-
: sourceFile.statements.filter(ts.isClassDeclaration);
|
|
229
|
+
const classDeclarations = classDeclarationsOf(sourceFile, className);
|
|
171
230
|
|
|
172
231
|
// A barrel: the class is only re-exported from here.
|
|
173
|
-
if (className
|
|
174
|
-
|
|
175
|
-
const target = resolveModule(specifier, path);
|
|
176
|
-
if (target) {
|
|
177
|
-
collect(target, className, kinds, visited);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
232
|
+
if (isBarrelMiss(className, classDeclarations)) {
|
|
233
|
+
collectFromReExports(sourceFile, path, className, kinds, visited);
|
|
180
234
|
return;
|
|
181
235
|
}
|
|
182
236
|
|
|
183
237
|
for (const classDeclaration of classDeclarations) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
const kind = kindOfMember(member);
|
|
189
|
-
// A subclass overriding a member wins: it is collected first.
|
|
190
|
-
if (kind && !kinds.has(member.name.text)) {
|
|
191
|
-
kinds.set(member.name.text, kind);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const baseName = extendedClassNameOf(classDeclaration);
|
|
196
|
-
if (!baseName) {
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
// Declaration bundles are flattened: the base class often sits in the same file.
|
|
200
|
-
if (findClass(sourceFile, baseName)) {
|
|
201
|
-
collect(path, baseName, kinds, visited);
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
const imported = importOf(sourceFile, baseName);
|
|
205
|
-
const target = imported && resolveModule(imported.specifier, path);
|
|
206
|
-
if (target) {
|
|
207
|
-
collect(target, imported.exportedName, kinds, visited);
|
|
208
|
-
}
|
|
238
|
+
collectMemberKinds(classDeclaration, kinds);
|
|
239
|
+
collectBase(sourceFile, path, classDeclaration, kinds, visited);
|
|
209
240
|
}
|
|
210
241
|
};
|
|
211
242
|
|