@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +186 -7
  2. package/CONTRIBUTING.md +43 -3
  3. package/GOOD_PRACTICES.md +329 -124
  4. package/UPGRADING_V18.md +1 -0
  5. package/UPGRADING_V19.md +224 -3
  6. package/dsivd-prestations-ng-19.2.0-beta.10.tgz +0 -0
  7. package/eslint/configs/spec-ts-recommended.mjs +34 -0
  8. package/eslint/configs/template-recommended.mjs +2 -0
  9. package/eslint/configs/template-rules.mjs +6 -2
  10. package/eslint/configs/ts-recommended.mjs +2 -0
  11. package/eslint/configs/ts-rules.mjs +6 -2
  12. package/eslint/index.mjs +17 -0
  13. package/eslint/rules/no-direct-signal-mutation.mjs +1 -1
  14. package/eslint/signal-names.mjs +66 -35
  15. package/fesm2022/dsivd-prestations-ng.mjs +5013 -4196
  16. package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
  17. package/package.json +1 -2
  18. package/schematics/ng-add/index.js +2 -4
  19. package/schematics/ng-update/migrations/update-13_2_0/index.js +7 -2
  20. package/schematics/utility/add-allow-synthetic-default-imports.js +2 -2
  21. package/schematics/utility/add-allowed-common-js-dependencies.js +39 -29
  22. package/schematics/utility/foehn2-bootstrap5-migration.js +20 -20
  23. package/schematics/utility/migrate-to-karma-coverage.js +2 -2
  24. package/schematics/utility/remove-emit-decorator-metadata.js +8 -9
  25. package/src/eslint/__tests__/signal-names.test.mjs +129 -0
  26. package/src/eslint/configs/__tests__/configs.test.mjs +46 -2
  27. package/src/eslint/configs/spec-ts-recommended.mjs +34 -0
  28. package/src/eslint/configs/template-recommended.mjs +2 -0
  29. package/src/eslint/configs/template-rules.mjs +6 -2
  30. package/src/eslint/configs/ts-recommended.mjs +2 -0
  31. package/src/eslint/configs/ts-rules.mjs +6 -2
  32. package/src/eslint/index.mjs +17 -0
  33. package/src/eslint/rules/no-direct-signal-mutation.mjs +1 -1
  34. package/src/eslint/signal-names.mjs +66 -35
  35. package/types/dsivd-prestations-ng.d.ts +1184 -998
  36. 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
- 'jdk' : '21',
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
+ ```
@@ -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
- // 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.
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
- // Only the rules written by this library. `tsRecommended` includes it, alongside the
4
- // house style; use this layer directly to opt out of the latter.
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 (!variable || variable.defs.length !== 1 || seen.has(variable)) {
244
+ if (variable?.defs.length !== 1 || seen.has(variable)) {
245
245
  return undefined;
246
246
  }
247
247
  seen.add(variable);
@@ -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 && classDeclarations.length === 0) {
174
- for (const specifier of reExportedModulesOf(sourceFile)) {
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
- for (const member of classDeclaration.members) {
185
- if (!member.name || !ts.isIdentifier(member.name)) {
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