@dsivd/prestations-ng 19.0.6 → 19.0.8-beta.1

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 +37 -4
  2. package/CONTRIBUTING.md +3 -2
  3. package/ESLINT_PLUGIN.md +170 -35
  4. package/UPGRADING_V19.md +22 -145
  5. package/dsivd-prestations-ng-19.0.8-beta.1.tgz +0 -0
  6. package/eslint/component-path.mjs +15 -0
  7. package/eslint/configs/template-base.mjs +2 -2
  8. package/eslint/configs/template-recommended.mjs +15 -6
  9. package/eslint/configs/template-rules.mjs +17 -0
  10. package/eslint/configs/ts-base.mjs +2 -2
  11. package/eslint/configs/ts-recommended.mjs +133 -3
  12. package/eslint/configs/ts-rules.mjs +14 -0
  13. package/eslint/index.mjs +10 -4
  14. package/eslint/rules/no-direct-signal-mutation.mjs +370 -142
  15. package/eslint/rules/no-uninvoked-signal-in-template.mjs +1 -12
  16. package/eslint/signal-kinds.mjs +57 -0
  17. package/eslint/signal-names.mjs +43 -49
  18. package/fesm2022/dsivd-prestations-ng.mjs +487 -487
  19. package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
  20. package/package.json +1 -1
  21. package/src/eslint/component-path.mjs +15 -0
  22. package/src/eslint/configs/__tests__/configs.test.mjs +113 -48
  23. package/src/eslint/configs/template-base.mjs +2 -2
  24. package/src/eslint/configs/template-recommended.mjs +15 -6
  25. package/src/eslint/configs/template-rules.mjs +17 -0
  26. package/src/eslint/configs/ts-base.mjs +2 -2
  27. package/src/eslint/configs/ts-recommended.mjs +133 -3
  28. package/src/eslint/configs/ts-rules.mjs +14 -0
  29. package/src/eslint/index.mjs +10 -4
  30. package/src/eslint/rules/__tests__/no-direct-signal-mutation.test.mjs +365 -128
  31. package/src/eslint/rules/no-direct-signal-mutation.mjs +370 -142
  32. package/src/eslint/rules/no-uninvoked-signal-in-template.mjs +1 -12
  33. package/src/eslint/signal-kinds.mjs +57 -0
  34. package/src/eslint/signal-names.mjs +43 -49
  35. package/types/dsivd-prestations-ng.d.ts +0 -1
  36. package/dsivd-prestations-ng-19.0.6.tgz +0 -0
package/package.json CHANGED
@@ -40,7 +40,7 @@
40
40
  "ng-update": {
41
41
  "migrations": "./schematics/migration-collection.json"
42
42
  },
43
- "version": "19.0.6",
43
+ "version": "19.0.8-beta.1",
44
44
  "module": "fesm2022/dsivd-prestations-ng.mjs",
45
45
  "typings": "types/dsivd-prestations-ng.d.ts",
