@nitra/cursor 1.9.6 → 1.9.8
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 +20 -0
- package/mdc/ga.mdc +3 -1
- package/mdc/js-lint.mdc +20 -3
- 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 +2 -9
- 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.8] - 2026-05-12
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Корінь монорепо:** у `eslint.config.js` після `...getConfig(...)` додано перевизначення `sonarjs/cognitive-complexity` на `['warn', 20]` (поріг 20, severity `warn`).
|
|
12
|
+
|
|
13
|
+
## [1.9.7] - 2026-05-12
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **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.
|
|
18
|
+
|
|
19
|
+
- **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`.
|
|
20
|
+
|
|
21
|
+
- **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` під свою структуру.
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
|
25
|
+
- **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`.
|
|
26
|
+
|
|
7
27
|
## [1.9.6] - 2026-05-12
|
|
8
28
|
|
|
9
29
|
### Changed
|
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.18'
|
|
|
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"
|
|
@@ -61,6 +61,22 @@ version: '1.18'
|
|
|
61
61
|
.claude/worktrees/
|
|
62
62
|
```
|
|
63
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
|
+
]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Пакет `knip` окремо в `devDependencies` не додавай — `bunx knip` тягне його ad-hoc, як oxlint/eslint/jscpd.
|
|
79
|
+
|
|
64
80
|
## jscpd: рефакторинг і структура
|
|
65
81
|
|
|
66
82
|
Коли **jscpd** знаходить клони, спочатку зменшуй дублювання кодом, а не конфігом.
|
|
@@ -118,6 +134,7 @@ jobs:
|
|
|
118
134
|
bunx oxlint
|
|
119
135
|
bunx eslint .
|
|
120
136
|
bunx jscpd .
|
|
137
|
+
bunx knip
|
|
121
138
|
```
|
|
122
139
|
|
|
123
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'
|
|
@@ -385,9 +380,7 @@ export function globToRegex(glob) {
|
|
|
385
380
|
*/
|
|
386
381
|
async function collectPublishedFiles(filesField) {
|
|
387
382
|
const positives = filesField.filter(p => typeof p === 'string' && !p.startsWith('!'))
|
|
388
|
-
const negatives = filesField
|
|
389
|
-
.filter(p => typeof p === 'string' && p.startsWith('!'))
|
|
390
|
-
.map(p => globToRegex(p.slice(1)))
|
|
383
|
+
const negatives = filesField.filter(p => typeof p === 'string' && p.startsWith('!')).map(p => globToRegex(p.slice(1)))
|
|
391
384
|
/** @type {Set<string>} */
|
|
392
385
|
const collected = new Set()
|
|
393
386
|
for (const entry of positives) {
|
|
@@ -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
|
-
}
|