@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
@@ -1,3 +1,4 @@
1
+ import vitest from '@vitest/eslint-plugin';
1
2
  import { builtinRules } from 'eslint/use-at-your-own-risk';
2
3
  import { describe, expect, it } from 'vitest';
3
4
 
@@ -6,6 +7,10 @@ import prestationsNg, { configs, plugin, rules } from '../../index.mjs';
6
7
  const PLUGIN_KEY = '@dsivd/prestations-ng';
7
8
 
8
9
  const configNames = Object.keys(configs);
10
+ // The configs of this library's plugin (the preset of the Vitest specs only configures @vitest/eslint-plugin)
11
+ const pluginConfigNames = configNames.filter(
12
+ (name) => name !== 'specTsRecommended',
13
+ );
9
14
 
10
15
  // Configs that only register this library's rules, and the presets built on top of them.
11
16
  const RULE_LAYERS = ['tsRules', 'templateRules'];
@@ -52,6 +57,7 @@ describe('eslint configs', () => {
52
57
  'templateRecommended',
53
58
  'tsRules',
54
59
  'templateRules',
60
+ 'specTsRecommended',
55
61
  ]);
56
62
  });
57
63
 
@@ -88,7 +94,7 @@ describe('eslint configs', () => {
88
94
 
89
95
  // Two entries declaring the same plugin key from different copies is a hard ESLint
90
96
  // failure, so the preset must register ours exactly once.
91
- it.each(configNames)('%s should register the plugin once', (name) => {
97
+ it.each(pluginConfigNames)('%s should register the plugin once', (name) => {
92
98
  const declarations = configs[name]
93
99
  .map((entry) => entry.plugins?.[PLUGIN_KEY])
94
100
  .filter(Boolean);
@@ -101,7 +107,9 @@ describe('eslint configs', () => {
101
107
  });
102
108
 
103
109
  it('should enable every rule in at least one config', () => {
104
- const enabled = new Set(configNames.flatMap((n) => ownIdsOf(configs[n])));
110
+ const enabled = new Set(
111
+ pluginConfigNames.flatMap((n) => ownIdsOf(configs[n])),
112
+ );
105
113
 
106
114
  for (const ruleName of Object.keys(rules)) {
107
115
  expect(enabled).toContain(`${PLUGIN_KEY}/${ruleName}`);
@@ -118,6 +126,42 @@ describe('eslint configs', () => {
118
126
  );
119
127
  });
120
128
 
129
+ describe('specTsRecommended', () => {
130
+ const rulesOf = (config) =>
131
+ Object.assign({}, ...config.map((entry) => entry.rules ?? {}));
132
+
133
+ it('should build on the recommended config of @vitest/eslint-plugin', () => {
134
+ expect(configs.specTsRecommended[0]).toBe(vitest.configs.recommended);
135
+ expect(pluginsOf(configs.specTsRecommended)).toEqual({ vitest });
136
+ });
137
+
138
+ it('should count the assertions of the helpers and of the HttpTestingController', () => {
139
+ const [, options] = rulesOf(configs.specTsRecommended)[
140
+ 'vitest/expect-expect'
141
+ ];
142
+
143
+ expect(options.assertFunctionNames).toEqual(
144
+ expect.arrayContaining([
145
+ 'expect',
146
+ 'expect*',
147
+ '*.expectOne',
148
+ '*.verify',
149
+ ]),
150
+ );
151
+ });
152
+
153
+ // An expect which may not run lets a test pass without asserting anything
154
+ it('should keep vitest/no-conditional-expect enabled', () => {
155
+ expect(
156
+ isOff(
157
+ rulesOf(configs.specTsRecommended)[
158
+ 'vitest/no-conditional-expect'
159
+ ],
160
+ ),
161
+ ).toBe(false);
162
+ });
163
+ });
164
+
121
165
  describe('eslint plugin', () => {
122
166
  it('should be named after the npm package', () => {
123
167
  expect(plugin.meta.name).toBe(PLUGIN_KEY);
@@ -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
  {
@@ -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