@valkyrjaio/ci-eslint 26.0.0 → 26.1.0

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/README.md CHANGED
@@ -28,19 +28,40 @@ itself.
28
28
 
29
29
  ## Usage
30
30
 
31
- Give `getRule` the repository's own package identifier, and register the rule
32
- that it returns:
31
+ A repository states only what is true of itself, and `Rules` supplies the rest:
33
32
 
34
33
  ```js
35
- import { CopyrightHeaderFactory } from '@valkyrjaio/ci-eslint';
36
-
37
- export default tseslint.config(eslint.configs.recommended, {
38
- plugins: {
39
- local: { rules: { 'copyright-header': CopyrightHeaderFactory.getRule('Valkyrja Framework') } },
40
- },
41
- rules: {
42
- 'local/copyright-header': 'error',
43
- },
34
+ import path from 'path';
35
+ import { Rules } from '@valkyrjaio/ci-eslint';
36
+
37
+ export default Rules.getConfig({
38
+ packageName: 'Valkyrja Framework',
39
+ tsconfigRootDir: path.resolve(import.meta.dirname, '../../..'),
40
+ project: ['./tsconfig.tests.json'],
41
+ });
42
+ ```
43
+
44
+ `getConfig` returns the whole flat configuration: the ESLint and typescript-eslint
45
+ presets, the shared rules, the copyright header rule, and the test-file rules.
46
+
47
+ A repository picks one of two ways to find its TypeScript project. Give `project`
48
+ when one tsconfig spans both `src` and `tests`. Give `projectService` to let
49
+ typescript-eslint find the project, which reads `tsconfig.json` only. Give exactly
50
+ one; `getConfig` rejects neither and both.
51
+
52
+ Warning: with neither option typescript-eslint reads no type information, every
53
+ type-aware rule goes quiet, and the run still reports success. That is why the
54
+ guard stops rather than picking a default.
55
+
56
+ Add what only this repository needs through `overrides`, which append after the
57
+ shared entries:
58
+
59
+ ```js
60
+ Rules.getConfig({
61
+ packageName: 'Valkyrja Application',
62
+ tsconfigRootDir: path.resolve(import.meta.dirname, '../../..'),
63
+ projectService: true,
64
+ overrides: [{ files: ['bin/**/*.ts'], rules: { 'no-console': 'off' } }],
44
65
  });
45
66
  ```
46
67
 
@@ -53,12 +74,16 @@ every repository to its identifier. `valkyrja-ts` takes `Valkyrja Framework`,
53
74
 
54
75
  | Member | Returns |
55
76
  | :---------------------------------------- | :-------------------------------------------------- |
77
+ | `Rules.getConfig(options)` | the whole flat configuration |
78
+ | `Rules.getRules()` | the rules that apply to every linted file |
79
+ | `Rules.getTestRules()` | the rules that apply to a test file only |
80
+ | `Rules.getParserOptions(options)` | the parser options that find the project |
56
81
  | `CopyrightHeaderFactory.getRule(name)` | the ESLint rule that requires the header |
57
82
  | `CopyrightHeaderFactory.getHeader(name)` | the full block comment, and the blank line below it |
58
83
  | `CopyrightHeaderFactory.getComment(name)` | the text between the two comment delimiters |
59
84
 
60
- Each member takes the package identifier, and each one rejects a value that
61
- names no package.
85
+ Each `CopyrightHeaderFactory` member takes the package identifier, and each one
86
+ rejects a value that names no package.
62
87
 
63
88
  ## Why the package name is guarded
64
89
 
@@ -1,5 +1,5 @@
1
1
  export declare class EslintInfo {
2
- static readonly VERSION: "26.0.0";
3
- static readonly VERSION_BUILD_DATE_TIME: "August 2 2026 20:32:26 MST";
2
+ static readonly VERSION: "26.1.0";
3
+ static readonly VERSION_BUILD_DATE_TIME: "August 3 2026 00:42:35 MST";
4
4
  }
5
5
  //# sourceMappingURL=EslintInfo.d.ts.map
@@ -6,7 +6,7 @@
6
6
  * Released under the MIT License. See LICENSE.md for details.
7
7
  */
8
8
  export class EslintInfo {
9
- static VERSION = '26.0.0';
10
- static VERSION_BUILD_DATE_TIME = 'August 2 2026 20:32:26 MST';
9
+ static VERSION = '26.1.0';
10
+ static VERSION_BUILD_DATE_TIME = 'August 3 2026 00:42:35 MST';
11
11
  }
