@nitra/cursor 1.13.26 → 1.13.31
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 +35 -0
- package/package.json +1 -1
- package/rules/changelog/changelog.mdc +44 -19
- package/rules/changelog/fix/consistency/check.mjs +318 -196
- package/rules/hasura/fix/internal_urls/check.mjs +3 -2
- package/rules/hasura/hasura.mdc +3 -3
- package/rules/hasura/policy/svc_hl/svc_hl.rego +37 -15
- package/rules/hasura/policy/svc_hl/target.json +3 -1
- package/rules/js-lint/js-lint.mdc +9 -1
- package/rules/js-run/js-run.mdc +6 -2
- package/rules/text/text.mdc +12 -2
- package/scripts/utils/package-manifest.mjs +158 -0
- package/scripts/utils/template.mjs +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.13.31] - 2026-05-18
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `changelog` rule (`n-changelog.mdc` `2.3` → `2.4`): підтримка Python — `pyproject.toml` (`[project]` / Poetry) поряд із `package.json`; discovery воркспейсів через `getMonorepoProjectRootDirs`; PyPI-порівняння для registry-published Python-пакетів. `package-manifest.mjs` — уніфікований маніфест npm/python.
|
|
12
|
+
|
|
13
|
+
## [1.13.30] - 2026-05-18
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `changelog` rule (`n-changelog.mdc` `2.2` → `2.3`): `alwaysApply: true` без `globs`; інверсія — не вимагати bump для `docs/`/`doc/` і шляхів з `.gitignore`. `check changelog`: інтеграційна база `dev` **або** `main` (перша наявна); на `dev`/`main` local-only пропускається; релевантні зміни фільтруються в git. Тести.
|
|
18
|
+
|
|
19
|
+
## [1.13.29] - 2026-05-18
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- `changelog` rule (`n-changelog.mdc` `2.1` → `2.2`): розширені `globs` (типові шляхи пакета + `*.rego` / `*.mdc`) — правило потрапляє в контекст агента при правках коду, не лише `package.json` / `CHANGELOG.md`; секція «Чеклист агента» для будь-якого репозиторію з правилом.
|
|
24
|
+
- `changelog/fix/consistency/check.mjs`: npm-published режим — якщо `version` збігається з реєстром, але в git є зміни workspace без bump (feature vs `dev` або незакомічене на `dev`) → fail. Регресійні тести.
|
|
25
|
+
|
|
26
|
+
## [1.13.28] - 2026-05-18
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `scripts/utils/template.mjs` (`stripJsonComments`): враховує контекст рядкових літералів. Раніше regex `\/\*[\s\S]*?\*\/` без розрізнення string-літералів агресивно вирізав блоки між `/*` і `*/`, які зустрічаються в glob-патернах JSON-значень (напр. `**/node_modules/**`, `**/k8s/**/*.yaml`), і канонічний `.cspell.json.snippet.json` чи `.oxfmtrc.json.snippet.json` після стрипу стягувався в один склеєний рядок замість 7-елементного масиву. Новий стриппер пропускає вміст `"..."` (з підтримкою backslash-escape) без змін і вирізає лише реальні JSONC-коментарі.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- `hasura` rule (`hasura.svc_hl`): іменування Service узгоджено з `k8s.svc_hl_yaml` — headless (`spec.clusterIP: None`) має суфікс `-h-hl` (напр. `db-h` → `db-h-hl`), clusterIP у `svc.yaml` — `-h`. Target розширено на `hasura/k8s/base/svc.yaml` і `svc-hl.yaml`; додано `svc_hl_test.rego`. `hasura.mdc` і `fix/internal_urls` оновлено під headless DNS (`contract-h-hl`). Bump `hasura.mdc` `1.1` → `1.2`.
|
|
35
|
+
|
|
36
|
+
## [1.13.27] - 2026-05-18
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- `text`, `js-lint`, `js-run` rules: додано markdown-посилання на template-файли у канонічні `<id>.mdc` — `findMissingMdcRefs` (викликається з `run-rule.mjs`) раніше падав, бо канонічні `.mdc` не містили `[name](./policy/<concern>/template/<file>)` для власних шаблонів. Bump rule versions: `text.mdc` `1.27` → `1.28`, `js-lint.mdc` `1.22` → `1.23`, `js-run.mdc` `1.8` → `1.9`.
|
|
41
|
+
|
|
7
42
|
## [1.13.26] - 2026-05-17
|
|
8
43
|
|
|
9
44
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,36 +1,61 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: CHANGELOG.md в кожному workspace, з двома моделями бази порівняння
|
|
3
|
-
version: '2.
|
|
4
|
-
|
|
5
|
-
alwaysApply: false
|
|
2
|
+
description: CHANGELOG.md в кожному workspace, з двома моделями бази порівняння (npm і Python)
|
|
3
|
+
version: '2.4'
|
|
4
|
+
alwaysApply: true
|
|
6
5
|
---
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
У кожному **пакетному** workspace (каталог із `package.json` або `pyproject.toml`) має бути власний **`CHANGELOG.md`**. Спільного на репозиторій змісту змін **не існує** — кожен пакет веде свій.
|
|
8
|
+
|
|
9
|
+
**Маніфест версії:**
|
|
10
|
+
|
|
11
|
+
- **JS / Bun / npm** — `<ws>/package.json` (`version`, для монорепо ще `workspaces` у кореневому `package.json`).
|
|
12
|
+
- **Python** — `<ws>/pyproject.toml`: `[project].name` / `[project].version` (PEP 621) або `[tool.poetry].name` / `[tool.poetry].version`.
|
|
13
|
+
|
|
14
|
+
Каталоги лише з `pyproject.toml` (без `package.json`) теж враховуються; `node_modules/`, `.venv/`, `venv/` при пошуку ігноруються.
|
|
15
|
+
|
|
16
|
+
## Чеклист агента (будь-який репозиторій з цим правилом)
|
|
17
|
+
|
|
18
|
+
**Інверсія (за замовчуванням не вимагають bump/CHANGELOG):**
|
|
19
|
+
|
|
20
|
+
- зміни **лише** під `docs/` або `doc/`;
|
|
21
|
+
- файли під **`.gitignore`**;
|
|
22
|
+
- правки **лише** `CHANGELOG.md` або поля `version` у маніфесті як сам релізний крок.
|
|
23
|
+
|
|
24
|
+
**Вимагають bump + нову секцію CHANGELOG** — усі інші зміни в каталозі workspace (код, rego, правила, конфіги, тести тощо).
|
|
25
|
+
|
|
26
|
+
Перед завершенням задачі:
|
|
27
|
+
|
|
28
|
+
1. **`version`** у `<ws>/package.json` або `[project].version` у `<ws>/pyproject.toml` підвищено (build/patch +1 на PR), якщо зміни входять у реліз.
|
|
29
|
+
2. **`CHANGELOG.md`** — **нова** секція `## [версія] - YYYY-MM-DD` зверху (не дописуй bullet-и в стару версію).
|
|
30
|
+
3. **`npx @nitra/cursor check changelog`** — exit `0`.
|
|
31
|
+
|
|
32
|
+
Перевірка програмна (`changelog/fix/consistency/check.mjs`).
|
|
9
33
|
|
|
10
34
|
## Дві моделі бази порівняння
|
|
11
35
|
|
|
12
|
-
|
|
36
|
+
Режим визначається автоматично з маніфесту.
|
|
13
37
|
|
|
14
|
-
###
|
|
38
|
+
### registry-published (npm / PyPI)
|
|
15
39
|
|
|
16
|
-
|
|
40
|
+
**npm:** непорожнє `name`, не `private: true`, масив `files`.
|
|
17
41
|
|
|
18
|
-
|
|
19
|
-
2. Якщо локальна `version` відрізняється від опублікованої — потрібен запис у `<ws>/CHANGELOG.md` для локальної версії (формат `## [версія] - YYYY-MM-DD`) і `"CHANGELOG.md"` у `files`.
|
|
20
|
-
3. Якщо реєстр недосяжний (офлайн / пакет ще не публікувався) — fail-safe pass із поясненням, щоб локальна розробка не блокувалася.
|
|
42
|
+
**Python:** статичні `project.name` і `project.version` у `pyproject.toml` (або Poetry-секція).
|
|
21
43
|
|
|
22
|
-
|
|
44
|
+
1. **Локальна `version` ≠ опублікованій** (npm / PyPI): запис у `<ws>/CHANGELOG.md`; для npm також `"CHANGELOG.md"` у `files`.
|
|
45
|
+
2. **Версії збігаються**, але в git є **релевантні** зміни без bump → fail.
|
|
46
|
+
3. **Реєстр недосяжний** — fail-safe pass.
|
|
47
|
+
4. **Немає релевантних змін** — pass.
|
|
23
48
|
|
|
24
|
-
### local-only
|
|
49
|
+
### local-only
|
|
25
50
|
|
|
26
|
-
|
|
51
|
+
**npm:** `private: true` або без `files`. **Python:** без пари name+version для реєстру. База = **`dev` або `main`** (перша наявна), `git merge-base`:
|
|
27
52
|
|
|
28
|
-
1. На
|
|
29
|
-
2. На feature-гілці
|
|
30
|
-
3.
|
|
31
|
-
4. Direct-commit на `main`
|
|
53
|
+
1. На **`dev`** local-only не активний (крім незакомічених registry-published).
|
|
54
|
+
2. На feature-гілці — bump + CHANGELOG **раз на PR**.
|
|
55
|
+
3. Після merge на інтеграційну гілку diff порожній → pass.
|
|
56
|
+
4. Direct-commit на `main` ловиться так само.
|
|
32
57
|
|
|
33
|
-
Якщо
|
|
58
|
+
Якщо немає git або немає `dev`/`main` — local-only пропускається.
|
|
34
59
|
|
|
35
60
|
## Формат CHANGELOG.md
|
|
36
61
|
|
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Перевіряє, що в кожному workspace із
|
|
3
|
-
*
|
|
4
|
-
* (формат Keep a Changelog).
|
|
2
|
+
* Перевіряє, що в кожному workspace із релізно-релевантними змінами підвищена `version`
|
|
3
|
+
* у маніфесті (`package.json` або `pyproject.toml`) і в `<ws>/CHANGELOG.md` є запис
|
|
4
|
+
* `## [version] - YYYY-MM-DD` (формат Keep a Changelog).
|
|
5
5
|
*
|
|
6
|
-
* Дві моделі
|
|
6
|
+
* Дві моделі бази — на рівні воркспейсу (див. n-changelog.mdc):
|
|
7
7
|
*
|
|
8
|
-
* 1) **
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* у
|
|
12
|
-
* (немає мережі / пакет ще не публікувався) — fail-safe pass із поясненням, щоб локальна
|
|
13
|
-
* розробка офлайн не блокувалась.
|
|
8
|
+
* 1) **registry-published** (npm: `name` + `files`, не `private`; Python: `project.name` +
|
|
9
|
+
* статична `project.version` у `pyproject.toml`): база = опублікована версія в npm / PyPI.
|
|
10
|
+
* Якщо локальна версія відрізняється — потрібен CHANGELOG; для npm також `"CHANGELOG.md"`
|
|
11
|
+
* у `files`. Якщо версії збігаються, але в git є релевантні зміни без bump — fail.
|
|
14
12
|
*
|
|
15
|
-
* 2) **local-only
|
|
16
|
-
*
|
|
17
|
-
* - на feature-гілці бачити лише унікальні коміти цієї гілки;
|
|
18
|
-
* - на `main` після merge `dev → main` diff був порожній (нічого не вимагати);
|
|
19
|
-
* - direct-commit на `main` поза PR-flow ловився як зміна, що потребує bump + CHANGELOG.
|
|
20
|
-
* Якщо не git-репо, поточна гілка = `dev`, або `dev`/`origin/dev` не існує — пропуск.
|
|
13
|
+
* 2) **local-only** (приватні npm, без `files`, Python без імені/версії для реєстру): PR-scoped
|
|
14
|
+
* перевірка проти `dev` / `main` через `git merge-base`.
|
|
21
15
|
*
|
|
22
|
-
* Усі `git` і
|
|
16
|
+
* Усі `git` і зовнішні виклики — через `execFile` / `fetch`, без shell-інтерполяції.
|
|
23
17
|
*/
|
|
24
18
|
import { execFile } from 'node:child_process'
|
|
25
19
|
import { existsSync } from 'node:fs'
|
|
@@ -28,20 +22,34 @@ import { join } from 'node:path'
|
|
|
28
22
|
import { promisify } from 'node:util'
|
|
29
23
|
|
|
30
24
|
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
31
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
getMonorepoProjectRootDirs,
|
|
27
|
+
manifestFilePath,
|
|
28
|
+
parsePyprojectFields,
|
|
29
|
+
readPackageManifest,
|
|
30
|
+
} from '../../../../scripts/utils/package-manifest.mjs'
|
|
32
31
|
|
|
33
32
|
const execFileAsync = promisify(execFile)
|
|
34
33
|
|
|
35
|
-
/**
|
|
36
|
-
const
|
|
34
|
+
/** Кандидати інтеграційної гілки (перша наявна в репо; див. n-changelog.mdc) */
|
|
35
|
+
const BASE_BRANCH_CANDIDATES = Object.freeze(['dev', 'main'])
|
|
37
36
|
|
|
38
|
-
/**
|
|
39
|
-
const
|
|
37
|
+
/** Гілки, на яких local-only перевірку пропускаємо (крім незакомічених registry-published). */
|
|
38
|
+
const INTEGRATION_BRANCHES = Object.freeze(['dev', 'main'])
|
|
39
|
+
|
|
40
|
+
/** Префікси шляхів (posix), які не вважаються релізними змінами — інверсія glob (n-changelog.mdc). */
|
|
41
|
+
const CHANGELOG_IGNORE_PATH_PREFIXES = Object.freeze(['docs/', 'doc/'])
|
|
42
|
+
|
|
43
|
+
/** Точні шляхи каталогів документації (posix), без bump. */
|
|
44
|
+
const CHANGELOG_IGNORE_PATH_EXACT = Object.freeze(['docs', 'doc'])
|
|
45
|
+
|
|
46
|
+
/** Таймаут на `npm view` / PyPI (мс) */
|
|
47
|
+
const REGISTRY_TIMEOUT_MS = 10_000
|
|
40
48
|
|
|
41
49
|
/**
|
|
42
50
|
* Тихо запускає `git` і повертає stdout або `null` при будь-якій помилці.
|
|
43
51
|
* @param {string[]} args аргументи `git`
|
|
44
|
-
* @returns {Promise<string | null>}
|
|
52
|
+
* @returns {Promise<string | null>}
|
|
45
53
|
*/
|
|
46
54
|
async function gitOrNull(args) {
|
|
47
55
|
try {
|
|
@@ -53,8 +61,7 @@ async function gitOrNull(args) {
|
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
/**
|
|
56
|
-
*
|
|
57
|
-
* @returns {Promise<boolean>} `true`, якщо `git rev-parse --is-inside-work-tree` повернув `true`
|
|
64
|
+
* @returns {Promise<boolean>}
|
|
58
65
|
*/
|
|
59
66
|
async function isInsideGitRepo() {
|
|
60
67
|
const out = await gitOrNull(['rev-parse', '--is-inside-work-tree'])
|
|
@@ -62,8 +69,7 @@ async function isInsideGitRepo() {
|
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
/**
|
|
65
|
-
*
|
|
66
|
-
* @returns {Promise<string | null>} назва гілки чи `'HEAD'`, або `null` (поза git / помилка)
|
|
72
|
+
* @returns {Promise<string | null>}
|
|
67
73
|
*/
|
|
68
74
|
async function currentBranchName() {
|
|
69
75
|
const out = await gitOrNull(['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
@@ -71,25 +77,64 @@ async function currentBranchName() {
|
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
80
|
+
* @param {string | null} branch
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
function isIntegrationBranch(branch) {
|
|
84
|
+
return branch !== null && INTEGRATION_BRANCHES.includes(branch)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} ref
|
|
89
|
+
* @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
function baseRefLabel(ref) {
|
|
92
|
+
return ref.startsWith('origin/') ? ref.slice('origin/'.length) : ref
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} relPath
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
function isChangelogIgnoredPath(relPath) {
|
|
100
|
+
const p = relPath.replace(/\\/g, '/').replace(/^\.\//, '')
|
|
101
|
+
if (CHANGELOG_IGNORE_PATH_EXACT.includes(p)) {
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
return CHANGELOG_IGNORE_PATH_PREFIXES.some(prefix => p.startsWith(prefix))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {string} relPath
|
|
109
|
+
* @returns {Promise<boolean>}
|
|
110
|
+
*/
|
|
111
|
+
async function isPathGitIgnored(relPath) {
|
|
112
|
+
try {
|
|
113
|
+
await execFileAsync('git', ['check-ignore', '-q', '--', relPath])
|
|
114
|
+
return true
|
|
115
|
+
} catch {
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @returns {Promise<string | null>}
|
|
77
122
|
*/
|
|
78
123
|
async function resolveBaseRef() {
|
|
79
|
-
for (const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
124
|
+
for (const name of BASE_BRANCH_CANDIDATES) {
|
|
125
|
+
for (const ref of [name, `origin/${name}`]) {
|
|
126
|
+
const out = await gitOrNull(['rev-parse', '--verify', '--quiet', ref])
|
|
127
|
+
if (typeof out === 'string' && out.trim().length > 0) {
|
|
128
|
+
return ref
|
|
129
|
+
}
|
|
83
130
|
}
|
|
84
131
|
}
|
|
85
132
|
return null
|
|
86
133
|
}
|
|
87
134
|
|
|
88
135
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* @param {string} baseRef SHA або ref-name бази (зазвичай `dev` / `origin/dev`)
|
|
92
|
-
* @returns {Promise<string | null>} SHA точки розгалуження або `null`, якщо merge-base нема
|
|
136
|
+
* @param {string} baseRef
|
|
137
|
+
* @returns {Promise<string | null>}
|
|
93
138
|
*/
|
|
94
139
|
async function resolveMergeBase(baseRef) {
|
|
95
140
|
const out = await gitOrNull(['merge-base', baseRef, 'HEAD'])
|
|
@@ -99,14 +144,9 @@ async function resolveMergeBase(baseRef) {
|
|
|
99
144
|
}
|
|
100
145
|
|
|
101
146
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* щоб зміни всередині sub-workspace не вважалися змінами кореня.
|
|
106
|
-
* Для звичайного воркспейсу — просто `<ws>/`.
|
|
107
|
-
* @param {string} ws шлях воркспейсу (`'.'` для кореня, інакше — відносний шлях, як у `workspaces`)
|
|
108
|
-
* @param {string[]} subWorkspaces усі під-воркспейси (зокрема для `'.'` потрібно виключити їх)
|
|
109
|
-
* @returns {string[]} pathspec для git: масив, що передається після `--`
|
|
147
|
+
* @param {string} ws
|
|
148
|
+
* @param {string[]} subWorkspaces
|
|
149
|
+
* @returns {string[]}
|
|
110
150
|
*/
|
|
111
151
|
function pathspecForWorkspace(ws, subWorkspaces) {
|
|
112
152
|
if (ws !== '.') return [`${ws}/`]
|
|
@@ -114,50 +154,74 @@ function pathspecForWorkspace(ws, subWorkspaces) {
|
|
|
114
154
|
}
|
|
115
155
|
|
|
116
156
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* правки tracked-файлів. Untracked-файли — `git ls-files --others --exclude-standard`.
|
|
121
|
-
* @param {string} baseRef SHA або ref-name (зокрема merge-base)
|
|
122
|
-
* @param {string} ws шлях воркспейсу (`'.'` для кореня)
|
|
123
|
-
* @param {string[]} subWorkspaces усі під-воркспейси для коректного формування pathspec кореня
|
|
124
|
-
* @returns {Promise<boolean>} `true`, якщо в межах воркспейсу є будь-які зміни (committed або untracked)
|
|
157
|
+
* @param {string} baseRef
|
|
158
|
+
* @param {string[]} pathspec
|
|
159
|
+
* @returns {Promise<string[]>}
|
|
125
160
|
*/
|
|
126
|
-
async function
|
|
161
|
+
async function listChangedPathsAgainstBase(baseRef, pathspec) {
|
|
162
|
+
/** @type {string[]} */
|
|
163
|
+
const out = []
|
|
164
|
+
const diffArgs =
|
|
165
|
+
baseRef === 'HEAD'
|
|
166
|
+
? ['diff', '--name-only', 'HEAD', '--', ...pathspec]
|
|
167
|
+
: ['diff', '--name-only', baseRef, '--', ...pathspec]
|
|
168
|
+
const diffOut = await gitOrNull(diffArgs)
|
|
169
|
+
if (typeof diffOut === 'string' && diffOut.trim().length > 0) {
|
|
170
|
+
out.push(...diffOut.trim().split('\n'))
|
|
171
|
+
}
|
|
172
|
+
const untrackedOut = await gitOrNull(['ls-files', '--others', '--exclude-standard', '--', ...pathspec])
|
|
173
|
+
if (typeof untrackedOut === 'string' && untrackedOut.trim().length > 0) {
|
|
174
|
+
out.push(...untrackedOut.trim().split('\n'))
|
|
175
|
+
}
|
|
176
|
+
return [...new Set(out)]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {string} baseRef
|
|
181
|
+
* @param {string} ws
|
|
182
|
+
* @param {string[]} subWorkspaces
|
|
183
|
+
* @returns {Promise<boolean>}
|
|
184
|
+
*/
|
|
185
|
+
async function workspaceHasRelevantChangesAgainstBase(baseRef, ws, subWorkspaces) {
|
|
127
186
|
const pathspec = pathspecForWorkspace(ws, subWorkspaces)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
187
|
+
const paths = await listChangedPathsAgainstBase(baseRef, pathspec)
|
|
188
|
+
for (const p of paths) {
|
|
189
|
+
if (isChangelogIgnoredPath(p)) {
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
if (await isPathGitIgnored(p)) {
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
return true
|
|
133
196
|
}
|
|
134
|
-
|
|
135
|
-
return typeof untracked === 'string' && untracked.trim().length > 0
|
|
197
|
+
return false
|
|
136
198
|
}
|
|
137
199
|
|
|
138
200
|
/**
|
|
139
|
-
* Версія з
|
|
140
|
-
* @param {string} baseRef
|
|
141
|
-
* @param {
|
|
142
|
-
* @returns {Promise<string | null>}
|
|
201
|
+
* Версія з маніфесту на `baseRef`.
|
|
202
|
+
* @param {string} baseRef
|
|
203
|
+
* @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
|
|
204
|
+
* @returns {Promise<string | null>}
|
|
143
205
|
*/
|
|
144
|
-
async function readBaseVersion(baseRef,
|
|
145
|
-
const wsPath = ws === '.' ?
|
|
206
|
+
async function readBaseVersion(baseRef, manifest) {
|
|
207
|
+
const wsPath = manifest.ws === '.' ? manifest.manifestRel : `${manifest.ws}/${manifest.manifestRel}`
|
|
146
208
|
const out = await gitOrNull(['show', `${baseRef}:${wsPath}`])
|
|
147
209
|
if (out === null) return null
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
210
|
+
if (manifest.kind === 'npm') {
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(out)
|
|
213
|
+
return typeof parsed?.version === 'string' ? parsed.version : null
|
|
214
|
+
} catch {
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
153
217
|
}
|
|
218
|
+
return parsePyprojectFields(out).version
|
|
154
219
|
}
|
|
155
220
|
|
|
156
221
|
/**
|
|
157
|
-
*
|
|
158
|
-
* @param {string}
|
|
159
|
-
* @
|
|
160
|
-
* @returns {boolean} `true`, якщо запис для `version` знайдено
|
|
222
|
+
* @param {string} text
|
|
223
|
+
* @param {string} version
|
|
224
|
+
* @returns {boolean}
|
|
161
225
|
*/
|
|
162
226
|
function changelogHasVersionEntry(text, version) {
|
|
163
227
|
const needle = `## [${version}]`
|
|
@@ -165,62 +229,68 @@ function changelogHasVersionEntry(text, version) {
|
|
|
165
229
|
}
|
|
166
230
|
|
|
167
231
|
/**
|
|
168
|
-
*
|
|
169
|
-
* @
|
|
170
|
-
* @returns {Promise<Record<string, unknown> | null>} розпарсений `package.json` або `null`
|
|
232
|
+
* @param {string} name
|
|
233
|
+
* @returns {Promise<string | null>}
|
|
171
234
|
*/
|
|
172
|
-
async function
|
|
173
|
-
const path = join(ws, 'package.json')
|
|
174
|
-
if (!existsSync(path)) return null
|
|
235
|
+
async function defaultGetPublishedNpmVersion(name) {
|
|
175
236
|
try {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
: null
|
|
237
|
+
const { stdout } = await execFileAsync('npm', ['view', name, 'version'], { timeout: REGISTRY_TIMEOUT_MS })
|
|
238
|
+
const v = stdout.trim()
|
|
239
|
+
return v.length > 0 ? v : null
|
|
180
240
|
} catch {
|
|
181
241
|
return null
|
|
182
242
|
}
|
|
183
243
|
}
|
|
184
244
|
|
|
185
245
|
/**
|
|
186
|
-
*
|
|
187
|
-
* @
|
|
188
|
-
* @returns {boolean} `true`, якщо пакет придатний для публікації в npm
|
|
246
|
+
* @param {string} name
|
|
247
|
+
* @returns {Promise<string | null>}
|
|
189
248
|
*/
|
|
190
|
-
function
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
249
|
+
async function defaultGetPublishedPyPiVersion(name) {
|
|
250
|
+
try {
|
|
251
|
+
const res = await fetch(`https://pypi.org/pypi/${encodeURIComponent(name)}/json`, {
|
|
252
|
+
signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),
|
|
253
|
+
})
|
|
254
|
+
if (!res.ok) return null
|
|
255
|
+
const data = await res.json()
|
|
256
|
+
const v = data?.info?.version
|
|
257
|
+
return typeof v === 'string' && v.length > 0 ? v : null
|
|
258
|
+
} catch {
|
|
259
|
+
return null
|
|
260
|
+
}
|
|
195
261
|
}
|
|
196
262
|
|
|
197
263
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* @
|
|
201
|
-
* @returns {Promise<string | null>} опублікована версія або `null` (нема пакета / офлайн)
|
|
264
|
+
* @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
|
|
265
|
+
* @param {(name: string, kind?: import('../../../../scripts/utils/package-manifest.mjs').PackageKind) => Promise<string | null>} getPublishedVersion
|
|
266
|
+
* @returns {Promise<string | null>}
|
|
202
267
|
*/
|
|
203
|
-
async function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
268
|
+
async function resolvePublishedVersion(manifest, getPublishedVersion) {
|
|
269
|
+
if (!manifest.name) return null
|
|
270
|
+
return getPublishedVersion(manifest.name, manifest.kind)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @returns {(name: string, kind?: import('../../../../scripts/utils/package-manifest.mjs').PackageKind) => Promise<string | null>}
|
|
275
|
+
*/
|
|
276
|
+
function createDefaultGetPublishedVersion() {
|
|
277
|
+
return async (name, kind = 'npm') => {
|
|
278
|
+
if (kind === 'python') {
|
|
279
|
+
return defaultGetPublishedPyPiVersion(name)
|
|
280
|
+
}
|
|
281
|
+
return defaultGetPublishedNpmVersion(name)
|
|
210
282
|
}
|
|
211
283
|
}
|
|
212
284
|
|
|
213
285
|
/**
|
|
214
|
-
*
|
|
215
|
-
* @param {
|
|
216
|
-
* @param {string
|
|
217
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
218
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
286
|
+
* @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
|
|
287
|
+
* @param {(msg: string) => void} pass
|
|
288
|
+
* @param {(msg: string) => void} fail
|
|
219
289
|
*/
|
|
220
|
-
function
|
|
221
|
-
if (
|
|
222
|
-
const pkgPath =
|
|
223
|
-
if (
|
|
290
|
+
function checkNpmFilesArrayContainsChangelog(manifest, pass, fail) {
|
|
291
|
+
if (manifest.kind !== 'npm' || !manifest.npmFiles) return
|
|
292
|
+
const pkgPath = manifestFilePath(manifest.ws, manifest)
|
|
293
|
+
if (manifest.npmFiles.includes('CHANGELOG.md')) {
|
|
224
294
|
pass(`${pkgPath}: files містить "CHANGELOG.md"`)
|
|
225
295
|
} else {
|
|
226
296
|
fail(`${pkgPath}: масив files має містити "CHANGELOG.md", щоб публікувати changelog із пакетом`)
|
|
@@ -228,12 +298,11 @@ function checkFilesArrayContainsChangelog(pkg, ws, pass, fail) {
|
|
|
228
298
|
}
|
|
229
299
|
|
|
230
300
|
/**
|
|
231
|
-
*
|
|
232
|
-
* @param {string}
|
|
233
|
-
* @param {string
|
|
234
|
-
* @param {(msg: string) => void}
|
|
235
|
-
* @
|
|
236
|
-
* @returns {Promise<boolean>} `false`, якщо файл відсутній або немає запису
|
|
301
|
+
* @param {string} ws
|
|
302
|
+
* @param {string} version
|
|
303
|
+
* @param {(msg: string) => void} pass
|
|
304
|
+
* @param {(msg: string) => void} fail
|
|
305
|
+
* @returns {Promise<boolean>}
|
|
237
306
|
*/
|
|
238
307
|
async function verifyChangelogEntry(ws, version, pass, fail) {
|
|
239
308
|
const label = ws === '.' ? '<root>' : ws
|
|
@@ -252,89 +321,147 @@ async function verifyChangelogEntry(ws, version, pass, fail) {
|
|
|
252
321
|
}
|
|
253
322
|
|
|
254
323
|
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* правило fail-safe пасує (щоб офлайн-розробка не блокувалась).
|
|
258
|
-
* @param {string} ws шлях воркспейсу (`'.'` для кореня)
|
|
259
|
-
* @param {Record<string, unknown>} pkg розпарсений `package.json` воркспейсу
|
|
260
|
-
* @param {(name: string) => Promise<string | null>} getPublishedVersion стаб/реальна функція отримання опублікованої версії
|
|
261
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
262
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
324
|
+
* @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
|
|
325
|
+
* @returns {string}
|
|
263
326
|
*/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
327
|
+
function workspaceLabel(manifest) {
|
|
328
|
+
return manifest.ws === '.' ? '<root>' : manifest.ws
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
|
|
333
|
+
* @param {string} Vcurrent
|
|
334
|
+
* @param {string[]} subWorkspaces
|
|
335
|
+
* @param {(msg: string) => void} pass
|
|
336
|
+
* @param {(msg: string) => void} fail
|
|
337
|
+
* @returns {Promise<void>}
|
|
338
|
+
*/
|
|
339
|
+
async function checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subWorkspaces, pass, fail) {
|
|
340
|
+
const label = workspaceLabel(manifest)
|
|
341
|
+
const mf = manifestFilePath(manifest.ws, manifest)
|
|
342
|
+
if (!(await isInsideGitRepo())) {
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const branch = await currentBranchName()
|
|
347
|
+
if (isIntegrationBranch(branch)) {
|
|
348
|
+
if (await workspaceHasRelevantChangesAgainstBase('HEAD', manifest.ws, subWorkspaces)) {
|
|
349
|
+
fail(
|
|
350
|
+
`${label}: у registry-published пакеті є незакомічені зміни при version ${Vcurrent}, що вже в реєстрі. ` +
|
|
351
|
+
`Підвищ version у ${mf} і додай запис у CHANGELOG.md (n-changelog.mdc)`
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const baseRef = await resolveBaseRef()
|
|
358
|
+
if (!baseRef) {
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
const mergeBase = await resolveMergeBase(baseRef)
|
|
362
|
+
if (!mergeBase) {
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
if (!(await workspaceHasRelevantChangesAgainstBase(mergeBase, manifest.ws, subWorkspaces))) {
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const Vbase = await readBaseVersion(mergeBase, manifest)
|
|
370
|
+
const baseLabel = baseRefLabel(baseRef)
|
|
371
|
+
if (Vbase === null || Vbase === Vcurrent) {
|
|
372
|
+
fail(
|
|
373
|
+
`${label}: у цій гілці є зміни в registry-published пакеті, але version у ${mf} ` +
|
|
374
|
+
`не підвищено (на ${baseLabel} — ${Vbase ?? '∅'}). Bump + запис у CHANGELOG.md обов'язкові на PR (n-changelog.mdc)`
|
|
375
|
+
)
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
pass(`${label}: version змінено (${Vbase} → ${Vcurrent}) — очікується запис CHANGELOG після bump`)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
|
|
383
|
+
* @param {string[]} subWorkspaces
|
|
384
|
+
* @param {(name: string, kind?: import('../../../../scripts/utils/package-manifest.mjs').PackageKind) => Promise<string | null>} getPublishedVersion
|
|
385
|
+
* @param {(msg: string) => void} pass
|
|
386
|
+
* @param {(msg: string) => void} fail
|
|
387
|
+
* @returns {Promise<void>}
|
|
388
|
+
*/
|
|
389
|
+
async function checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVersion, pass, fail) {
|
|
390
|
+
const label = workspaceLabel(manifest)
|
|
391
|
+
const mf = manifestFilePath(manifest.ws, manifest)
|
|
392
|
+
const Vcurrent = manifest.version
|
|
267
393
|
if (!Vcurrent) {
|
|
268
|
-
fail(`${label}: у
|
|
394
|
+
fail(`${label}: у ${mf} відсутнє поле version (registry-published воркспейс)`)
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
const name = manifest.name
|
|
398
|
+
if (!name) {
|
|
399
|
+
fail(`${label}: у ${mf} відсутнє ім'я пакета (registry-published воркспейс)`)
|
|
269
400
|
return
|
|
270
401
|
}
|
|
271
|
-
const
|
|
272
|
-
const Vpublished = await getPublishedVersion(name)
|
|
402
|
+
const Vpublished = await resolvePublishedVersion(manifest, getPublishedVersion)
|
|
273
403
|
if (Vpublished === null) {
|
|
274
404
|
pass(`${label}: ${name} — опублікована версія недоступна (мережа/реєстр), перевірку пропущено`)
|
|
275
405
|
return
|
|
276
406
|
}
|
|
277
407
|
if (Vpublished === Vcurrent) {
|
|
278
|
-
pass(`${label}: ${name}@${Vcurrent}
|
|
408
|
+
pass(`${label}: ${name}@${Vcurrent} збігається з реєстром — перевіряємо git на незрелізні зміни`)
|
|
409
|
+
await checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subWorkspaces, pass, fail)
|
|
279
410
|
return
|
|
280
411
|
}
|
|
281
412
|
pass(`${label}: ${name} — нова локальна версія (${Vpublished} → ${Vcurrent})`)
|
|
282
|
-
await verifyChangelogEntry(ws, Vcurrent, pass, fail)
|
|
283
|
-
|
|
413
|
+
await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail)
|
|
414
|
+
checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
|
|
284
415
|
}
|
|
285
416
|
|
|
286
417
|
/**
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
* @param {string}
|
|
290
|
-
* @param {string
|
|
291
|
-
* @param {
|
|
292
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
293
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
418
|
+
* @param {string} mergeBase
|
|
419
|
+
* @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
|
|
420
|
+
* @param {string} baseLabel
|
|
421
|
+
* @param {(msg: string) => void} pass
|
|
422
|
+
* @param {(msg: string) => void} fail
|
|
294
423
|
*/
|
|
295
|
-
async function checkLocalOnlyChangedWorkspace(mergeBase,
|
|
296
|
-
const label =
|
|
297
|
-
const
|
|
424
|
+
async function checkLocalOnlyChangedWorkspace(mergeBase, manifest, baseLabel, pass, fail) {
|
|
425
|
+
const label = workspaceLabel(manifest)
|
|
426
|
+
const mf = manifestFilePath(manifest.ws, manifest)
|
|
427
|
+
const Vcurrent = manifest.version
|
|
298
428
|
if (!Vcurrent) {
|
|
299
|
-
fail(`${label}: у
|
|
429
|
+
fail(`${label}: у ${mf} відсутнє поле version (потрібне для запису в CHANGELOG)`)
|
|
300
430
|
return
|
|
301
431
|
}
|
|
302
|
-
const Vbase = await readBaseVersion(mergeBase,
|
|
303
|
-
if (Vbase
|
|
432
|
+
const Vbase = await readBaseVersion(mergeBase, manifest)
|
|
433
|
+
if (Vbase === null || Vbase === Vcurrent) {
|
|
304
434
|
fail(
|
|
305
|
-
`${label}: у цій гілці є зміни, але version у ${
|
|
435
|
+
`${label}: у цій гілці є зміни, але version у ${mf} не підвищено (на ${baseLabel} — ${Vbase ?? '∅'}). Bump + запис у CHANGELOG.md обов'язкові на PR`
|
|
306
436
|
)
|
|
307
437
|
return
|
|
308
438
|
}
|
|
309
|
-
pass(`${label}: version підвищено (${Vbase
|
|
310
|
-
if (!(await verifyChangelogEntry(ws, Vcurrent, pass, fail))) return
|
|
311
|
-
|
|
439
|
+
pass(`${label}: version підвищено (${Vbase} → ${Vcurrent})`)
|
|
440
|
+
if (!(await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail))) return
|
|
441
|
+
checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
|
|
312
442
|
}
|
|
313
443
|
|
|
314
444
|
/**
|
|
315
|
-
*
|
|
316
|
-
* @param {string[]}
|
|
317
|
-
* @param {
|
|
318
|
-
* @param {string
|
|
319
|
-
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
320
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
321
|
-
* @returns {Promise<void>} резолвиться по завершенню перевірок усіх local-only воркспейсів
|
|
445
|
+
* @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest[]} localOnly
|
|
446
|
+
* @param {string[]} subWorkspaces
|
|
447
|
+
* @param {(msg: string) => void} pass
|
|
448
|
+
* @param {(msg: string) => void} fail
|
|
322
449
|
*/
|
|
323
|
-
async function runLocalOnlyChecks(
|
|
324
|
-
if (
|
|
450
|
+
async function runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail) {
|
|
451
|
+
if (localOnly.length === 0) return
|
|
325
452
|
|
|
326
453
|
if (!(await isInsideGitRepo())) {
|
|
327
454
|
pass('changelog: не git-репозиторій — local-only перевірку пропущено')
|
|
328
455
|
return
|
|
329
456
|
}
|
|
330
457
|
const branch = await currentBranchName()
|
|
331
|
-
if (branch ===
|
|
332
|
-
pass(
|
|
458
|
+
if (branch === 'dev') {
|
|
459
|
+
pass('changelog: поточна гілка = dev — local-only перевірку пропущено')
|
|
333
460
|
return
|
|
334
461
|
}
|
|
335
462
|
const baseRef = await resolveBaseRef()
|
|
336
463
|
if (!baseRef) {
|
|
337
|
-
pass(
|
|
464
|
+
pass('changelog: ref dev/main (та origin/*) не знайдено — local-only перевірку пропущено')
|
|
338
465
|
return
|
|
339
466
|
}
|
|
340
467
|
const mergeBase = await resolveMergeBase(baseRef)
|
|
@@ -343,11 +470,12 @@ async function runLocalOnlyChecks(localOnlyWorkspaces, pkgByWs, subWorkspaces, p
|
|
|
343
470
|
return
|
|
344
471
|
}
|
|
345
472
|
|
|
473
|
+
const baseLabel = baseRefLabel(baseRef)
|
|
346
474
|
let checkedAny = false
|
|
347
|
-
for (const
|
|
348
|
-
if (!(await
|
|
475
|
+
for (const manifest of localOnly) {
|
|
476
|
+
if (!(await workspaceHasRelevantChangesAgainstBase(mergeBase, manifest.ws, subWorkspaces))) continue
|
|
349
477
|
checkedAny = true
|
|
350
|
-
await checkLocalOnlyChangedWorkspace(mergeBase,
|
|
478
|
+
await checkLocalOnlyChangedWorkspace(mergeBase, manifest, baseLabel, pass, fail)
|
|
351
479
|
}
|
|
352
480
|
if (!checkedAny) {
|
|
353
481
|
pass(`changelog: local-only воркспейси без змін відносно merge-base(${baseRef})`)
|
|
@@ -355,46 +483,40 @@ async function runLocalOnlyChecks(localOnlyWorkspaces, pkgByWs, subWorkspaces, p
|
|
|
355
483
|
}
|
|
356
484
|
|
|
357
485
|
/**
|
|
358
|
-
*
|
|
359
|
-
* @param {
|
|
360
|
-
* @
|
|
361
|
-
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
486
|
+
* @param {object} [opts]
|
|
487
|
+
* @param {(name: string, kind?: import('../../../../scripts/utils/package-manifest.mjs').PackageKind) => Promise<string | null>} [opts.getPublishedVersion] перевизначення npm/PyPI у тестах
|
|
488
|
+
* @returns {Promise<number>}
|
|
362
489
|
*/
|
|
363
490
|
export async function check(opts = {}) {
|
|
364
491
|
const reporter = createCheckReporter()
|
|
365
492
|
const { pass, fail } = reporter
|
|
366
|
-
const getPublishedVersion = opts.getPublishedVersion ??
|
|
493
|
+
const getPublishedVersion = opts.getPublishedVersion ?? createDefaultGetPublishedVersion()
|
|
367
494
|
|
|
368
|
-
const workspaces = await
|
|
495
|
+
const workspaces = await getMonorepoProjectRootDirs(process.cwd())
|
|
369
496
|
const subWorkspaces = workspaces.filter(w => w !== '.')
|
|
370
497
|
|
|
371
|
-
/** @type {
|
|
372
|
-
const
|
|
373
|
-
/** @type {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
const localOnlyWorkspaces = []
|
|
498
|
+
/** @type {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest[]} */
|
|
499
|
+
const published = []
|
|
500
|
+
/** @type {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest[]} */
|
|
501
|
+
const localOnly = []
|
|
502
|
+
|
|
377
503
|
for (const ws of workspaces) {
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
504
|
+
const manifest = await readPackageManifest(ws)
|
|
505
|
+
if (!manifest) {
|
|
506
|
+
continue
|
|
507
|
+
}
|
|
508
|
+
if (manifest.registryPublishable) {
|
|
509
|
+
published.push(manifest)
|
|
382
510
|
} else {
|
|
383
|
-
|
|
511
|
+
localOnly.push(manifest)
|
|
384
512
|
}
|
|
385
513
|
}
|
|
386
514
|
|
|
387
|
-
for (const
|
|
388
|
-
await checkPublishedWorkspace(
|
|
389
|
-
ws,
|
|
390
|
-
/** @type {Record<string, unknown>} */ (pkgByWs.get(ws)),
|
|
391
|
-
getPublishedVersion,
|
|
392
|
-
pass,
|
|
393
|
-
fail
|
|
394
|
-
)
|
|
515
|
+
for (const manifest of published) {
|
|
516
|
+
await checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVersion, pass, fail)
|
|
395
517
|
}
|
|
396
518
|
|
|
397
|
-
await runLocalOnlyChecks(
|
|
519
|
+
await runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail)
|
|
398
520
|
|
|
399
521
|
return reporter.getExitCode()
|
|
400
522
|
}
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Очікуваний формат URL — кластерний DNS-суфікс `<cluster>.internal`:
|
|
11
11
|
* - GKE / GCP: `http://<service>.<namespace>.svc.<cluster>.internal:<port>`
|
|
12
|
-
* приклад: `http://contract-h.ua-contract.svc.abie-ua.internal:8080`
|
|
12
|
+
* приклад: `http://contract-h-hl.ua-contract.svc.abie-ua.internal:8080`
|
|
13
13
|
*
|
|
14
14
|
* Сегменти беруться з `hasura/k8s/base/svc-hl.yaml` (`metadata.name` —
|
|
15
|
-
* має закінчуватись на `-h
|
|
15
|
+
* headless, має закінчуватись на `-h-hl`; див. `hasura.svc_hl` / k8s.svc_hl_yaml) і
|
|
16
|
+
* `hasura/k8s/base/namespace.yaml`
|
|
16
17
|
* (`metadata.name` — namespace). Якщо ці YAML є в репозиторії, у URL додатково
|
|
17
18
|
* звіряються конкретні `<service>` і `<namespace>` з ними.
|
|
18
19
|
*
|
package/rules/hasura/hasura.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила для директорії з hasura graphql-engine
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.2'
|
|
4
4
|
globs: "**/hasura/**,**/*.env"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -18,10 +18,10 @@ HASURA_GRAPHQL_ENDPOINT=https://vybeerai.com.ua/contract/ql
|
|
|
18
18
|
Правильне значення:
|
|
19
19
|
|
|
20
20
|
```env
|
|
21
|
-
HASURA_GRAPHQL_ENDPOINT=http://contract-h.ua-contract.svc.abie-ua.internal:8080
|
|
21
|
+
HASURA_GRAPHQL_ENDPOINT=http://contract-h-hl.ua-contract.svc.abie-ua.internal:8080
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
де `contract-h` — це `metadata.name`
|
|
24
|
+
де `contract-h-hl` — це `metadata.name` headless Service з `hasura/k8s/base/svc-hl.yaml` (пара з clusterIP `contract-h` у `svc.yaml`, якщо є; узгоджено з k8s: суфікс `-hl` на базі `-h`), а `ua-contract` — `metadata.name` namespace з `hasura/k8s/base/namespace.yaml`.
|
|
25
25
|
|
|
26
26
|
Правило застосовується для проєктів **nitra** (у кореневому `package.json` `"repository": "https://github.com/nitra/*"`) і **abie** (`"repository": "https://github.com/abinbevefes/*"`); для інших репозиторіїв перевірка пропускається.
|
|
27
27
|
|
|
@@ -1,28 +1,50 @@
|
|
|
1
|
-
#
|
|
2
|
-
# `
|
|
3
|
-
#
|
|
1
|
+
# Іменування Service у `hasura/k8s/base/svc.yaml` та `svc-hl.yaml`, узгоджене з
|
|
2
|
+
# `k8s.svc_hl_yaml` / `k8s.svc_yaml` (пара clusterIP + headless під `k8s/**/`).
|
|
3
|
+
#
|
|
4
|
+
# Hasura-конвенція: базовий сегмент закінчується на `-h`; headless додає `-hl`
|
|
5
|
+
# → повне ім'я `*-h-hl` (також задовольняє k8s-вимогу суфікса `-hl`).
|
|
4
6
|
#
|
|
5
7
|
# Запуск (локально):
|
|
6
|
-
# conftest test hasura/k8s/base/svc-hl.yaml -p npm/policy/
|
|
8
|
+
# conftest test hasura/k8s/base/svc-hl.yaml -p npm/rules/hasura/policy/svc_hl \
|
|
7
9
|
# --namespace hasura.svc_hl
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
# з `<service>.<namespace>.svc.<cluster>` через regex по всьому дереву репо, gating
|
|
11
|
-
# на `repository` у кореневому `package.json`) — у JS: вона потребує текстового
|
|
12
|
-
# парсингу `.env`-файлів, обходу дерева й cross-file resolution. JS authoritative;
|
|
13
|
-
# ця Rego — додатковий gate (JS неявно перевіряє суфікс через звірку URL).
|
|
14
|
-
#
|
|
15
|
-
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
16
|
-
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
17
|
-
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
11
|
+
# Cross-file (`HASURA_GRAPHQL_ENDPOINT` ↔ YAML) — `fix/internal_urls/check.mjs`.
|
|
18
12
|
package hasura.svc_hl
|
|
19
13
|
|
|
20
14
|
import rego.v1
|
|
21
15
|
|
|
16
|
+
# Суфікс clusterIP Service у hasura/k8s/base (і база для пари з svc-hl.yaml).
|
|
17
|
+
hasura_cluster_suffix := "-h"
|
|
18
|
+
|
|
19
|
+
# Headless: `<cluster-name>-hl`, напр. `db-h` → `db-h-hl`.
|
|
20
|
+
hasura_headless_suffix := "-h-hl"
|
|
21
|
+
|
|
22
|
+
service_is_headless if {
|
|
23
|
+
input.kind == "Service"
|
|
24
|
+
spec := object.get(input, "spec", {})
|
|
25
|
+
is_object(spec)
|
|
26
|
+
spec.clusterIP == "None"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
deny contains msg if {
|
|
30
|
+
service_is_headless
|
|
31
|
+
name := object.get(object.get(input, "metadata", {}), "name", "")
|
|
32
|
+
name != ""
|
|
33
|
+
not endswith(name, hasura_headless_suffix)
|
|
34
|
+
msg := sprintf(
|
|
35
|
+
"hasura svc-hl.yaml: headless Service %q має закінчуватись на `%s` (узгоджено з k8s.svc_hl_yaml `-hl`; hasura.mdc)",
|
|
36
|
+
[name, hasura_headless_suffix],
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
deny contains msg if {
|
|
23
41
|
input.kind == "Service"
|
|
42
|
+
not service_is_headless
|
|
24
43
|
name := object.get(object.get(input, "metadata", {}), "name", "")
|
|
25
44
|
name != ""
|
|
26
|
-
not endswith(name,
|
|
27
|
-
msg := sprintf(
|
|
45
|
+
not endswith(name, hasura_cluster_suffix)
|
|
46
|
+
msg := sprintf(
|
|
47
|
+
"hasura svc.yaml: clusterIP Service %q має закінчуватись на `%s` (hasura.mdc / k8s.mdc)",
|
|
48
|
+
[name, hasura_cluster_suffix],
|
|
49
|
+
)
|
|
28
50
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Перевірка JavaScript коду
|
|
3
3
|
globs: "**/{.oxlintrc.json,eslint.config.js,.jscpd.json,knip.json,package.json},**/*.{js,mjs,cjs,jsx,ts,tsx}"
|
|
4
4
|
alwaysApply: false
|
|
5
|
-
version: '1.
|
|
5
|
+
version: '1.23'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
**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 не додавай без потреби монорепо.
|
|
@@ -21,6 +21,10 @@ version: '1.22'
|
|
|
21
21
|
}
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
Канон `type` + `scripts.lint-js` (substring requirement): [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json)
|
|
25
|
+
|
|
26
|
+
У `.vscode/extensions.json` `recommendations` мають містити `dbaeumer.vscode-eslint`, `github.vscode-github-actions`, `oxc.oxc-vscode`: [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
|
|
27
|
+
|
|
24
28
|
У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/scripts/utils/oxlint-canonical.json`** (plugins, jsPlugins з **`@e18e/eslint-plugin`**, categories, повний набір **rules** із канону — додаткові записи в **`rules`** дозволені; також **`settings`**, **`env`**, **`globals`**). Поле **`ignorePatterns`** працює як **`rules`**: канонічні патерни з **`oxlint-canonical.json`** (наразі **`**/schema.graphql`**, **`**/auto-imports.d.ts`**) мають бути присутні, додаткові локальні glob-и дозволені. Оновити канон можна з репозиторію пакета або скопіювавши файл після **`bun ./scripts/utils/rebuild-oxlint-canonical.mjs`** (джерело правил — **`oxlint-rules.tsv`** + скелет **`oxlint-canonical-skeleton.json`**). Модуль **`@e18e/eslint-plugin`** не оголошуй окремо в **`package.json`** — він уже в залежностях **`@nitra/eslint-config`** (з **3.8.0**), oxlint підвантажує його з **`node_modules`**.
|
|
25
29
|
|
|
26
30
|
Мінімум для розуміння структури (реальний корінь конфігу має збігатися з каноном повністю):
|
|
@@ -48,6 +52,8 @@ version: '1.22'
|
|
|
48
52
|
}
|
|
49
53
|
```
|
|
50
54
|
|
|
55
|
+
Канон базових ключів `.jscpd.json` (`gitignore`, `exitCode`, `reporters`, `minLines`): [.jscpd.json.snippet.json](./policy/jscpd/template/.jscpd.json.snippet.json)
|
|
56
|
+
|
|
51
57
|
```text title=".gitignore (фрагмент)"
|
|
52
58
|
.claude/worktrees/
|
|
53
59
|
```
|
|
@@ -122,6 +128,8 @@ jobs:
|
|
|
122
128
|
bunx knip --no-config-hints
|
|
123
129
|
```
|
|
124
130
|
|
|
131
|
+
Канон workflow `.github/workflows/lint-js.yml`: [lint-js.yml.snippet.yml](./policy/lint_js_yml/template/lint-js.yml.snippet.yml)
|
|
132
|
+
|
|
125
133
|
Перед **`./.github/actions/setup-bun-deps`** — **`actions/checkout@v6`** (див. **ga.mdc**). Composite: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
|
|
126
134
|
|
|
127
135
|
Один workflow на лінт JS; зайвий `lint.yml` з тими самими кроками — прибери.
|
package/rules/js-run/js-run.mdc
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Це правила для backend проектів на JavaScript/Node.js, сюди входять і job і WEB сервери.
|
|
3
3
|
globs: "**/package.json,**/jsconfig.json,**/src/**/*.{js,mjs,cjs,ts,tsx}"
|
|
4
4
|
alwaysApply: false
|
|
5
|
-
version: '1.
|
|
5
|
+
version: '1.9'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## Область застосування
|
|
@@ -46,16 +46,20 @@ readme.md
|
|
|
46
46
|
}
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
Канон: [jsconfig.json.snippet.json](./policy/jsconfig/template/jsconfig.json.snippet.json)
|
|
50
|
+
|
|
49
51
|
Якщо пакет не слідує структурі з `src/` (наприклад, лише `scripts/` у корені) — ця вимога не застосовується; для типових сервісів із `src/` файл обов’язковий і має збігатися з каноном.
|
|
50
52
|
|
|
51
53
|
## Використання @nitra/pino
|
|
52
54
|
|
|
53
55
|
Проект використовує @nitra/pino для логування.
|
|
54
|
-
Якщо в проекті присутній @nitra/bunyan, то він повинен бути замінений на @nitra/pino — як у `package.json`, так і в коді: усі `import` / `require` / динамічні `import()` з `@nitra/bunyan` (і застарілого `bunyan`) треба замінити на `@nitra/pino` і за потреби адаптувати виклики під його API.
|
|
56
|
+
Якщо в проекті присутній @nitra/bunyan, то він повинен бути замінений на @nitra/pino — як у `package.json`, так і в коді: усі `import` / `require` / динамічні `import()` з `@nitra/bunyan` (і застарілого `bunyan`) треба замінити на `@nitra/pino` і за потреби адаптувати виклики під його API. Канон заборонених `dependencies` / `devDependencies` (`bunyan`, `@nitra/bunyan`): [package.json.deny.json](./policy/package_json/template/package.json.deny.json)
|
|
55
57
|
|
|
56
58
|
В **/k8s/base/configmap.yaml повинен бути заданий OTEL_RESOURCE_ATTRIBUTES: 'service.name=<project_name>,service.namespace=<project_namespace>'
|
|
57
59
|
а в директоріях з kustomize повинні бути перевизначені значення OTEL_RESOURCE_ATTRIBUTES і в них service.namespace повинен відповідати namespace, в якому знаходиться дана директорія.
|
|
58
60
|
|
|
61
|
+
Канон обовʼязкових substring у `data.OTEL_RESOURCE_ATTRIBUTES` (`service.name=`, `service.namespace=`): [configmap.yaml.contains.yml](./policy/configmap/template/configmap.yaml.contains.yml)
|
|
62
|
+
|
|
59
63
|
## Внутрішні аліаси
|
|
60
64
|
|
|
61
65
|
Якщо в проекті є підключення до баз даних, зовнішніх graphql на кшталт:
|
package/rules/text/text.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Обробка та перевірка текстових файлів, oxfmt, cspell, shellcheck (sh), dotenv-linter (.env*), markdownlint-cli2, v8r, CI
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.28'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
**oxfmt** (`.oxfmtrc.json`, редактор), **cspell**, **shellcheck** (tracked `*.sh` у `lint-text`), **[dotenv-linter](https://dotenv-linter.github.io/)** (`.env*` у `lint-text`), **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint** / **timonwong.shellcheck**, workflow **`lint-text`**.
|
|
@@ -20,6 +20,8 @@ version: '1.27'
|
|
|
20
20
|
}
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
Канон `recommendations` (substring requirement): [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
|
|
24
|
+
|
|
23
25
|
```json title=".vscode/settings.json"
|
|
24
26
|
{
|
|
25
27
|
"files.associations": {
|
|
@@ -79,6 +81,8 @@ version: '1.27'
|
|
|
79
81
|
}
|
|
80
82
|
```
|
|
81
83
|
|
|
84
|
+
Канон `editor.formatOnSave` + `editor.defaultFormatter` для основних мов: [settings.json.snippet.json](./policy/vscode_settings/template/settings.json.snippet.json)
|
|
85
|
+
|
|
82
86
|
У корені проєкту має бути файл з правилами форматування для **oxfmt**:
|
|
83
87
|
|
|
84
88
|
```json title=".oxfmtrc.json"
|
|
@@ -105,9 +109,11 @@ version: '1.27'
|
|
|
105
109
|
}
|
|
106
110
|
```
|
|
107
111
|
|
|
112
|
+
Канон мінімального набору ключів і `ignorePatterns`: [.oxfmtrc.json.snippet.json](./policy/oxfmtrc/template/.oxfmtrc.json.snippet.json)
|
|
113
|
+
|
|
108
114
|
Поле **`ignorePatterns`** обовʼязкове: у масиві мають бути **`**/hasura/metadata/**`**, **`**/schema.graphql`** і **`**/auto-imports.d.ts`**; інші glob-и додавай за потреби (згенеровані каталоги тощо) — канон задає мінімум, локальні розширення дозволені.
|
|
109
115
|
|
|
110
|
-
Також потрібно прибрати, якщо є в проєкті, модуль **`@nitra/prettier-config`**, **prettier** та всі виклики prettier і налаштування для нього.
|
|
116
|
+
Також потрібно прибрати, якщо є в проєкті, модуль **`@nitra/prettier-config`**, **prettier** та всі виклики prettier і налаштування для нього. Канон заборонених top-level/`dependencies`/`devDependencies` (prettier, `@nitra/prettier-config`, `markdownlint-cli2`): [package.json.deny.json](./policy/package_json/template/package.json.deny.json)
|
|
111
117
|
|
|
112
118
|
Завжди пиши **JSDoc** до функцій та методів.
|
|
113
119
|
|
|
@@ -156,6 +162,8 @@ version: '1.27'
|
|
|
156
162
|
}
|
|
157
163
|
```
|
|
158
164
|
|
|
165
|
+
Канон: [.markdownlint-cli2.jsonc.snippet.jsonc](./policy/markdownlint/template/.markdownlint-cli2.jsonc.snippet.jsonc)
|
|
166
|
+
|
|
159
167
|
**MD041** off навмисно (`.mdc` з frontmatter). Деталі — [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2).
|
|
160
168
|
|
|
161
169
|
## Cspell
|
|
@@ -238,6 +246,8 @@ jobs:
|
|
|
238
246
|
}
|
|
239
247
|
```
|
|
240
248
|
|
|
249
|
+
Канон базових ключів `.cspell.json` (`version`, `ignorePaths`): [.cspell.json.snippet.json](./policy/cspell/template/.cspell.json.snippet.json). Обовʼязковий запис у `import`: [.cspell.json.contains.json](./policy/cspell/template/.cspell.json.contains.json). Заборонено імпортувати окремі `@cspell/dict-*` у `.cspell.json`: [.cspell.json.deny.json](./policy/cspell/template/.cspell.json.deny.json).
|
|
250
|
+
|
|
241
251
|
```json title="package.json"
|
|
242
252
|
{
|
|
243
253
|
"scripts": {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Уніфікований маніфест пакета для перевірок changelog: `package.json` (npm/JS)
|
|
3
|
+
* або `pyproject.toml` (Python / PEP 621, Poetry).
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from 'node:fs'
|
|
6
|
+
import { glob, readFile } from 'node:fs/promises'
|
|
7
|
+
import { dirname, join, relative } from 'node:path'
|
|
8
|
+
|
|
9
|
+
import { parse as parseToml } from 'smol-toml'
|
|
10
|
+
|
|
11
|
+
import { getMonorepoPackageRootDirs } from './workspaces.mjs'
|
|
12
|
+
|
|
13
|
+
/** @typedef {'npm' | 'python'} PackageKind */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} PackageManifest
|
|
17
|
+
* @property {PackageKind} kind
|
|
18
|
+
* @property {string} ws відносний шлях воркспейсу (`'.'` для кореня)
|
|
19
|
+
* @property {string} manifestRel `package.json` | `pyproject.toml`
|
|
20
|
+
* @property {string | null} name ім'я пакета (npm / PyPI)
|
|
21
|
+
* @property {string | null} version semver-рядок
|
|
22
|
+
* @property {boolean} registryPublishable чи застосовується режим порівняння з реєстром
|
|
23
|
+
* @property {string[] | null} [npmFiles] лише npm: `files` з package.json
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const PYPROJECT_GLOB_IGNORE = ['**/node_modules/**', '**/.git/**', '**/.venv/**', '**/venv/**']
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {unknown} doc розпарсений pyproject.toml
|
|
30
|
+
* @returns {{ name: string | null, version: string | null }}
|
|
31
|
+
*/
|
|
32
|
+
function projectFieldsFromPyprojectDoc(doc) {
|
|
33
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
34
|
+
return { name: null, version: null }
|
|
35
|
+
}
|
|
36
|
+
const root = /** @type {Record<string, unknown>} */ (doc)
|
|
37
|
+
const project = root.project
|
|
38
|
+
if (project && typeof project === 'object' && !Array.isArray(project)) {
|
|
39
|
+
const p = /** @type {Record<string, unknown>} */ (project)
|
|
40
|
+
return {
|
|
41
|
+
name: typeof p.name === 'string' ? p.name : null,
|
|
42
|
+
version: typeof p.version === 'string' ? p.version : null,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const tool = root.tool
|
|
46
|
+
if (tool && typeof tool === 'object' && !Array.isArray(tool)) {
|
|
47
|
+
const poetry = /** @type {Record<string, unknown>} */ (tool).poetry
|
|
48
|
+
if (poetry && typeof poetry === 'object' && !Array.isArray(poetry)) {
|
|
49
|
+
const po = /** @type {Record<string, unknown>} */ (poetry)
|
|
50
|
+
return {
|
|
51
|
+
name: typeof po.name === 'string' ? po.name : null,
|
|
52
|
+
version: typeof po.version === 'string' ? po.version : null,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { name: null, version: null }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} text вміст pyproject.toml
|
|
61
|
+
* @returns {{ name: string | null, version: string | null }}
|
|
62
|
+
*/
|
|
63
|
+
export function parsePyprojectFields(text) {
|
|
64
|
+
try {
|
|
65
|
+
return projectFieldsFromPyprojectDoc(parseToml(text))
|
|
66
|
+
} catch {
|
|
67
|
+
return { name: null, version: null }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} ws шлях воркспейсу
|
|
73
|
+
* @returns {Promise<PackageManifest | null>}
|
|
74
|
+
*/
|
|
75
|
+
export async function readPackageManifest(ws) {
|
|
76
|
+
const pkgPath = join(ws, 'package.json')
|
|
77
|
+
if (existsSync(pkgPath)) {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
80
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
const pkg = /** @type {Record<string, unknown>} */ (parsed)
|
|
84
|
+
const registryPublishable =
|
|
85
|
+
typeof pkg.name === 'string' &&
|
|
86
|
+
pkg.name.length > 0 &&
|
|
87
|
+
pkg.private !== true &&
|
|
88
|
+
Array.isArray(pkg.files)
|
|
89
|
+
return {
|
|
90
|
+
kind: 'npm',
|
|
91
|
+
ws,
|
|
92
|
+
manifestRel: 'package.json',
|
|
93
|
+
name: typeof pkg.name === 'string' ? pkg.name : null,
|
|
94
|
+
version: typeof pkg.version === 'string' ? pkg.version : null,
|
|
95
|
+
registryPublishable,
|
|
96
|
+
npmFiles: Array.isArray(pkg.files) ? pkg.files : null,
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const pyPath = join(ws, 'pyproject.toml')
|
|
104
|
+
if (!existsSync(pyPath)) {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
const fields = parsePyprojectFields(await readFile(pyPath, 'utf8'))
|
|
108
|
+
const registryPublishable = Boolean(fields.name && fields.version)
|
|
109
|
+
return {
|
|
110
|
+
kind: 'python',
|
|
111
|
+
ws,
|
|
112
|
+
manifestRel: 'pyproject.toml',
|
|
113
|
+
name: fields.name,
|
|
114
|
+
version: fields.version,
|
|
115
|
+
registryPublishable,
|
|
116
|
+
npmFiles: null,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Каталоги пакетів: npm (`package.json` / workspaces) + Python (`pyproject.toml` без package.json).
|
|
122
|
+
* @param {string} [repoRoot]
|
|
123
|
+
* @returns {Promise<string[]>}
|
|
124
|
+
*/
|
|
125
|
+
export async function getMonorepoProjectRootDirs(repoRoot = '.') {
|
|
126
|
+
const roots = new Set(await getMonorepoPackageRootDirs(repoRoot))
|
|
127
|
+
|
|
128
|
+
if (existsSync(join(repoRoot, 'pyproject.toml')) && !existsSync(join(repoRoot, 'package.json'))) {
|
|
129
|
+
roots.add('.')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for await (const relPy of glob('**/pyproject.toml', { cwd: repoRoot, ignore: PYPROJECT_GLOB_IGNORE })) {
|
|
133
|
+
const absDir = dirname(join(repoRoot, relPy))
|
|
134
|
+
const relRoot = relative(repoRoot, absDir)
|
|
135
|
+
const ws = relRoot === '' ? '.' : relRoot
|
|
136
|
+
if (!existsSync(join(repoRoot, ws, 'package.json'))) {
|
|
137
|
+
roots.add(ws)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const list = [...roots]
|
|
142
|
+
list.sort((a, b) => {
|
|
143
|
+
if (a === '.') return -1
|
|
144
|
+
if (b === '.') return 1
|
|
145
|
+
return a.localeCompare(b)
|
|
146
|
+
})
|
|
147
|
+
return list
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Шлях до файлу маніфесту воркспейсу.
|
|
152
|
+
* @param {string} ws
|
|
153
|
+
* @param {PackageManifest} manifest
|
|
154
|
+
* @returns {string}
|
|
155
|
+
*/
|
|
156
|
+
export function manifestFilePath(ws, manifest) {
|
|
157
|
+
return join(ws, manifest.manifestRel)
|
|
158
|
+
}
|
|
@@ -28,8 +28,9 @@ async function parseByExt(path) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function stripJsonComments(s) {
|
|
31
|
-
//
|
|
32
|
-
|
|
31
|
+
// Match string literals OR comments. Strings are returned unchanged so we never
|
|
32
|
+
// strip `/*` / `//` / `*/` that appear inside values (e.g. glob `**/node_modules/**`).
|
|
33
|
+
return s.replace(/"(?:\\.|[^"\\])*"|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, m => (m.startsWith('"') ? m : ''))
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
async function walk(dir, base = dir) {
|