@nitra/cursor 1.9.4 → 1.9.5
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 +10 -0
- 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 +202 -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,6 +4,16 @@
|
|
|
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.5] - 2026-05-12
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **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.
|
|
12
|
+
- **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` разом з виклик-сайтом.
|
|
13
|
+
- **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`).
|
|
14
|
+
- **npm/package.json — приведено до правила:** видалено секцію `devDependencies` (`@nitra/cursor` вже є у корені як `workspace:*`). `policy/**/*_test.rego` свідомо лишаються у tarball — як виняток для conftest-конвенції.
|
|
15
|
+
- **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`.
|
|
16
|
+
|
|
7
17
|
## [1.9.4] - 2026-05-11
|
|
8
18
|
|
|
9
19
|
### Removed
|
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.5",
|
|
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,33 @@ 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
|
+
|
|
39
83
|
/**
|
|
40
84
|
* Чи є під `npm/src` хоча б один `.js` (рекурсивно).
|
|
41
85
|
* @param {string[]} [ignorePaths] абсолютні шляхи каталогів, повністю виключених з обходу
|
|
@@ -293,6 +337,162 @@ function checkPublishWorkflow(passFn, failFn) {
|
|
|
293
337
|
}
|
|
294
338
|
}
|
|
295
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Перетворює glob-патерн (як у npm `files`) у анкоровану `RegExp`. Підтримує
|
|
342
|
+
* globstar (нуль або більше сегментів), `*` (символи без `/`) і `?` (один
|
|
343
|
+
* символ без `/`). Не підтримує brace-expansion і class `[…]` — у негативних
|
|
344
|
+
* патернах `files` цього достатньо для практичних випадків (приклад:
|
|
345
|
+
* negation з префіксом `!` і двома зірочками поряд з `_test.rego`).
|
|
346
|
+
* @param {string} glob posix-шлях у glob-нотації
|
|
347
|
+
* @returns {RegExp} регулярка, анкорована з обох боків
|
|
348
|
+
*/
|
|
349
|
+
export function globToRegex(glob) {
|
|
350
|
+
const parts = glob.split('/')
|
|
351
|
+
const tokens = parts.map(p => {
|
|
352
|
+
if (p === '**') return '__GLOBSTAR__'
|
|
353
|
+
let out = ''
|
|
354
|
+
for (const c of p) {
|
|
355
|
+
if (c === '*') out += '[^/]*'
|
|
356
|
+
else if (c === '?') out += '[^/]'
|
|
357
|
+
else if ('.+^${}()|[]\\'.includes(c)) out += `\\${c}`
|
|
358
|
+
else out += c
|
|
359
|
+
}
|
|
360
|
+
return out
|
|
361
|
+
})
|
|
362
|
+
let re = tokens.join('/')
|
|
363
|
+
re = re.replace(/\/__GLOBSTAR__\//gu, '(?:/.*/|/)')
|
|
364
|
+
re = re.replace(/^__GLOBSTAR__\//u, '(?:.*/)?')
|
|
365
|
+
re = re.replace(/\/__GLOBSTAR__$/u, '(?:/.*)?')
|
|
366
|
+
re = re.replace(/__GLOBSTAR__/gu, '.*')
|
|
367
|
+
return new RegExp(`^${re}$`, 'u')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Збирає список файлів, що потраплять у tarball, виходячи з `files` у
|
|
372
|
+
* `npm/package.json`. Підтримує позитивні patterns (директорії або файли) і
|
|
373
|
+
* негативні (`!…`). Шляхи повертаються у posix-формі, відносно `npm/`.
|
|
374
|
+
* Не пробує дублювати всю логіку `npm pack` (license/readme/тощо) — тут лише
|
|
375
|
+
* простір імен `files`, бо саме його сканує check.
|
|
376
|
+
* @param {string[]} filesField значення поля `files`
|
|
377
|
+
* @returns {Promise<string[]>} відсортовані posix-шляхи без `npm/` префікса
|
|
378
|
+
*/
|
|
379
|
+
async function collectPublishedFiles(filesField) {
|
|
380
|
+
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)))
|
|
384
|
+
/** @type {Set<string>} */
|
|
385
|
+
const collected = new Set()
|
|
386
|
+
for (const entry of positives) {
|
|
387
|
+
const fullPath = join('npm', entry)
|
|
388
|
+
if (!existsSync(fullPath)) continue
|
|
389
|
+
const s = await stat(fullPath)
|
|
390
|
+
if (s.isFile()) {
|
|
391
|
+
collected.add(entry)
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
394
|
+
if (!s.isDirectory()) continue
|
|
395
|
+
await walkDir(fullPath, p => {
|
|
396
|
+
const rel = p.slice('npm/'.length).split(sep).join('/')
|
|
397
|
+
collected.add(rel)
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
const filtered = [...collected].filter(rel => !negatives.some(re => re.test(rel)))
|
|
401
|
+
filtered.sort()
|
|
402
|
+
return filtered
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Чи є у файлі імпорт/require/dynamic-import з модуля тест-фреймворку.
|
|
407
|
+
* Парсимо через oxc-parser (як `bunyan-imports`/`redis-imports`). При помилці
|
|
408
|
+
* парсингу повертаємо `null` — це не наш checker для синтаксису.
|
|
409
|
+
* @param {string} content повний текст файлу
|
|
410
|
+
* @param {string} virtualPath шлях для вибору `lang`
|
|
411
|
+
* @returns {string | null} модуль, через який видно тест, або `null`
|
|
412
|
+
*/
|
|
413
|
+
export function findTestFrameworkImport(content, virtualPath) {
|
|
414
|
+
const lang = langFromPath(virtualPath)
|
|
415
|
+
let result
|
|
416
|
+
try {
|
|
417
|
+
result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
|
|
418
|
+
} catch {
|
|
419
|
+
return null
|
|
420
|
+
}
|
|
421
|
+
if (result.errors?.length) return null
|
|
422
|
+
for (const imp of result.module?.staticImports ?? []) {
|
|
423
|
+
const mod = imp.moduleRequest?.value
|
|
424
|
+
if (typeof mod === 'string' && TEST_FRAMEWORK_MODULES.has(mod)) return mod
|
|
425
|
+
}
|
|
426
|
+
/** @type {string | null} */
|
|
427
|
+
let found = null
|
|
428
|
+
walkAstWithAncestors(result.program, [], node => {
|
|
429
|
+
if (found) return
|
|
430
|
+
const reqMod = requireCallModule(node)
|
|
431
|
+
if (reqMod && TEST_FRAMEWORK_MODULES.has(reqMod)) {
|
|
432
|
+
found = reqMod
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
const dynMod = dynamicImportModule(node)
|
|
436
|
+
if (dynMod && TEST_FRAMEWORK_MODULES.has(dynMod)) {
|
|
437
|
+
found = dynMod
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
return found
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Класифікує опублікований файл як test/fixture, якщо хоча б одна з ознак:
|
|
445
|
+
* (1) у шляху є каталог із `TEST_DIR_NAMES`; (2) basename відповідає
|
|
446
|
+
* `TEST_FILE_PATTERNS`; (3) для JS/TS-розширень — імпорт test-фреймворку.
|
|
447
|
+
* @param {string} relPath posix-шлях відносно `npm/`
|
|
448
|
+
* @returns {Promise<string | null>} причина порушення або `null`
|
|
449
|
+
*/
|
|
450
|
+
export async function classifyPublishedFileAsTest(relPath) {
|
|
451
|
+
const segments = relPath.split('/')
|
|
452
|
+
const base = segments[segments.length - 1]
|
|
453
|
+
const dirs = segments.slice(0, -1)
|
|
454
|
+
const testDir = dirs.find(seg => TEST_DIR_NAMES.has(seg.toLowerCase()))
|
|
455
|
+
if (testDir) return `test-style каталог "${testDir}/"`
|
|
456
|
+
if (TEST_FILE_PATTERNS.some(re => re.test(base))) return `test-style ім'я файлу`
|
|
457
|
+
if (JS_LIKE_EXT_RE.test(base)) {
|
|
458
|
+
const content = await readFile(join('npm', relPath), 'utf8')
|
|
459
|
+
const mod = findTestFrameworkImport(content, relPath)
|
|
460
|
+
if (mod) return `імпорт test-фреймворку "${mod}"`
|
|
461
|
+
}
|
|
462
|
+
return null
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Для всіх файлів, що потрапили б у tarball (positive `files` мінус negative
|
|
467
|
+
* patterns), забороняє test-style каталоги/імена/імпорти. Так пакет лишається
|
|
468
|
+
* компактним і не везе користувачам тести й фікстури.
|
|
469
|
+
* @param {(msg: string) => void} pass callback при успіху
|
|
470
|
+
* @param {(msg: string) => void} fail callback при порушенні
|
|
471
|
+
* @returns {Promise<void>}
|
|
472
|
+
*/
|
|
473
|
+
async function checkNoTestsInPublishedFiles(pass, fail) {
|
|
474
|
+
if (!existsSync('npm/package.json')) return
|
|
475
|
+
const pkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
|
|
476
|
+
if (!Array.isArray(pkg.files)) return
|
|
477
|
+
const files = await collectPublishedFiles(pkg.files)
|
|
478
|
+
/** @type {{ file: string, reason: string }[]} */
|
|
479
|
+
const violations = []
|
|
480
|
+
for (const rel of files) {
|
|
481
|
+
const reason = await classifyPublishedFileAsTest(rel)
|
|
482
|
+
if (reason) violations.push({ file: rel, reason })
|
|
483
|
+
}
|
|
484
|
+
if (violations.length === 0) {
|
|
485
|
+
pass(`npm/: усі ${files.length} опублікованих файли без тестів/фікстур`)
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
for (const v of violations) {
|
|
489
|
+
fail(
|
|
490
|
+
`npm/${v.file}: ${v.reason} — винеси за межі шляхів з "files" або додай негативний glob ` +
|
|
491
|
+
'(наприклад "!**/*_test.rego") у npm/package.json (npm-module.mdc)'
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
296
496
|
/**
|
|
297
497
|
* Перевіряє базову структуру монорепо: наявність каталогу `npm/` і
|
|
298
498
|
* `npm/package.json`. Поле `workspaces ∋ "npm"` у кореневому `package.json`
|
|
@@ -334,6 +534,7 @@ export async function check() {
|
|
|
334
534
|
const { pass, fail } = reporter
|
|
335
535
|
|
|
336
536
|
await checkNpmModuleBasicStructure(pass, fail)
|
|
537
|
+
await checkNoTestsInPublishedFiles(pass, fail)
|
|
337
538
|
|
|
338
539
|
const ignorePaths = await loadCursorIgnorePaths(process.cwd())
|
|
339
540
|
const useSrcJsLayout = await npmSrcTreeHasJsFile(ignorePaths)
|