12
12
  //# sourceMappingURL=EslintInfo.js.map
@@ -0,0 +1,59 @@
1
+ import type { Linter } from 'eslint';
2
+ /**
3
+ * What a repository states about itself.
4
+ *
5
+ * Every other part of the configuration is the same in each repository, so this package holds it.
6
+ */
7
+ export interface RulesOptions {
8
+ /** The package identifier the header states, such as `Valkyrja Framework`. */
9
+ packageName: string;
10
+ /** The absolute directory that a `project` path resolves from. */
11
+ tsconfigRootDir: string;
12
+ /** The TypeScript projects to read. Give this, or `projectService`, and never both. */
13
+ project?: string[];
14
+ /** Let typescript-eslint find the project. Give this, or `project`, and never both. */
15
+ projectService?: boolean;
16
+ /** Flat configuration entries this repository adds after the shared ones. */
17
+ overrides?: Linter.Config[];
18
+ }
19
+ /**
20
+ * Builds the ESLint configuration that every Valkyrja TypeScript repository runs.
21
+ *
22
+ * A repository that keeps its own copy of these rules drifts from the others, and no tool reports
23
+ * the drift. This class therefore holds the rules, and a repository states only what is true of
24
+ * itself: its package identifier, and how to find its TypeScript project.
25
+ */
26
+ export declare class Rules {
27
+ /**
28
+ * Builds the whole configuration.
29
+ *
30
+ * @throws EslintInvalidParserOptionsException When the caller names neither `project` nor
31
+ * `projectService`, or names both
32
+ */
33
+ static getConfig(options: RulesOptions): Linter.Config[];
34
+ /**
35
+ * The rules that apply to every linted file.
36
+ */
37
+ static getRules(): Linter.RulesRecord;
38
+ /**
39
+ * The rules that apply to a test file only.
40
+ *
41
+ * A test builds a stub as `{ method: vi.fn() } as unknown as SomeContract`, so
42
+ * `expect(stub.method)` reads the method off the contract type purely to assert on the spy. The
43
+ * test never invokes the method unbound. `unbound-method` sees the declared contract type only,
44
+ * and it cannot tell the two apart, so it reports every such assertion. The rule still applies
45
+ * in full to `src`.
46
+ */
47
+ static getTestRules(): Linter.RulesRecord;
48
+ /**
49
+ * The parser options that find the repository's TypeScript project.
50
+ *
51
+ * A repository picks one of the two ways. `project` names the tsconfig files to read, which a
52
+ * repository needs when one tsconfig spans both `src` and `tests`. `projectService` lets
53
+ * typescript-eslint find the project, which reads `tsconfig.json` only.
54
+ *
55
+ * @throws EslintInvalidParserOptionsException When the caller names neither, or both
56
+ */
57
+ static getParserOptions(options: RulesOptions): Record<string, unknown>;
58
+ }
59
+ //# sourceMappingURL=Rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Rules.d.ts","sourceRoot":"","sources":["../../src/Eslint/Rules.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAQrC;;;;GAIG;AACH,MAAM,WAAW,YAAY;IACzB,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,eAAe,EAAE,MAAM,CAAC;IACxB,uFAAuF;IACvF,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,uFAAuF;IACvF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,qBAAa,KAAK;IACd;;;;;OAKG;IACH,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE;IAqBxD;;OAEG;IACH,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,WAAW;IAWrC;;;;;;;;OAQG;IACH,MAAM,CAAC,YAAY,IAAI,MAAM,CAAC,WAAW;IAMzC;;;;;;;;OAQG;IACH,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAuB1E"}
@@ -0,0 +1,103 @@
1
+ /*
2
+ * This file is part of the Valkyrja ESLint package.
3
+ *
4
+ * Copyright (c) 2016-present Melech Mizrachi
5
+ *
6
+ * Released under the MIT License. See LICENSE.md for details.
7
+ */
8
+ import eslint from '@eslint/js';
9
+ // `defineConfig` comes from ESLint core. `tseslint.config` did the same job and is deprecated.
10
+ import { defineConfig } from 'eslint/config';
11
+ import tseslint from 'typescript-eslint';
12
+ import { CopyrightHeaderFactory } from "./Factory/CopyrightHeaderFactory.js";
13
+ import { EslintInvalidParserOptionsException } from "./Throwable/Exception/EslintInvalidParserOptionsException.js";
14
+ /**
15
+ * Builds the ESLint configuration that every Valkyrja TypeScript repository runs.
16
+ *
17
+ * A repository that keeps its own copy of these rules drifts from the others, and no tool reports
18
+ * the drift. This class therefore holds the rules, and a repository states only what is true of
19
+ * itself: its package identifier, and how to find its TypeScript project.
20
+ */
21
+ export class Rules {
22
+ /**
23
+ * Builds the whole configuration.
24
+ *
25
+ * @throws EslintInvalidParserOptionsException When the caller names neither `project` nor
26
+ * `projectService`, or names both
27
+ */
28
+ static getConfig(options) {
29
+ return defineConfig([
30
+ eslint.configs.recommended,
31
+ tseslint.configs.strictTypeChecked,
32
+ {
33
+ languageOptions: {
34
+ parserOptions: Rules.getParserOptions(options),
35
+ },
36
+ plugins: {
37
+ local: { rules: { 'copyright-header': CopyrightHeaderFactory.getRule(options.packageName) } },
38
+ },
39
+ rules: Rules.getRules(),
40
+ },
41
+ {
42
+ files: ['tests/**/*.ts'],
43
+ rules: Rules.getTestRules(),
44
+ },
45
+ ...(options.overrides ?? []),
46
+ ]);
47
+ }
48
+ /**
49
+ * The rules that apply to every linted file.
50
+ */
51
+ static getRules() {
52
+ return {
53
+ 'local/copyright-header': 'error',
54
+ '@typescript-eslint/no-namespace': 'off',
55
+ '@typescript-eslint/no-extraneous-class': 'off',
56
+ '@typescript-eslint/no-unnecessary-type-parameters': 'off',
57
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
58
+ '@typescript-eslint/no-invalid-void-type': ['error', { allowAsThisParameter: true }],
59
+ };
60
+ }
61
+ /**
62
+ * The rules that apply to a test file only.
63
+ *
64
+ * A test builds a stub as `{ method: vi.fn() } as unknown as SomeContract`, so
65
+ * `expect(stub.method)` reads the method off the contract type purely to assert on the spy. The
66
+ * test never invokes the method unbound. `unbound-method` sees the declared contract type only,
67
+ * and it cannot tell the two apart, so it reports every such assertion. The rule still applies
68
+ * in full to `src`.
69
+ */
70
+ static getTestRules() {
71
+ return {
72
+ '@typescript-eslint/unbound-method': 'off',
73
+ };
74
+ }
75
+ /**
76
+ * The parser options that find the repository's TypeScript project.
77
+ *
78
+ * A repository picks one of the two ways. `project` names the tsconfig files to read, which a
79
+ * repository needs when one tsconfig spans both `src` and `tests`. `projectService` lets
80
+ * typescript-eslint find the project, which reads `tsconfig.json` only.
81
+ *
82
+ * @throws EslintInvalidParserOptionsException When the caller names neither, or both
83
+ */
84
+ static getParserOptions(options) {
85
+ const hasProject = options.project !== undefined;
86
+ const hasProjectService = options.projectService !== undefined;
87
+ // Warning: neither option set means typescript-eslint parses no type information, and every
88
+ // type-aware rule goes quiet while the run still reports success. Both set is ambiguous.
89
+ // Stop on each, rather than pick one and lint less than the repository expects.
90
+ if (hasProject === hasProjectService) {
91
+ // The message names the options rather than a method. Both `getConfig` and
92
+ // `getParserOptions` are public and reach this guard, so a method name in the text is
93
+ // wrong for one of the two callers.
94
+ throw new EslintInvalidParserOptionsException('The options take either `project` or `projectService`, and they name ' +
95
+ (hasProject ? 'both.' : 'neither.'));
96
+ }
97
+ if (options.project !== undefined) {
98
+ return { project: options.project, tsconfigRootDir: options.tsconfigRootDir };
99
+ }
100
+ return { projectService: options.projectService, tsconfigRootDir: options.tsconfigRootDir };
101
+ }
102
+ }
103
+ //# sourceMappingURL=Rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Rules.js","sourceRoot":"","sources":["../../src/Eslint/Rules.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,MAAM,MAAM,YAAY,CAAC;AAEhC,+FAA+F;AAC/F,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,QAAQ,MAAM,mBAAmB,CAAC;AAEzC,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAAE,mCAAmC,EAAE,MAAM,8DAA8D,CAAC;AAoBnH;;;;;;GAMG;AACH,MAAM,OAAO,KAAK;IACd;;;;;OAKG;IACH,MAAM,CAAC,SAAS,CAAC,OAAqB;QAClC,OAAO,YAAY,CAAC;YAChB,MAAM,CAAC,OAAO,CAAC,WAAW;YAC1B,QAAQ,CAAC,OAAO,CAAC,iBAAiB;YAClC;gBACI,eAAe,EAAE;oBACb,aAAa,EAAE,KAAK,CAAC,gBAAgB,CAAC,OAAO,CAAC;iBACjD;gBACD,OAAO,EAAE;oBACL,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,kBAAkB,EAAE,sBAAsB,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE;iBAChG;gBACD,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE;aAC1B;YACD;gBACI,KAAK,EAAE,CAAC,eAAe,CAAC;gBACxB,KAAK,EAAE,KAAK,CAAC,YAAY,EAAE;aAC9B;YACD,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;SAC/B,CAAC,CAAC;IACP,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,QAAQ;QACX,OAAO;YACH,wBAAwB,EAAE,OAAO;YACjC,iCAAiC,EAAE,KAAK;YACxC,wCAAwC,EAAE,KAAK;YAC/C,mDAAmD,EAAE,KAAK;YAC1D,mCAAmC,EAAE,CAAC,OAAO,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC;YACpG,yCAAyC,EAAE,CAAC,OAAO,EAAE,EAAE,oBAAoB,EAAE,IAAI,EAAE,CAAC;SACvF,CAAC;IACN,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,YAAY;QACf,OAAO;YACH,mCAAmC,EAAE,KAAK;SAC7C,CAAC;IACN,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,gBAAgB,CAAC,OAAqB;QACzC,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC;QACjD,MAAM,iBAAiB,GAAG,OAAO,CAAC,cAAc,KAAK,SAAS,CAAC;QAE/D,4FAA4F;QAC5F,yFAAyF;QACzF,gFAAgF;QAChF,IAAI,UAAU,KAAK,iBAAiB,EAAE,CAAC;YACnC,2EAA2E;YAC3E,sFAAsF;YACtF,oCAAoC;YACpC,MAAM,IAAI,mCAAmC,CACzC,uEAAuE;gBACnE,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAC1C,CAAC;QACN,CAAC;QAED,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAChC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,CAAC,eAAe,EAAE,CAAC;QAClF,CAAC;QAED,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,eAAe,EAAE,OAAO,CAAC,eAAe,EAAE,CAAC;IAChG,CAAC;CACJ"}
@@ -0,0 +1,8 @@
1
+ import { EslintInvalidArgumentException } from './Abstract/EslintInvalidArgumentException.ts';
2
+ /**
3
+ * The caller named neither, or both, of the two ways to find the TypeScript project.
4
+ */
5
+ export declare class EslintInvalidParserOptionsException extends EslintInvalidArgumentException {
6
+ constructor(message: string);
7
+ }
8
+ //# sourceMappingURL=EslintInvalidParserOptionsException.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EslintInvalidParserOptionsException.d.ts","sourceRoot":"","sources":["../../../../src/Eslint/Throwable/Exception/EslintInvalidParserOptionsException.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,8BAA8B,EAAE,MAAM,8CAA8C,CAAC;AAE9F;;GAEG;AACH,qBAAa,mCAAoC,SAAQ,8BAA8B;gBACvE,OAAO,EAAE,MAAM;CAK9B"}
@@ -0,0 +1,18 @@
1
+ /*
2
+ * This file is part of the Valkyrja ESLint package.
3
+ *
4
+ * Copyright (c) 2016-present Melech Mizrachi
5
+ *
6
+ * Released under the MIT License. See LICENSE.md for details.
7
+ */
8
+ import { EslintInvalidArgumentException } from "./Abstract/EslintInvalidArgumentException.js";
9
+ /**
10
+ * The caller named neither, or both, of the two ways to find the TypeScript project.
11
+ */
12
+ export class EslintInvalidParserOptionsException extends EslintInvalidArgumentException {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = 'EslintInvalidParserOptionsException';
16
+ }
17
+ }
18
+ //# sourceMappingURL=EslintInvalidParserOptionsException.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EslintInvalidParserOptionsException.js","sourceRoot":"","sources":["../../../../src/Eslint/Throwable/Exception/EslintInvalidParserOptionsException.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,8BAA8B,EAAE,MAAM,8CAA8C,CAAC;AAE9F;;GAEG;AACH,MAAM,OAAO,mCAAoC,SAAQ,8BAA8B;IACnF,YAAY,OAAe;QACvB,KAAK,CAAC,OAAO,CAAC,CAAC;QAEf,IAAI,CAAC,IAAI,GAAG,qCAAqC,CAAC;IACtD,CAAC;CACJ"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { EslintInfo } from './Eslint/Constant/EslintInfo.ts';
2
2
  export { CopyrightHeaderFactory } from './Eslint/Factory/CopyrightHeaderFactory.ts';
