@nitra/cursor 1.9.5 → 1.9.7
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/CHANGELOG.md +21 -1
- package/mdc/ga.mdc +3 -1
- package/mdc/js-lint.mdc +28 -5
- package/mdc/js-run.mdc +1 -21
- package/package.json +1 -1
- package/policy/ga/workflow_common/workflow_common.rego +18 -0
- package/policy/js_lint/lint_js_yml/lint_js_yml.rego +5 -0
- package/policy/js_lint/package_json/package_json.rego +1 -1
- package/scripts/check-js-lint.mjs +30 -1
- package/scripts/check-js-run.mjs +2 -36
- package/scripts/check-npm-module.mjs +22 -22
- package/scripts/utils/depcheck-workflow.mjs +0 -205
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.9.7] - 2026-05-12
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **js-lint (mdc v1.18 → v1.19) — `depcheck` мігровано на `knip`:** канонічний `lint-js` тепер `bunx oxlint --fix && bunx eslint --fix . && bunx jscpd . && bunx knip` (раніше — без `bunx knip`); крок `bunx knip` додано і в приклад workflow `lint-js.yml`. У корені має бути `knip.json` з мінімальним `ignoreDependencies: ["graphql"]` (peer-залежність, яку `knip` не розпізнає як використану). Пакет `knip` окремо в `devDependencies` не оголошуй — `bunx` тягне його ad-hoc. `CANONICAL_LINT_JS` у `npm/scripts/check-js-lint.mjs` і `canonical_lint_js` у `npm/policy/js_lint/package_json/package_json.rego` оновлено; додано `checkKnipConfig` (наявність файла + `ignoreDependencies` містить `graphql`) і `deny`-правило у `npm/policy/js_lint/lint_js_yml/` на відсутність `bunx knip` у `run:` кроці lint-js workflow.
|
|
12
|
+
|
|
13
|
+
- **ga (mdc v1.8 → v1.9) — заборона `depcheck` у workflow-файлах:** додано полісі `ga.workflow_common.deny` на будь-який виклик `depcheck` (через `npx`/`bunx`/`npm exec`/`pnpm exec` чи як standalone-команду) у `run:` кроку `.github/workflows/*.yml`. Перевірка невикористаних залежностей виконується разом з рештою лінтерів у `bun run lint-js` (`bunx knip`), окремий depcheck-крок у workflow зайвий. У `npm/mdc/ga.mdc` додано буліт «`depcheck`: не використовувати» з посиланням на `js-lint.mdc` і `ga.workflow_common`.
|
|
14
|
+
|
|
15
|
+
- **ci (тільки в цьому репо) — lint-ga встановлює conftest; knip.json налаштовано під монорепо:** `.github/workflows/lint-ga.yml` отримав крок `Install conftest` (curl-розпаковка релізу), бо `check-ga.mjs::runAllGaRego` ходить у `runConftestBatch` і hard-fail без бінарника. Кореневий `knip.json` розширено `workspaces.npm.entry` (всі CLI/scripts/tests як entry points — інакше knip false-positive репортить їх як unused), `ignoreBinaries` для `cspell`/`oxfmt`/`stylelint`/`vite` (всі через `bunx`/`npx`, не з deps), і `ignoreDependencies` для workspace self-refs. Це налаштування є кастомним для цього репо; інші проєкти налаштовують `knip.json` під свою структуру.
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
|
|
19
|
+
- **js-run (mdc v1.6 → v1.7) — секцію «depcheck у GitHub Actions з path-фільтром» прибрано:** правило про обовʼязковий `npx depcheck --ignores="graphql,bun"` з `working-directory` у path-scoped workflow більше не діє — `depcheck` повністю мігровано на `knip` (див. js-lint.mdc), окремий крок у per-package workflow не потрібен. Файл `npm/scripts/utils/depcheck-workflow.mjs` видалено. У `npm/scripts/check-js-run.mjs` прибрано `checkDepcheckInWorkflows`, імпорти `findDepcheckViolationsForPackage` / `readAllWorkflowFiles` і параметр `workflows` у `checkWorkspacePackage`. У `npm/tests/check-js-run-fixture.test.mjs` видалено `describe('check-js-run: depcheck у path-scoped workflow', …)` (9 тест-кейсів) і допоміжну `writeRepoWithCronJobAndWorkflow`. У `.github/workflows/npm-publish.yml` прибрано крок `npx depcheck --ignores="graphql,bun,bun:test,@nitra/cursor"` з `working-directory: npm` — `lint-js` workflow покриває цю перевірку через `bunx knip`.
|
|
20
|
+
|
|
21
|
+
## [1.9.6] - 2026-05-12
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **js-lint — ігнорувати `.claude/worktrees/` для jscpd і всіх лінтів:** правило `js-lint.mdc` (v1.18) тепер документує, що каталог `.claude/worktrees/` (робочі копії, які Claude Code створює через superpowers-skill `using-git-worktrees`) має бути виключений з лінт-перевірок. Канонічне місце — `.gitignore` (паралельні воркті — це за визначенням не-комітні робочі копії; `gitignore: true` у `.jscpd.json` уже є, тож запис у `.gitignore` каскадно вимикає сканування). Як страховку на випадок запуску jscpd без `gitignore: true` рекомендовано додати `.claude/worktrees/**` у `ignore` `.jscpd.json` — приклад у правилі оновлено. Без цього `bunx jscpd .` фіксує дзеркальні «клони» між кореневим репо і його worktree-копією у `.claude/worktrees/<name>/…`.
|
|
26
|
+
|
|
7
27
|
## [1.9.5] - 2026-05-12
|
|
8
28
|
|
|
9
29
|
### Changed
|
|
@@ -18,7 +38,7 @@
|
|
|
18
38
|
|
|
19
39
|
### Removed
|
|
20
40
|
|
|
21
|
-
- **graphql — вимога `scripts.dump-schema` у `package.json` прибрана:** правило `graphql.mdc` більше не вимагає канонічний скрипт `dump-schema` (раніше — `bunx graphqurl http://localhost:4040/v1/graphql -H 'X-Hasura-Admin-Secret: secret' --introspect > schema.graphql`) у корені проєкту за наявності gql tagged template literals. У `.mdc` відповідну буліт-точку та JSON-фрагмент видалено; фраза про «стандартний спосіб оновлення локальної `schema.graphql`» теж прибрана з підсумкового речення. Каталог `npm/policy/graphql/` (єдиний файл `package_json/package_json.rego` з deny-правилами на відсутність/неканонічний `scripts.dump-schema`) видалено повністю. Запис реєстру `graphql.package_json` (policyDir `graphql`, rule `graphql`, single `package.json`) прибрано з `npm/scripts/lint-conftest.mjs` (заголовок секції перейменовано — `graphql` вилучено). JSDoc-преамбулу `npm/scripts/check-graphql.mjs` оновлено: видалено абзац про rego-порт перевірки `dump-schema` і згадку `scripts.dump-schema` з
|
|
41
|
+
- **graphql — вимога `scripts.dump-schema` у `package.json` прибрана:** правило `graphql.mdc` більше не вимагає канонічний скрипт `dump-schema` (раніше — `bunx graphqurl http://localhost:4040/v1/graphql -H 'X-Hasura-Admin-Secret: secret' --introspect > schema.graphql`) у корені проєкту за наявності gql tagged template literals. У `.mdc` відповідну буліт-точку та JSON-фрагмент видалено; фраза про «стандартний спосіб оновлення локальної `schema.graphql`» теж прибрана з підсумкового речення. Каталог `npm/policy/graphql/` (єдиний файл `package_json/package_json.rego` з deny-правилами на відсутність/неканонічний `scripts.dump-schema`) видалено повністю. Запис реєстру `graphql.package_json` (policyDir `graphql`, rule `graphql`, single `package.json`) прибрано з `npm/scripts/lint-conftest.mjs` (заголовок секції перейменовано — `graphql` вилучено). JSDoc-преамбулу `npm/scripts/check-graphql.mjs` оновлено: видалено абзац про rego-порт перевірки `dump-schema` і згадку `scripts.dump-schema` з JSDoc функції `check()`. Сам JS-чек і так не торкався `package.json` — після видалення rego-полісі ніяких runtime-перевірок `dump-schema` не лишається. У кореневому `package.json` репо cursor скрипт `dump-schema` теж видалено, оскільки тримати його як shim без правила немає сенсу.
|
|
22
42
|
|
|
23
43
|
## [1.9.3] - 2026-05-11
|
|
24
44
|
|
package/mdc/ga.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила форматів для .github/workflows
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.9'
|
|
4
4
|
globs: ".github/workflows/*.yml"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -287,6 +287,8 @@ rules:
|
|
|
287
287
|
|
|
288
288
|
**MegaLinter:** не використовувати; прибрати workflow, конфіги (`.mega-linter.yml`, `.megalinter.yaml`, `.mega-linter.yaml`), залежності та згадки в CI / pre-commit / документації.
|
|
289
289
|
|
|
290
|
+
**`depcheck`:** не використовувати у `.github/workflows/*.yml` — мігровано на `knip` (див. `js-lint.mdc`). Перевірка невикористаних залежностей виконується разом з рештою лінтерів у `lint-js`, окремий крок `npx depcheck` у workflow не потрібен і блокується полісі `ga.workflow_common`.
|
|
291
|
+
|
|
290
292
|
## Перевірка
|
|
291
293
|
|
|
292
294
|
- `bun run lint-ga`
|
package/mdc/js-lint.mdc
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Перевірка JavaScript коду
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.19'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
**oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config` мінімум `^3.9.2`** (з **3.8.0** правило `no-restricted-syntax` забороняє `for...in`; з **3.9.2** у `getConfig` вбудовано ignore для **`**/adr/**`** — ADR-документи не валідуються ESLint, локально цей glob додавати не потрібно; також транзитивно йде **`@e18e/eslint-plugin`** для oxlint); пакет **`@e18e/eslint-plugin`** окремо не додавай. Пакети oxlint/eslint/jscpd не додавай без потреби монорепо.
|
|
7
|
+
**oxlint**, **ESLint**, **jscpd**, **knip**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`**, **`bunx knip`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config` мінімум `^3.9.2`** (з **3.8.0** правило `no-restricted-syntax` забороняє `for...in`; з **3.9.2** у `getConfig` вбудовано ignore для **`**/adr/**`** — ADR-документи не валідуються ESLint, локально цей glob додавати не потрібно; також транзитивно йде **`@e18e/eslint-plugin`** для oxlint); пакет **`@e18e/eslint-plugin`** окремо не додавай. Пакети oxlint/eslint/jscpd/knip не додавай без потреби монорепо.
|
|
8
8
|
|
|
9
9
|
```json title=".vscode/extensions.json"
|
|
10
10
|
{
|
|
@@ -22,7 +22,7 @@ version: '1.17'
|
|
|
22
22
|
{
|
|
23
23
|
"type": "module",
|
|
24
24
|
"scripts": {
|
|
25
|
-
"lint-js": "bunx oxlint --fix && bunx eslint --fix . && bunx jscpd ."
|
|
25
|
+
"lint-js": "bunx oxlint --fix && bunx eslint --fix . && bunx jscpd . && bunx knip"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@nitra/eslint-config": "^3.9.2"
|
|
@@ -43,7 +43,9 @@ version: '1.17'
|
|
|
43
43
|
}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
У корені проєкту має бути `.jscpd.json`. Мінімум: увімкнути облік `.gitignore`, ненульовий код виходу при знаходженні клонів, консольний звіт. За потреби додай `ignore` (дзеркальні каталоги, шаблони) та `minLines`, щоб відсікти дрібні
|
|
46
|
+
У корені проєкту має бути `.jscpd.json`. Мінімум: увімкнути облік `.gitignore`, ненульовий код виходу при знаходженні клонів, консольний звіт. За потреби додай `ignore` (дзеркальні каталоги, шаблони) та `minLines`, щоб відсікти дрібні збіги.
|
|
47
|
+
|
|
48
|
+
Каталог `.claude/worktrees/` (робочі копії, які Claude Code створює через **superpowers:using-git-worktrees**) має ігноруватися: додай його у кореневий `.gitignore` (це штатне місце для не-комітних робочих копій), а в `.jscpd.json` додай `.claude/worktrees/**` у `ignore` як страховку на випадок запуску без `gitignore: true`. Без цього jscpd сканує паралельну копію репо в worktree і фіксує самозбіги між дзеркальними файлами.
|
|
47
49
|
|
|
48
50
|
```json title=".jscpd.json"
|
|
49
51
|
{
|
|
@@ -51,10 +53,30 @@ version: '1.17'
|
|
|
51
53
|
"exitCode": 1,
|
|
52
54
|
"reporters": ["console"],
|
|
53
55
|
"minLines": 25,
|
|
54
|
-
"ignore": ["**/dist/**"]
|
|
56
|
+
"ignore": [".claude/worktrees/**", "**/dist/**"]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```text title=".gitignore (фрагмент)"
|
|
61
|
+
.claude/worktrees/
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## knip
|
|
65
|
+
|
|
66
|
+
Перевірку невикористаних залежностей і експортів виконує **knip** (заміна `depcheck`). Викликається у скрипті `lint-js` і в CI разом з oxlint/eslint/jscpd — окремий крок у CI не потрібен.
|
|
67
|
+
|
|
68
|
+
У корені проєкту має бути `knip.json` із `ignoreDependencies` для `graphql` (peer-залежність, часто використовується без прямого імпорту):
|
|
69
|
+
|
|
70
|
+
```json title="knip.json"
|
|
71
|
+
{
|
|
72
|
+
"ignoreDependencies": [
|
|
73
|
+
"graphql"
|
|
74
|
+
]
|
|
55
75
|
}
|
|
56
76
|
```
|
|
57
77
|
|
|
78
|
+
Пакет `knip` окремо в `devDependencies` не додавай — `bunx knip` тягне його ad-hoc, як oxlint/eslint/jscpd.
|
|
79
|
+
|
|
58
80
|
## jscpd: рефакторинг і структура
|
|
59
81
|
|
|
60
82
|
Коли **jscpd** знаходить клони, спочатку зменшуй дублювання кодом, а не конфігом.
|
|
@@ -112,6 +134,7 @@ jobs:
|
|
|
112
134
|
bunx oxlint
|
|
113
135
|
bunx eslint .
|
|
114
136
|
bunx jscpd .
|
|
137
|
+
bunx knip
|
|
115
138
|
```
|
|
116
139
|
|
|
117
140
|
Перед **`./.github/actions/setup-bun-deps`** — **`actions/checkout@v6`** (див. **ga.mdc**). Composite: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
|
package/mdc/js-run.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Це правила для backend проектів на JavaScript/Node.js, сюди входять і job і WEB сервери.
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.7'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Область застосування
|
|
@@ -209,26 +209,6 @@ await setTimeout(500)
|
|
|
209
209
|
|
|
210
210
|
Імпорт `setTimeout` з `node:timers/promises` затіняє глобальний таймер у файлі — якщо в тому ж файлі потрібен callback-варіант, імпортуй його під іншим іменем (наприклад, `import { setTimeout as setTimeoutCb } from 'node:timers'`).
|
|
211
211
|
|
|
212
|
-
## depcheck у GitHub Actions з path-фільтром
|
|
213
|
-
|
|
214
|
-
Якщо в `.github/workflows/*.yml` є тригер з `paths:`, який обмежує запуск workflow змінами в каталозі конкретного backend-пакета, в job цього workflow має бути крок `npx depcheck` з `working-directory`, який вказує на той самий каталог пакета. Це гарантує, що декларація залежностей у `package.json` пакета відповідає реальним імпортам — інакше можна випадково зламати білд після видалення «зайвої» залежності, яка насправді використовується через побічний імпорт.
|
|
215
|
-
|
|
216
|
-
Список `--ignores` **обов'язково** містить як мінімум `graphql,bun` (це ті, які `depcheck` не вміє коректно розпізнавати: `graphql` — peer-залежність, що часто використовується без прямого імпорту в коді; `bun` — рантайм, не npm-пакет). За потреби список можна розширити іншими модулями, специфічними для пакета — список значень розділяється комою без пробілів.
|
|
217
|
-
|
|
218
|
-
```yaml title="Приклад: workflow для cron-jobs/refund-loyalty-points"
|
|
219
|
-
on:
|
|
220
|
-
push:
|
|
221
|
-
paths:
|
|
222
|
-
- 'cron-jobs/refund-loyalty-points/**'
|
|
223
|
-
|
|
224
|
-
# …
|
|
225
|
-
|
|
226
|
-
- run: npx depcheck --ignores="graphql,bun"
|
|
227
|
-
working-directory: cron-jobs/refund-loyalty-points
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
Правило не застосовується до workflow без `paths:` або з `paths:`, який не звужує тригер до одного backend-пакета (наприклад, кореневі `lint-*.yml` з глобальними `**/*.js`).
|
|
231
|
-
|
|
232
212
|
## Перевірка
|
|
233
213
|
|
|
234
214
|
`npx @nitra/cursor check js-run` — зокрема для кожного backend workspace-пакета з каталогом **`src/`** перевіряє наявність **`jsconfig.json`** і збіг вмісту з каноном вище. Додатково для файлів у каталозі `#conn/` (за замовчуванням `src/conn/`) перевіряється:
|
package/package.json
CHANGED
|
@@ -33,6 +33,11 @@ forbidden_step_substrings := {
|
|
|
33
33
|
"bun install": "використовуй .github/actions/setup-bun-deps замість bun install",
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
# Заборонені бінарки у `run:` кроках (ga.mdc). `depcheck` мігровано на `knip`
|
|
37
|
+
# у `lint-js.mdc` — окремий крок у workflow не потрібен. Регексп ловить виклики
|
|
38
|
+
# через `npx`, `bunx`, `npm exec`, або як standalone-команду на початку рядка.
|
|
39
|
+
forbidden_run_command_patterns := {"depcheck": `(?:^|[\s;&|])(?:npx|bunx|npm exec|pnpm exec)?[ \t]*depcheck\b`}
|
|
40
|
+
|
|
36
41
|
# Шаблони довгих повідомлень — через `concat`, щоб дотримуватися regal style/line-length.
|
|
37
42
|
|
|
38
43
|
concurrency_missing_template := concat(" ", [
|
|
@@ -73,6 +78,19 @@ deny contains msg if {
|
|
|
73
78
|
msg := sprintf("jobs.%s.steps[%d]: %s (ga.mdc)", [entry.job_id, entry.step_index, hint])
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
# ── deny: depcheck у будь-якому `run:` ────────────────────────────────────
|
|
82
|
+
#
|
|
83
|
+
# `depcheck` мігровано на `knip` (js-lint.mdc); `knip` вже запускається у lint-js
|
|
84
|
+
# CI як частина `bunx knip` у скрипті, тож окремий depcheck-крок зайвий і має
|
|
85
|
+
# бути видалений з workflow-файлів.
|
|
86
|
+
|
|
87
|
+
deny contains msg if {
|
|
88
|
+
some entry in all_flat_steps
|
|
89
|
+
some name, pattern in forbidden_run_command_patterns
|
|
90
|
+
regex.match(pattern, step_run_text(entry.step))
|
|
91
|
+
msg := sprintf("jobs.%s.steps[%d]: `%s` заборонено у workflow — мігровано на knip (js-lint.mdc, ga.mdc)", [entry.job_id, entry.step_index, name])
|
|
92
|
+
}
|
|
93
|
+
|
|
76
94
|
# ── deny: shell-продовження `\` перед переносом рядка у `run:` ─────────────
|
|
77
95
|
#
|
|
78
96
|
# `\` + `\n` — bash line-continuation; у workflow замінюй на folded block `>-`
|
|
@@ -71,6 +71,11 @@ deny contains msg if {
|
|
|
71
71
|
msg := "lint-js.yml: у run немає `bunx jscpd .` (js-lint.mdc)"
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
deny contains msg if {
|
|
75
|
+
not contains(all_run_blob, "bunx knip")
|
|
76
|
+
msg := "lint-js.yml: у run немає `bunx knip` (js-lint.mdc)"
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
# ── deny: --fix у CI заборонено ───────────────────────────────────────────
|
|
75
80
|
|
|
76
81
|
deny contains msg if {
|
|
@@ -17,7 +17,7 @@ package js_lint.package_json
|
|
|
17
17
|
|
|
18
18
|
import rego.v1
|
|
19
19
|
|
|
20
|
-
canonical_lint_js := "bunx oxlint --fix && bunx eslint --fix . && bunx jscpd ."
|
|
20
|
+
canonical_lint_js := "bunx oxlint --fix && bunx eslint --fix . && bunx jscpd . && bunx knip"
|
|
21
21
|
|
|
22
22
|
# ── deny: `lint-js` скрипт ─────────────────────────────────────────────────
|
|
23
23
|
|
|
@@ -26,7 +26,7 @@ export const OXLINT_CANONICAL_JSON_PATH = join(
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
/** Очікуваний локальний скрипт. */
|
|
29
|
-
export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bunx jscpd .'
|
|
29
|
+
export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bunx jscpd . && bunx knip'
|
|
30
30
|
|
|
31
31
|
/** Мінімальні рекомендації розширень редактора з js-lint.mdc (eslint, oxlint, GA). */
|
|
32
32
|
export const REQUIRED_VSCODE_EXTENSIONS = ['dbaeumer.vscode-eslint', 'github.vscode-github-actions', 'oxc.oxc-vscode']
|
|
@@ -472,6 +472,34 @@ async function checkJscpdConfig(passFn, failFn) {
|
|
|
472
472
|
}
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Перевіряє `knip.json`: файл існує і `ignoreDependencies` містить `graphql`
|
|
477
|
+
* (peer-залежність, що часто використовується без прямого імпорту — без цього
|
|
478
|
+
* `knip` фолсово репортить її як unused).
|
|
479
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
480
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
481
|
+
*/
|
|
482
|
+
async function checkKnipConfig(passFn, failFn) {
|
|
483
|
+
if (!existsSync('knip.json')) {
|
|
484
|
+
failFn('knip.json не існує — додай конфіг з ignoreDependencies: ["graphql"] (js-lint.mdc)')
|
|
485
|
+
return
|
|
486
|
+
}
|
|
487
|
+
let cfg
|
|
488
|
+
try {
|
|
489
|
+
cfg = JSON.parse(await readFile('knip.json', 'utf8'))
|
|
490
|
+
} catch {
|
|
491
|
+
failFn('knip.json не є валідним JSON')
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
passFn('knip.json існує')
|
|
495
|
+
const ignoreDeps = cfg?.ignoreDependencies
|
|
496
|
+
if (Array.isArray(ignoreDeps) && ignoreDeps.includes('graphql')) {
|
|
497
|
+
passFn('knip.json: ignoreDependencies містить "graphql"')
|
|
498
|
+
} else {
|
|
499
|
+
failFn('knip.json: ignoreDependencies має містити "graphql" (peer-залежність) (js-lint.mdc)')
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
475
503
|
/**
|
|
476
504
|
* Перевіряє відповідність проєкту правилам js-lint.mdc
|
|
477
505
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -486,6 +514,7 @@ export async function check() {
|
|
|
486
514
|
await checkVscodeExtensions(pass, fail)
|
|
487
515
|
await checkLintJsWorkflows(pass, fail)
|
|
488
516
|
await checkJscpdConfig(pass, fail)
|
|
517
|
+
await checkKnipConfig(pass, fail)
|
|
489
518
|
|
|
490
519
|
for (const dup of ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml']) {
|
|
491
520
|
if (existsSync(dup)) fail(`Знайдено застарілий конфіг ESLint: ${dup} — видали, використовуй flat config`)
|
package/scripts/check-js-run.mjs
CHANGED
|
@@ -23,10 +23,6 @@
|
|
|
23
23
|
* кожен `env.X` має бути закритий літеральним викликом `checkEnv(['X', ...])`
|
|
24
24
|
* у тому ж файлі або коментарем `// \@nitra/cursor ignore-next-line checkEnv`
|
|
25
25
|
* на попередньому рядку (див. `utils/check-env-scan.mjs`);
|
|
26
|
-
* - «depcheck у GitHub Actions з path-фільтром»: для кожного workflow з `paths:`,
|
|
27
|
-
* обмеженим каталогом цього пакета (`<rootDir>/...`), має бути крок
|
|
28
|
-
* `npx depcheck --ignores="graphql,bun"` (плюс інші, за потреби) з
|
|
29
|
-
* `working-directory: <rootDir>` (див. `utils/depcheck-workflow.mjs`);
|
|
30
26
|
* - «Паузи через setTimeout»: `new Promise(resolve => setTimeout(resolve, ms))` (з/без `await`)
|
|
31
27
|
* треба замінити на `await setTimeout(ms)` з `node:timers/promises`
|
|
32
28
|
* (див. `utils/promise-settimeout-scan.mjs`);
|
|
@@ -44,7 +40,6 @@ import {
|
|
|
44
40
|
} from './utils/bunyan-imports.mjs'
|
|
45
41
|
import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from './utils/check-env-scan.mjs'
|
|
46
42
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
47
|
-
import { findDepcheckViolationsForPackage, readAllWorkflowFiles } from './utils/depcheck-workflow.mjs'
|
|
48
43
|
import { findConnFileRuleViolations, isConnFileRulesSourceFile } from './utils/conn-file-rules.mjs'
|
|
49
44
|
import {
|
|
50
45
|
findConnFactoryImportsInText,
|
|
@@ -310,12 +305,11 @@ async function checkPromiseSetTimeoutPause(absPackageRoot, sourcePaths, label, f
|
|
|
310
305
|
* Перевіряє відповідність правилам js-run.mdc для одного workspace-пакета.
|
|
311
306
|
* @param {string} rootDir відносний шлях workspace (не `'.'`)
|
|
312
307
|
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
313
|
-
* @param {{ relPath: string, content: string }[]} workflows кешований список workflow-файлів репо
|
|
314
308
|
* @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
|
|
315
309
|
* @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
|
|
316
310
|
* @returns {Promise<void>} завершується після перевірок цього пакета
|
|
317
311
|
*/
|
|
318
|
-
async function checkWorkspacePackage(rootDir, ignorePaths,
|
|
312
|
+
async function checkWorkspacePackage(rootDir, ignorePaths, fail, passFn) {
|
|
319
313
|
const label = `[${rootDir}] `
|
|
320
314
|
const absPackageRoot = join(process.cwd(), rootDir)
|
|
321
315
|
const pkgJson = await loadPackageJson(rootDir)
|
|
@@ -365,33 +359,6 @@ async function checkWorkspacePackage(rootDir, ignorePaths, workflows, fail, pass
|
|
|
365
359
|
}
|
|
366
360
|
|
|
367
361
|
checkOtelConfigmap(rootDir, passFn)
|
|
368
|
-
|
|
369
|
-
checkDepcheckInWorkflows(rootDir, workflows, label, fail, passFn)
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Перевіряє правило «depcheck у workflow» для одного пакета.
|
|
374
|
-
*
|
|
375
|
-
* Для кожного `.github/workflows/*.yml`, чий `paths:` обмежено до `<rootDir>/...`,
|
|
376
|
-
* має бути крок `npx depcheck --ignores="graphql,bun"` з `working-directory: <rootDir>`.
|
|
377
|
-
* Якщо в репо немає каталогу `.github/workflows`, перевірка no-op.
|
|
378
|
-
* @param {string} rootDir відносний шлях workspace-пакета
|
|
379
|
-
* @param {{ relPath: string, content: string }[]} workflows кешований список workflow-файлів
|
|
380
|
-
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
381
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
382
|
-
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
383
|
-
* @returns {void}
|
|
384
|
-
*/
|
|
385
|
-
function checkDepcheckInWorkflows(rootDir, workflows, label, fail, passFn) {
|
|
386
|
-
if (workflows.length === 0) return
|
|
387
|
-
const violations = findDepcheckViolationsForPackage(workflows, rootDir.replaceAll('\\', '/'))
|
|
388
|
-
if (violations.length === 0) {
|
|
389
|
-
passFn(`${label}depcheck у path-scoped workflow налаштовано (або відсутній path-scoped workflow для пакета)`)
|
|
390
|
-
return
|
|
391
|
-
}
|
|
392
|
-
for (const v of violations) {
|
|
393
|
-
fail(`${label}${v}`)
|
|
394
|
-
}
|
|
395
362
|
}
|
|
396
363
|
|
|
397
364
|
/**
|
|
@@ -454,9 +421,8 @@ export async function check() {
|
|
|
454
421
|
}
|
|
455
422
|
|
|
456
423
|
const ignorePaths = await loadCursorIgnorePaths(process.cwd())
|
|
457
|
-
const workflows = await readAllWorkflowFiles(process.cwd())
|
|
458
424
|
for (const r of workspaceRoots) {
|
|
459
|
-
await checkWorkspacePackage(r, ignorePaths,
|
|
425
|
+
await checkWorkspacePackage(r, ignorePaths, fail, pass)
|
|
460
426
|
}
|
|
461
427
|
|
|
462
428
|
return reporter.getExitCode()
|
|
@@ -32,12 +32,7 @@ import { promisify } from 'node:util'
|
|
|
32
32
|
|
|
33
33
|
import { parseSync } from 'oxc-parser'
|
|
34
34
|
|
|
35
|
-
import {
|
|
36
|
-
dynamicImportModule,
|
|
37
|
-
langFromPath,
|
|
38
|
-
requireCallModule,
|
|
39
|
-
walkAstWithAncestors
|
|
40
|
-
} from './utils/ast-scan-utils.mjs'
|
|
35
|
+
import { dynamicImportModule, langFromPath, requireCallModule, walkAstWithAncestors } from './utils/ast-scan-utils.mjs'
|
|
41
36
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
42
37
|
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
43
38
|
import { walkDir } from './utils/walkDir.mjs'
|
|
@@ -80,6 +75,13 @@ const TEST_FRAMEWORK_MODULES = new Set([
|
|
|
80
75
|
'tape'
|
|
81
76
|
])
|
|
82
77
|
|
|
78
|
+
/** Символи у glob-сегменті, які треба екранувати для RegExp (без `*` / `?` — їх обробляємо окремо). */
|
|
79
|
+
const REGEX_SPECIAL_IN_GLOB = new Set(['.', '+', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\'])
|
|
80
|
+
|
|
81
|
+
/** Збіги для post-обробки glob → regex після злиття сегментів через `/` (див. `globToRegex`). */
|
|
82
|
+
const GLOBSTAR_LEADING_RE = /^__GLOBSTAR__\//u
|
|
83
|
+
const GLOBSTAR_TRAILING_RE = /\/__GLOBSTAR__$/u
|
|
84
|
+
|
|
83
85
|
/**
|
|
84
86
|
* Чи є під `npm/src` хоча б один `.js` (рекурсивно).
|
|
85
87
|
* @param {string[]} [ignorePaths] абсолютні шляхи каталогів, повністю виключених з обходу
|
|
@@ -338,13 +340,13 @@ function checkPublishWorkflow(passFn, failFn) {
|
|
|
338
340
|
}
|
|
339
341
|
|
|
340
342
|
/**
|
|
341
|
-
* Перетворює glob-патерн (як у npm `files`) у
|
|
342
|
-
* globstar (нуль або більше сегментів), `*` (символи без `/`) і `?`
|
|
343
|
-
* символ без `/`). Не підтримує brace-expansion і class `[…]` — у
|
|
344
|
-
* патернах `files` цього достатньо для практичних випадків
|
|
345
|
-
* negation з префіксом `!` і двома зірочками поряд з `_test.rego`).
|
|
343
|
+
* Перетворює glob-патерн (як у npm `files`) у `RegExp` з якорями `^` / `$`.
|
|
344
|
+
* Підтримує globstar (нуль або більше сегментів), `*` (символи без `/`) і `?`
|
|
345
|
+
* (один символ без `/`). Не підтримує brace-expansion і class `[…]` — у
|
|
346
|
+
* негативних патернах `files` цього достатньо для практичних випадків
|
|
347
|
+
* (приклад: negation з префіксом `!` і двома зірочками поряд з `_test.rego`).
|
|
346
348
|
* @param {string} glob posix-шлях у glob-нотації
|
|
347
|
-
* @returns {RegExp}
|
|
349
|
+
* @returns {RegExp} `RegExp` з якорями `^` / `$`
|
|
348
350
|
*/
|
|
349
351
|
export function globToRegex(glob) {
|
|
350
352
|
const parts = glob.split('/')
|
|
@@ -354,16 +356,16 @@ export function globToRegex(glob) {
|
|
|
354
356
|
for (const c of p) {
|
|
355
357
|
if (c === '*') out += '[^/]*'
|
|
356
358
|
else if (c === '?') out += '[^/]'
|
|
357
|
-
else if (
|
|
359
|
+
else if (REGEX_SPECIAL_IN_GLOB.has(c)) out += `\\${c}`
|
|
358
360
|
else out += c
|
|
359
361
|
}
|
|
360
362
|
return out
|
|
361
363
|
})
|
|
362
364
|
let re = tokens.join('/')
|
|
363
|
-
re = re.
|
|
364
|
-
re = re.replace(
|
|
365
|
-
re = re.replace(
|
|
366
|
-
re = re.
|
|
365
|
+
re = re.replaceAll('/__GLOBSTAR__/', '(?:/.*/|/)')
|
|
366
|
+
re = re.replace(GLOBSTAR_LEADING_RE, '(?:.*/)?')
|
|
367
|
+
re = re.replace(GLOBSTAR_TRAILING_RE, '(?:/.*)?')
|
|
368
|
+
re = re.replaceAll('__GLOBSTAR__', '.*')
|
|
367
369
|
return new RegExp(`^${re}$`, 'u')
|
|
368
370
|
}
|
|
369
371
|
|
|
@@ -378,9 +380,7 @@ export function globToRegex(glob) {
|
|
|
378
380
|
*/
|
|
379
381
|
async function collectPublishedFiles(filesField) {
|
|
380
382
|
const positives = filesField.filter(p => typeof p === 'string' && !p.startsWith('!'))
|
|
381
|
-
const negatives = filesField
|
|
382
|
-
.filter(p => typeof p === 'string' && p.startsWith('!'))
|
|
383
|
-
.map(p => globToRegex(p.slice(1)))
|
|
383
|
+
const negatives = filesField.filter(p => typeof p === 'string' && p.startsWith('!')).map(p => globToRegex(p.slice(1)))
|
|
384
384
|
/** @type {Set<string>} */
|
|
385
385
|
const collected = new Set()
|
|
386
386
|
for (const entry of positives) {
|
|
@@ -449,7 +449,7 @@ export function findTestFrameworkImport(content, virtualPath) {
|
|
|
449
449
|
*/
|
|
450
450
|
export async function classifyPublishedFileAsTest(relPath) {
|
|
451
451
|
const segments = relPath.split('/')
|
|
452
|
-
const base = segments
|
|
452
|
+
const base = segments.at(-1)
|
|
453
453
|
const dirs = segments.slice(0, -1)
|
|
454
454
|
const testDir = dirs.find(seg => TEST_DIR_NAMES.has(seg.toLowerCase()))
|
|
455
455
|
if (testDir) return `test-style каталог "${testDir}/"`
|
|
@@ -482,7 +482,7 @@ async function checkNoTestsInPublishedFiles(pass, fail) {
|
|
|
482
482
|
if (reason) violations.push({ file: rel, reason })
|
|
483
483
|
}
|
|
484
484
|
if (violations.length === 0) {
|
|
485
|
-
pass(`npm/: усі ${files.length} опублікованих файли без
|
|
485
|
+
pass(`npm/: усі ${files.length} опублікованих файли без тестів і fixtures`)
|
|
486
486
|
return
|
|
487
487
|
}
|
|
488
488
|
for (const v of violations) {
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Аналіз GitHub Actions workflow на правило «depcheck для path-scoped backend-пакета»
|
|
3
|
-
* (див. секцію в `npm/mdc/js-run.mdc`).
|
|
4
|
-
*
|
|
5
|
-
* Алгоритм для одного workspace-пакета (`<rootDir>`):
|
|
6
|
-
* 1. Шукаємо всі workflow, у яких `on.push.paths` або `on.pull_request.paths` містить
|
|
7
|
-
* glob, що починається з `<rootDir>/` — це означає, що workflow обмежено саме цим пакетом
|
|
8
|
-
* (повністю або частково).
|
|
9
|
-
* 2. У кожному такому workflow має бути крок, чий `run` починається з `npx depcheck …`,
|
|
10
|
-
* `working-directory` дорівнює `<rootDir>`, а список `--ignores="…"` містить
|
|
11
|
-
* щонайменше `graphql` і `bun` (інші значення допустимі).
|
|
12
|
-
*
|
|
13
|
-
* Якщо паттерн `paths:` стосується цього пакета, але крок depcheck відсутній / без потрібних
|
|
14
|
-
* ignores / у неправильному working-directory — фіксується порушення.
|
|
15
|
-
*
|
|
16
|
-
* Workflow без `paths:` або з глобальними патернами (`**\/*.js`, `npm/**`) ігноруються —
|
|
17
|
-
* вони не «належать» жодному окремому пакету і виходять за межі правила.
|
|
18
|
-
*/
|
|
19
|
-
import { readdir, readFile } from 'node:fs/promises'
|
|
20
|
-
import { join, relative } from 'node:path'
|
|
21
|
-
|
|
22
|
-
import { flattenWorkflowSteps, getStepRun, parseWorkflowYaml } from './gha-workflow.mjs'
|
|
23
|
-
|
|
24
|
-
const WORKFLOWS_DIR_REL = '.github/workflows'
|
|
25
|
-
const REQUIRED_IGNORES = ['graphql', 'bun']
|
|
26
|
-
// `npx depcheck` як ціла команда у одному рядку shell-скрипту.
|
|
27
|
-
// `[^\n]*` обмежено явним `\n`-stop'ом — `*` не може backtrack-нутися за межі рядка.
|
|
28
|
-
const DEPCHECK_RUN_RE = /(?:^|[\s;&|])npx[ \t]+depcheck\b([^\n]*)/u
|
|
29
|
-
// `--ignores=…` або `--ignores …` з трьома формами значення (двійкові, одинарні, без лапок).
|
|
30
|
-
// Розділювач — або `=` з опційними пробілами, або один+ пробіл. Альтернативи значення
|
|
31
|
-
// не перетинаються (стартують з різних символів), тож backtrack-у між ними нема.
|
|
32
|
-
const IGNORES_FLAG_RE = /--ignores(?:=[ \t]*|[ \t]+)(?:"([^"]*)"|'([^']*)'|([^\s"']+))/u
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Нормалізує шлях: бекслеші → forward, обрізає trailing-слеші. Без regex-у на trailing,
|
|
36
|
-
* щоб не тригерити `sonarjs/slow-regex` на `\/+$`.
|
|
37
|
-
* @param {string} p вхідний шлях
|
|
38
|
-
* @returns {string} нормалізований шлях
|
|
39
|
-
*/
|
|
40
|
-
function normalizePath(p) {
|
|
41
|
-
let end = p.length
|
|
42
|
-
while (end > 0) {
|
|
43
|
-
const cp = p.codePointAt(end - 1)
|
|
44
|
-
if (cp !== 47 && cp !== 92) break
|
|
45
|
-
end--
|
|
46
|
-
}
|
|
47
|
-
let out = end === p.length ? p : p.slice(0, end)
|
|
48
|
-
if (out.includes('\\')) out = out.replaceAll('\\', '/')
|
|
49
|
-
return out
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Чи містить workflow.on[event].paths хоча б один patten, що починається з `<pkgRoot>/`.
|
|
54
|
-
* @param {Record<string, unknown>} root корінь workflow
|
|
55
|
-
* @param {string} pkgRoot відносний (POSIX) шлях каталогу пакета (наприклад `cron-jobs/refund-loyalty-points`)
|
|
56
|
-
* @returns {boolean} `true`, якщо знайдено хоча б один підходящий glob
|
|
57
|
-
*/
|
|
58
|
-
export function workflowHasPathsScopedToPackage(root, pkgRoot) {
|
|
59
|
-
const prefix = `${normalizePath(pkgRoot)}/`
|
|
60
|
-
const on = root?.on
|
|
61
|
-
if (!on || typeof on !== 'object') return false
|
|
62
|
-
for (const event of /** @type {const} */ (['push', 'pull_request'])) {
|
|
63
|
-
const ev = /** @type {Record<string, unknown>} */ (on)[event]
|
|
64
|
-
if (!ev || typeof ev !== 'object') continue
|
|
65
|
-
const paths = /** @type {Record<string, unknown>} */ (ev).paths
|
|
66
|
-
if (!Array.isArray(paths)) continue
|
|
67
|
-
if (paths.some(p => typeof p === 'string' && p.startsWith(prefix))) return true
|
|
68
|
-
}
|
|
69
|
-
return false
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Розбирає `--ignores="a,b,c"` (також `--ignores=a,b`, single-quotes тощо) з аргументів `npx depcheck`.
|
|
74
|
-
* @param {string} depcheckArgs частина рядка `run` після `npx depcheck`
|
|
75
|
-
* @returns {string[] | null} масив значень ignores або `null`, якщо прапор відсутній
|
|
76
|
-
*/
|
|
77
|
-
export function parseDepcheckIgnoresArg(depcheckArgs) {
|
|
78
|
-
const m = IGNORES_FLAG_RE.exec(depcheckArgs)
|
|
79
|
-
if (!m) return null
|
|
80
|
-
const raw = m[1] ?? m[2] ?? m[3] ?? ''
|
|
81
|
-
return raw
|
|
82
|
-
.split(',')
|
|
83
|
-
.map(s => s.trim())
|
|
84
|
-
.filter(s => s.length > 0)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Шукає `npx depcheck` у `run` кроку. Повертає рядок аргументів після `npx depcheck` або `null`.
|
|
89
|
-
* @param {string} runText значення `run:` (можливо багаторядкове)
|
|
90
|
-
* @returns {string | null} текст аргументів depcheck або `null`
|
|
91
|
-
*/
|
|
92
|
-
export function extractDepcheckArgs(runText) {
|
|
93
|
-
if (typeof runText !== 'string' || runText.length === 0) return null
|
|
94
|
-
const m = DEPCHECK_RUN_RE.exec(runText)
|
|
95
|
-
return m ? m[1] : null
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Чи `working-directory` кроку дорівнює очікуваному pkgRoot (з нормалізацією слешів і хвостових `/`).
|
|
100
|
-
* @param {Record<string, unknown>} step об'єкт кроку
|
|
101
|
-
* @param {string} pkgRoot очікуваний шлях
|
|
102
|
-
* @returns {boolean} `true`, якщо збігаються
|
|
103
|
-
*/
|
|
104
|
-
export function stepWorkingDirectoryEquals(step, pkgRoot) {
|
|
105
|
-
const wd = step['working-directory']
|
|
106
|
-
if (typeof wd !== 'string') return false
|
|
107
|
-
return normalizePath(wd) === normalizePath(pkgRoot)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Перевіряє один workflow на наявність валідного depcheck-кроку для пакета.
|
|
112
|
-
* @param {Record<string, unknown>} root корінь workflow
|
|
113
|
-
* @param {string} pkgRoot відносний шлях пакета
|
|
114
|
-
* @returns {{ kind: 'ok' } | { kind: 'missing' } | { kind: 'wrong-cwd', actual: string } | { kind: 'missing-ignores', missing: string[] }} результат
|
|
115
|
-
*/
|
|
116
|
-
export function evaluateDepcheckStepForPackage(root, pkgRoot) {
|
|
117
|
-
/** @type {{ args: string, step: Record<string, unknown> }[]} */
|
|
118
|
-
const depcheckSteps = []
|
|
119
|
-
for (const { step } of flattenWorkflowSteps(root)) {
|
|
120
|
-
const args = extractDepcheckArgs(getStepRun(step))
|
|
121
|
-
if (args !== null) depcheckSteps.push({ args, step })
|
|
122
|
-
}
|
|
123
|
-
if (depcheckSteps.length === 0) return { kind: 'missing' }
|
|
124
|
-
|
|
125
|
-
// Серед усіх знайдених depcheck-кроків шукаємо хоча б один, що відповідає пакету.
|
|
126
|
-
const stepsForThisPackage = depcheckSteps.filter(s => stepWorkingDirectoryEquals(s.step, pkgRoot))
|
|
127
|
-
if (stepsForThisPackage.length === 0) {
|
|
128
|
-
const actual = depcheckSteps
|
|
129
|
-
.map(s => /** @type {string} */ (s.step['working-directory'] ?? '<repo root>'))
|
|
130
|
-
.join(', ')
|
|
131
|
-
return { kind: 'wrong-cwd', actual }
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
for (const { args } of stepsForThisPackage) {
|
|
135
|
-
const ignores = parseDepcheckIgnoresArg(args) ?? []
|
|
136
|
-
const missing = REQUIRED_IGNORES.filter(req => !ignores.includes(req))
|
|
137
|
-
if (missing.length === 0) return { kind: 'ok' }
|
|
138
|
-
}
|
|
139
|
-
// Усі знайдені кроки існують, але жоден не має повного списку обов'язкових ignores —
|
|
140
|
-
// повертаємо missing з першого, щоб дати конкретний фідбек.
|
|
141
|
-
const firstMissing = REQUIRED_IGNORES.filter(
|
|
142
|
-
req => !(parseDepcheckIgnoresArg(stepsForThisPackage[0].args) ?? []).includes(req)
|
|
143
|
-
)
|
|
144
|
-
return { kind: 'missing-ignores', missing: firstMissing }
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Зчитує всі `.github/workflows/*.yml` (без `*.yaml` — за правилом n-ga) з коренем у `repoRoot`.
|
|
149
|
-
* @param {string} repoRoot абсолютний корінь репозиторію
|
|
150
|
-
* @returns {Promise<{ relPath: string, content: string }[]>} список workflow-файлів
|
|
151
|
-
*/
|
|
152
|
-
export async function readAllWorkflowFiles(repoRoot) {
|
|
153
|
-
const dir = join(repoRoot, WORKFLOWS_DIR_REL)
|
|
154
|
-
/** @type {{ relPath: string, content: string }[]} */
|
|
155
|
-
const out = []
|
|
156
|
-
let entries
|
|
157
|
-
try {
|
|
158
|
-
entries = await readdir(dir, { withFileTypes: true })
|
|
159
|
-
} catch {
|
|
160
|
-
return out
|
|
161
|
-
}
|
|
162
|
-
for (const ent of entries) {
|
|
163
|
-
if (!ent.isFile() || !ent.name.endsWith('.yml')) continue
|
|
164
|
-
const abs = join(dir, ent.name)
|
|
165
|
-
const content = await readFile(abs, 'utf8')
|
|
166
|
-
out.push({ relPath: relative(repoRoot, abs).split('\\').join('/'), content })
|
|
167
|
-
}
|
|
168
|
-
return out
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Знаходить порушення правила depcheck для конкретного workspace-пакета.
|
|
173
|
-
*
|
|
174
|
-
* Повертає список повідомлень про порушення (порожній — все ok). Для кожного workflow,
|
|
175
|
-
* чий `paths:` обмежено до цього пакета, перевіряє, що серед кроків є валідний `npx depcheck`
|
|
176
|
-
* з потрібним `working-directory` та `--ignores`.
|
|
177
|
-
* @param {{ relPath: string, content: string }[]} workflows список workflow-файлів (з `readAllWorkflowFiles`)
|
|
178
|
-
* @param {string} pkgRoot відносний шлях workspace-пакета
|
|
179
|
-
* @returns {string[]} повідомлення про порушення, по одному на workflow
|
|
180
|
-
*/
|
|
181
|
-
export function findDepcheckViolationsForPackage(workflows, pkgRoot) {
|
|
182
|
-
/** @type {string[]} */
|
|
183
|
-
const violations = []
|
|
184
|
-
for (const { relPath, content } of workflows) {
|
|
185
|
-
const root = parseWorkflowYaml(content)
|
|
186
|
-
if (!root) continue
|
|
187
|
-
if (!workflowHasPathsScopedToPackage(root, pkgRoot)) continue
|
|
188
|
-
const result = evaluateDepcheckStepForPackage(root, pkgRoot)
|
|
189
|
-
if (result.kind === 'ok') continue
|
|
190
|
-
if (result.kind === 'missing') {
|
|
191
|
-
violations.push(
|
|
192
|
-
`${relPath}: paths обмежено до '${pkgRoot}/**', але немає кроку 'npx depcheck --ignores="graphql,bun"' з working-directory: ${pkgRoot}`
|
|
193
|
-
)
|
|
194
|
-
} else if (result.kind === 'wrong-cwd') {
|
|
195
|
-
violations.push(
|
|
196
|
-
`${relPath}: 'npx depcheck' знайдено, але working-directory не дорівнює '${pkgRoot}' (фактично: ${result.actual})`
|
|
197
|
-
)
|
|
198
|
-
} else {
|
|
199
|
-
violations.push(
|
|
200
|
-
`${relPath}: 'npx depcheck' у '${pkgRoot}' має містити --ignores з '${result.missing.join(',')}' (мінімум: graphql,bun)`
|
|
201
|
-
)
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return violations
|
|
205
|
-
}
|