@valkyrjaio/ci-eslint 26.0.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.
Files changed (28) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +127 -0
  3. package/dist/Eslint/Constant/EslintInfo.d.ts +5 -0
  4. package/dist/Eslint/Constant/EslintInfo.d.ts.map +1 -0
  5. package/dist/Eslint/Constant/EslintInfo.js +12 -0
  6. package/dist/Eslint/Constant/EslintInfo.js.map +1 -0
  7. package/dist/Eslint/Factory/CopyrightHeaderFactory.d.ts +35 -0
  8. package/dist/Eslint/Factory/CopyrightHeaderFactory.d.ts.map +1 -0
  9. package/dist/Eslint/Factory/CopyrightHeaderFactory.js +122 -0
  10. package/dist/Eslint/Factory/CopyrightHeaderFactory.js.map +1 -0
  11. package/dist/Eslint/Throwable/Exception/Abstract/EslintInvalidArgumentException.d.ts +10 -0
  12. package/dist/Eslint/Throwable/Exception/Abstract/EslintInvalidArgumentException.d.ts.map +1 -0
  13. package/dist/Eslint/Throwable/Exception/Abstract/EslintInvalidArgumentException.js +17 -0
  14. package/dist/Eslint/Throwable/Exception/Abstract/EslintInvalidArgumentException.js.map +1 -0
  15. package/dist/Eslint/Throwable/Exception/EslintInvalidPackageNameException.d.ts +8 -0
  16. package/dist/Eslint/Throwable/Exception/EslintInvalidPackageNameException.d.ts.map +1 -0
  17. package/dist/Eslint/Throwable/Exception/EslintInvalidPackageNameException.js +18 -0
  18. package/dist/Eslint/Throwable/Exception/EslintInvalidPackageNameException.js.map +1 -0
  19. package/dist/index.d.ts +5 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +12 -0
  22. package/dist/index.js.map +1 -0
  23. package/package.json +60 -0
  24. package/src/Eslint/Constant/EslintInfo.ts +12 -0
  25. package/src/Eslint/Factory/CopyrightHeaderFactory.ts +145 -0
  26. package/src/Eslint/Throwable/Exception/Abstract/EslintInvalidArgumentException.ts +16 -0
  27. package/src/Eslint/Throwable/Exception/EslintInvalidPackageNameException.ts +20 -0
  28. package/src/index.ts +12 -0
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016-present Melech Mizrachi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is furnished
10
+ to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ <p align="center"><a href="https://valkyrja.io" target="_blank">
2
+ <img src="https://raw.githubusercontent.com/valkyrjaio/art/refs/heads/master/long-banner/orange/typescript.png" width="100%">
3
+ </a></p>
4
+
5
+ # Valkyrja ESLint (TypeScript)
6
+
7
+ Shared ESLint configuration for the Valkyrja TypeScript repositories.
8
+
9
+ The package holds the copyright header that every Valkyrja TypeScript file
10
+ carries, and the ESLint rule that enforces it. A repository supplies only its own
11
+ package identifier. The header text lives here, in one place.
12
+
13
+ <p>
14
+ <a href="https://www.npmjs.com/package/@valkyrjaio/ci-eslint"><img src="https://img.shields.io/npm/v/@valkyrjaio/ci-eslint.svg" alt="Latest Version on npm"></a>
15
+ <a href="https://www.npmjs.com/package/@valkyrjaio/ci-eslint"><img src="https://img.shields.io/node/v/@valkyrjaio/ci-eslint.svg" alt="Supported Node.js Version"></a>
16
+ <a href="https://github.com/valkyrjaio/ci-eslint-ts/blob/26.x/LICENSE.md"><img src="https://img.shields.io/github/license/valkyrjaio/ci-eslint-ts.svg" alt="License"></a>
17
+ <a href="https://github.com/valkyrjaio/ci-eslint-ts/actions/workflows/ci.yml?query=branch%3A26.x"><img src="https://github.com/valkyrjaio/ci-eslint-ts/actions/workflows/ci.yml/badge.svg?branch=26.x" alt="CI Status"></a>
18
+ </p>
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install --save-dev @valkyrjaio/ci-eslint
24
+ ```
25
+
26
+ Install it in the repository's `.github/ci/eslint/` directory, next to `eslint`
27
+ itself.
28
+
29
+ ## Usage
30
+
31
+ Give `getRule` the repository's own package identifier, and register the rule
32
+ that it returns:
33
+
34
+ ```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
+ },
44
+ });
45
+ ```
46
+
47
+ `COPYRIGHT_HEADER.md` in the [`.github`][copyright header url] repository maps
48
+ every repository to its identifier. `valkyrja-ts` takes `Valkyrja Framework`,
49
+ `sindri-ts` takes `Sindri`, `valkyrja-starter-app-ts` takes
50
+ `Valkyrja Application`, and `project-template-ts` takes `Project Template`.
51
+
52
+ ## The API
53
+
54
+ | Member | Returns |
55
+ | :---------------------------------------- | :-------------------------------------------------- |
56
+ | `CopyrightHeaderFactory.getRule(name)` | the ESLint rule that requires the header |
57
+ | `CopyrightHeaderFactory.getHeader(name)` | the full block comment, and the blank line below it |
58
+ | `CopyrightHeaderFactory.getComment(name)` | the text between the two comment delimiters |
59
+
60
+ Each member takes the package identifier, and each one rejects a value that
61
+ names no package.
62
+
63
+ ## Why the package name is guarded
64
+
65
+ Warning: the rule is fixable. It rewrites the header of every file it reads, and
66
+ it then reports each file as correct, because the file and the configuration now
67
+ agree. A wrong package identifier therefore corrupts a repository behind a green
68
+ gate.
69
+
70
+ The PHP port shipped that defect. A function that took an assembled header was
71
+ changed to take a package name, one caller was not updated, and the formatter
72
+ wrote `This file is part of the ⟨whole five-line header⟩ package.` into every
73
+ file. Every check passed.
74
+
75
+ `getHeader` therefore rejects a name before it builds any text. It rejects a name
76
+ that spans more than one line, a name that carries the header's opening sentence,
77
+ and a name that holds no characters.
78
+
79
+ ```js
80
+ // Wrong — the caller passes a built header, and the rule would rewrite every file.
81
+ CopyrightHeaderFactory.getRule(existingHeaderText); // throws EslintInvalidPackageNameException
82
+
83
+ // Right — the caller passes the identifier for this repository.
84
+ CopyrightHeaderFactory.getRule('Valkyrja Framework');
85
+ ```
86
+
87
+ ## Versioning and Release Process
88
+
89
+ This package follows [semantic versioning][semantic versioning url] with a major
90
+ release every year, and support for each major version for 2 years from the date
91
+ of release.
92
+
93
+ For more information see our
94
+ [Versioning and Release Process documentation][Versioning and Release Process url].
95
+
96
+ ### Supported Versions
97
+
98
+ Bug fixes are provided until 3 months after the next major release. Security
99
+ fixes are provided for 2 years after the initial release.
100
+
101
+ | Version | Node | Release | Bug Fixes Until | Security Fixes Until |
102
+ | :------ | :--- | :------------- | :-------------- | :------------------- |
103
+ | 26 | 22+ | March 31, 2026 | Q2 2027 | Q1 2028 |
104
+
105
+ ## Contributing
106
+
107
+ See [`CONTRIBUTING.md`][contributing url] for the submission process and
108
+ [`VOCABULARY.md`][vocabulary url] for the terminology used across Valkyrja.
109
+
110
+ ## Security Issues
111
+
112
+ If you discover a security vulnerability, please follow our
113
+ [disclosure procedure][security vulnerabilities url].
114
+
115
+ ## License
116
+
117
+ This package is open-source software licensed under the
118
+ [MIT license][MIT license url]. See [`LICENSE.md`](./LICENSE.md).
119
+
120
+ [Valkyrja url]: https://valkyrja.io
121
+ [copyright header url]: https://github.com/valkyrjaio/.github/blob/master/COPYRIGHT_HEADER.md
122
+ [vocabulary url]: https://github.com/valkyrjaio/.github/blob/master/VOCABULARY.md
123
+ [contributing url]: https://github.com/valkyrjaio/.github/blob/master/CONTRIBUTING.md
124
+ [security vulnerabilities url]: https://github.com/valkyrjaio/.github/blob/master/SECURITY.md
125
+ [Versioning and Release Process url]: https://github.com/valkyrjaio/architecture/blob/master/VERSIONING.md
126
+ [semantic versioning url]: https://semver.org/
127
+ [MIT license url]: https://opensource.org/licenses/MIT
@@ -0,0 +1,5 @@
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";
4
+ }
5
+ //# sourceMappingURL=EslintInfo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EslintInfo.d.ts","sourceRoot":"","sources":["../../../src/Eslint/Constant/EslintInfo.ts"],"names":[],"mappings":"AAQA,qBAAa,UAAU;IACnB,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAG,QAAQ,CAAU;IAC5C,MAAM,CAAC,QAAQ,CAAC,uBAAuB,EAAG,4BAA4B,CAAU;CACnF"}
@@ -0,0 +1,12 @@
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
+ export class EslintInfo {
9
+ static VERSION = '26.0.0';
10
+ static VERSION_BUILD_DATE_TIME = 'August 2 2026 20:32:26 MST';
11
+ }
12
+ //# sourceMappingURL=EslintInfo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EslintInfo.js","sourceRoot":"","sources":["../../../src/Eslint/Constant/EslintInfo.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,OAAO,UAAU;IACnB,MAAM,CAAU,OAAO,GAAG,QAAiB,CAAC;IAC5C,MAAM,CAAU,uBAAuB,GAAG,4BAAqC,CAAC"}
@@ -0,0 +1,35 @@
1
+ import type { Rule } from 'eslint';
2
+ /**
3
+ * Builds the copyright header, and the ESLint rule that enforces it.
4
+ *
5
+ * Every Valkyrja TypeScript repository takes its header from this factory and supplies only its
6
+ * own package identifier, so the text lives in one place.
7
+ */
8
+ export declare class CopyrightHeaderFactory {
9
+ /**
10
+ * Gets the text that sits between the two block comment delimiters.
11
+ *
12
+ * The rule compares the first comment of a file against this, so a file whose header body is
13
+ * wrong fails too, and not only a file that carries no header at all.
14
+ */
15
+ static getComment(packageName: string): string;
16
+ /**
17
+ * Gets the full header block, and the empty line that separates it from the code below it.
18
+ */
19
+ static getHeader(packageName: string): string;
20
+ /**
21
+ * Gets the ESLint rule that requires the header at the top of each linted file.
22
+ */
23
+ static getRule(packageName: string): Rule.RuleModule;
24
+ /**
25
+ * Rejects a value that names no package.
26
+ *
27
+ * Warning: the rule rewrites the header of every file it reads, and it then reports the file
28
+ * as correct, because the file and the configuration agree. A wrong package name therefore
29
+ * corrupts a repository behind a green gate, so this guard runs before any text is built.
30
+ * The same defect landed in the PHP port, where a caller passed the assembled header to an
31
+ * argument that had become the package name.
32
+ */
33
+ private static validatePackageName;
34
+ }
35
+ //# sourceMappingURL=CopyrightHeaderFactory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CopyrightHeaderFactory.d.ts","sourceRoot":"","sources":["../../../src/Eslint/Factory/CopyrightHeaderFactory.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AA2BnC;;;;;GAKG;AACH,qBAAa,sBAAsB;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAU9C;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAI7C;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC,UAAU;IAiDpD;;;;;;;;OAQG;IACH,OAAO,CAAC,MAAM,CAAC,mBAAmB;CAkBrC"}
@@ -0,0 +1,122 @@
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 { EslintInvalidPackageNameException } from "../Throwable/Exception/EslintInvalidPackageNameException.js";
9
+ /**
10
+ * The header body, one entry per line, with `{package}` where the package identifier goes.
11
+ *
12
+ * COPYRIGHT_HEADER.md in the `.github` repository holds the same text, and it maps every
13
+ * repository to its own identifier. An empty entry renders as a bare ` *` line.
14
+ */
15
+ const LINES = [
16
+ 'This file is part of the {package} package.',
17
+ '',
18
+ 'Copyright (c) 2016-present Melech Mizrachi',
19
+ '',
20
+ 'Released under the MIT License. See LICENSE.md for details.',
21
+ ];
22
+ /**
23
+ * The sentence that opens the header.
24
+ *
25
+ * A caller that passes a whole assembled header where a package identifier belongs builds
26
+ * `This file is part of the ⟨whole header⟩ package.`, so the factory rejects a name that
27
+ * carries this sentence. See `validatePackageName`.
28
+ */
29
+ const OPENING_SENTENCE = 'This file is part of the';
30
+ /**
31
+ * Builds the copyright header, and the ESLint rule that enforces it.
32
+ *
33
+ * Every Valkyrja TypeScript repository takes its header from this factory and supplies only its
34
+ * own package identifier, so the text lives in one place.
35
+ */
36
+ export class CopyrightHeaderFactory {
37
+ /**
38
+ * Gets the text that sits between the two block comment delimiters.
39
+ *
40
+ * The rule compares the first comment of a file against this, so a file whose header body is
41
+ * wrong fails too, and not only a file that carries no header at all.
42
+ */
43
+ static getComment(packageName) {
44
+ CopyrightHeaderFactory.validatePackageName(packageName);
45
+ const body = LINES.map((line) => (line === '' ? ' *' : ` * ${line.replace('{package}', packageName)}`)).join('\n');
46
+ return `\n${body}\n `;
47
+ }
48
+ /**
49
+ * Gets the full header block, and the empty line that separates it from the code below it.
50
+ */
51
+ static getHeader(packageName) {
52
+ return `/*${CopyrightHeaderFactory.getComment(packageName)}*/\n\n`;
53
+ }
54
+ /**
55
+ * Gets the ESLint rule that requires the header at the top of each linted file.
56
+ */
57
+ static getRule(packageName) {
58
+ const header = CopyrightHeaderFactory.getHeader(packageName);
59
+ const comment = CopyrightHeaderFactory.getComment(packageName);
60
+ return {
61
+ meta: {
62
+ type: 'layout',
63
+ fixable: 'code',
64
+ schema: [],
65
+ messages: {
66
+ missing: 'Missing copyright header. Add the standard block comment at the top of the file.',
67
+ incorrect: 'Incorrect copyright header. The block comment must match the standard header exactly.',
68
+ },
69
+ },
70
+ create(context) {
71
+ return {
72
+ Program(node) {
73
+ // `getAllComments` returns the comments in source order, so the first block
74
+ // comment is the one that a header occupies.
75
+ const first = context.sourceCode.getAllComments().find((c) => c.type === 'Block');
76
+ const loc = first?.loc;
77
+ if (first === undefined || loc == null || loc.start.line !== 1) {
78
+ context.report({
79
+ node,
80
+ messageId: 'missing',
81
+ fix: (fixer) => fixer.insertTextBefore(node, header),
82
+ });
83
+ return;
84
+ }
85
+ if (first.value === comment) {
86
+ return;
87
+ }
88
+ // The file carries a header whose text differs. Replace that comment. An
89
+ // insert would put a second header above the first one.
90
+ context.report({
91
+ loc,
92
+ messageId: 'incorrect',
93
+ fix: (fixer) => fixer.replaceText(first, header.trimEnd()),
94
+ });
95
+ },
96
+ };
97
+ },
98
+ };
99
+ }
100
+ /**
101
+ * Rejects a value that names no package.
102
+ *
103
+ * Warning: the rule rewrites the header of every file it reads, and it then reports the file
104
+ * as correct, because the file and the configuration agree. A wrong package name therefore
105
+ * corrupts a repository behind a green gate, so this guard runs before any text is built.
106
+ * The same defect landed in the PHP port, where a caller passed the assembled header to an
107
+ * argument that had become the package name.
108
+ */
109
+ static validatePackageName(packageName) {
110
+ if (packageName.includes('\n') || packageName.includes('\r')) {
111
+ throw new EslintInvalidPackageNameException(`A package name is one line. Got ${JSON.stringify(packageName)}.`);
112
+ }
113
+ if (packageName.includes(OPENING_SENTENCE)) {
114
+ throw new EslintInvalidPackageNameException(`A package name is an identifier such as 'Valkyrja Framework', not a built header. ` +
115
+ `Got ${JSON.stringify(packageName)}.`);
116
+ }
117
+ if (packageName.trim() === '') {
118
+ throw new EslintInvalidPackageNameException('A package name holds at least one character.');
119
+ }
120
+ }
121
+ }
122
+ //# sourceMappingURL=CopyrightHeaderFactory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CopyrightHeaderFactory.js","sourceRoot":"","sources":["../../../src/Eslint/Factory/CopyrightHeaderFactory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The base for every exception this package throws for a bad argument.
3
+ *
4
+ * The chain roots in the native `Error` rather than in `ValkyrjaInvalidArgumentException`. An
5
+ * ESLint configuration is plain JavaScript that Node loads directly, so this package cannot
6
+ * depend on `@valkyrjaio/valkyrja`, which ships TypeScript source that Node does not compile.
7
+ */
8
+ export declare abstract class EslintInvalidArgumentException extends Error {
9
+ }
10
+ //# sourceMappingURL=EslintInvalidArgumentException.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EslintInvalidArgumentException.d.ts","sourceRoot":"","sources":["../../../../../src/Eslint/Throwable/Exception/Abstract/EslintInvalidArgumentException.ts"],"names":[],"mappings":"AAQA;;;;;;GAMG;AACH,8BAAsB,8BAA+B,SAAQ,KAAK;CAAG"}
@@ -0,0 +1,17 @@
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
+ * The base for every exception this package throws for a bad argument.
10
+ *
11
+ * The chain roots in the native `Error` rather than in `ValkyrjaInvalidArgumentException`. An
12
+ * ESLint configuration is plain JavaScript that Node loads directly, so this package cannot
13
+ * depend on `@valkyrjaio/valkyrja`, which ships TypeScript source that Node does not compile.
14
+ */
15
+ export class EslintInvalidArgumentException extends Error {
16
+ }
17
+ //# sourceMappingURL=EslintInvalidArgumentException.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EslintInvalidArgumentException.js","sourceRoot":"","sources":["../../../../../src/Eslint/Throwable/Exception/Abstract/EslintInvalidArgumentException.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;GAMG;AACH,MAAM,OAAgB,8BAA+B,SAAQ,KAAK;CAAG"}
@@ -0,0 +1,8 @@
1
+ import { EslintInvalidArgumentException } from './Abstract/EslintInvalidArgumentException.ts';
2
+ /**
3
+ * The package name that a caller gave does not name a package.
4
+ */
5
+ export declare class EslintInvalidPackageNameException extends EslintInvalidArgumentException {
6
+ constructor(message: string);
7
+ }
8
+ //# sourceMappingURL=EslintInvalidPackageNameException.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EslintInvalidPackageNameException.d.ts","sourceRoot":"","sources":["../../../../src/Eslint/Throwable/Exception/EslintInvalidPackageNameException.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,8BAA8B,EAAE,MAAM,8CAA8C,CAAC;AAE9F;;GAEG;AACH,qBAAa,iCAAkC,SAAQ,8BAA8B;gBACrE,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 package name that a caller gave does not name a package.
11
+ */
12
+ export class EslintInvalidPackageNameException extends EslintInvalidArgumentException {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = 'EslintInvalidPackageNameException';
16
+ }
17
+ }
18
+ //# sourceMappingURL=EslintInvalidPackageNameException.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EslintInvalidPackageNameException.js","sourceRoot":"","sources":["../../../../src/Eslint/Throwable/Exception/EslintInvalidPackageNameException.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,8BAA8B,EAAE,MAAM,8CAA8C,CAAC;AAE9F;;GAEG;AACH,MAAM,OAAO,iCAAkC,SAAQ,8BAA8B;IACjF,YAAY,OAAe;QACvB,KAAK,CAAC,OAAO,CAAC,CAAC;QAEf,IAAI,CAAC,IAAI,GAAG,mCAAmC,CAAC;IACpD,CAAC;CACJ"}
@@ -0,0 +1,5 @@
1
+ export { EslintInfo } from './Eslint/Constant/EslintInfo.ts';
2
+ export { CopyrightHeaderFactory } from './Eslint/Factory/CopyrightHeaderFactory.ts';
3
+ export { EslintInvalidArgumentException } from './Eslint/Throwable/Exception/Abstract/EslintInvalidArgumentException.ts';
4
+ export { EslintInvalidPackageNameException } from './Eslint/Throwable/Exception/EslintInvalidPackageNameException.ts';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
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
+ export { EslintInfo } from "./Eslint/Constant/EslintInfo.js";
9
+ export { CopyrightHeaderFactory } from "./Eslint/Factory/CopyrightHeaderFactory.js";
10
+ export { EslintInvalidArgumentException } from "./Eslint/Throwable/Exception/Abstract/EslintInvalidArgumentException.js";
11
+ export { EslintInvalidPackageNameException } from "./Eslint/Throwable/Exception/EslintInvalidPackageNameException.js";
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@valkyrjaio/ci-eslint",
3
+ "version": "26.0.0",
4
+ "description": "Shared ESLint configuration for Valkyrja TypeScript repositories",
5
+ "homepage": "https://www.valkyrja.io",
6
+ "keywords": [
7
+ "ci",
8
+ "eslint",
9
+ "valkyrja"
10
+ ],
11
+ "license": "MIT",
12
+ "author": {
13
+ "name": "Melech Mizrachi",
14
+ "email": "melechmizrachi@gmail.com"
15
+ },
16
+ "type": "module",
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.js",
20
+ "types": "./dist/index.d.ts"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src"
26
+ ],
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
30
+ "scripts": {
31
+ "build": "cd .github/ci/typescript && npm run build",
32
+ "prepublishOnly": "npm run build",
33
+ "typescript": "cd .github/ci/typescript && npm run check",
34
+ "typescript-outdated-check": "cd .github/ci/typescript && npm install && npm outdated",
35
+ "typescript-outdated-check-major-release": "cd .github/ci/typescript && npm install && npm outdated",
36
+ "typescript-npm-update": "cd .github/ci/typescript && npm update",
37
+ "eslint": "cd .github/ci/eslint && npm run check",
38
+ "eslint-outdated-check": "cd .github/ci/eslint && npm install && npm outdated",
39
+ "eslint-outdated-check-major-release": "cd .github/ci/eslint && npm install && npm outdated",
40
+ "eslint-npm-update": "cd .github/ci/eslint && npm update",
41
+ "prettier": "cd .github/ci/prettier && npm run fix",
42
+ "prettier-check": "cd .github/ci/prettier && npm run check",
43
+ "prettier-outdated-check": "cd .github/ci/prettier && npm install && npm outdated",
44
+ "prettier-outdated-check-major-release": "cd .github/ci/prettier && npm install && npm outdated",
45
+ "prettier-npm-update": "cd .github/ci/prettier && npm update",
46
+ "vitest": "cd .github/ci/vitest && npm run check",
47
+ "vitest-coverage": "cd .github/ci/vitest && npm run coverage",
48
+ "vitest-outdated-check": "cd .github/ci/vitest && npm install && npm outdated",
49
+ "vitest-outdated-check-major-release": "cd .github/ci/vitest && npm install && npm outdated",
50
+ "vitest-npm-update": "cd .github/ci/vitest && npm update",
51
+ "root-outdated-check": "npm install && npm outdated",
52
+ "root-outdated-check-major-release": "npm install && npm outdated",
53
+ "root-and-suggested-outdated-check": "cd .github/ci/root-and-suggested && npm install && npm outdated",
54
+ "root-and-suggested-outdated-check-major-release": "cd .github/ci/root-and-suggested && npm install && npm outdated",
55
+ "root-and-suggested-npm-update": "cd .github/ci/root-and-suggested && npm update"
56
+ },
57
+ "peerDependencies": {
58
+ "eslint": ">=9"
59
+ }
60
+ }
@@ -0,0 +1,12 @@
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
+ 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;
12
+ }
@@ -0,0 +1,145 @@
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 type { Rule } from 'eslint';
10
+
11
+ import { EslintInvalidPackageNameException } from '../Throwable/Exception/EslintInvalidPackageNameException.ts';
12
+
13
+ /**
14
+ * The header body, one entry per line, with `{package}` where the package identifier goes.
15
+ *
16
+ * COPYRIGHT_HEADER.md in the `.github` repository holds the same text, and it maps every
17
+ * repository to its own identifier. An empty entry renders as a bare ` *` line.
18
+ */
19
+ const LINES = [
20
+ 'This file is part of the {package} package.',
21
+ '',
22
+ 'Copyright (c) 2016-present Melech Mizrachi',
23
+ '',
24
+ 'Released under the MIT License. See LICENSE.md for details.',
25
+ ] as const;
26
+
27
+ /**
28
+ * The sentence that opens the header.
29
+ *
30
+ * A caller that passes a whole assembled header where a package identifier belongs builds
31
+ * `This file is part of the ⟨whole header⟩ package.`, so the factory rejects a name that
32
+ * carries this sentence. See `validatePackageName`.
33
+ */
34
+ const OPENING_SENTENCE = 'This file is part of the';
35
+
36
+ /**
37
+ * Builds the copyright header, and the ESLint rule that enforces it.
38
+ *
39
+ * Every Valkyrja TypeScript repository takes its header from this factory and supplies only its
40
+ * own package identifier, so the text lives in one place.
41
+ */
42
+ export class CopyrightHeaderFactory {
43
+ /**
44
+ * Gets the text that sits between the two block comment delimiters.
45
+ *
46
+ * The rule compares the first comment of a file against this, so a file whose header body is
47
+ * wrong fails too, and not only a file that carries no header at all.
48
+ */
49
+ static getComment(packageName: string): string {
50
+ CopyrightHeaderFactory.validatePackageName(packageName);
51
+
52
+ const body = LINES.map((line) => (line === '' ? ' *' : ` * ${line.replace('{package}', packageName)}`)).join(
53
+ '\n',
54
+ );
55
+
56
+ return `\n${body}\n `;
57
+ }
58
+
59
+ /**
60
+ * Gets the full header block, and the empty line that separates it from the code below it.
61
+ */
62
+ static getHeader(packageName: string): string {
63
+ return `/*${CopyrightHeaderFactory.getComment(packageName)}*/\n\n`;
64
+ }
65
+
66
+ /**
67
+ * Gets the ESLint rule that requires the header at the top of each linted file.
68
+ */
69
+ static getRule(packageName: string): Rule.RuleModule {
70
+ const header = CopyrightHeaderFactory.getHeader(packageName);
71
+ const comment = CopyrightHeaderFactory.getComment(packageName);
72
+
73
+ return {
74
+ meta: {
75
+ type: 'layout',
76
+ fixable: 'code',
77
+ schema: [],
78
+ messages: {
79
+ missing: 'Missing copyright header. Add the standard block comment at the top of the file.',
80
+ incorrect: 'Incorrect copyright header. The block comment must match the standard header exactly.',
81
+ },
82
+ },
83
+ create(context: Rule.RuleContext): Rule.RuleListener {
84
+ return {
85
+ Program(node): void {
86
+ // `getAllComments` returns the comments in source order, so the first block
87
+ // comment is the one that a header occupies.
88
+ const first = context.sourceCode.getAllComments().find((c) => c.type === 'Block');
89
+ const loc = first?.loc;
90
+
91
+ if (first === undefined || loc == null || loc.start.line !== 1) {
92
+ context.report({
93
+ node,
94
+ messageId: 'missing',
95
+ fix: (fixer) => fixer.insertTextBefore(node, header),
96
+ });
97
+
98
+ return;
99
+ }
100
+
101
+ if (first.value === comment) {
102
+ return;
103
+ }
104
+
105
+ // The file carries a header whose text differs. Replace that comment. An
106
+ // insert would put a second header above the first one.
107
+ context.report({
108
+ loc,
109
+ messageId: 'incorrect',
110
+ fix: (fixer) => fixer.replaceText(first, header.trimEnd()),
111
+ });
112
+ },
113
+ };
114
+ },
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Rejects a value that names no package.
120
+ *
121
+ * Warning: the rule rewrites the header of every file it reads, and it then reports the file
122
+ * as correct, because the file and the configuration agree. A wrong package name therefore
123
+ * corrupts a repository behind a green gate, so this guard runs before any text is built.
124
+ * The same defect landed in the PHP port, where a caller passed the assembled header to an
125
+ * argument that had become the package name.
126
+ */
127
+ private static validatePackageName(packageName: string): void {
128
+ if (packageName.includes('\n') || packageName.includes('\r')) {
129
+ throw new EslintInvalidPackageNameException(
130
+ `A package name is one line. Got ${JSON.stringify(packageName)}.`,
131
+ );
132
+ }
133
+
134
+ if (packageName.includes(OPENING_SENTENCE)) {
135
+ throw new EslintInvalidPackageNameException(
136
+ `A package name is an identifier such as 'Valkyrja Framework', not a built header. ` +
137
+ `Got ${JSON.stringify(packageName)}.`,
138
+ );
139
+ }
140
+
141
+ if (packageName.trim() === '') {
142
+ throw new EslintInvalidPackageNameException('A package name holds at least one character.');
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,16 @@
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
+ /**
10
+ * The base for every exception this package throws for a bad argument.
11
+ *
12
+ * The chain roots in the native `Error` rather than in `ValkyrjaInvalidArgumentException`. An
13
+ * ESLint configuration is plain JavaScript that Node loads directly, so this package cannot
14
+ * depend on `@valkyrjaio/valkyrja`, which ships TypeScript source that Node does not compile.
15
+ */
16
+ export abstract class EslintInvalidArgumentException extends Error {}
@@ -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 package name that a caller gave does not name a package.
13
+ */
14
+ export class EslintInvalidPackageNameException extends EslintInvalidArgumentException {
15
+ constructor(message: string) {
16
+ super(message);
17
+
18
+ this.name = 'EslintInvalidPackageNameException';
19
+ }
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
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
+ export { EslintInfo } from './Eslint/Constant/EslintInfo.ts';
10
+ export { CopyrightHeaderFactory } from './Eslint/Factory/CopyrightHeaderFactory.ts';
11
+ export { EslintInvalidArgumentException } from './Eslint/Throwable/Exception/Abstract/EslintInvalidArgumentException.ts';
12
+ export { EslintInvalidPackageNameException } from './Eslint/Throwable/Exception/EslintInvalidPackageNameException.ts';