@nitra/cursor 1.9.4 → 1.9.6
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/.claude-template/npm-CLAUDE.md +10 -2
- package/CHANGELOG.md +17 -1
- package/mdc/js-lint.mdc +9 -3
- package/mdc/npm-module.mdc +14 -1
- package/package.json +1 -4
- package/policy/npm_module/npm_package_json/npm_package_json.rego +35 -8
- package/policy/npm_module/npm_package_json/npm_package_json_test.rego +81 -0
- package/scripts/check-npm-module.mjs +209 -1
|
@@ -21,9 +21,17 @@ npx @nitra/cursor check changelog
|
|
|
21
21
|
npx @nitra/cursor check npm-module
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
## Перш ніж писати `check-*.mjs`
|
|
24
|
+
## Перш ніж писати / розширювати `check-*.mjs`
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
**STOP — спершу пройди алгоритм Rego-first** (`.cursor/rules/conftest.mdc`, alwaysApply). Це стосується **і нової** перевірки, **і додавання нового deny у вже існуючий** `check-<rule>.mjs`: подивись `npm/policy/<rule>/`, чи задача не лягає у вже існуючий rego-пакет як ще одне `deny contains`.
|
|
27
|
+
|
|
28
|
+
Швидкий self-check для нової перевірки (порядок важливий):
|
|
29
|
+
|
|
30
|
+
1. **Це пер-документна перевірка одного JSON/YAML?** (наявність / форма поля, regex по значенню, перелік дозволених літералів). → **Rego, без JS-коду.** Пиши у `npm/policy/<rule>/<name>/<name>.rego` + `<name>_test.rego`.
|
|
31
|
+
2. Потрібен `readdir`, `stat`, парність файлів, AST-парсинг JS/TS, autofix, modeline до YAML-body? → **JS** у `check-<rule>.mjs`. Per-document частина (якщо є) усе одно лишається у rego — JS викликає її через `runConftestBatch`.
|
|
32
|
+
3. Не впевнений? Подивись референс **`npm/policy/k8s/*`** ↔ **`npm/scripts/check-k8s.mjs`** (Plan B: Rego-authoritative + JS-orchestrator) і список «що Rego об'єктивно не вміє» у `conftest.mdc`.
|
|
33
|
+
|
|
34
|
+
**Червоний прапор:** дописуєш `if (pkg.<field>) fail(…)` у JS — майже завжди це варто було робити як `deny contains msg if { … }` у відповідному rego-пакеті. Перевір `npm/policy/<rule>/` **перед** редагуванням `check-<rule>.mjs`.
|
|
27
35
|
|
|
28
36
|
## Джерело правил
|
|
29
37
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,27 @@
|
|
|
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.6] - 2026-05-12
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **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>/…`.
|
|
12
|
+
|
|
13
|
+
## [1.9.5] - 2026-05-12
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **npm-module — компактний пакет: whitelist `files`, без `devDependencies`, тести/фікстури поза опублікованим деревом:** правило `npm-module.mdc` тепер вимагає максимально компактний tarball. (1) Поле `"files"` у `npm/package.json` обовʼязкове як whitelist (без нього npm пакує майже все). (2) `npm/package.json` не повинен містити `devDependencies` — інструментарій для розробки тримаємо у кореневому `package.json` монорепо, щоб `npm install @nitra/<pkg>` не тягнув його кінцевим користувачам. (3) Тести й фікстури не повинні потрапляти у tarball: канонічне місце — `npm/tests/` (не додається до `"files"`); це стосується і test-style каталогів (`tests/`, `__tests__/`, `fixtures/`, `__fixtures__/`, `spec/`, `test/`), і файлів за патернами `*.test.*` / `*.spec.*`, і JS/TS-файлів з імпортами test-фреймворків (`bun:test`, `node:test`, `vitest`, `@jest/globals`, `mocha`, `jest`, `ava`, …). **Виняток — Rego (`*_test.rego`):** за конвенцією conftest юніт-тест лежить поруч з полісі у тому самому `package`, тож rego-тести дозволені всередині опублікованого `policy/`-каталогу і входять у tarball.
|
|
18
|
+
- **npm-module — пер-документні deny у rego (Rego-authoritative):** `npm/policy/npm_module/npm_package_json/npm_package_json.rego` розширено двома deny: (а) `"files"` як whitelist обовʼязковий (відсутній / не масив / порожній); (б) `"devDependencies"` мають бути відсутні або порожні. Додано `npm_package_json_test.rego` з happy-path + 7 негативних кейсів (`json.patch` фікстури). Покривається `bun run lint-rego` (`conftest verify`) і `bun run lint-conftest` (батч проти реального `npm/package.json`). Раніше я помилково реалізував ці перевірки у JS — це порушує `.cursor/rules/conftest.mdc` (Rego-default для пер-документних структурних перевірок). Тепер виправлено: JS-функцію `checkPackageCompactness` видалено з `check-npm-module.mjs` разом з виклик-сайтом.
|
|
19
|
+
- **npm-module — `check-npm-module.mjs` лишає лише FS/AST-частину:** функція `checkNoTestsInPublishedFiles` резолвить позитивні patterns поля `files`, віднімає негативні (підтримка `!…` glob з `*` / `**` / `?`), і для кожного файлу-кандидата ловить test-style ім'я каталога/файлу або імпорт тест-фреймворку через oxc-parser (`module.staticImports` + `require()` + динамічний `import()`). `*_test.rego` свідомо не входить у `TEST_FILE_PATTERNS` — дозволений виняток для conftest-конвенції (юніт-тест поруч з полісі у тому самому `package`).
|
|
20
|
+
- **npm/package.json — приведено до правила:** видалено секцію `devDependencies` (`@nitra/cursor` вже є у корені як `workspace:*`). `policy/**/*_test.rego` свідомо лишаються у tarball — як виняток для conftest-конвенції.
|
|
21
|
+
- **conftest.mdc + npm/.claude-template/npm-CLAUDE.md — гостріший Rego-first сигнал:** у `.cursor/rules/conftest.mdc` додано STOP-блок перед `Edit` будь-якого `check-<rule>.mjs` (стосується і нових перевірок, і розширення вже існуючих; типовий ляп — `if (pkg.<field>) fail(…)` у JS замість ще одного `deny contains` у відповідному rego-пакеті). Перший пункт алгоритму уточнено прикладом «заборона/наявність ключа верхнього рівня типу `devDependencies` / `scripts.<name>`». У `npm-CLAUDE.md` секцію «Перш ніж писати `check-*.mjs`» переписано у self-check з 3 пунктів і червоним прапором. Регенеровано `npm/CLAUDE.md`.
|
|
22
|
+
|
|
7
23
|
## [1.9.4] - 2026-05-11
|
|
8
24
|
|
|
9
25
|
### Removed
|
|
10
26
|
|
|
11
|
-
- **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` з
|
|
27
|
+
- **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 без правила немає сенсу.
|
|
12
28
|
|
|
13
29
|
## [1.9.3] - 2026-05-11
|
|
14
30
|
|
package/mdc/js-lint.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Перевірка JavaScript коду
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.18'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
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 не додавай без потреби монорепо.
|
|
@@ -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,14 @@ version: '1.17'
|
|
|
51
53
|
"exitCode": 1,
|
|
52
54
|
"reporters": ["console"],
|
|
53
55
|
"minLines": 25,
|
|
54
|
-
"ignore": ["**/dist/**"]
|
|
56
|
+
"ignore": [".claude/worktrees/**", "**/dist/**"]
|
|
55
57
|
}
|
|
56
58
|
```
|
|
57
59
|
|
|
60
|
+
```text title=".gitignore (фрагмент)"
|
|
61
|
+
.claude/worktrees/
|
|
62
|
+
```
|
|
63
|
+
|
|
58
64
|
## jscpd: рефакторинг і структура
|
|
59
65
|
|
|
60
66
|
Коли **jscpd** знаходить клони, спочатку зменшуй дублювання кодом, а не конфігом.
|
package/mdc/npm-module.mdc
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Оформлення репозиторію для npm модуля
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.11'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
Bun monorepo: workspace **`npm/`**, кореневий **`package.json`**, **`.github/workflows/`**; опційно **`demo/`**.
|
|
8
8
|
|
|
9
|
+
## Компактний пакет
|
|
10
|
+
|
|
11
|
+
Мета — **максимально компактний** опублікований пакет: у npm потрапляє тільки те, що потрібно під час `require`/`import` користувачем.
|
|
12
|
+
|
|
13
|
+
- **`"files"` обовʼязковий** у `npm/package.json` як **whitelist** того, що публікується (без `"files"` npm пакує майже все — це антипатерн для цього правила).
|
|
14
|
+
- **Тести й фікстури — поза будь-яким шляхом з `"files"`.** Канонічне місце для всього тестового — `npm/tests/` (його **не** додаємо до `"files"`). Це стосується **і** фреймворк-тестів (`bun:test`, `node:test`, `vitest`, `@jest/globals`, `mocha`, …), **і** test-style каталогів (`tests/`, `__tests__/`, `fixtures/`, `__fixtures__/`, `spec/`), **і** файлів за патернами `*.test.*` / `*.spec.*`. **Виняток — Rego (`*_test.rego`):** за конвенцією conftest юніт-тест лежить поруч з полісі у тому самому `package`, тож rego-тести дозволені всередині опублікованого `policy/`-каталогу.
|
|
15
|
+
- **Лише runtime-залежності у `npm/package.json`.** `devDependencies` тримай у **кореневому** `package.json` монорепо — тоді `npm install @nitra/<pkg>` не тягне інструментарій, потрібний лише для розробки самого пакета.
|
|
16
|
+
|
|
17
|
+
Деталі алгоритму перевірки:
|
|
18
|
+
|
|
19
|
+
- Пер-документні правила для `npm/package.json` (whitelist `files`, заборона `devDependencies`, форма `types`) — у rego-пакеті `npm_module.npm_package_json` (`npm/policy/npm_module/npm_package_json/`).
|
|
20
|
+
- Скан опублікованого простору файлів на тест-патерни (walking + AST JS/TS) — у `check-npm-module.mjs::checkNoTestsInPublishedFiles` (FS / AST не лягають у rego).
|
|
21
|
+
|
|
9
22
|
## TypeScript declaration (`npm/types`)
|
|
10
23
|
|
|
11
24
|
Файл **`npm/package.json`** має містити **`"types"`** (шлях до головного `.d.ts` або `.d.mts` під **`./types/…`**) і запис **`"types"`** у **`files`**, щоб npm публікував декларації.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/cursor",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.6",
|
|
4
4
|
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -45,9 +45,6 @@
|
|
|
45
45
|
"oxc-parser": "^0.128.0",
|
|
46
46
|
"yaml": "^2.8.3"
|
|
47
47
|
},
|
|
48
|
-
"devDependencies": {
|
|
49
|
-
"@nitra/cursor": "^1.8.170"
|
|
50
|
-
},
|
|
51
48
|
"engines": {
|
|
52
49
|
"bun": ">=1.3",
|
|
53
50
|
"node": ">=25"
|
|
@@ -5,13 +5,18 @@
|
|
|
5
5
|
# conftest test npm/package.json -p npm/policy/npm_module \
|
|
6
6
|
# --namespace npm_module.npm_package_json
|
|
7
7
|
#
|
|
8
|
-
# Перевіряє:
|
|
9
|
-
# - `./types/index.d.ts`
|
|
10
|
-
#
|
|
8
|
+
# Перевіряє:
|
|
9
|
+
# - поле `types` має один із двох канонічних патернів: `./types/index.d.ts`
|
|
10
|
+
# (layout `npm/src` з `.js`) або `./types/<…>.d.ts`/`.d.mts` (emit-types);
|
|
11
|
+
# - масив `files` присутній, непорожній і містить `"types"` (whitelist
|
|
12
|
+
# обовʼязковий — без нього npm пакує майже все);
|
|
13
|
+
# - `devDependencies` відсутні або порожні: dev-інструментарій тримаємо у
|
|
14
|
+
# кореневому `package.json` монорепо, щоб `npm install @nitra/<pkg>` його
|
|
15
|
+
# не тягнув (npm-module.mdc: компактний пакет).
|
|
11
16
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
17
|
+
# Те, який саме типовий layout активний (наявність `.js` під `npm/src`),
|
|
18
|
+
# існування файлу зі шляху `types` і скан тест-патернів у tarball — у
|
|
19
|
+
# JS-перевірці (`check-npm-module.mjs`: cross-file / FS-access / AST).
|
|
15
20
|
#
|
|
16
21
|
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
17
22
|
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
@@ -27,6 +32,12 @@ types_field_template := concat(" ", [
|
|
|
27
32
|
"або \"./types/<…>.d.ts|.d.mts\" (зараз: %v) (npm-module.mdc)",
|
|
28
33
|
])
|
|
29
34
|
|
|
35
|
+
# Шаблон повідомлення про присутність `devDependencies`.
|
|
36
|
+
dev_deps_template := concat(" ", [
|
|
37
|
+
"npm/package.json: \"devDependencies\" не публікуються користувачам пакета —",
|
|
38
|
+
"перенеси у кореневий package.json: %v (npm-module.mdc: компактний пакет)",
|
|
39
|
+
])
|
|
40
|
+
|
|
30
41
|
# ── deny: types ────────────────────────────────────────────────────────────
|
|
31
42
|
|
|
32
43
|
deny contains msg if {
|
|
@@ -35,19 +46,35 @@ deny contains msg if {
|
|
|
35
46
|
msg := sprintf(types_field_template, [types_field])
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
# ── deny: files має містити "types"
|
|
49
|
+
# ── deny: files має існувати, бути непорожнім, містити "types" ────────────
|
|
39
50
|
|
|
40
51
|
deny contains msg if {
|
|
41
52
|
not is_array(object.get(input, "files", null))
|
|
42
|
-
msg := "npm/package.json:
|
|
53
|
+
msg := "npm/package.json: обовʼязковий whitelist \"files\" (без нього npm пакує майже все) (npm-module.mdc)"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
deny contains msg if {
|
|
57
|
+
is_array(input.files)
|
|
58
|
+
count(input.files) == 0
|
|
59
|
+
msg := "npm/package.json: масив \"files\" не повинен бути порожнім (npm-module.mdc: компактний пакет)"
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
deny contains msg if {
|
|
46
63
|
is_array(input.files)
|
|
64
|
+
count(input.files) > 0
|
|
47
65
|
not "types" in {f | some f in input.files}
|
|
48
66
|
msg := "npm/package.json: масив \"files\" має містити \"types\" (npm-module.mdc)"
|
|
49
67
|
}
|
|
50
68
|
|
|
69
|
+
# ── deny: жодних devDependencies у npm/package.json ───────────────────────
|
|
70
|
+
|
|
71
|
+
deny contains msg if {
|
|
72
|
+
dev := object.get(input, "devDependencies", {})
|
|
73
|
+
count(dev) > 0
|
|
74
|
+
names := concat(", ", sort([n | some n, _ in dev]))
|
|
75
|
+
msg := sprintf(dev_deps_template, [names])
|
|
76
|
+
}
|
|
77
|
+
|
|
51
78
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
52
79
|
|
|
53
80
|
valid_types_field("./types/index.d.ts")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Тести для `npm_module.npm_package_json`. Запуск:
|
|
2
|
+
# conftest verify -p npm/policy/npm_module/npm_package_json
|
|
3
|
+
package npm_module.npm_package_json_test
|
|
4
|
+
|
|
5
|
+
import rego.v1
|
|
6
|
+
|
|
7
|
+
import data.npm_module.npm_package_json
|
|
8
|
+
|
|
9
|
+
valid_pkg := {
|
|
10
|
+
"name": "@nitra/cursor",
|
|
11
|
+
"version": "1.9.5",
|
|
12
|
+
"types": "./types/bin/n-cursor.d.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"types",
|
|
15
|
+
"mdc",
|
|
16
|
+
"bin",
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {"oxc-parser": "^0.128.0"},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# ── happy path ────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
test_allow_canonical if {
|
|
25
|
+
count(npm_package_json.deny) == 0 with input as valid_pkg
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test_allow_types_index_d_ts if {
|
|
29
|
+
pkg := json.patch(valid_pkg, [{"op": "replace", "path": "/types", "value": "./types/index.d.ts"}])
|
|
30
|
+
count(npm_package_json.deny) == 0 with input as pkg
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# ── types ─────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
test_deny_types_outside_types_dir if {
|
|
36
|
+
pkg := json.patch(valid_pkg, [{"op": "replace", "path": "/types", "value": "./dist/index.d.ts"}])
|
|
37
|
+
count(npm_package_json.deny) > 0 with input as pkg
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test_deny_types_wrong_extension if {
|
|
41
|
+
pkg := json.patch(valid_pkg, [{"op": "replace", "path": "/types", "value": "./types/index.ts"}])
|
|
42
|
+
count(npm_package_json.deny) > 0 with input as pkg
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# ── files ─────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
test_deny_missing_files if {
|
|
48
|
+
pkg := json.patch(valid_pkg, [{"op": "remove", "path": "/files"}])
|
|
49
|
+
count(npm_package_json.deny) > 0 with input as pkg
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test_deny_files_not_array if {
|
|
53
|
+
pkg := json.patch(valid_pkg, [{"op": "replace", "path": "/files", "value": "types"}])
|
|
54
|
+
count(npm_package_json.deny) > 0 with input as pkg
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test_deny_files_empty if {
|
|
58
|
+
pkg := json.patch(valid_pkg, [{"op": "replace", "path": "/files", "value": []}])
|
|
59
|
+
count(npm_package_json.deny) > 0 with input as pkg
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test_deny_files_without_types if {
|
|
63
|
+
pkg := json.patch(valid_pkg, [{"op": "replace", "path": "/files", "value": ["bin", "mdc"]}])
|
|
64
|
+
count(npm_package_json.deny) > 0 with input as pkg
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# ── devDependencies ──────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
test_allow_no_dev_dependencies if {
|
|
70
|
+
count(npm_package_json.deny) == 0 with input as valid_pkg
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
test_allow_empty_dev_dependencies if {
|
|
74
|
+
pkg := json.patch(valid_pkg, [{"op": "add", "path": "/devDependencies", "value": {}}])
|
|
75
|
+
count(npm_package_json.deny) == 0 with input as pkg
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test_deny_dev_dependencies_present if {
|
|
79
|
+
pkg := json.patch(valid_pkg, [{"op": "add", "path": "/devDependencies", "value": {"@nitra/cursor": "^1.9.5"}}])
|
|
80
|
+
count(npm_package_json.deny) > 0 with input as pkg
|
|
81
|
+
}
|
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Поля workflow перевіряються після **YAML parse**, щоб не плутати з коментарями.
|
|
13
13
|
*
|
|
14
|
+
* Компактність опублікованого пакета (cross-file / FS / AST частина):
|
|
15
|
+
* - Пер-документні структурні deny для `npm/package.json` (`files` whitelist обовʼязковий,
|
|
16
|
+
* без `devDependencies`) — у rego-пакеті `npm_module.npm_package_json` (Rego-authoritative).
|
|
17
|
+
* - Тут лишається лише `checkNoTestsInPublishedFiles`: walk шляхів з `"files"` (з урахуванням
|
|
18
|
+
* негативних glob-патернів) і скан test-style каталогів (`tests/`, `__tests__/`, `fixtures/`,
|
|
19
|
+
* `__fixtures__/`, `spec/`, `test/`), імен файлів (`*.test.*` / `*.spec.*`) і AST-імпортів
|
|
20
|
+
* test-фреймворків (`bun:test`, `node:test`, `vitest`, `@jest/globals`, `mocha`, `jest`, `ava`, …).
|
|
21
|
+
* Виняток: `*_test.rego` дозволені поруч з полісі — це конвенція conftest.
|
|
22
|
+
*
|
|
14
23
|
* Версія та CHANGELOG: перший заголовок `## [version]` у `npm/CHANGELOG.md` має збігатися з `version` у
|
|
15
24
|
* `npm/package.json` (найсвіжіший реліз зверху). Якщо в git є незакомічені зміни під `npm/`, `version` у робочому
|
|
16
25
|
* файлі має відрізнятися від `HEAD` — інакше типовий пропуск bump після правок у пакеті.
|
|
@@ -18,9 +27,17 @@
|
|
|
18
27
|
import { execFile } from 'node:child_process'
|
|
19
28
|
import { existsSync } from 'node:fs'
|
|
20
29
|
import { readFile, stat } from 'node:fs/promises'
|
|
21
|
-
import { join } from 'node:path'
|
|
30
|
+
import { join, sep } from 'node:path'
|
|
22
31
|
import { promisify } from 'node:util'
|
|
23
32
|
|
|
33
|
+
import { parseSync } from 'oxc-parser'
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
dynamicImportModule,
|
|
37
|
+
langFromPath,
|
|
38
|
+
requireCallModule,
|
|
39
|
+
walkAstWithAncestors
|
|
40
|
+
} from './utils/ast-scan-utils.mjs'
|
|
24
41
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
25
42
|
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
26
43
|
import { walkDir } from './utils/walkDir.mjs'
|
|
@@ -36,6 +53,40 @@ const PACKAGE_JSON_VERSION_RE = /"version":\s*"([^"]+)"/u
|
|
|
36
53
|
/** Файл проєкту TypeScript для emit без каталогу `src` (див. npm-module.mdc) */
|
|
37
54
|
const EMIT_TYPES_CONFIG = 'npm/tsconfig.emit-types.json'
|
|
38
55
|
|
|
56
|
+
/** Каталоги, які за конвенцією тримають тести / фікстури і не повинні публікуватися. */
|
|
57
|
+
const TEST_DIR_NAMES = new Set(['tests', '__tests__', 'fixtures', '__fixtures__', 'spec', 'test'])
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Імена файлів за патернами test/spec (тільки basename, без path). Rego
|
|
61
|
+
* (`*_test.rego`) свідомо не входить: за конвенцією conftest юніт-тест лежить
|
|
62
|
+
* поруч з полісі у тому самому `package` — і це дозволений виняток усередині
|
|
63
|
+
* опублікованого `policy/`-каталогу (npm-module.mdc).
|
|
64
|
+
*/
|
|
65
|
+
const TEST_FILE_PATTERNS = [/^.+\.(test|spec)\.[cm]?[jt]sx?$/iu]
|
|
66
|
+
|
|
67
|
+
/** Розширення, у яких ловимо імпорти test-фреймворків. */
|
|
68
|
+
const JS_LIKE_EXT_RE = /\.[cm]?[jt]sx?$/iu
|
|
69
|
+
|
|
70
|
+
/** Імпорти/require/dynamic-import, які видають test-файл. */
|
|
71
|
+
const TEST_FRAMEWORK_MODULES = new Set([
|
|
72
|
+
'bun:test',
|
|
73
|
+
'node:test',
|
|
74
|
+
'vitest',
|
|
75
|
+
'@jest/globals',
|
|
76
|
+
'jest',
|
|
77
|
+
'mocha',
|
|
78
|
+
'ava',
|
|
79
|
+
'tap',
|
|
80
|
+
'tape'
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
/** Символи у glob-сегменті, які треба екранувати для RegExp (без `*` / `?` — їх обробляємо окремо). */
|
|
84
|
+
const REGEX_SPECIAL_IN_GLOB = new Set(['.', '+', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\'])
|
|
85
|
+
|
|
86
|
+
/** Збіги для post-обробки glob → regex після злиття сегментів через `/` (див. `globToRegex`). */
|
|
87
|
+
const GLOBSTAR_LEADING_RE = /^__GLOBSTAR__\//u
|
|
88
|
+
const GLOBSTAR_TRAILING_RE = /\/__GLOBSTAR__$/u
|
|
89
|
+
|
|
39
90
|
/**
|
|
40
91
|
* Чи є під `npm/src` хоча б один `.js` (рекурсивно).
|
|
41
92
|
* @param {string[]} [ignorePaths] абсолютні шляхи каталогів, повністю виключених з обходу
|
|
@@ -293,6 +344,162 @@ function checkPublishWorkflow(passFn, failFn) {
|
|
|
293
344
|
}
|
|
294
345
|
}
|
|
295
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Перетворює glob-патерн (як у npm `files`) у `RegExp` з якорями `^` / `$`.
|
|
349
|
+
* Підтримує globstar (нуль або більше сегментів), `*` (символи без `/`) і `?`
|
|
350
|
+
* (один символ без `/`). Не підтримує brace-expansion і class `[…]` — у
|
|
351
|
+
* негативних патернах `files` цього достатньо для практичних випадків
|
|
352
|
+
* (приклад: negation з префіксом `!` і двома зірочками поряд з `_test.rego`).
|
|
353
|
+
* @param {string} glob posix-шлях у glob-нотації
|
|
354
|
+
* @returns {RegExp} `RegExp` з якорями `^` / `$`
|
|
355
|
+
*/
|
|
356
|
+
export function globToRegex(glob) {
|
|
357
|
+
const parts = glob.split('/')
|
|
358
|
+
const tokens = parts.map(p => {
|
|
359
|
+
if (p === '**') return '__GLOBSTAR__'
|
|
360
|
+
let out = ''
|
|
361
|
+
for (const c of p) {
|
|
362
|
+
if (c === '*') out += '[^/]*'
|
|
363
|
+
else if (c === '?') out += '[^/]'
|
|
364
|
+
else if (REGEX_SPECIAL_IN_GLOB.has(c)) out += `\\${c}`
|
|
365
|
+
else out += c
|
|
366
|
+
}
|
|
367
|
+
return out
|
|
368
|
+
})
|
|
369
|
+
let re = tokens.join('/')
|
|
370
|
+
re = re.replaceAll('/__GLOBSTAR__/', '(?:/.*/|/)')
|
|
371
|
+
re = re.replace(GLOBSTAR_LEADING_RE, '(?:.*/)?')
|
|
372
|
+
re = re.replace(GLOBSTAR_TRAILING_RE, '(?:/.*)?')
|
|
373
|
+
re = re.replaceAll('__GLOBSTAR__', '.*')
|
|
374
|
+
return new RegExp(`^${re}$`, 'u')
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Збирає список файлів, що потраплять у tarball, виходячи з `files` у
|
|
379
|
+
* `npm/package.json`. Підтримує позитивні patterns (директорії або файли) і
|
|
380
|
+
* негативні (`!…`). Шляхи повертаються у posix-формі, відносно `npm/`.
|
|
381
|
+
* Не пробує дублювати всю логіку `npm pack` (license/readme/тощо) — тут лише
|
|
382
|
+
* простір імен `files`, бо саме його сканує check.
|
|
383
|
+
* @param {string[]} filesField значення поля `files`
|
|
384
|
+
* @returns {Promise<string[]>} відсортовані posix-шляхи без `npm/` префікса
|
|
385
|
+
*/
|
|
386
|
+
async function collectPublishedFiles(filesField) {
|
|
387
|
+
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)))
|
|
391
|
+
/** @type {Set<string>} */
|
|
392
|
+
const collected = new Set()
|
|
393
|
+
for (const entry of positives) {
|
|
394
|
+
const fullPath = join('npm', entry)
|
|
395
|
+
if (!existsSync(fullPath)) continue
|
|
396
|
+
const s = await stat(fullPath)
|
|
397
|
+
if (s.isFile()) {
|
|
398
|
+
collected.add(entry)
|
|
399
|
+
continue
|
|
400
|
+
}
|
|
401
|
+
if (!s.isDirectory()) continue
|
|
402
|
+
await walkDir(fullPath, p => {
|
|
403
|
+
const rel = p.slice('npm/'.length).split(sep).join('/')
|
|
404
|
+
collected.add(rel)
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
const filtered = [...collected].filter(rel => !negatives.some(re => re.test(rel)))
|
|
408
|
+
filtered.sort()
|
|
409
|
+
return filtered
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Чи є у файлі імпорт/require/dynamic-import з модуля тест-фреймворку.
|
|
414
|
+
* Парсимо через oxc-parser (як `bunyan-imports`/`redis-imports`). При помилці
|
|
415
|
+
* парсингу повертаємо `null` — це не наш checker для синтаксису.
|
|
416
|
+
* @param {string} content повний текст файлу
|
|
417
|
+
* @param {string} virtualPath шлях для вибору `lang`
|
|
418
|
+
* @returns {string | null} модуль, через який видно тест, або `null`
|
|
419
|
+
*/
|
|
420
|
+
export function findTestFrameworkImport(content, virtualPath) {
|
|
421
|
+
const lang = langFromPath(virtualPath)
|
|
422
|
+
let result
|
|
423
|
+
try {
|
|
424
|
+
result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
|
|
425
|
+
} catch {
|
|
426
|
+
return null
|
|
427
|
+
}
|
|
428
|
+
if (result.errors?.length) return null
|
|
429
|
+
for (const imp of result.module?.staticImports ?? []) {
|
|
430
|
+
const mod = imp.moduleRequest?.value
|
|
431
|
+
if (typeof mod === 'string' && TEST_FRAMEWORK_MODULES.has(mod)) return mod
|
|
432
|
+
}
|
|
433
|
+
/** @type {string | null} */
|
|
434
|
+
let found = null
|
|
435
|
+
walkAstWithAncestors(result.program, [], node => {
|
|
436
|
+
if (found) return
|
|
437
|
+
const reqMod = requireCallModule(node)
|
|
438
|
+
if (reqMod && TEST_FRAMEWORK_MODULES.has(reqMod)) {
|
|
439
|
+
found = reqMod
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
const dynMod = dynamicImportModule(node)
|
|
443
|
+
if (dynMod && TEST_FRAMEWORK_MODULES.has(dynMod)) {
|
|
444
|
+
found = dynMod
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
return found
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Класифікує опублікований файл як test/fixture, якщо хоча б одна з ознак:
|
|
452
|
+
* (1) у шляху є каталог із `TEST_DIR_NAMES`; (2) basename відповідає
|
|
453
|
+
* `TEST_FILE_PATTERNS`; (3) для JS/TS-розширень — імпорт test-фреймворку.
|
|
454
|
+
* @param {string} relPath posix-шлях відносно `npm/`
|
|
455
|
+
* @returns {Promise<string | null>} причина порушення або `null`
|
|
456
|
+
*/
|
|
457
|
+
export async function classifyPublishedFileAsTest(relPath) {
|
|
458
|
+
const segments = relPath.split('/')
|
|
459
|
+
const base = segments.at(-1)
|
|
460
|
+
const dirs = segments.slice(0, -1)
|
|
461
|
+
const testDir = dirs.find(seg => TEST_DIR_NAMES.has(seg.toLowerCase()))
|
|
462
|
+
if (testDir) return `test-style каталог "${testDir}/"`
|
|
463
|
+
if (TEST_FILE_PATTERNS.some(re => re.test(base))) return `test-style ім'я файлу`
|
|
464
|
+
if (JS_LIKE_EXT_RE.test(base)) {
|
|
465
|
+
const content = await readFile(join('npm', relPath), 'utf8')
|
|
466
|
+
const mod = findTestFrameworkImport(content, relPath)
|
|
467
|
+
if (mod) return `імпорт test-фреймворку "${mod}"`
|
|
468
|
+
}
|
|
469
|
+
return null
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Для всіх файлів, що потрапили б у tarball (positive `files` мінус negative
|
|
474
|
+
* patterns), забороняє test-style каталоги/імена/імпорти. Так пакет лишається
|
|
475
|
+
* компактним і не везе користувачам тести й фікстури.
|
|
476
|
+
* @param {(msg: string) => void} pass callback при успіху
|
|
477
|
+
* @param {(msg: string) => void} fail callback при порушенні
|
|
478
|
+
* @returns {Promise<void>}
|
|
479
|
+
*/
|
|
480
|
+
async function checkNoTestsInPublishedFiles(pass, fail) {
|
|
481
|
+
if (!existsSync('npm/package.json')) return
|
|
482
|
+
const pkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
|
|
483
|
+
if (!Array.isArray(pkg.files)) return
|
|
484
|
+
const files = await collectPublishedFiles(pkg.files)
|
|
485
|
+
/** @type {{ file: string, reason: string }[]} */
|
|
486
|
+
const violations = []
|
|
487
|
+
for (const rel of files) {
|
|
488
|
+
const reason = await classifyPublishedFileAsTest(rel)
|
|
489
|
+
if (reason) violations.push({ file: rel, reason })
|
|
490
|
+
}
|
|
491
|
+
if (violations.length === 0) {
|
|
492
|
+
pass(`npm/: усі ${files.length} опублікованих файли без тестів і fixtures`)
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
for (const v of violations) {
|
|
496
|
+
fail(
|
|
497
|
+
`npm/${v.file}: ${v.reason} — винеси за межі шляхів з "files" або додай негативний glob ` +
|
|
498
|
+
'(наприклад "!**/*_test.rego") у npm/package.json (npm-module.mdc)'
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
296
503
|
/**
|
|
297
504
|
* Перевіряє базову структуру монорепо: наявність каталогу `npm/` і
|
|
298
505
|
* `npm/package.json`. Поле `workspaces ∋ "npm"` у кореневому `package.json`
|
|
@@ -334,6 +541,7 @@ export async function check() {
|
|
|
334
541
|
const { pass, fail } = reporter
|
|
335
542
|
|
|
336
543
|
await checkNpmModuleBasicStructure(pass, fail)
|
|
544
|
+
await checkNoTestsInPublishedFiles(pass, fail)
|
|
337
545
|
|
|
338
546
|
const ignorePaths = await loadCursorIgnorePaths(process.cwd())
|
|
339
547
|
const useSrcJsLayout = await npmSrcTreeHasJsFile(ignorePaths)
|