eslint-plugin-comment-policy 0.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/AGENTS.md +36 -0
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/README.ru.md +128 -0
- package/dist/index.cjs +446 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +446 -0
- package/package.json +71 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# AGENTS.md — adopting eslint-plugin-comment-policy
|
|
2
|
+
|
|
3
|
+
Five ESLint 9 (flat-config) rules enforcing a comment policy, namespace
|
|
4
|
+
`comment-policy/`:
|
|
5
|
+
|
|
6
|
+
- `comment-policy/max-comment-lines` — prose-line cap per comment block
|
|
7
|
+
(default `max` 4, `anchoredMax` 3).
|
|
8
|
+
- `comment-policy/no-comment-narrative` — change-narrative / history prose.
|
|
9
|
+
- `comment-policy/no-comment-code-snippet` — code snippet inside a comment.
|
|
10
|
+
- `comment-policy/no-decorative-comment` — decorative / section markers.
|
|
11
|
+
- `comment-policy/no-line-comment` — `//` forbidden, `/* */` required.
|
|
12
|
+
|
|
13
|
+
## Adopt in a repo
|
|
14
|
+
|
|
15
|
+
1. `npm install --save-dev eslint-plugin-comment-policy`.
|
|
16
|
+
2. In `eslint.config.mjs`, use a bundled config or register rules explicitly:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import commentPolicy from "eslint-plugin-comment-policy";
|
|
20
|
+
export default [commentPolicy.configs.recommended];
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`recommended` turns **all five rules on at `error`** with defaults. `sdd`
|
|
24
|
+
adds default `protectedPatterns` for spec-driven projects.
|
|
25
|
+
|
|
26
|
+
## Rule behavior an agent must know
|
|
27
|
+
|
|
28
|
+
- A **comment block** is a run of consecutive full-line `//` separated only by
|
|
29
|
+
whitespace (no blank line); they are linted and fixed as one unit.
|
|
30
|
+
- **Protected** blocks (matching `protectedPatterns`) get the lower cap in
|
|
31
|
+
`max-comment-lines` and are exempt from narrative / snippet / decorative
|
|
32
|
+
checks. Marker/anchor-only lines never count as prose.
|
|
33
|
+
- Fixable: `no-comment-code-snippet` (delete only when the block is entirely
|
|
34
|
+
code), `no-decorative-comment` (drop the line), `no-line-comment` (convert and
|
|
35
|
+
merge `//` into one `/* */`; skipped when the prose contains `*/`).
|
|
36
|
+
`max-comment-lines` and `no-comment-narrative` are judgment calls, so no fix.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cyberash
|
|
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
|
|
10
|
+
furnished 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 THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# eslint-plugin-comment-policy
|
|
2
|
+
|
|
3
|
+
[Русская версия](./README.ru.md)
|
|
4
|
+
|
|
5
|
+
ESLint 9 (flat-config) rules that enforce a **comment policy** in the editor and
|
|
6
|
+
in CI: cap prose, keep change-history out of comments, ban code snippets and
|
|
7
|
+
decorative markers, and require block comments. The policy shows up as you type
|
|
8
|
+
instead of only on a separate review pass.
|
|
9
|
+
|
|
10
|
+
Namespace: `comment-policy/`. Plugin scope (`meta.name`): `cyberash`.
|
|
11
|
+
|
|
12
|
+
## Rules
|
|
13
|
+
|
|
14
|
+
| Rule | What it flags | Fixable |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `comment-policy/max-comment-lines` | a comment block with more prose lines than the cap (lower cap for anchored blocks) | no |
|
|
17
|
+
| `comment-policy/no-comment-narrative` | change-narrative / history prose (`renamed from`, `previously`, `v1.2`, bare ISO dates, …) | no |
|
|
18
|
+
| `comment-policy/no-comment-code-snippet` | a code snippet (usage example) inside a comment | yes (only when the block is entirely code) |
|
|
19
|
+
| `comment-policy/no-decorative-comment` | decorative / section markers (`=====`, `#region`, `===text===`) | yes |
|
|
20
|
+
| `comment-policy/no-line-comment` | any `//` comment; requires `/* */` | yes (converts and merges runs of `//`) |
|
|
21
|
+
|
|
22
|
+
A **comment block** is a run of consecutive full-line `//` comments separated
|
|
23
|
+
only by whitespace (no blank line). A **prose line** is a comment line that
|
|
24
|
+
still has a real word (≥3 letters) after comment markers and protected markers
|
|
25
|
+
are stripped, so anchor/marker-only lines do not count toward the cap.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm install --save-dev eslint-plugin-comment-policy
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
`eslint.config.mjs` — bundled config:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import commentPolicy from "eslint-plugin-comment-policy";
|
|
39
|
+
|
|
40
|
+
export default [commentPolicy.configs.recommended];
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`recommended` turns all five rules on at `error` with defaults. There is also an
|
|
44
|
+
`sdd` config that ships default `protectedPatterns` for spec-driven projects
|
|
45
|
+
(anchors `partition:TYPE-NNN`, `@covers`, short ids, milestones):
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import commentPolicy from "eslint-plugin-comment-policy";
|
|
49
|
+
|
|
50
|
+
export default [commentPolicy.configs.sdd];
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or register the plugin and enable rules explicitly:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import commentPolicy from "eslint-plugin-comment-policy";
|
|
57
|
+
|
|
58
|
+
export default [
|
|
59
|
+
{
|
|
60
|
+
plugins: { "comment-policy": commentPolicy },
|
|
61
|
+
rules: {
|
|
62
|
+
"comment-policy/max-comment-lines": ["error", { max: 4, anchoredMax: 3 }],
|
|
63
|
+
"comment-policy/no-comment-narrative": "error",
|
|
64
|
+
"comment-policy/no-comment-code-snippet": "error",
|
|
65
|
+
"comment-policy/no-decorative-comment": "error",
|
|
66
|
+
"comment-policy/no-line-comment": "error",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Options
|
|
73
|
+
|
|
74
|
+
### `protectedPatterns`
|
|
75
|
+
|
|
76
|
+
Shared across `max-comment-lines`, `no-comment-narrative`,
|
|
77
|
+
`no-comment-code-snippet`, `no-decorative-comment`, and `no-line-comment`. An
|
|
78
|
+
array of regular-expression **source strings**. A block that matches any pattern
|
|
79
|
+
is "protected": it gets the lower cap in `max-comment-lines` and is exempt from
|
|
80
|
+
`no-comment-narrative` / `no-comment-code-snippet` / `no-decorative-comment`.
|
|
81
|
+
|
|
82
|
+
Order matters: put the longer/more specific pattern first so a marker is fully
|
|
83
|
+
stripped before a shorter pattern that is its suffix.
|
|
84
|
+
|
|
85
|
+
### `max-comment-lines`
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
["error", { max: 4, anchoredMax: 3, protectedPatterns: [] }]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `max` (default `4`) — prose-line cap for an ordinary block.
|
|
92
|
+
- `anchoredMax` (default `3`) — prose-line cap for a protected (anchored) block.
|
|
93
|
+
|
|
94
|
+
### `no-comment-narrative`
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
["error", { protectedPatterns: [], extraPatterns: [] }]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- `extraPatterns` — extra narrative patterns (source strings) added to the
|
|
101
|
+
built-in set.
|
|
102
|
+
|
|
103
|
+
### `no-comment-code-snippet`, `no-decorative-comment`, `no-line-comment`
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
["error", { protectedPatterns: [] }]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`no-comment-code-snippet` auto-deletes a block only when it is entirely code
|
|
110
|
+
(every non-empty line is code-ish) and occupies whole lines. `no-line-comment`
|
|
111
|
+
converts a run of `//` into one `/* */` block; it leaves a comment untouched
|
|
112
|
+
when its prose contains `*/` (which would terminate the block early).
|
|
113
|
+
|
|
114
|
+
## Notes
|
|
115
|
+
|
|
116
|
+
- `no-decorative-comment` detects markers by content in both `//` and `/* */`
|
|
117
|
+
comment forms.
|
|
118
|
+
- ESLint applies non-overlapping fixes per pass and re-lints, so a block that is
|
|
119
|
+
both a code snippet and a line comment is resolved across passes.
|
|
120
|
+
|
|
121
|
+
## Develop
|
|
122
|
+
|
|
123
|
+
- `npm run build` — tsdown, dual ESM + CJS + `.d.ts` into `dist/` (git-ignored).
|
|
124
|
+
- `npm test` — `RuleTester` suites via `tsx` (no build needed).
|
|
125
|
+
- `npm run typecheck` — `tsc --noEmit`.
|
|
126
|
+
- `npm run lint` — dogfoods `typescript-eslint` on the source.
|
package/README.ru.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# eslint-plugin-comment-policy
|
|
2
|
+
|
|
3
|
+
[English version](./README.md)
|
|
4
|
+
|
|
5
|
+
Правила ESLint 9 (flat-config), которые применяют **политику комментариев** в
|
|
6
|
+
редакторе и в CI: ограничивают прозу, не пускают историю изменений в
|
|
7
|
+
комментарии, запрещают код-сниппеты и декоративные маркеры, требуют блочные
|
|
8
|
+
комментарии. Политика видна по мере набора, а не только при отдельном прогоне.
|
|
9
|
+
|
|
10
|
+
Неймспейс: `comment-policy/`. Scope плагина (`meta.name`): `cyberash`.
|
|
11
|
+
|
|
12
|
+
## Правила
|
|
13
|
+
|
|
14
|
+
| Правило | Что ловит | Автофикс |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `comment-policy/max-comment-lines` | блок комментария, где прозаических строк больше капа (для анкорных блоков кап ниже) | нет |
|
|
17
|
+
| `comment-policy/no-comment-narrative` | change-narrative / историю (`renamed from`, `previously`, `v1.2`, голые ISO-даты, …) | нет |
|
|
18
|
+
| `comment-policy/no-comment-code-snippet` | код-сниппет (пример использования) внутри комментария | да (только если блок целиком код) |
|
|
19
|
+
| `comment-policy/no-decorative-comment` | декоративные / секционные маркеры (`=====`, `#region`, `===text===`) | да |
|
|
20
|
+
| `comment-policy/no-line-comment` | любой `//`; требует `/* */` | да (конвертация и склейка подряд идущих `//`) |
|
|
21
|
+
|
|
22
|
+
**Блок комментария** — подряд идущие full-line `//`, разделённые только
|
|
23
|
+
пробелами (без пустой строки). **Прозаическая строка** — строка комментария, в
|
|
24
|
+
которой после снятия маркеров комментария и protected-маркеров остаётся реальное
|
|
25
|
+
слово (≥3 букв); поэтому чисто анкорные/маркерные строки в кап не идут.
|
|
26
|
+
|
|
27
|
+
## Установка
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm install --save-dev eslint-plugin-comment-policy
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Использование
|
|
34
|
+
|
|
35
|
+
`eslint.config.mjs` — готовый конфиг:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import commentPolicy from "eslint-plugin-comment-policy";
|
|
39
|
+
|
|
40
|
+
export default [commentPolicy.configs.recommended];
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`recommended` включает все пять правил на `error` с дефолтами. Есть также конфиг
|
|
44
|
+
`sdd` с дефолтными `protectedPatterns` для spec-driven проектов (анкоры
|
|
45
|
+
`partition:TYPE-NNN`, `@covers`, short-id, milestone):
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import commentPolicy from "eslint-plugin-comment-policy";
|
|
49
|
+
|
|
50
|
+
export default [commentPolicy.configs.sdd];
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Либо подключить плагин и включить правила вручную:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import commentPolicy from "eslint-plugin-comment-policy";
|
|
57
|
+
|
|
58
|
+
export default [
|
|
59
|
+
{
|
|
60
|
+
plugins: { "comment-policy": commentPolicy },
|
|
61
|
+
rules: {
|
|
62
|
+
"comment-policy/max-comment-lines": ["error", { max: 4, anchoredMax: 3 }],
|
|
63
|
+
"comment-policy/no-comment-narrative": "error",
|
|
64
|
+
"comment-policy/no-comment-code-snippet": "error",
|
|
65
|
+
"comment-policy/no-decorative-comment": "error",
|
|
66
|
+
"comment-policy/no-line-comment": "error",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Опции
|
|
73
|
+
|
|
74
|
+
### `protectedPatterns`
|
|
75
|
+
|
|
76
|
+
Общая для `max-comment-lines`, `no-comment-narrative`,
|
|
77
|
+
`no-comment-code-snippet`, `no-decorative-comment` и `no-line-comment`. Массив
|
|
78
|
+
**строк-исходников** регулярных выражений. Блок, совпавший с любым паттерном,
|
|
79
|
+
считается «защищённым»: получает пониженный кап в `max-comment-lines` и
|
|
80
|
+
исключается из `no-comment-narrative` / `no-comment-code-snippet` /
|
|
81
|
+
`no-decorative-comment`.
|
|
82
|
+
|
|
83
|
+
Порядок важен: более длинный/специфичный паттерн ставьте раньше, чтобы маркер
|
|
84
|
+
снимался целиком до более короткого паттерна, который является его суффиксом.
|
|
85
|
+
|
|
86
|
+
### `max-comment-lines`
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
["error", { max: 4, anchoredMax: 3, protectedPatterns: [] }]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- `max` (деф. `4`) — кап прозаических строк для обычного блока.
|
|
93
|
+
- `anchoredMax` (деф. `3`) — кап для защищённого (анкорного) блока.
|
|
94
|
+
|
|
95
|
+
### `no-comment-narrative`
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
["error", { protectedPatterns: [], extraPatterns: [] }]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- `extraPatterns` — дополнительные narrative-паттерны (строки-исходники) к
|
|
102
|
+
встроенному набору.
|
|
103
|
+
|
|
104
|
+
### `no-comment-code-snippet`, `no-decorative-comment`, `no-line-comment`
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
["error", { protectedPatterns: [] }]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`no-comment-code-snippet` удаляет блок автофиксом только если он целиком код
|
|
111
|
+
(каждая непустая строка код-подобна) и занимает целые строки. `no-line-comment`
|
|
112
|
+
конвертирует подряд идущие `//` в один блок `/* */`; комментарий остаётся
|
|
113
|
+
нетронутым, если его проза содержит `*/` (это преждевременно закрыло бы блок).
|
|
114
|
+
|
|
115
|
+
## Замечания
|
|
116
|
+
|
|
117
|
+
- `no-decorative-comment` детектит маркеры по содержимому в обеих формах
|
|
118
|
+
(`//` и `/* */`).
|
|
119
|
+
- ESLint применяет непересекающиеся фиксы за проход и перезапускает линт, поэтому
|
|
120
|
+
блок, который одновременно код-сниппет и line-комментарий, разрешается за
|
|
121
|
+
несколько проходов.
|
|
122
|
+
|
|
123
|
+
## Разработка
|
|
124
|
+
|
|
125
|
+
- `npm run build` — tsdown, dual ESM + CJS + `.d.ts` в `dist/` (в gitignore).
|
|
126
|
+
- `npm test` — наборы `RuleTester` через `tsx` (сборка не нужна).
|
|
127
|
+
- `npm run typecheck` — `tsc --noEmit`.
|
|
128
|
+
- `npm run lint` — self-lint `typescript-eslint` по исходникам.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
//#region src/lib/protected.ts
|
|
2
|
+
function compileProtected(patterns) {
|
|
3
|
+
return {
|
|
4
|
+
detect: patterns.map((source) => new RegExp(source)),
|
|
5
|
+
strip: patterns.map((source) => new RegExp(source, "g"))
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function hasProtectedToken(text, detect) {
|
|
9
|
+
return detect.some((re) => re.test(text));
|
|
10
|
+
}
|
|
11
|
+
function strippedLine(rawLine) {
|
|
12
|
+
return rawLine.replace(/^\s*\/\//, "").replace(/^\s*\/\*+/, "").replace(/\*+\/\s*$/, "").replace(/^\s*\*/, "").trim();
|
|
13
|
+
}
|
|
14
|
+
function isProseLine(rawLine, strip) {
|
|
15
|
+
let content = strippedLine(rawLine);
|
|
16
|
+
for (const re of strip) content = content.replace(re, " ");
|
|
17
|
+
return /[A-Za-z]{3,}/.test(content);
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/lib/comment-blocks.ts
|
|
21
|
+
function lineStarts(text) {
|
|
22
|
+
const starts = [0];
|
|
23
|
+
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);
|
|
24
|
+
return starts;
|
|
25
|
+
}
|
|
26
|
+
function makeLineIndex(text) {
|
|
27
|
+
const starts = lineStarts(text);
|
|
28
|
+
return {
|
|
29
|
+
lineStart: (lineNo) => starts[lineNo - 1],
|
|
30
|
+
lineEnd: (lineNo) => lineNo < starts.length ? starts[lineNo] : text.length
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function onlyWhitespaceNoBlank(between) {
|
|
34
|
+
return /^[ \t]*\r?\n[ \t]*$/.test(between);
|
|
35
|
+
}
|
|
36
|
+
function commentBlocks(sourceCode, detect) {
|
|
37
|
+
const text = sourceCode.text;
|
|
38
|
+
const isFullLine = (c) => {
|
|
39
|
+
const lineText = sourceCode.lines[c.loc.start.line - 1] ?? "";
|
|
40
|
+
return /^\s*$/.test(lineText.slice(0, c.loc.start.column));
|
|
41
|
+
};
|
|
42
|
+
const blocks = [];
|
|
43
|
+
let cur = null;
|
|
44
|
+
for (const c of sourceCode.getAllComments()) {
|
|
45
|
+
const kind = c.type === "Line" ? "line" : "block";
|
|
46
|
+
const fullLine = isFullLine(c);
|
|
47
|
+
if (cur !== null && cur.kind === "line" && kind === "line" && cur.fullLine && fullLine && onlyWhitespaceNoBlank(text.slice(cur.end, c.range[0])) && cur) {
|
|
48
|
+
cur.end = c.range[1];
|
|
49
|
+
cur.comments.push(c);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (cur) blocks.push(finalize(cur, text, detect));
|
|
53
|
+
cur = {
|
|
54
|
+
kind,
|
|
55
|
+
start: c.range[0],
|
|
56
|
+
end: c.range[1],
|
|
57
|
+
fullLine,
|
|
58
|
+
comments: [c]
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (cur) blocks.push(finalize(cur, text, detect));
|
|
62
|
+
return blocks;
|
|
63
|
+
}
|
|
64
|
+
function finalize(b, text, detect) {
|
|
65
|
+
const raw = text.slice(b.start, b.end);
|
|
66
|
+
const startLine = b.comments[0].loc.start.line;
|
|
67
|
+
const endLine = b.comments[b.comments.length - 1].loc.end.line;
|
|
68
|
+
return {
|
|
69
|
+
kind: b.kind,
|
|
70
|
+
start: b.start,
|
|
71
|
+
end: b.end,
|
|
72
|
+
fullLine: b.fullLine,
|
|
73
|
+
raw,
|
|
74
|
+
startLine,
|
|
75
|
+
endLine,
|
|
76
|
+
lineCount: endLine - startLine + 1,
|
|
77
|
+
hasProtected: hasProtectedToken(raw, detect),
|
|
78
|
+
comments: b.comments
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function blockLoc(block) {
|
|
82
|
+
return {
|
|
83
|
+
start: block.comments[0].loc.start,
|
|
84
|
+
end: block.comments[block.comments.length - 1].loc.end
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/rules/max-comment-lines.ts
|
|
89
|
+
const DEFAULT_MAX = 4;
|
|
90
|
+
const DEFAULT_ANCHORED_MAX = 3;
|
|
91
|
+
const rule$4 = {
|
|
92
|
+
defaultOptions: [{}],
|
|
93
|
+
meta: {
|
|
94
|
+
type: "suggestion",
|
|
95
|
+
docs: {
|
|
96
|
+
description: "Cap the number of prose lines in a comment block (anchored blocks get a lower cap).",
|
|
97
|
+
url: "https://github.com/cyberash-dev/eslint-plugin-comment-policy#max-comment-lines"
|
|
98
|
+
},
|
|
99
|
+
schema: [{
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
max: {
|
|
103
|
+
type: "integer",
|
|
104
|
+
minimum: 0
|
|
105
|
+
},
|
|
106
|
+
anchoredMax: {
|
|
107
|
+
type: "integer",
|
|
108
|
+
minimum: 0
|
|
109
|
+
},
|
|
110
|
+
protectedPatterns: {
|
|
111
|
+
type: "array",
|
|
112
|
+
items: { type: "string" }
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
additionalProperties: false
|
|
116
|
+
}],
|
|
117
|
+
messages: {
|
|
118
|
+
tooManyProse: "comment block has {{count}} prose lines (> {{max}}); keep it to a short why or move it into a spec record",
|
|
119
|
+
tooManyProseAnchored: "anchored comment has {{count}} prose lines (> {{max}}); the rationale belongs in the spec record — keep the marker plus at most a one-line pointer"
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
create(context) {
|
|
123
|
+
const option = context.options[0] ?? {};
|
|
124
|
+
const max = option.max ?? DEFAULT_MAX;
|
|
125
|
+
const anchoredMax = option.anchoredMax ?? DEFAULT_ANCHORED_MAX;
|
|
126
|
+
const { detect, strip } = compileProtected(option.protectedPatterns ?? []);
|
|
127
|
+
const sourceCode = context.sourceCode;
|
|
128
|
+
return { "Program:exit"() {
|
|
129
|
+
for (const block of commentBlocks(sourceCode, detect)) {
|
|
130
|
+
const proseLineCount = block.raw.split("\n").filter((line) => isProseLine(line, strip)).length;
|
|
131
|
+
const cap = block.hasProtected ? anchoredMax : max;
|
|
132
|
+
if (proseLineCount > cap) context.report({
|
|
133
|
+
loc: blockLoc(block),
|
|
134
|
+
messageId: block.hasProtected ? "tooManyProseAnchored" : "tooManyProse",
|
|
135
|
+
data: {
|
|
136
|
+
count: proseLineCount,
|
|
137
|
+
max: cap
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} };
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/rules/no-comment-code-snippet.ts
|
|
146
|
+
const CODEISH = [
|
|
147
|
+
/^(?:import|export|const|let|var|function|class|return|await|async|if|for|while|switch)\b/,
|
|
148
|
+
/=>/,
|
|
149
|
+
/^[\w.$]+\([^)]*\)\s*;?$/,
|
|
150
|
+
/^[}{]/,
|
|
151
|
+
/;\s*$/
|
|
152
|
+
];
|
|
153
|
+
function isCodeish(content) {
|
|
154
|
+
return content.length > 0 && CODEISH.some((re) => re.test(content));
|
|
155
|
+
}
|
|
156
|
+
function snippetInfo(block) {
|
|
157
|
+
if (block.hasProtected) return {
|
|
158
|
+
isSnippet: false,
|
|
159
|
+
pure: false
|
|
160
|
+
};
|
|
161
|
+
const nonEmpty = block.raw.split("\n").map(strippedLine).filter((c) => c.length > 0);
|
|
162
|
+
const codeish = nonEmpty.filter(isCodeish);
|
|
163
|
+
return {
|
|
164
|
+
isSnippet: codeish.length >= 2,
|
|
165
|
+
pure: nonEmpty.length >= 2 && codeish.length === nonEmpty.length
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const rule$3 = {
|
|
169
|
+
defaultOptions: [{}],
|
|
170
|
+
meta: {
|
|
171
|
+
type: "suggestion",
|
|
172
|
+
fixable: "code",
|
|
173
|
+
docs: {
|
|
174
|
+
description: "Forbid code snippets (usage examples) inside comments; auto-removable only when the block is entirely code.",
|
|
175
|
+
url: "https://github.com/cyberash-dev/eslint-plugin-comment-policy#no-comment-code-snippet"
|
|
176
|
+
},
|
|
177
|
+
schema: [{
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: { protectedPatterns: {
|
|
180
|
+
type: "array",
|
|
181
|
+
items: { type: "string" }
|
|
182
|
+
} },
|
|
183
|
+
additionalProperties: false
|
|
184
|
+
}],
|
|
185
|
+
messages: { codeSnippet: "code snippet inside comment (usage example)" }
|
|
186
|
+
},
|
|
187
|
+
create(context) {
|
|
188
|
+
const { detect } = compileProtected((context.options[0] ?? {}).protectedPatterns ?? []);
|
|
189
|
+
const sourceCode = context.sourceCode;
|
|
190
|
+
const lineIndex = makeLineIndex(sourceCode.text);
|
|
191
|
+
return { "Program:exit"() {
|
|
192
|
+
for (const block of commentBlocks(sourceCode, detect)) {
|
|
193
|
+
const info = snippetInfo(block);
|
|
194
|
+
if (!info.isSnippet) continue;
|
|
195
|
+
const deletable = info.pure && block.fullLine;
|
|
196
|
+
context.report({
|
|
197
|
+
loc: blockLoc(block),
|
|
198
|
+
messageId: "codeSnippet",
|
|
199
|
+
fix: deletable ? (fixer) => fixer.removeRange([lineIndex.lineStart(block.startLine), lineIndex.lineEnd(block.endLine)]) : null
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
} };
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/rules/no-comment-narrative.ts
|
|
207
|
+
const NARRATIVE = [
|
|
208
|
+
/\brenamed?\s+from\b/i,
|
|
209
|
+
/\bformerly\b/i,
|
|
210
|
+
/\bpreviously\b/i,
|
|
211
|
+
/\bas before\b/i,
|
|
212
|
+
/\bused to\b/i,
|
|
213
|
+
/\badded\s+(?:for|to|because)\b/i,
|
|
214
|
+
/\bfix(?:es|ed)?\s+(?:bug|issue)\b/i,
|
|
215
|
+
/\bslice\s+\d+\b/i,
|
|
216
|
+
/\bv\d+\.\d+\b/i,
|
|
217
|
+
/\b\d{4}-\d{2}-\d{2}\b/
|
|
218
|
+
];
|
|
219
|
+
const rule$2 = {
|
|
220
|
+
defaultOptions: [{}],
|
|
221
|
+
meta: {
|
|
222
|
+
type: "suggestion",
|
|
223
|
+
docs: {
|
|
224
|
+
description: "Forbid change-narrative / history prose in comments (it belongs in the commit message or a spec record).",
|
|
225
|
+
url: "https://github.com/cyberash-dev/eslint-plugin-comment-policy#no-comment-narrative"
|
|
226
|
+
},
|
|
227
|
+
schema: [{
|
|
228
|
+
type: "object",
|
|
229
|
+
properties: {
|
|
230
|
+
protectedPatterns: {
|
|
231
|
+
type: "array",
|
|
232
|
+
items: { type: "string" }
|
|
233
|
+
},
|
|
234
|
+
extraPatterns: {
|
|
235
|
+
type: "array",
|
|
236
|
+
items: { type: "string" }
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
additionalProperties: false
|
|
240
|
+
}],
|
|
241
|
+
messages: { changeNarrative: "change-narrative / history prose (belongs in commit message or spec record)" }
|
|
242
|
+
},
|
|
243
|
+
create(context) {
|
|
244
|
+
const option = context.options[0] ?? {};
|
|
245
|
+
const { detect } = compileProtected(option.protectedPatterns ?? []);
|
|
246
|
+
const extra = (option.extraPatterns ?? []).map((source) => new RegExp(source));
|
|
247
|
+
const patterns = [...NARRATIVE, ...extra];
|
|
248
|
+
const sourceCode = context.sourceCode;
|
|
249
|
+
return { "Program:exit"() {
|
|
250
|
+
for (const block of commentBlocks(sourceCode, detect)) {
|
|
251
|
+
if (block.hasProtected) continue;
|
|
252
|
+
if (patterns.some((re) => re.test(block.raw))) context.report({
|
|
253
|
+
loc: blockLoc(block),
|
|
254
|
+
messageId: "changeNarrative"
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
} };
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/lib/decorative.ts
|
|
262
|
+
const DECORATIVE = [
|
|
263
|
+
/^[=*#_-]{3,}$/,
|
|
264
|
+
/^#?\s*(?:region|endregion)\b/i,
|
|
265
|
+
/^[=*#_-]{2,}.*[=*#_-]{2,}$/
|
|
266
|
+
];
|
|
267
|
+
function isDecorativeLine(content) {
|
|
268
|
+
return content.length > 0 && DECORATIVE.some((re) => re.test(content));
|
|
269
|
+
}
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/rules/no-decorative-comment.ts
|
|
272
|
+
const schema$1 = [{
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: { protectedPatterns: {
|
|
275
|
+
type: "array",
|
|
276
|
+
items: { type: "string" }
|
|
277
|
+
} },
|
|
278
|
+
additionalProperties: false
|
|
279
|
+
}];
|
|
280
|
+
function lineLoc(lineNo, text) {
|
|
281
|
+
return {
|
|
282
|
+
start: {
|
|
283
|
+
line: lineNo,
|
|
284
|
+
column: 0
|
|
285
|
+
},
|
|
286
|
+
end: {
|
|
287
|
+
line: lineNo,
|
|
288
|
+
column: text.length
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const rule$1 = {
|
|
293
|
+
defaultOptions: [{}],
|
|
294
|
+
meta: {
|
|
295
|
+
type: "suggestion",
|
|
296
|
+
fixable: "code",
|
|
297
|
+
docs: {
|
|
298
|
+
description: "Forbid decorative / section-marker comments (e.g. =====, #region, ===text===), in both // and /* */ forms.",
|
|
299
|
+
url: "https://github.com/cyberash-dev/eslint-plugin-comment-policy#no-decorative-comment"
|
|
300
|
+
},
|
|
301
|
+
schema: schema$1,
|
|
302
|
+
messages: { decorativeComment: "decorative / section-marker comment" }
|
|
303
|
+
},
|
|
304
|
+
create(context) {
|
|
305
|
+
const { detect } = compileProtected((context.options[0] ?? {}).protectedPatterns ?? []);
|
|
306
|
+
const sourceCode = context.sourceCode;
|
|
307
|
+
const lineIndex = makeLineIndex(sourceCode.text);
|
|
308
|
+
return { "Program:exit"() {
|
|
309
|
+
for (const block of commentBlocks(sourceCode, detect)) for (const comment of block.comments) {
|
|
310
|
+
const rawText = sourceCode.getText(comment);
|
|
311
|
+
if (comment.type === "Line") {
|
|
312
|
+
if (!isDecorativeLine(strippedLine(rawText)) || hasProtectedToken(rawText, detect)) continue;
|
|
313
|
+
const line = comment.loc.start.line;
|
|
314
|
+
context.report({
|
|
315
|
+
loc: comment.loc,
|
|
316
|
+
messageId: "decorativeComment",
|
|
317
|
+
fix: block.fullLine ? (fixer) => fixer.removeRange([lineIndex.lineStart(line), lineIndex.lineEnd(line)]) : null
|
|
318
|
+
});
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
rawText.split("\n").forEach((rawLine, i) => {
|
|
322
|
+
if (!isDecorativeLine(strippedLine(rawLine)) || hasProtectedToken(rawLine, detect)) return;
|
|
323
|
+
const line = comment.loc.start.line + i;
|
|
324
|
+
context.report({
|
|
325
|
+
loc: lineLoc(line, rawLine),
|
|
326
|
+
messageId: "decorativeComment"
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
} };
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/rules/no-line-comment.ts
|
|
335
|
+
const schema = [{
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: { protectedPatterns: {
|
|
338
|
+
type: "array",
|
|
339
|
+
items: { type: "string" }
|
|
340
|
+
} },
|
|
341
|
+
additionalProperties: false
|
|
342
|
+
}];
|
|
343
|
+
function buildConversion(block, text, detect, lineIndex) {
|
|
344
|
+
const indent = text.slice(lineIndex.lineStart(block.startLine), block.start);
|
|
345
|
+
const kept = [];
|
|
346
|
+
for (const rawLine of block.raw.split("\n")) {
|
|
347
|
+
const content = strippedLine(rawLine);
|
|
348
|
+
if (block.fullLine && isDecorativeLine(content) && !hasProtectedToken(rawLine, detect)) continue;
|
|
349
|
+
kept.push(content);
|
|
350
|
+
}
|
|
351
|
+
if (kept.some((c) => c.includes("*/"))) return null;
|
|
352
|
+
if (block.fullLine && kept.every((c) => c.length === 0)) return {
|
|
353
|
+
range: [lineIndex.lineStart(block.startLine), lineIndex.lineEnd(block.endLine)],
|
|
354
|
+
text: ""
|
|
355
|
+
};
|
|
356
|
+
const body = kept.length === 1 ? `/* ${kept[0]} */` : `/*\n${kept.map((c) => c ? `${indent} * ${c}` : `${indent} *`).join("\n")}\n${indent} */`;
|
|
357
|
+
return {
|
|
358
|
+
range: [block.start, block.end],
|
|
359
|
+
text: body
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const rule = {
|
|
363
|
+
defaultOptions: [{}],
|
|
364
|
+
meta: {
|
|
365
|
+
type: "suggestion",
|
|
366
|
+
fixable: "code",
|
|
367
|
+
docs: {
|
|
368
|
+
description: "Forbid line comments (`//`); require block `/* */` comments. Auto-fix converts and merges runs of `//`.",
|
|
369
|
+
url: "https://github.com/cyberash-dev/eslint-plugin-comment-policy#no-line-comment"
|
|
370
|
+
},
|
|
371
|
+
schema,
|
|
372
|
+
messages: { lineComment: "line comment; use a block /* */ comment" }
|
|
373
|
+
},
|
|
374
|
+
create(context) {
|
|
375
|
+
const { detect } = compileProtected((context.options[0] ?? {}).protectedPatterns ?? []);
|
|
376
|
+
const sourceCode = context.sourceCode;
|
|
377
|
+
const lineIndex = makeLineIndex(sourceCode.text);
|
|
378
|
+
return { "Program:exit"() {
|
|
379
|
+
for (const block of commentBlocks(sourceCode, detect)) {
|
|
380
|
+
if (block.kind !== "line") continue;
|
|
381
|
+
const conv = buildConversion(block, sourceCode.text, detect, lineIndex);
|
|
382
|
+
context.report({
|
|
383
|
+
loc: blockLoc(block),
|
|
384
|
+
messageId: "lineComment",
|
|
385
|
+
fix: conv ? (fixer) => fixer.replaceTextRange(conv.range, conv.text) : null
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
} };
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region src/lib/sdd-patterns.ts
|
|
393
|
+
const TYPES = "BL|SUR|CON|INV|POL|DEL|DLT|NFR|REQ|MIG|CST|SCN|LCN|GAR|EXT";
|
|
394
|
+
const SDD_PROTECTED_PATTERNS = [
|
|
395
|
+
"@covers\\s+\\S+(?:\\s+\\w+=\\S+)*",
|
|
396
|
+
`\\b[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?::[a-z0-9]+(?:-[a-z0-9]+)*)*:(?:${TYPES})-\\d+\\b`,
|
|
397
|
+
`\\b(?:${TYPES})-\\d+\\b`,
|
|
398
|
+
"\\bM\\d+[A-Z]+-\\d+\\b"
|
|
399
|
+
];
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/index.ts
|
|
402
|
+
const configs = {};
|
|
403
|
+
const plugin = {
|
|
404
|
+
meta: {
|
|
405
|
+
name: "cyberash",
|
|
406
|
+
version: "0.1.0"
|
|
407
|
+
},
|
|
408
|
+
rules: {
|
|
409
|
+
"max-comment-lines": rule$4,
|
|
410
|
+
"no-comment-narrative": rule$2,
|
|
411
|
+
"no-comment-code-snippet": rule$3,
|
|
412
|
+
"no-decorative-comment": rule$1,
|
|
413
|
+
"no-line-comment": rule
|
|
414
|
+
},
|
|
415
|
+
configs
|
|
416
|
+
};
|
|
417
|
+
configs.recommended = {
|
|
418
|
+
plugins: { "comment-policy": plugin },
|
|
419
|
+
rules: {
|
|
420
|
+
"comment-policy/max-comment-lines": ["error", {
|
|
421
|
+
max: 4,
|
|
422
|
+
anchoredMax: 3
|
|
423
|
+
}],
|
|
424
|
+
"comment-policy/no-comment-narrative": ["error"],
|
|
425
|
+
"comment-policy/no-comment-code-snippet": ["error"],
|
|
426
|
+
"comment-policy/no-decorative-comment": ["error"],
|
|
427
|
+
"comment-policy/no-line-comment": ["error"]
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
const sddProtected = [...SDD_PROTECTED_PATTERNS];
|
|
431
|
+
configs.sdd = {
|
|
432
|
+
plugins: { "comment-policy": plugin },
|
|
433
|
+
rules: {
|
|
434
|
+
"comment-policy/max-comment-lines": ["error", {
|
|
435
|
+
max: 4,
|
|
436
|
+
anchoredMax: 3,
|
|
437
|
+
protectedPatterns: sddProtected
|
|
438
|
+
}],
|
|
439
|
+
"comment-policy/no-comment-narrative": ["error", { protectedPatterns: sddProtected }],
|
|
440
|
+
"comment-policy/no-comment-code-snippet": ["error", { protectedPatterns: sddProtected }],
|
|
441
|
+
"comment-policy/no-decorative-comment": ["error", { protectedPatterns: sddProtected }],
|
|
442
|
+
"comment-policy/no-line-comment": ["error", { protectedPatterns: sddProtected }]
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
//#endregion
|
|
446
|
+
module.exports = plugin;
|
package/dist/index.d.cts
ADDED
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
//#region src/lib/protected.ts
|
|
2
|
+
function compileProtected(patterns) {
|
|
3
|
+
return {
|
|
4
|
+
detect: patterns.map((source) => new RegExp(source)),
|
|
5
|
+
strip: patterns.map((source) => new RegExp(source, "g"))
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function hasProtectedToken(text, detect) {
|
|
9
|
+
return detect.some((re) => re.test(text));
|
|
10
|
+
}
|
|
11
|
+
function strippedLine(rawLine) {
|
|
12
|
+
return rawLine.replace(/^\s*\/\//, "").replace(/^\s*\/\*+/, "").replace(/\*+\/\s*$/, "").replace(/^\s*\*/, "").trim();
|
|
13
|
+
}
|
|
14
|
+
function isProseLine(rawLine, strip) {
|
|
15
|
+
let content = strippedLine(rawLine);
|
|
16
|
+
for (const re of strip) content = content.replace(re, " ");
|
|
17
|
+
return /[A-Za-z]{3,}/.test(content);
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/lib/comment-blocks.ts
|
|
21
|
+
function lineStarts(text) {
|
|
22
|
+
const starts = [0];
|
|
23
|
+
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);
|
|
24
|
+
return starts;
|
|
25
|
+
}
|
|
26
|
+
function makeLineIndex(text) {
|
|
27
|
+
const starts = lineStarts(text);
|
|
28
|
+
return {
|
|
29
|
+
lineStart: (lineNo) => starts[lineNo - 1],
|
|
30
|
+
lineEnd: (lineNo) => lineNo < starts.length ? starts[lineNo] : text.length
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function onlyWhitespaceNoBlank(between) {
|
|
34
|
+
return /^[ \t]*\r?\n[ \t]*$/.test(between);
|
|
35
|
+
}
|
|
36
|
+
function commentBlocks(sourceCode, detect) {
|
|
37
|
+
const text = sourceCode.text;
|
|
38
|
+
const isFullLine = (c) => {
|
|
39
|
+
const lineText = sourceCode.lines[c.loc.start.line - 1] ?? "";
|
|
40
|
+
return /^\s*$/.test(lineText.slice(0, c.loc.start.column));
|
|
41
|
+
};
|
|
42
|
+
const blocks = [];
|
|
43
|
+
let cur = null;
|
|
44
|
+
for (const c of sourceCode.getAllComments()) {
|
|
45
|
+
const kind = c.type === "Line" ? "line" : "block";
|
|
46
|
+
const fullLine = isFullLine(c);
|
|
47
|
+
if (cur !== null && cur.kind === "line" && kind === "line" && cur.fullLine && fullLine && onlyWhitespaceNoBlank(text.slice(cur.end, c.range[0])) && cur) {
|
|
48
|
+
cur.end = c.range[1];
|
|
49
|
+
cur.comments.push(c);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (cur) blocks.push(finalize(cur, text, detect));
|
|
53
|
+
cur = {
|
|
54
|
+
kind,
|
|
55
|
+
start: c.range[0],
|
|
56
|
+
end: c.range[1],
|
|
57
|
+
fullLine,
|
|
58
|
+
comments: [c]
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (cur) blocks.push(finalize(cur, text, detect));
|
|
62
|
+
return blocks;
|
|
63
|
+
}
|
|
64
|
+
function finalize(b, text, detect) {
|
|
65
|
+
const raw = text.slice(b.start, b.end);
|
|
66
|
+
const startLine = b.comments[0].loc.start.line;
|
|
67
|
+
const endLine = b.comments[b.comments.length - 1].loc.end.line;
|
|
68
|
+
return {
|
|
69
|
+
kind: b.kind,
|
|
70
|
+
start: b.start,
|
|
71
|
+
end: b.end,
|
|
72
|
+
fullLine: b.fullLine,
|
|
73
|
+
raw,
|
|
74
|
+
startLine,
|
|
75
|
+
endLine,
|
|
76
|
+
lineCount: endLine - startLine + 1,
|
|
77
|
+
hasProtected: hasProtectedToken(raw, detect),
|
|
78
|
+
comments: b.comments
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function blockLoc(block) {
|
|
82
|
+
return {
|
|
83
|
+
start: block.comments[0].loc.start,
|
|
84
|
+
end: block.comments[block.comments.length - 1].loc.end
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/rules/max-comment-lines.ts
|
|
89
|
+
const DEFAULT_MAX = 4;
|
|
90
|
+
const DEFAULT_ANCHORED_MAX = 3;
|
|
91
|
+
const rule$4 = {
|
|
92
|
+
defaultOptions: [{}],
|
|
93
|
+
meta: {
|
|
94
|
+
type: "suggestion",
|
|
95
|
+
docs: {
|
|
96
|
+
description: "Cap the number of prose lines in a comment block (anchored blocks get a lower cap).",
|
|
97
|
+
url: "https://github.com/cyberash-dev/eslint-plugin-comment-policy#max-comment-lines"
|
|
98
|
+
},
|
|
99
|
+
schema: [{
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
max: {
|
|
103
|
+
type: "integer",
|
|
104
|
+
minimum: 0
|
|
105
|
+
},
|
|
106
|
+
anchoredMax: {
|
|
107
|
+
type: "integer",
|
|
108
|
+
minimum: 0
|
|
109
|
+
},
|
|
110
|
+
protectedPatterns: {
|
|
111
|
+
type: "array",
|
|
112
|
+
items: { type: "string" }
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
additionalProperties: false
|
|
116
|
+
}],
|
|
117
|
+
messages: {
|
|
118
|
+
tooManyProse: "comment block has {{count}} prose lines (> {{max}}); keep it to a short why or move it into a spec record",
|
|
119
|
+
tooManyProseAnchored: "anchored comment has {{count}} prose lines (> {{max}}); the rationale belongs in the spec record — keep the marker plus at most a one-line pointer"
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
create(context) {
|
|
123
|
+
const option = context.options[0] ?? {};
|
|
124
|
+
const max = option.max ?? DEFAULT_MAX;
|
|
125
|
+
const anchoredMax = option.anchoredMax ?? DEFAULT_ANCHORED_MAX;
|
|
126
|
+
const { detect, strip } = compileProtected(option.protectedPatterns ?? []);
|
|
127
|
+
const sourceCode = context.sourceCode;
|
|
128
|
+
return { "Program:exit"() {
|
|
129
|
+
for (const block of commentBlocks(sourceCode, detect)) {
|
|
130
|
+
const proseLineCount = block.raw.split("\n").filter((line) => isProseLine(line, strip)).length;
|
|
131
|
+
const cap = block.hasProtected ? anchoredMax : max;
|
|
132
|
+
if (proseLineCount > cap) context.report({
|
|
133
|
+
loc: blockLoc(block),
|
|
134
|
+
messageId: block.hasProtected ? "tooManyProseAnchored" : "tooManyProse",
|
|
135
|
+
data: {
|
|
136
|
+
count: proseLineCount,
|
|
137
|
+
max: cap
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} };
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/rules/no-comment-code-snippet.ts
|
|
146
|
+
const CODEISH = [
|
|
147
|
+
/^(?:import|export|const|let|var|function|class|return|await|async|if|for|while|switch)\b/,
|
|
148
|
+
/=>/,
|
|
149
|
+
/^[\w.$]+\([^)]*\)\s*;?$/,
|
|
150
|
+
/^[}{]/,
|
|
151
|
+
/;\s*$/
|
|
152
|
+
];
|
|
153
|
+
function isCodeish(content) {
|
|
154
|
+
return content.length > 0 && CODEISH.some((re) => re.test(content));
|
|
155
|
+
}
|
|
156
|
+
function snippetInfo(block) {
|
|
157
|
+
if (block.hasProtected) return {
|
|
158
|
+
isSnippet: false,
|
|
159
|
+
pure: false
|
|
160
|
+
};
|
|
161
|
+
const nonEmpty = block.raw.split("\n").map(strippedLine).filter((c) => c.length > 0);
|
|
162
|
+
const codeish = nonEmpty.filter(isCodeish);
|
|
163
|
+
return {
|
|
164
|
+
isSnippet: codeish.length >= 2,
|
|
165
|
+
pure: nonEmpty.length >= 2 && codeish.length === nonEmpty.length
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const rule$3 = {
|
|
169
|
+
defaultOptions: [{}],
|
|
170
|
+
meta: {
|
|
171
|
+
type: "suggestion",
|
|
172
|
+
fixable: "code",
|
|
173
|
+
docs: {
|
|
174
|
+
description: "Forbid code snippets (usage examples) inside comments; auto-removable only when the block is entirely code.",
|
|
175
|
+
url: "https://github.com/cyberash-dev/eslint-plugin-comment-policy#no-comment-code-snippet"
|
|
176
|
+
},
|
|
177
|
+
schema: [{
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: { protectedPatterns: {
|
|
180
|
+
type: "array",
|
|
181
|
+
items: { type: "string" }
|
|
182
|
+
} },
|
|
183
|
+
additionalProperties: false
|
|
184
|
+
}],
|
|
185
|
+
messages: { codeSnippet: "code snippet inside comment (usage example)" }
|
|
186
|
+
},
|
|
187
|
+
create(context) {
|
|
188
|
+
const { detect } = compileProtected((context.options[0] ?? {}).protectedPatterns ?? []);
|
|
189
|
+
const sourceCode = context.sourceCode;
|
|
190
|
+
const lineIndex = makeLineIndex(sourceCode.text);
|
|
191
|
+
return { "Program:exit"() {
|
|
192
|
+
for (const block of commentBlocks(sourceCode, detect)) {
|
|
193
|
+
const info = snippetInfo(block);
|
|
194
|
+
if (!info.isSnippet) continue;
|
|
195
|
+
const deletable = info.pure && block.fullLine;
|
|
196
|
+
context.report({
|
|
197
|
+
loc: blockLoc(block),
|
|
198
|
+
messageId: "codeSnippet",
|
|
199
|
+
fix: deletable ? (fixer) => fixer.removeRange([lineIndex.lineStart(block.startLine), lineIndex.lineEnd(block.endLine)]) : null
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
} };
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/rules/no-comment-narrative.ts
|
|
207
|
+
const NARRATIVE = [
|
|
208
|
+
/\brenamed?\s+from\b/i,
|
|
209
|
+
/\bformerly\b/i,
|
|
210
|
+
/\bpreviously\b/i,
|
|
211
|
+
/\bas before\b/i,
|
|
212
|
+
/\bused to\b/i,
|
|
213
|
+
/\badded\s+(?:for|to|because)\b/i,
|
|
214
|
+
/\bfix(?:es|ed)?\s+(?:bug|issue)\b/i,
|
|
215
|
+
/\bslice\s+\d+\b/i,
|
|
216
|
+
/\bv\d+\.\d+\b/i,
|
|
217
|
+
/\b\d{4}-\d{2}-\d{2}\b/
|
|
218
|
+
];
|
|
219
|
+
const rule$2 = {
|
|
220
|
+
defaultOptions: [{}],
|
|
221
|
+
meta: {
|
|
222
|
+
type: "suggestion",
|
|
223
|
+
docs: {
|
|
224
|
+
description: "Forbid change-narrative / history prose in comments (it belongs in the commit message or a spec record).",
|
|
225
|
+
url: "https://github.com/cyberash-dev/eslint-plugin-comment-policy#no-comment-narrative"
|
|
226
|
+
},
|
|
227
|
+
schema: [{
|
|
228
|
+
type: "object",
|
|
229
|
+
properties: {
|
|
230
|
+
protectedPatterns: {
|
|
231
|
+
type: "array",
|
|
232
|
+
items: { type: "string" }
|
|
233
|
+
},
|
|
234
|
+
extraPatterns: {
|
|
235
|
+
type: "array",
|
|
236
|
+
items: { type: "string" }
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
additionalProperties: false
|
|
240
|
+
}],
|
|
241
|
+
messages: { changeNarrative: "change-narrative / history prose (belongs in commit message or spec record)" }
|
|
242
|
+
},
|
|
243
|
+
create(context) {
|
|
244
|
+
const option = context.options[0] ?? {};
|
|
245
|
+
const { detect } = compileProtected(option.protectedPatterns ?? []);
|
|
246
|
+
const extra = (option.extraPatterns ?? []).map((source) => new RegExp(source));
|
|
247
|
+
const patterns = [...NARRATIVE, ...extra];
|
|
248
|
+
const sourceCode = context.sourceCode;
|
|
249
|
+
return { "Program:exit"() {
|
|
250
|
+
for (const block of commentBlocks(sourceCode, detect)) {
|
|
251
|
+
if (block.hasProtected) continue;
|
|
252
|
+
if (patterns.some((re) => re.test(block.raw))) context.report({
|
|
253
|
+
loc: blockLoc(block),
|
|
254
|
+
messageId: "changeNarrative"
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
} };
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/lib/decorative.ts
|
|
262
|
+
const DECORATIVE = [
|
|
263
|
+
/^[=*#_-]{3,}$/,
|
|
264
|
+
/^#?\s*(?:region|endregion)\b/i,
|
|
265
|
+
/^[=*#_-]{2,}.*[=*#_-]{2,}$/
|
|
266
|
+
];
|
|
267
|
+
function isDecorativeLine(content) {
|
|
268
|
+
return content.length > 0 && DECORATIVE.some((re) => re.test(content));
|
|
269
|
+
}
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/rules/no-decorative-comment.ts
|
|
272
|
+
const schema$1 = [{
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: { protectedPatterns: {
|
|
275
|
+
type: "array",
|
|
276
|
+
items: { type: "string" }
|
|
277
|
+
} },
|
|
278
|
+
additionalProperties: false
|
|
279
|
+
}];
|
|
280
|
+
function lineLoc(lineNo, text) {
|
|
281
|
+
return {
|
|
282
|
+
start: {
|
|
283
|
+
line: lineNo,
|
|
284
|
+
column: 0
|
|
285
|
+
},
|
|
286
|
+
end: {
|
|
287
|
+
line: lineNo,
|
|
288
|
+
column: text.length
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const rule$1 = {
|
|
293
|
+
defaultOptions: [{}],
|
|
294
|
+
meta: {
|
|
295
|
+
type: "suggestion",
|
|
296
|
+
fixable: "code",
|
|
297
|
+
docs: {
|
|
298
|
+
description: "Forbid decorative / section-marker comments (e.g. =====, #region, ===text===), in both // and /* */ forms.",
|
|
299
|
+
url: "https://github.com/cyberash-dev/eslint-plugin-comment-policy#no-decorative-comment"
|
|
300
|
+
},
|
|
301
|
+
schema: schema$1,
|
|
302
|
+
messages: { decorativeComment: "decorative / section-marker comment" }
|
|
303
|
+
},
|
|
304
|
+
create(context) {
|
|
305
|
+
const { detect } = compileProtected((context.options[0] ?? {}).protectedPatterns ?? []);
|
|
306
|
+
const sourceCode = context.sourceCode;
|
|
307
|
+
const lineIndex = makeLineIndex(sourceCode.text);
|
|
308
|
+
return { "Program:exit"() {
|
|
309
|
+
for (const block of commentBlocks(sourceCode, detect)) for (const comment of block.comments) {
|
|
310
|
+
const rawText = sourceCode.getText(comment);
|
|
311
|
+
if (comment.type === "Line") {
|
|
312
|
+
if (!isDecorativeLine(strippedLine(rawText)) || hasProtectedToken(rawText, detect)) continue;
|
|
313
|
+
const line = comment.loc.start.line;
|
|
314
|
+
context.report({
|
|
315
|
+
loc: comment.loc,
|
|
316
|
+
messageId: "decorativeComment",
|
|
317
|
+
fix: block.fullLine ? (fixer) => fixer.removeRange([lineIndex.lineStart(line), lineIndex.lineEnd(line)]) : null
|
|
318
|
+
});
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
rawText.split("\n").forEach((rawLine, i) => {
|
|
322
|
+
if (!isDecorativeLine(strippedLine(rawLine)) || hasProtectedToken(rawLine, detect)) return;
|
|
323
|
+
const line = comment.loc.start.line + i;
|
|
324
|
+
context.report({
|
|
325
|
+
loc: lineLoc(line, rawLine),
|
|
326
|
+
messageId: "decorativeComment"
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
} };
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/rules/no-line-comment.ts
|
|
335
|
+
const schema = [{
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: { protectedPatterns: {
|
|
338
|
+
type: "array",
|
|
339
|
+
items: { type: "string" }
|
|
340
|
+
} },
|
|
341
|
+
additionalProperties: false
|
|
342
|
+
}];
|
|
343
|
+
function buildConversion(block, text, detect, lineIndex) {
|
|
344
|
+
const indent = text.slice(lineIndex.lineStart(block.startLine), block.start);
|
|
345
|
+
const kept = [];
|
|
346
|
+
for (const rawLine of block.raw.split("\n")) {
|
|
347
|
+
const content = strippedLine(rawLine);
|
|
348
|
+
if (block.fullLine && isDecorativeLine(content) && !hasProtectedToken(rawLine, detect)) continue;
|
|
349
|
+
kept.push(content);
|
|
350
|
+
}
|
|
351
|
+
if (kept.some((c) => c.includes("*/"))) return null;
|
|
352
|
+
if (block.fullLine && kept.every((c) => c.length === 0)) return {
|
|
353
|
+
range: [lineIndex.lineStart(block.startLine), lineIndex.lineEnd(block.endLine)],
|
|
354
|
+
text: ""
|
|
355
|
+
};
|
|
356
|
+
const body = kept.length === 1 ? `/* ${kept[0]} */` : `/*\n${kept.map((c) => c ? `${indent} * ${c}` : `${indent} *`).join("\n")}\n${indent} */`;
|
|
357
|
+
return {
|
|
358
|
+
range: [block.start, block.end],
|
|
359
|
+
text: body
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const rule = {
|
|
363
|
+
defaultOptions: [{}],
|
|
364
|
+
meta: {
|
|
365
|
+
type: "suggestion",
|
|
366
|
+
fixable: "code",
|
|
367
|
+
docs: {
|
|
368
|
+
description: "Forbid line comments (`//`); require block `/* */` comments. Auto-fix converts and merges runs of `//`.",
|
|
369
|
+
url: "https://github.com/cyberash-dev/eslint-plugin-comment-policy#no-line-comment"
|
|
370
|
+
},
|
|
371
|
+
schema,
|
|
372
|
+
messages: { lineComment: "line comment; use a block /* */ comment" }
|
|
373
|
+
},
|
|
374
|
+
create(context) {
|
|
375
|
+
const { detect } = compileProtected((context.options[0] ?? {}).protectedPatterns ?? []);
|
|
376
|
+
const sourceCode = context.sourceCode;
|
|
377
|
+
const lineIndex = makeLineIndex(sourceCode.text);
|
|
378
|
+
return { "Program:exit"() {
|
|
379
|
+
for (const block of commentBlocks(sourceCode, detect)) {
|
|
380
|
+
if (block.kind !== "line") continue;
|
|
381
|
+
const conv = buildConversion(block, sourceCode.text, detect, lineIndex);
|
|
382
|
+
context.report({
|
|
383
|
+
loc: blockLoc(block),
|
|
384
|
+
messageId: "lineComment",
|
|
385
|
+
fix: conv ? (fixer) => fixer.replaceTextRange(conv.range, conv.text) : null
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
} };
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region src/lib/sdd-patterns.ts
|
|
393
|
+
const TYPES = "BL|SUR|CON|INV|POL|DEL|DLT|NFR|REQ|MIG|CST|SCN|LCN|GAR|EXT";
|
|
394
|
+
const SDD_PROTECTED_PATTERNS = [
|
|
395
|
+
"@covers\\s+\\S+(?:\\s+\\w+=\\S+)*",
|
|
396
|
+
`\\b[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?::[a-z0-9]+(?:-[a-z0-9]+)*)*:(?:${TYPES})-\\d+\\b`,
|
|
397
|
+
`\\b(?:${TYPES})-\\d+\\b`,
|
|
398
|
+
"\\bM\\d+[A-Z]+-\\d+\\b"
|
|
399
|
+
];
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/index.ts
|
|
402
|
+
const configs = {};
|
|
403
|
+
const plugin = {
|
|
404
|
+
meta: {
|
|
405
|
+
name: "cyberash",
|
|
406
|
+
version: "0.1.0"
|
|
407
|
+
},
|
|
408
|
+
rules: {
|
|
409
|
+
"max-comment-lines": rule$4,
|
|
410
|
+
"no-comment-narrative": rule$2,
|
|
411
|
+
"no-comment-code-snippet": rule$3,
|
|
412
|
+
"no-decorative-comment": rule$1,
|
|
413
|
+
"no-line-comment": rule
|
|
414
|
+
},
|
|
415
|
+
configs
|
|
416
|
+
};
|
|
417
|
+
configs.recommended = {
|
|
418
|
+
plugins: { "comment-policy": plugin },
|
|
419
|
+
rules: {
|
|
420
|
+
"comment-policy/max-comment-lines": ["error", {
|
|
421
|
+
max: 4,
|
|
422
|
+
anchoredMax: 3
|
|
423
|
+
}],
|
|
424
|
+
"comment-policy/no-comment-narrative": ["error"],
|
|
425
|
+
"comment-policy/no-comment-code-snippet": ["error"],
|
|
426
|
+
"comment-policy/no-decorative-comment": ["error"],
|
|
427
|
+
"comment-policy/no-line-comment": ["error"]
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
const sddProtected = [...SDD_PROTECTED_PATTERNS];
|
|
431
|
+
configs.sdd = {
|
|
432
|
+
plugins: { "comment-policy": plugin },
|
|
433
|
+
rules: {
|
|
434
|
+
"comment-policy/max-comment-lines": ["error", {
|
|
435
|
+
max: 4,
|
|
436
|
+
anchoredMax: 3,
|
|
437
|
+
protectedPatterns: sddProtected
|
|
438
|
+
}],
|
|
439
|
+
"comment-policy/no-comment-narrative": ["error", { protectedPatterns: sddProtected }],
|
|
440
|
+
"comment-policy/no-comment-code-snippet": ["error", { protectedPatterns: sddProtected }],
|
|
441
|
+
"comment-policy/no-decorative-comment": ["error", { protectedPatterns: sddProtected }],
|
|
442
|
+
"comment-policy/no-line-comment": ["error", { protectedPatterns: sddProtected }]
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
//#endregion
|
|
446
|
+
export { plugin as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-comment-policy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint rules enforcing a per-file comment policy: prose-line caps, no change-narrative, no code snippets, no decorative markers, block comments only (JS + TS).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "cyberash <mail@cyberash.dev>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/cyberash-dev/eslint-plugin-comment-policy.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/cyberash-dev/eslint-plugin-comment-policy/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/cyberash-dev/eslint-plugin-comment-policy#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"eslint",
|
|
18
|
+
"eslint-plugin",
|
|
19
|
+
"eslintplugin",
|
|
20
|
+
"comments",
|
|
21
|
+
"comment-policy",
|
|
22
|
+
"typescript"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"main": "./dist/index.cjs",
|
|
26
|
+
"module": "./dist/index.mjs",
|
|
27
|
+
"types": "./dist/index.d.mts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"default": "./dist/index.mjs"
|
|
33
|
+
},
|
|
34
|
+
"require": {
|
|
35
|
+
"types": "./dist/index.d.cts",
|
|
36
|
+
"default": "./dist/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"AGENTS.md"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsdown",
|
|
46
|
+
"test": "tsx tests/run-all.ts",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"lint": "eslint .",
|
|
49
|
+
"prepublishOnly": "npm run build && npm test"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"eslint": ">=9"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@typescript-eslint/types": "8.60.0",
|
|
56
|
+
"@typescript-eslint/utils": "8.60.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@eslint/js": "9.39.4",
|
|
60
|
+
"@types/node": "22.19.19",
|
|
61
|
+
"eslint": "9.39.4",
|
|
62
|
+
"globals": "15.15.0",
|
|
63
|
+
"tsdown": "0.22.1",
|
|
64
|
+
"tsx": "4.22.3",
|
|
65
|
+
"typescript": "5.9.3",
|
|
66
|
+
"typescript-eslint": "8.60.0"
|
|
67
|
+
},
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": ">=20"
|
|
70
|
+
}
|
|
71
|
+
}
|