3
+ export { Rules } from './Eslint/Rules.ts';
4
+ export type { RulesOptions } from './Eslint/Rules.ts';
3
5
  export { EslintInvalidArgumentException } from './Eslint/Throwable/Exception/Abstract/EslintInvalidArgumentException.ts';
4
6
  export { EslintInvalidPackageNameException } from './Eslint/Throwable/Exception/EslintInvalidPackageNameException.ts';
7
+ export { EslintInvalidParserOptionsException } from './Eslint/Throwable/Exception/EslintInvalidParserOptionsException.ts';
5
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AACpF,OAAO,EAAE,8BAA8B,EAAE,MAAM,yEAAyE,CAAC;AACzH,OAAO,EAAE,iCAAiC,EAAE,MAAM,mEAAmE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AACpF,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,8BAA8B,EAAE,MAAM,yEAAyE,CAAC;AACzH,OAAO,EAAE,iCAAiC,EAAE,MAAM,mEAAmE,CAAC;AACtH,OAAO,EAAE,mCAAmC,EAAE,MAAM,qEAAqE,CAAC"}
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@
7
7
  */
8
8
  export { EslintInfo } from "./Eslint/Constant/EslintInfo.js";
9
9
  export { CopyrightHeaderFactory } from "./Eslint/Factory/CopyrightHeaderFactory.js";