46
46
  "exports": {
@@ -0,0 +1,15 @@
1
+ // Path of the component a linted template belongs to, `undefined` when the file is not a
2
+ // template.
3
+ //
4
+ // An inline template is a virtual block whose path is derived from the `.ts`, and whose
5
+ // name ends with `.html` too: `foo.component.ts/1_inline-template-….component.html`.
6
+ // It has to be matched BEFORE the external template, otherwise it resolves to garbage.
7
+ export function resolveComponentPath(filename) {
8
+ const inlineMatch = filename.match(/^(.*\.ts)(?:[/\\]|$)/);
9
+ if (inlineMatch) {
10
+ return inlineMatch[1];
11
+ }
12
+ return filename.endsWith(".html")
13
+ ? `${filename.slice(0, -".html".length)}.ts`
14
+ : undefined;
15
+ }
@@ -1,3 +1,4 @@
1
+ import { builtinRules } from 'eslint/use-at-your-own-risk';
1
2
  import { describe, expect, it } from 'vitest';
2
3
 
3
4
  import prestationsNg, { configs, plugin, rules } from '../../index.mjs';
@@ -6,65 +7,129 @@ const PLUGIN_KEY = '@dsivd/prestations-ng';
6
7
 
7
8
  const configNames = Object.keys(configs);
8
9
 
9
- // Rule ids enabled by a config, whatever its severity.
10
+ // Configs that only register this library's rules, and the presets built on top of them.
11
+ const RULE_LAYERS = ['tsRules', 'templateRules'];
12
+ const PRESETS = [
13
+ ['tsRecommended', 'tsRules'],
14
+ ['templateRecommended', 'templateRules'],
15
+ ];
16
+
17
+ const isOff = (severity) =>
18
+ severity === 'off' ||
19
+ severity === 0 ||
20
+ (Array.isArray(severity) && (severity[0] === 'off' || severity[0] === 0));
21
+
22
+ // Rule ids a config turns on. Ids set to 'off' are excluded: ESLint accepts those even
23
+ // when the rule does not exist, which is precisely the escape hatch consumers rely on.
10
24
  const enabledIdsOf = (config) =>
11
- config.flatMap((entry) => Object.keys(entry.rules ?? {}));
25
+ config.flatMap((entry) =>
26
+ Object.entries(entry.rules ?? {})
27
+ .filter(([, severity]) => !isOff(severity))
28
+ .map(([id]) => id),
29
+ );
30
+
31
+ const ownIdsOf = (config) =>
32
+ enabledIdsOf(config).filter((id) => id.startsWith(`${PLUGIN_KEY}/`));
33
+
34
+ const pluginsOf = (config) =>
35
+ Object.assign({}, ...config.map((entry) => entry.plugins ?? {}));
36
+
37
+ // An id is either `plugin-key/rule` (possibly nested, as in `@angular-eslint/template/x`)
38
+ // or a bare core rule.
39
+ const ruleExists = (id, plugins) => {
40
+ for (const [key, declared] of Object.entries(plugins)) {
41
+ if (id.startsWith(`${key}/`)) {
42
+ return Boolean(declared.rules?.[id.slice(key.length + 1)]);
43
+ }
44
+ }
45
+ return builtinRules.has(id);
46
+ };
12
47
 
13
48
  describe('eslint configs', () => {
14
- it('should expose one config per lint target', () => {
15
- expect(configNames).toEqual(['tsRecommended', 'templateRecommended']);
16
- });
49
+ it('should expose a rule layer and a preset per lint target', () => {
50
+ expect(configNames).toEqual([
51
+ 'tsRecommended',
52
+ 'templateRecommended',
53
+ 'tsRules',
54
+ 'templateRules',
55
+ ]);
56
+ });
17
57
 
18
- it.each(configNames)('%s should register the plugin first', (name) => {
19
- const [base, ...rest] = configs[name];
58
+ it.each(RULE_LAYERS)('%s should register the plugin first', (name) => {
59
+ const [base, ...rest] = configs[name];
20
60
 
21
- expect(base.plugins).toEqual({ [PLUGIN_KEY]: plugin });
22
- expect(base.rules).toBeUndefined();
23
- expect(rest.every((entry) => entry.plugins === undefined)).toBe(true);
24
- });
61
+ expect(base.plugins).toEqual({ [PLUGIN_KEY]: plugin });
62
+ expect(base.rules).toBeUndefined();
63
+ expect(rest.every((entry) => entry.plugins === undefined)).toBe(true);
64
+ });
25
65
 
26
- it.each(configNames)('%s should name every entry', (name) => {
27
- for (const entry of configs[name]) {
28
- expect(entry.name).toMatch(/^prestations-ng\//);
29
- }
30
- });
31
-
32
- // A config referencing a rule that does not exist breaks the lint of every
33
- // consuming project at once, so it must never reach a release.
34
- it.each(configNames)('%s should only enable existing rules', (name) => {
35
- for (const id of enabledIdsOf(configs[name])) {
36
- expect(id.startsWith(`${PLUGIN_KEY}/`)).toBe(true);
37
- expect(rules).toHaveProperty(id.slice(PLUGIN_KEY.length + 1));
38
- }
39
- });
66
+ it.each(RULE_LAYERS)('%s should carry no third-party rule', (name) => {
67
+ expect(enabledIdsOf(configs[name])).toEqual(ownIdsOf(configs[name]));
68
+ });
40
69
 
41
- it('should enable every rule in at least one config', () => {
42
- const enabled = new Set(configNames.flatMap((n) => enabledIdsOf(configs[n])));
70
+ it.each(configNames)('%s should name its own entries', (name) => {
71
+ const own = configs[name].filter((entry) =>
72
+ entry.name?.startsWith('prestations-ng/'),
73
+ );
43
74
 
44
- for (const ruleName of Object.keys(rules)) {
45
- expect(enabled).toContain(`${PLUGIN_KEY}/${ruleName}`);
46
- }
47
- });
75
+ expect(own.length).toBeGreaterThan(0);
76
+ });
48
77
 
49
- it('should keep the template-only rule out of the TypeScript config', () => {
50
- expect(enabledIdsOf(configs.tsRecommended)).not.toContain(
51
- `${PLUGIN_KEY}/no-uninvoked-signal-in-template`,
78
+ // A config referencing a rule that does not exist breaks the lint of every consuming
79
+ // project at once, so it must never reach a release. `builtinRules` is an unstable
80
+ // ESLint export, but it is the only way to check core rule ids.
81
+ it.each(configNames)('%s should only enable existing rules', (name) => {
82
+ const plugins = pluginsOf(configs[name]);
83
+
84
+ for (const id of enabledIdsOf(configs[name])) {
85
+ expect(ruleExists(id, plugins), `unknown rule ${id}`).toBe(true);
86
+ }
87
+ });
88
+
89
+ // Two entries declaring the same plugin key from different copies is a hard ESLint
90
+ // failure, so the preset must register ours exactly once.
91
+ it.each(configNames)('%s should register the plugin once', (name) => {
92
+ const declarations = configs[name]
93
+ .map((entry) => entry.plugins?.[PLUGIN_KEY])
94
+ .filter(Boolean);
95
+
96
+ expect(declarations).toEqual([plugin]);
97
+ });
98
+
99
+ it.each(PRESETS)('%s should build on %s', (preset, layer) => {
100
+ expect(ownIdsOf(configs[preset])).toEqual(ownIdsOf(configs[layer]));
101
+ });
102
+
103
+ it('should enable every rule in at least one config', () => {
104
+ const enabled = new Set(configNames.flatMap((n) => ownIdsOf(configs[n])));
105
+
106
+ for (const ruleName of Object.keys(rules)) {
107
+ expect(enabled).toContain(`${PLUGIN_KEY}/${ruleName}`);
108
+ }
109
+ });
110
+
111
+ it.each(['tsRecommended', 'tsRules'])(
112
+ '%s should keep the template-only rule out',
113
+ (name) => {
114
+ expect(enabledIdsOf(configs[name])).not.toContain(
115
+ `${PLUGIN_KEY}/no-uninvoked-signal-in-template`,
116
+ );
117
+ },
52
118
  );
53
- });
54
119
  });
55
120
 
56
121
  describe('eslint plugin', () => {
57
- it('should be named after the npm package', () => {
58
- expect(plugin.meta.name).toBe(PLUGIN_KEY);
59
- });
60
-
61
- it('should mirror the named exports on the default export', () => {
62
- expect(prestationsNg).toEqual({ configs, plugin, rules });
63
- });
64
-
65
- it.each(Object.entries(rules))('%s should be a usable rule', (_, rule) => {
66
- expect(typeof rule.create).toBe('function');
67
- expect(Object.keys(rule.meta.messages).length).toBeGreaterThan(0);
68
- expect(rule.meta.type).toBe('problem');
69
- });
122
+ it('should be named after the npm package', () => {
123
+ expect(plugin.meta.name).toBe(PLUGIN_KEY);
124
+ });
125
+
126
+ it('should mirror the named exports on the default export', () => {
127
+ expect(prestationsNg).toEqual({ configs, plugin, rules });
128
+ });
129
+
130
+ it.each(Object.entries(rules))('%s should be a usable rule', (_, rule) => {
131
+ expect(typeof rule.create).toBe('function');
132
+ expect(Object.keys(rule.meta.messages).length).toBeGreaterThan(0);
133
+ expect(rule.meta.type).toBe('problem');
134
+ });
70
135
  });
@@ -1,6 +1,6 @@
1
1
  // Registers the plugin without enabling any rule. Meant to be used through
2
- // `templateRecommended`, inside the `extends` of a block that already targets
3
- // `**/*.html` and sets the Angular template parser.
2
+ // `templateRules` or `templateRecommended`, inside the `extends` of a block that
3
+ // already targets `**/*.html` and sets the Angular template parser.
4
4
  const templateBase = (plugin) => ({
5
5
  name: 'prestations-ng/template-base',
6
6
  plugins: {
@@ -1,14 +1,23 @@
1
- import templateBase from './template-base.mjs';
1
+ import angular from 'angular-eslint';
2
2
 
3
+ import templateRules from './template-rules.mjs';
4
+
5
+ /**
6
+ * The house Angular template preset: the recommended and accessibility baselines, this
7
+ * library's own rules, and the conventions every prestations-ng project follows.
8
+ *
9
+ * Meant for the `extends` of a `**\/*.html` block. It brings its own parser and plugins,
10
+ * so do not spread `angular.configs.templateRecommended` alongside it.
11
+ */
3
12
  const templateRecommended = (plugin) => [
4
- templateBase(plugin),
13
+ ...angular.configs.templateRecommended,
14
+ ...angular.configs.templateAccessibility,
15
+ ...templateRules(plugin),
5
16
  {
6
17
  name: 'prestations-ng/template-recommended',
7
18
  rules: {
8
- // Also in `tsRecommended`: the rule carries both an ESTree and an Angular
9
- // template visitor, and each fires on its own kind of file.
10
- '@dsivd/prestations-ng/no-direct-signal-mutation': 'error',
11
- '@dsivd/prestations-ng/no-uninvoked-signal-in-template': 'error',
19
+ '@angular-eslint/template/no-negated-async': 'off',
20
+ '@angular-eslint/template/button-has-type': 'error',
12
21
  },
13
22
  },
14
23
  ];
@@ -0,0 +1,17 @@
1
+ import templateBase from './template-base.mjs';
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.
5
+ const templateRules = (plugin) => [
6
+ templateBase(plugin),
7
+ {
8
+ name: 'prestations-ng/template-rules',
9
+ rules: {
10
+ // Also in `tsRules`: the rule carries both an ESTree and an Angular
11
+ // template visitor, and each fires on its own kind of file.
12
+ '@dsivd/prestations-ng/no-direct-signal-mutation': 'error',
13
+ '@dsivd/prestations-ng/no-uninvoked-signal-in-template': 'error',
14
+ },
15
+ },
16
+ ];
17
+ export default templateRules;
@@ -1,6 +1,6 @@
1
1
  // Registers the plugin without enabling any rule. Meant to be used through
2
- // `tsRecommended`, inside the `extends` of a block that already targets `**/*.ts`
3
- // and sets the TypeScript parser.
2
+ // `tsRules` or `tsRecommended`, inside the `extends` of a block that already
3
+ // targets `**/*.ts` and sets the TypeScript parser.
4
4
  const tsBase = (plugin) => ({
5
5
  name: 'prestations-ng/ts-base',
6
6
  plugins: {
@@ -1,11 +1,141 @@
1
- import tsBase from './ts-base.mjs';
1
+ import eslint from '@eslint/js';
2
+ import rxjs from '@smarttools/eslint-plugin-rxjs';
3
+ import angular from 'angular-eslint';
4
+ import importX from 'eslint-plugin-import-x';
5
+ import simpleImportSort from 'eslint-plugin-simple-import-sort';
6
+ import tseslint from 'typescript-eslint';
2
7
 
8
+ import tsRules from './ts-rules.mjs';
9
+
10
+ /**
11
+ * The house TypeScript preset: the shared baselines, this library's own rules, and the
12
+ * conventions every prestations-ng project follows.
13
+ *
14
+ * Meant for the `extends` of a `**\/*.ts` block. It brings its own parser and plugins,
15
+ * so do not spread `angular.configs.tsRecommended` alongside it — declaring the same
16
+ * plugin twice from two different copies makes ESLint fail outright.
17
+ */
3
18
  const tsRecommended = (plugin) => [
4
- tsBase(plugin),
19
+ eslint.configs.recommended,
20
+ ...tseslint.configs.recommended, // covers: no-explicit-any, ban-ts-comment,
21
+ // no-unused-expressions, no-var, no-unused-vars…
22
+ ...tseslint.configs.stylistic, // covers: array-type, consistent-type-assertions,
23
+ // dot-notation, no-inferrable-types (default options),
24
+ // no-empty-function (error), prefer-for-of,
25
+ // prefer-function-type…
26
+ ...angular.configs.tsRecommended, // covers: prefer-inject, no-empty-lifecycle-method,
27
+ // use-lifecycle-interface…
28
+ ...tsRules(plugin),
5
29
  {
6
30
  name: 'prestations-ng/ts-recommended',
31
+ plugins: {
32
+ rxjs,
33
+ 'simple-import-sort': simpleImportSort,
34
+ 'import-x': importX,
35
+ },
7
36
  rules: {
8
- '@dsivd/prestations-ng/no-direct-signal-mutation': 'error',
37
+ // ── @angular-eslint ──────────────────────────────────────────────
38
+ '@angular-eslint/component-selector': [
39
+ 'error',
40
+ {
41
+ type: 'element',
42
+ prefix: 'app',
43
+ style: 'kebab-case',
44
+ },
45
+ ],
46
+ '@angular-eslint/directive-selector': [
47
+ 'error',
48
+ {
49
+ type: 'attribute',
50
+ prefix: 'app',
51
+ style: 'camelCase',
52
+ },
53
+ ],
54
+ '@angular-eslint/no-uncalled-signals': ['error'],
55
+ '@angular-eslint/prefer-signal-model': ['error'],
56
+ '@angular-eslint/prefer-signals': ['error'],
57
+
58
+ // ── @typescript-eslint ───────────────────────────────────────────
59
+ '@typescript-eslint/explicit-function-return-type': [
60
+ 'error',
61
+ {
62
+ allowExpressions: true,
63
+ allowTypedFunctionExpressions: true,
64
+ allowHigherOrderFunctions: true,
65
+ allowDirectConstAssertionInArrowFunctions: true,
66
+ },
67
+ ],
68
+ '@typescript-eslint/explicit-member-accessibility': [
69
+ 'error',
70
+ { accessibility: 'no-public' },
71
+ ],
72
+ '@typescript-eslint/member-ordering': 'error',
73
+ '@typescript-eslint/no-base-to-string': 'warn',
74
+ // stylistic enables no-inferrable-types with default options (ignoreParameters: false);
75
+ // override here to allow typed parameters explicitly
76
+ '@typescript-eslint/no-inferrable-types': [
77
+ 'error',
78
+ { ignoreParameters: true },
79
+ ],
80
+ // stylistic sets no-empty-function to error; downgrade to warn
81
+ '@typescript-eslint/no-empty-function': 'warn',
82
+ '@typescript-eslint/prefer-includes': 'warn',
83
+ '@typescript-eslint/return-await': ['error', 'never'],
84
+ '@typescript-eslint/typedef': ['error', { parameter: true }],
85
+ '@typescript-eslint/unified-signatures': 'error',
86
+ // off here — @typescript-eslint/no-shadow handles it correctly for TS
87
+ 'no-shadow': 'off',
88
+ '@typescript-eslint/no-shadow': ['error', { hoist: 'all' }],
89
+ '@typescript-eslint/no-unused-vars': [
90
+ 'error',
91
+ {
92
+ varsIgnorePattern: '^_',
93
+ argsIgnorePattern: '^_',
94
+ caughtErrorsIgnorePattern: '^_',
95
+ },
96
+ ],
97
+
98
+ // ── eslint core ──────────────────────────────────────────────────
99
+ 'arrow-body-style': 'error',
100
+ 'arrow-parens': ['error', 'as-needed'],
101
+ curly: 'error',
102
+ eqeqeq: ['error', 'always'],
103
+ 'guard-for-in': 'error',
104
+ 'no-bitwise': 'error',
105
+ 'no-caller': 'error',
106
+ 'no-console': ['error', { allow: ['log', 'warn', 'error'] }],
107
+ 'no-eval': 'error',
108
+ 'no-extra-boolean-cast': 'off',
109
+ 'no-multiple-empty-lines': 'error',
110
+ 'no-new-wrappers': 'error',
111
+ 'no-restricted-imports': [
112
+ 'error',
113
+ { paths: ['rxjs/Rx', 'primeng/primeng', 'primeng'] },
114
+ ],
115
+ 'no-return-assign': 'error',
116
+ 'no-throw-literal': 'error',
117
+ 'no-undef-init': 'error',
118
+ 'no-useless-concat': 'error',
119
+ 'object-shorthand': ['error', 'always', { avoidQuotes: true }],
120
+ 'one-var': ['error', 'never'],
121
+ 'prefer-arrow-callback': 'error',
122
+ 'prefer-template': 'error',
123
+ radix: 'error',
124
+
125
+ // ── rxjs ─────────────────────────────────────────────────────────
126
+ 'rxjs/no-implicit-any-catch': ['error', { allowExplicitAny: true }],
127
+ 'rxjs/no-sharereplay': 'off',
128
+
129
+ // ── import-x ─────────────────────────────────────────────────────
130
+ 'import-x/no-cycle': ['warn', { maxDepth: 3, ignoreExternal: true }],
131
+ 'import-x/no-deprecated': 'warn',
132
+ 'import-x/first': 'error',
133
+ 'import-x/newline-after-import': 'error',
134
+ 'import-x/no-duplicates': 'error',
135
+
136
+ // ── simple-import-sort ───────────────────────────────────────────
137
+ 'simple-import-sort/imports': 'error',
138
+ 'simple-import-sort/exports': 'error',
9
139
  },
10
140
  },
11
141
  ];
@@ -0,0 +1,14 @@
1
+ import tsBase from './ts-base.mjs';
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.
5
+ const tsRules = (plugin) => [
6
+ tsBase(plugin),
7
+ {
8
+ name: 'prestations-ng/ts-rules',
9
+ rules: {
10
+ '@dsivd/prestations-ng/no-direct-signal-mutation': 'error',
11
+ },
12
+ },
13
+ ];
14
+ export default tsRules;
@@ -1,15 +1,21 @@
1
1
  import templateRecommended from './configs/template-recommended.mjs';
2
+ import templateRules from './configs/template-rules.mjs';
2
3
  import tsRecommended from './configs/ts-recommended.mjs';
4
+ import tsRules from './configs/ts-rules.mjs';
3
5
  import rules from './rules/index.mjs';
4
6
 
5
7
  const plugin = {
6
- meta: { name: '@dsivd/prestations-ng' },
7
- rules,
8
+ meta: { name: '@dsivd/prestations-ng' },
9
+ rules,
8
10
  };
9
11
 
10
12
  const configs = {
11
- tsRecommended: tsRecommended(plugin),
12
- templateRecommended: templateRecommended(plugin),
13
+ // The house presets: baselines, this library's rules, and the shared conventions.
14
+ tsRecommended: tsRecommended(plugin),
15
+ templateRecommended: templateRecommended(plugin),
16
+ // Only this library's own rules, for a project that supplies its own style.
17
+ tsRules: tsRules(plugin),
18
+ templateRules: templateRules(plugin),
13
19
  };
14
20
 
15
21
  export { configs, plugin, rules };