10
+ export { Rules } from "./Eslint/Rules.js";
10
11
  export { EslintInvalidArgumentException } from "./Eslint/Throwable/Exception/Abstract/EslintInvalidArgumentException.js";
11
12
  export { EslintInvalidPackageNameException } from "./Eslint/Throwable/Exception/EslintInvalidPackageNameException.js";
13
+ export { EslintInvalidParserOptionsException } from "./Eslint/Throwable/Exception/EslintInvalidParserOptionsException.js";
12
14
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AACpF,OAAO,EAAE,8BAA8B,EAAE,MAAM,yEAAyE,CAAC;AACzH,OAAO,EAAE,iCAAiC,EAAE,MAAM,mEAAmE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AACpF,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,8BAA8B,EAAE,MAAM,yEAAyE,CAAC;AACzH,OAAO,EAAE,iCAAiC,EAAE,MAAM,mEAAmE,CAAC;AACtH,OAAO,EAAE,mCAAmC,EAAE,MAAM,qEAAqE,CAAC"}
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "@valkyrjaio/ci-eslint",
3
- "version": "26.0.0",
3
+ "version": "26.1.0",
4
4
  "description": "Shared ESLint configuration for Valkyrja TypeScript repositories",
5
5
  "homepage": "https://www.valkyrja.io",
6
+ "bugs": {
7
+ "url": "https://github.com/valkyrjaio/ci-eslint-ts/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/valkyrjaio/ci-eslint-ts.git"
12
+ },
6
13
  "keywords": [
7
14
  "ci",
8
15
  "eslint",
@@ -34,7 +41,8 @@
34
41
  "typescript-outdated-check": "cd .github/ci/typescript && npm install && npm outdated",
35
42
  "typescript-outdated-check-major-release": "cd .github/ci/typescript && npm install && npm outdated",
36
43
  "typescript-npm-update": "cd .github/ci/typescript && npm update",
37
- "eslint": "cd .github/ci/eslint && npm run check",
44
+ "eslint": "cd .github/ci/eslint && npm run fix",
45
+ "eslint-check": "cd .github/ci/eslint && npm run check",
38
46
  "eslint-outdated-check": "cd .github/ci/eslint && npm install && npm outdated",
39
47
  "eslint-outdated-check-major-release": "cd .github/ci/eslint && npm install && npm outdated",
40
48
  "eslint-npm-update": "cd .github/ci/eslint && npm update",
@@ -56,5 +64,9 @@
56
64
  },
57
65
  "peerDependencies": {
58
66
  "eslint": ">=9"
67
+ },
68
+ "dependencies": {
69
+ "@eslint/js": "^10.0.1",
70
+ "typescript-eslint": "^8.65.0"
59
71
  }
60
72
  }
@@ -7,6 +7,6 @@
7
7
  */
8
8
 
9
9
  export class EslintInfo {
10
- static readonly VERSION = '26.0.0' as const;
11
- static readonly VERSION_BUILD_DATE_TIME = 'August 2 2026 20:32:26 MST' as const;
10
+ static readonly VERSION = '26.1.0' as const;
11
+ static readonly VERSION_BUILD_DATE_TIME = 'August 3 2026 00:42:35 MST' as const;
12
12
  }
@@ -0,0 +1,132 @@
1
+ /*
2
+ * This file is part of the Valkyrja ESLint package.
3
+ *
4
+ * Copyright (c) 2016-present Melech Mizrachi
5
+ *
6
+ * Released under the MIT License. See LICENSE.md for details.
7
+ */
8
+
9
+ import eslint from '@eslint/js';
10
+ import type { Linter } from 'eslint';
11
+ // `defineConfig` comes from ESLint core. `tseslint.config` did the same job and is deprecated.
12
+ import { defineConfig } from 'eslint/config';
13
+ import tseslint from 'typescript-eslint';
14
+
15
+ import { CopyrightHeaderFactory } from './Factory/CopyrightHeaderFactory.ts';
16
+ import { EslintInvalidParserOptionsException } from './Throwable/Exception/EslintInvalidParserOptionsException.ts';
17
+
18
+ /**
19
+ * What a repository states about itself.
20
+ *
21
+ * Every other part of the configuration is the same in each repository, so this package holds it.
22
+ */
23
+ export interface RulesOptions {
24
+ /** The package identifier the header states, such as `Valkyrja Framework`. */
25
+ packageName: string;
26
+ /** The absolute directory that a `project` path resolves from. */
27
+ tsconfigRootDir: string;
28
+ /** The TypeScript projects to read. Give this, or `projectService`, and never both. */
29
+ project?: string[];
30
+ /** Let typescript-eslint find the project. Give this, or `project`, and never both. */
31
+ projectService?: boolean;
32
+ /** Flat configuration entries this repository adds after the shared ones. */
33
+ overrides?: Linter.Config[];
34
+ }
35
+
36
+ /**
37
+ * Builds the ESLint configuration that every Valkyrja TypeScript repository runs.
38
+ *
39
+ * A repository that keeps its own copy of these rules drifts from the others, and no tool reports
40
+ * the drift. This class therefore holds the rules, and a repository states only what is true of
41
+ * itself: its package identifier, and how to find its TypeScript project.
42
+ */
43
+ export class Rules {
44
+ /**
45
+ * Builds the whole configuration.
46
+ *
47
+ * @throws EslintInvalidParserOptionsException When the caller names neither `project` nor
48
+ * `projectService`, or names both
49
+ */
50
+ static getConfig(options: RulesOptions): Linter.Config[] {
51
+ return defineConfig([
52
+ eslint.configs.recommended,
53
+ tseslint.configs.strictTypeChecked,
54
+ {
55
+ languageOptions: {
56
+ parserOptions: Rules.getParserOptions(options),
57
+ },
58
+ plugins: {
59
+ local: { rules: { 'copyright-header': CopyrightHeaderFactory.getRule(options.packageName) } },
60
+ },
61
+ rules: Rules.getRules(),
62
+ },
63
+ {
64
+ files: ['tests/**/*.ts'],
65
+ rules: Rules.getTestRules(),
66
+ },
67
+ ...(options.overrides ?? []),
68
+ ]);
69
+ }
70
+
71
+ /**
72
+ * The rules that apply to every linted file.
73
+ */
74
+ static getRules(): Linter.RulesRecord {
75
+ return {
76
+ 'local/copyright-header': 'error',
77
+ '@typescript-eslint/no-namespace': 'off',
78
+ '@typescript-eslint/no-extraneous-class': 'off',
79
+ '@typescript-eslint/no-unnecessary-type-parameters': 'off',
80
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
81
+ '@typescript-eslint/no-invalid-void-type': ['error', { allowAsThisParameter: true }],
82
+ };
83
+ }
84
+
85
+ /**
86
+ * The rules that apply to a test file only.
87
+ *
88
+ * A test builds a stub as `{ method: vi.fn() } as unknown as SomeContract`, so
89
+ * `expect(stub.method)` reads the method off the contract type purely to assert on the spy. The
90
+ * test never invokes the method unbound. `unbound-method` sees the declared contract type only,
91
+ * and it cannot tell the two apart, so it reports every such assertion. The rule still applies
92
+ * in full to `src`.
93
+ */
94
+ static getTestRules(): Linter.RulesRecord {
95
+ return {
96
+ '@typescript-eslint/unbound-method': 'off',
97
+ };
98
+ }
99
+
100
+ /**
101
+ * The parser options that find the repository's TypeScript project.
102
+ *
103
+ * A repository picks one of the two ways. `project` names the tsconfig files to read, which a
104
+ * repository needs when one tsconfig spans both `src` and `tests`. `projectService` lets
105
+ * typescript-eslint find the project, which reads `tsconfig.json` only.
106
+ *
107
+ * @throws EslintInvalidParserOptionsException When the caller names neither, or both
108
+ */
109
+ static getParserOptions(options: RulesOptions): Record<string, unknown> {
110
+ const hasProject = options.project !== undefined;
111
+ const hasProjectService = options.projectService !== undefined;
112
+
113
+ // Warning: neither option set means typescript-eslint parses no type information, and every
114
+ // type-aware rule goes quiet while the run still reports success. Both set is ambiguous.
115
+ // Stop on each, rather than pick one and lint less than the repository expects.
116
+ if (hasProject === hasProjectService) {
117
+ // The message names the options rather than a method. Both `getConfig` and
118
+ // `getParserOptions` are public and reach this guard, so a method name in the text is
119
+ // wrong for one of the two callers.
120
+ throw new EslintInvalidParserOptionsException(
121
+ 'The options take either `project` or `projectService`, and they name ' +
122
+ (hasProject ? 'both.' : 'neither.'),
123
+ );
124
+ }
125
+
126
+ if (options.project !== undefined) {
127
+ return { project: options.project, tsconfigRootDir: options.tsconfigRootDir };
128
+ }
129
+
130
+ return { projectService: options.projectService, tsconfigRootDir: options.tsconfigRootDir };
131
+ }
132
+ }
@@ -0,0 +1,20 @@
1
+ /*
2
+ * This file is part of the Valkyrja ESLint package.
3
+ *
4
+ * Copyright (c) 2016-present Melech Mizrachi
5
+ *
6
+ * Released under the MIT License. See LICENSE.md for details.
7
+ */
8
+
9
+ import { EslintInvalidArgumentException } from './Abstract/EslintInvalidArgumentException.ts';
10
+
11
+ /**
12
+ * The caller named neither, or both, of the two ways to find the TypeScript project.
13
+ */
14
+ export class EslintInvalidParserOptionsException extends EslintInvalidArgumentException {
15
+ constructor(message: string) {
16
+ super(message);
17
+
18
+ this.name = 'EslintInvalidParserOptionsException';
19
+ }
20
+ }
package/src/index.ts CHANGED
@@ -8,5 +8,8 @@
8
8
 
9
9
  export { EslintInfo } from './Eslint/Constant/EslintInfo.ts';
10
10
  export { CopyrightHeaderFactory } from './Eslint/Factory/CopyrightHeaderFactory.ts';
11
+ export { Rules } from './Eslint/Rules.ts';
12
+ export type { RulesOptions } from './Eslint/Rules.ts';
11
13
  export { EslintInvalidArgumentException } from './Eslint/Throwable/Exception/Abstract/EslintInvalidArgumentException.ts';
12
14
  export { EslintInvalidPackageNameException } from './Eslint/Throwable/Exception/EslintInvalidPackageNameException.ts';
15
+ export { EslintInvalidParserOptionsException } from './Eslint/Throwable/Exception/EslintInvalidParserOptionsException.ts';