@nitra/cursor 1.8.171 → 1.8.177
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 +9 -4
- package/CHANGELOG.md +47 -0
- package/README.md +14 -0
- package/bin/auto-rules.md +3 -1
- package/mdc/changelog.mdc +64 -0
- package/mdc/k8s.mdc +57 -0
- package/mdc/npm-module.mdc +2 -6
- package/package.json +1 -1
- package/schemas/n-cursor.json +1 -1
- package/scripts/auto-rules.mjs +4 -8
- package/scripts/check-abie.mjs +17 -11
- package/scripts/check-changelog.mjs +402 -0
- package/scripts/check-docker.mjs +12 -5
- package/scripts/check-graphql.mjs +15 -9
- package/scripts/check-hasura.mjs +14 -8
- package/scripts/check-image.mjs +15 -9
- package/scripts/check-js-bun-db.mjs +25 -15
- package/scripts/check-js-mssql.mjs +27 -17
- package/scripts/check-js-run.mjs +26 -16
- package/scripts/check-k8s.mjs +131 -14
- package/scripts/check-nginx-default-tpl.mjs +26 -16
- package/scripts/check-npm-module.mjs +13 -62
- package/scripts/check-vue.mjs +27 -17
- package/scripts/rename-yaml-extensions.mjs +35 -29
- package/scripts/run-docker.mjs +11 -5
- package/scripts/run-k8s.mjs +14 -8
- package/scripts/utils/load-cursor-config.mjs +53 -0
- package/scripts/utils/walkDir.mjs +49 -8
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє, що в кожному workspace із незакомічаними/незрелізнутими змінами підвищена `version` у
|
|
3
|
+
* `<ws>/package.json` і в `<ws>/CHANGELOG.md` присутній запис `## [version] - YYYY-MM-DD`
|
|
4
|
+
* (формат Keep a Changelog).
|
|
5
|
+
*
|
|
6
|
+
* Дві моделі визначення «бази для порівняння» — на рівні воркспейсу:
|
|
7
|
+
*
|
|
8
|
+
* 1) **npm-published mode** (`<ws>/package.json` має непорожнє `name`, не `private: true`,
|
|
9
|
+
* і має масив `files`): база = опублікована версія в npm-реєстрі (`npm view <name> version`).
|
|
10
|
+
* Git не задіяний. Якщо локальна версія відрізняється від опублікованої — потрібен запис
|
|
11
|
+
* у CHANGELOG для локальної версії й `"CHANGELOG.md"` у `files`. Якщо `npm view` недосяжний
|
|
12
|
+
* (немає мережі / пакет ще не публікувався) — fail-safe pass із поясненням, щоб локальна
|
|
13
|
+
* розробка офлайн не блокувалась.
|
|
14
|
+
*
|
|
15
|
+
* 2) **local-only mode** (приватні / без `files` воркспейси): PR-scoped перевірка проти `dev`.
|
|
16
|
+
* База = `git merge-base <dev> HEAD` (точка розгалуження поточної гілки від `dev`), щоб:
|
|
17
|
+
* - на feature-гілці бачити лише унікальні коміти цієї гілки;
|
|
18
|
+
* - на `main` після merge `dev → main` diff був порожній (нічого не вимагати);
|
|
19
|
+
* - direct-commit на `main` поза PR-flow ловився як зміна, що потребує bump + CHANGELOG.
|
|
20
|
+
* Якщо не git-репо, поточна гілка = `dev`, або `dev`/`origin/dev` не існує — пропуск.
|
|
21
|
+
*
|
|
22
|
+
* Усі `git` і `npm` виклики — через `execFile`, без shell-інтерполяції.
|
|
23
|
+
*/
|
|
24
|
+
import { execFile } from 'node:child_process'
|
|
25
|
+
import { existsSync } from 'node:fs'
|
|
26
|
+
import { readFile } from 'node:fs/promises'
|
|
27
|
+
import { join } from 'node:path'
|
|
28
|
+
import { promisify } from 'node:util'
|
|
29
|
+
|
|
30
|
+
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
31
|
+
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
32
|
+
|
|
33
|
+
const execFileAsync = promisify(execFile)
|
|
34
|
+
|
|
35
|
+
/** Базова гілка PR — фіксована, без конфіга (див. n-changelog.mdc) */
|
|
36
|
+
const BASE_BRANCH = 'dev'
|
|
37
|
+
|
|
38
|
+
/** Таймаут на `npm view <name> version` (мс), щоб не блокуватись на офлайні */
|
|
39
|
+
const NPM_VIEW_TIMEOUT_MS = 10_000
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Тихо запускає `git` і повертає stdout або `null` при будь-якій помилці.
|
|
43
|
+
* @param {string[]} args аргументи `git`
|
|
44
|
+
* @returns {Promise<string | null>}
|
|
45
|
+
*/
|
|
46
|
+
async function gitOrNull(args) {
|
|
47
|
+
try {
|
|
48
|
+
const { stdout } = await execFileAsync('git', args)
|
|
49
|
+
return stdout
|
|
50
|
+
} catch {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Чи робочий каталог — git-репозиторій.
|
|
57
|
+
* @returns {Promise<boolean>}
|
|
58
|
+
*/
|
|
59
|
+
async function isInsideGitRepo() {
|
|
60
|
+
const out = await gitOrNull(['rev-parse', '--is-inside-work-tree'])
|
|
61
|
+
return typeof out === 'string' && out.trim() === 'true'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Назва поточної гілки (або `HEAD` для detached state).
|
|
66
|
+
* @returns {Promise<string | null>}
|
|
67
|
+
*/
|
|
68
|
+
async function currentBranchName() {
|
|
69
|
+
const out = await gitOrNull(['rev-parse', '--abbrev-ref', 'HEAD'])
|
|
70
|
+
return typeof out === 'string' ? out.trim() : null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Знаходить ref для базової гілки. Перевага локальному `dev`, далі `origin/dev`. Повертає `null`,
|
|
75
|
+
* якщо жоден не існує.
|
|
76
|
+
* @returns {Promise<string | null>}
|
|
77
|
+
*/
|
|
78
|
+
async function resolveBaseRef() {
|
|
79
|
+
for (const ref of [BASE_BRANCH, `origin/${BASE_BRANCH}`]) {
|
|
80
|
+
const out = await gitOrNull(['rev-parse', '--verify', '--quiet', ref])
|
|
81
|
+
if (typeof out === 'string' && out.trim().length > 0) {
|
|
82
|
+
return ref
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Точка розгалуження поточної гілки від `baseRef`. На feature-гілці = коли вона відгалузилась;
|
|
90
|
+
* на `main` після merge `dev → main` = поточний `dev`. Повертає `null`, якщо merge-base нема.
|
|
91
|
+
* @param {string} baseRef
|
|
92
|
+
* @returns {Promise<string | null>}
|
|
93
|
+
*/
|
|
94
|
+
async function resolveMergeBase(baseRef) {
|
|
95
|
+
const out = await gitOrNull(['merge-base', baseRef, 'HEAD'])
|
|
96
|
+
if (typeof out !== 'string') return null
|
|
97
|
+
const sha = out.trim()
|
|
98
|
+
return sha.length > 0 ? sha : null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Будує pathspec для `git diff` / `ls-files` для воркспейсу.
|
|
103
|
+
*
|
|
104
|
+
* Для кореня `.` — це точка плюс magic-виключення кожного підворкспейсу через `:(exclude)<sub>/`,
|
|
105
|
+
* щоб зміни всередині sub-workspace не вважалися змінами кореня.
|
|
106
|
+
* Для звичайного воркспейсу — просто `<ws>/`.
|
|
107
|
+
* @param {string} ws
|
|
108
|
+
* @param {string[]} subWorkspaces
|
|
109
|
+
* @returns {string[]}
|
|
110
|
+
*/
|
|
111
|
+
function pathspecForWorkspace(ws, subWorkspaces) {
|
|
112
|
+
if (ws !== '.') return [`${ws}/`]
|
|
113
|
+
return ['.', ...subWorkspaces.filter(s => s !== '.').map(s => `:(exclude)${s}/`)]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Чи є зміни (committed або в робочому дереві) у каталозі `<ws>` відносно `baseRef`.
|
|
118
|
+
*
|
|
119
|
+
* `git diff --quiet <baseRef> -- <pathspec>` ловить committed-зміни на цій гілці й незбережені
|
|
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
|
|
124
|
+
* @returns {Promise<boolean>}
|
|
125
|
+
*/
|
|
126
|
+
async function workspaceHasChangesAgainstBase(baseRef, ws, subWorkspaces) {
|
|
127
|
+
const pathspec = pathspecForWorkspace(ws, subWorkspaces)
|
|
128
|
+
try {
|
|
129
|
+
await execFileAsync('git', ['diff', '--quiet', baseRef, '--', ...pathspec])
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const code = /** @type {{ code?: number }} */ (err).code
|
|
132
|
+
if (code === 1) return true
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
const untracked = await gitOrNull(['ls-files', '--others', '--exclude-standard', '--', ...pathspec])
|
|
136
|
+
return typeof untracked === 'string' && untracked.trim().length > 0
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Версія з `<ws>/package.json` на `baseRef` або `null`.
|
|
141
|
+
* @param {string} baseRef
|
|
142
|
+
* @param {string} ws
|
|
143
|
+
* @returns {Promise<string | null>}
|
|
144
|
+
*/
|
|
145
|
+
async function readBaseVersion(baseRef, ws) {
|
|
146
|
+
const wsPath = ws === '.' ? 'package.json' : `${ws}/package.json`
|
|
147
|
+
const out = await gitOrNull(['show', `${baseRef}:${wsPath}`])
|
|
148
|
+
if (out === null) return null
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(out)
|
|
151
|
+
return typeof parsed?.version === 'string' ? parsed.version : null
|
|
152
|
+
} catch {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Чи містить текст `CHANGELOG.md` запис `## [version]` (з опційним `- YYYY-MM-DD`).
|
|
159
|
+
* @param {string} text
|
|
160
|
+
* @param {string} version
|
|
161
|
+
* @returns {boolean}
|
|
162
|
+
*/
|
|
163
|
+
function changelogHasVersionEntry(text, version) {
|
|
164
|
+
const escaped = version.replaceAll(/[.+*?^$()[\]{}|\\]/g, String.raw`\$&`)
|
|
165
|
+
const re = new RegExp(String.raw`^##\s+\[${escaped}\]`, 'm')
|
|
166
|
+
return re.test(text)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Зчитує `<ws>/package.json`. `null`, якщо файл відсутній або JSON некоректний.
|
|
171
|
+
* @param {string} ws
|
|
172
|
+
* @returns {Promise<Record<string, unknown> | null>}
|
|
173
|
+
*/
|
|
174
|
+
async function readPackageJsonOrNull(ws) {
|
|
175
|
+
const path = join(ws, 'package.json')
|
|
176
|
+
if (!existsSync(path)) return null
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'))
|
|
179
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
180
|
+
? /** @type {Record<string, unknown>} */ (parsed)
|
|
181
|
+
: null
|
|
182
|
+
} catch {
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Воркспейс публікується в npm: має непорожній `name`, не `private: true`, і має масив `files`.
|
|
189
|
+
* @param {Record<string, unknown> | null} pkg
|
|
190
|
+
* @returns {boolean}
|
|
191
|
+
*/
|
|
192
|
+
function isNpmPublishable(pkg) {
|
|
193
|
+
if (!pkg) return false
|
|
194
|
+
if (typeof pkg.name !== 'string' || pkg.name.length === 0) return false
|
|
195
|
+
if (pkg.private === true) return false
|
|
196
|
+
return Array.isArray(pkg.files)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Опублікована версія пакета в npm-реєстрі. `null` — пакет не знайдено / нема мережі / помилка.
|
|
201
|
+
* Дефолтна імплементація — `npm view <name> version` із таймаутом, щоб не блокуватись офлайн.
|
|
202
|
+
* @param {string} name
|
|
203
|
+
* @returns {Promise<string | null>}
|
|
204
|
+
*/
|
|
205
|
+
async function defaultGetPublishedVersion(name) {
|
|
206
|
+
try {
|
|
207
|
+
const { stdout } = await execFileAsync('npm', ['view', name, 'version'], { timeout: NPM_VIEW_TIMEOUT_MS })
|
|
208
|
+
const v = stdout.trim()
|
|
209
|
+
return v.length > 0 ? v : null
|
|
210
|
+
} catch {
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Перевіряє масив `files` у `<ws>/package.json`: якщо оголошено — має містити `"CHANGELOG.md"`.
|
|
217
|
+
* @param {Record<string, unknown> | null} pkg
|
|
218
|
+
* @param {string} ws
|
|
219
|
+
* @param {(msg: string) => void} pass
|
|
220
|
+
* @param {(msg: string) => void} fail
|
|
221
|
+
*/
|
|
222
|
+
function checkFilesArrayContainsChangelog(pkg, ws, pass, fail) {
|
|
223
|
+
if (!pkg || !Array.isArray(pkg.files)) return
|
|
224
|
+
const pkgPath = join(ws, 'package.json')
|
|
225
|
+
if (pkg.files.includes('CHANGELOG.md')) {
|
|
226
|
+
pass(`${pkgPath}: files містить "CHANGELOG.md"`)
|
|
227
|
+
} else {
|
|
228
|
+
fail(`${pkgPath}: масив files має містити "CHANGELOG.md", щоб публікувати changelog із пакетом`)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Перевіряє наявність запису у `<ws>/CHANGELOG.md` для версії `version`.
|
|
234
|
+
* @param {string} ws
|
|
235
|
+
* @param {string} version
|
|
236
|
+
* @param {(msg: string) => void} pass
|
|
237
|
+
* @param {(msg: string) => void} fail
|
|
238
|
+
* @returns {Promise<boolean>} `false`, якщо файл відсутній або немає запису
|
|
239
|
+
*/
|
|
240
|
+
async function verifyChangelogEntry(ws, version, pass, fail) {
|
|
241
|
+
const label = ws === '.' ? '<root>' : ws
|
|
242
|
+
const changelogPath = join(ws, 'CHANGELOG.md')
|
|
243
|
+
if (!existsSync(changelogPath)) {
|
|
244
|
+
fail(`${label}: відсутній ${changelogPath} (Keep a Changelog, див. n-changelog.mdc)`)
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
const text = await readFile(changelogPath, 'utf8')
|
|
248
|
+
if (changelogHasVersionEntry(text, version)) {
|
|
249
|
+
pass(`${changelogPath}: знайдено запис для версії ${version}`)
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
fail(`${changelogPath}: відсутній запис для ${version} (формат "## [${version}] - YYYY-MM-DD")`)
|
|
253
|
+
return false
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* npm-published режим: порівнює локальну `version` з опублікованою в реєстрі. Якщо вони
|
|
258
|
+
* відрізняються — вимагає запис у CHANGELOG і `"CHANGELOG.md"` у `files`. Якщо реєстр недосяжний,
|
|
259
|
+
* правило fail-safe пасує (щоб офлайн-розробка не блокувалась).
|
|
260
|
+
* @param {string} ws
|
|
261
|
+
* @param {Record<string, unknown>} pkg
|
|
262
|
+
* @param {(name: string) => Promise<string | null>} getPublishedVersion
|
|
263
|
+
* @param {(msg: string) => void} pass
|
|
264
|
+
* @param {(msg: string) => void} fail
|
|
265
|
+
*/
|
|
266
|
+
async function checkPublishedWorkspace(ws, pkg, getPublishedVersion, pass, fail) {
|
|
267
|
+
const label = ws === '.' ? '<root>' : ws
|
|
268
|
+
const Vcurrent = typeof pkg.version === 'string' ? pkg.version : null
|
|
269
|
+
if (!Vcurrent) {
|
|
270
|
+
fail(`${label}: у package.json відсутнє поле version (npm-published воркспейс)`)
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
const name = /** @type {string} */ (pkg.name)
|
|
274
|
+
const Vpublished = await getPublishedVersion(name)
|
|
275
|
+
if (Vpublished === null) {
|
|
276
|
+
pass(`${label}: ${name} — опублікована версія недоступна (мережа/реєстр), перевірку пропущено`)
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
if (Vpublished === Vcurrent) {
|
|
280
|
+
pass(`${label}: ${name}@${Vcurrent} вже опубліковано — змін до релізу немає`)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
pass(`${label}: ${name} — нова локальна версія (${Vpublished} → ${Vcurrent})`)
|
|
284
|
+
await verifyChangelogEntry(ws, Vcurrent, pass, fail)
|
|
285
|
+
checkFilesArrayContainsChangelog(pkg, ws, pass, fail)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* local-only режим: PR-scoped перевірка проти `dev` через `git merge-base`. Викликається лише
|
|
290
|
+
* для воркспейсів, де є реальні зміни щодо merge-base.
|
|
291
|
+
* @param {string} mergeBase SHA точки розгалуження
|
|
292
|
+
* @param {string} ws
|
|
293
|
+
* @param {Record<string, unknown> | null} pkg
|
|
294
|
+
* @param {(msg: string) => void} pass
|
|
295
|
+
* @param {(msg: string) => void} fail
|
|
296
|
+
*/
|
|
297
|
+
async function checkLocalOnlyChangedWorkspace(mergeBase, ws, pkg, pass, fail) {
|
|
298
|
+
const label = ws === '.' ? '<root>' : ws
|
|
299
|
+
const Vcurrent = typeof pkg?.version === 'string' ? pkg.version : null
|
|
300
|
+
if (!Vcurrent) {
|
|
301
|
+
fail(`${label}: у package.json відсутнє поле version (потрібне для запису в CHANGELOG)`)
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
const Vbase = await readBaseVersion(mergeBase, ws)
|
|
305
|
+
if (Vbase !== null && Vbase === Vcurrent) {
|
|
306
|
+
fail(
|
|
307
|
+
`${label}: у цій гілці є зміни, але version у ${join(ws, 'package.json')} не підвищено (на ${BASE_BRANCH} — ${Vbase}). Bump + запис у CHANGELOG.md обов'язкові на PR`
|
|
308
|
+
)
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
pass(`${label}: version підвищено (${Vbase ?? '∅'} → ${Vcurrent})`)
|
|
312
|
+
if (!(await verifyChangelogEntry(ws, Vcurrent, pass, fail))) return
|
|
313
|
+
checkFilesArrayContainsChangelog(pkg, ws, pass, fail)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Виконує local-only перевірку для всіх workspace-ів, у яких немає npm-published режиму.
|
|
318
|
+
* @param {string[]} localOnlyWorkspaces
|
|
319
|
+
* @param {Map<string, Record<string, unknown> | null>} pkgByWs
|
|
320
|
+
* @param {string[]} subWorkspaces
|
|
321
|
+
* @param {(msg: string) => void} pass
|
|
322
|
+
* @param {(msg: string) => void} fail
|
|
323
|
+
* @returns {Promise<void>}
|
|
324
|
+
*/
|
|
325
|
+
async function runLocalOnlyChecks(localOnlyWorkspaces, pkgByWs, subWorkspaces, pass, fail) {
|
|
326
|
+
if (localOnlyWorkspaces.length === 0) return
|
|
327
|
+
|
|
328
|
+
if (!(await isInsideGitRepo())) {
|
|
329
|
+
pass('changelog: не git-репозиторій — local-only перевірку пропущено')
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
const branch = await currentBranchName()
|
|
333
|
+
if (branch === BASE_BRANCH) {
|
|
334
|
+
pass(`changelog: поточна гілка = ${BASE_BRANCH} — local-only перевірку пропущено`)
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
const baseRef = await resolveBaseRef()
|
|
338
|
+
if (!baseRef) {
|
|
339
|
+
pass(`changelog: ref ${BASE_BRANCH} (та origin/${BASE_BRANCH}) не знайдено — local-only перевірку пропущено`)
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
const mergeBase = await resolveMergeBase(baseRef)
|
|
343
|
+
if (!mergeBase) {
|
|
344
|
+
pass(`changelog: merge-base з ${baseRef} не знайдено — local-only перевірку пропущено`)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
let checkedAny = false
|
|
349
|
+
for (const ws of localOnlyWorkspaces) {
|
|
350
|
+
if (!(await workspaceHasChangesAgainstBase(mergeBase, ws, subWorkspaces))) continue
|
|
351
|
+
checkedAny = true
|
|
352
|
+
await checkLocalOnlyChangedWorkspace(mergeBase, ws, pkgByWs.get(ws) ?? null, pass, fail)
|
|
353
|
+
}
|
|
354
|
+
if (!checkedAny) {
|
|
355
|
+
pass(`changelog: local-only воркспейси без змін відносно merge-base(${baseRef})`)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Перевіряє відповідність проєкту правилу changelog.mdc.
|
|
361
|
+
* @param {object} [opts]
|
|
362
|
+
* @param {(name: string) => Promise<string | null>} [opts.getPublishedVersion] перевизначення для тестів
|
|
363
|
+
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
364
|
+
*/
|
|
365
|
+
export async function check(opts = {}) {
|
|
366
|
+
const reporter = createCheckReporter()
|
|
367
|
+
const { pass, fail } = reporter
|
|
368
|
+
const getPublishedVersion = opts.getPublishedVersion ?? defaultGetPublishedVersion
|
|
369
|
+
|
|
370
|
+
const workspaces = await getMonorepoPackageRootDirs(process.cwd())
|
|
371
|
+
const subWorkspaces = workspaces.filter(w => w !== '.')
|
|
372
|
+
|
|
373
|
+
/** @type {Map<string, Record<string, unknown> | null>} */
|
|
374
|
+
const pkgByWs = new Map()
|
|
375
|
+
/** @type {string[]} */
|
|
376
|
+
const publishedWorkspaces = []
|
|
377
|
+
/** @type {string[]} */
|
|
378
|
+
const localOnlyWorkspaces = []
|
|
379
|
+
for (const ws of workspaces) {
|
|
380
|
+
const pkg = await readPackageJsonOrNull(ws)
|
|
381
|
+
pkgByWs.set(ws, pkg)
|
|
382
|
+
if (isNpmPublishable(pkg)) {
|
|
383
|
+
publishedWorkspaces.push(ws)
|
|
384
|
+
} else {
|
|
385
|
+
localOnlyWorkspaces.push(ws)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const ws of publishedWorkspaces) {
|
|
390
|
+
await checkPublishedWorkspace(
|
|
391
|
+
ws,
|
|
392
|
+
/** @type {Record<string, unknown>} */ (pkgByWs.get(ws)),
|
|
393
|
+
getPublishedVersion,
|
|
394
|
+
pass,
|
|
395
|
+
fail
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
await runLocalOnlyChecks(localOnlyWorkspaces, pkgByWs, subWorkspaces, pass, fail)
|
|
400
|
+
|
|
401
|
+
return reporter.getExitCode()
|
|
402
|
+
}
|
package/scripts/check-docker.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import { basename } from 'node:path'
|
|
|
33
33
|
import { getMirrorGcrHint, getFromImageToken } from './utils/docker-mirror.mjs'
|
|
34
34
|
import { lintDockerfileWithHadolint, posixRel } from './utils/docker-hadolint.mjs'
|
|
35
35
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
36
|
+
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
36
37
|
import { walkDir } from './utils/walkDir.mjs'
|
|
37
38
|
|
|
38
39
|
const NEWLINE_RE = /\r?\n/
|
|
@@ -65,14 +66,19 @@ export function isDockerfileName(name) {
|
|
|
65
66
|
/**
|
|
66
67
|
* Збирає абсолютні шляхи до Dockerfile / Containerfile від кореня cwd.
|
|
67
68
|
* @param {string} root корінь репозиторію
|
|
69
|
+
* @param {string[]} [ignorePaths=[]] шляхи каталогів, повністю виключених з обходу
|
|
68
70
|
* @returns {Promise<string[]>} відсортовані абсолютні шляхи
|
|
69
71
|
*/
|
|
70
|
-
export async function findDockerfilePaths(root) {
|
|
72
|
+
export async function findDockerfilePaths(root, ignorePaths = []) {
|
|
71
73
|
/** @type {string[]} */
|
|
72
74
|
const out = []
|
|
73
|
-
await walkDir(
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
await walkDir(
|
|
76
|
+
root,
|
|
77
|
+
p => {
|
|
78
|
+
if (isDockerfileName(basename(p))) out.push(p)
|
|
79
|
+
},
|
|
80
|
+
ignorePaths
|
|
81
|
+
)
|
|
76
82
|
return out.toSorted((a, b) => a.localeCompare(b))
|
|
77
83
|
}
|
|
78
84
|
|
|
@@ -285,7 +291,8 @@ export async function check() {
|
|
|
285
291
|
const { pass } = reporter
|
|
286
292
|
|
|
287
293
|
const root = process.cwd()
|
|
288
|
-
const
|
|
294
|
+
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
295
|
+
const files = await findDockerfilePaths(root, ignorePaths)
|
|
289
296
|
|
|
290
297
|
if (files.length === 0) {
|
|
291
298
|
pass('Немає Dockerfile / Containerfile — перевірку hadolint пропущено')
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
shouldSkipFileForGqlScan,
|
|
18
18
|
sourceFileHasGqlTaggedTemplate
|
|
19
19
|
} from './utils/graphql-gql-scan.mjs'
|
|
20
|
+
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
20
21
|
import { walkDir } from './utils/walkDir.mjs'
|
|
21
22
|
|
|
22
23
|
/** Очікуваний файл GraphQL Config у корені (graphql.mdc). */
|
|
@@ -33,16 +34,20 @@ export const REQUIRED_DUMP_SCHEMA_SCRIPT =
|
|
|
33
34
|
* @param {string} root абсолютний шлях кореня
|
|
34
35
|
* @returns {Promise<string[]>} список кандидатів
|
|
35
36
|
*/
|
|
36
|
-
async function collectScanCandidates(root) {
|
|
37
|
+
async function collectScanCandidates(root, ignorePaths) {
|
|
37
38
|
/** @type {string[]} */
|
|
38
39
|
const candidates = []
|
|
39
|
-
await walkDir(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
await walkDir(
|
|
41
|
+
root,
|
|
42
|
+
absPath => {
|
|
43
|
+
const rel = relative(root, absPath).split('\\').join('/')
|
|
44
|
+
if (shouldSkipFileForGqlScan(rel) || !isGqlScanSourceFile(rel)) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
candidates.push(absPath)
|
|
48
|
+
},
|
|
49
|
+
ignorePaths
|
|
50
|
+
)
|
|
46
51
|
return candidates
|
|
47
52
|
}
|
|
48
53
|
|
|
@@ -148,7 +153,8 @@ export async function check() {
|
|
|
148
153
|
const { pass, fail } = reporter
|
|
149
154
|
|
|
150
155
|
const root = process.cwd()
|
|
151
|
-
const
|
|
156
|
+
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
157
|
+
const candidates = await collectScanCandidates(root, ignorePaths)
|
|
152
158
|
const hits = await collectGqlHits(root, candidates)
|
|
153
159
|
|
|
154
160
|
if (hits.length === 0) {
|
package/scripts/check-hasura.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import { parseAllDocuments } from 'yaml'
|
|
|
30
30
|
|
|
31
31
|
import { getRepositoryUrl } from './auto-rules.mjs'
|
|
32
32
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
33
|
+
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
33
34
|
import { walkDir } from './utils/walkDir.mjs'
|
|
34
35
|
|
|
35
36
|
const NITRA_REPOSITORY_URL_MARKER = 'https://github.com/nitra/'
|
|
@@ -103,15 +104,19 @@ export function isEnvFile(relPath) {
|
|
|
103
104
|
* @param {string} root абсолютний шлях кореня
|
|
104
105
|
* @returns {Promise<string[]>} відсортовані posix-шляхи відносно кореня
|
|
105
106
|
*/
|
|
106
|
-
async function collectEnvFiles(root) {
|
|
107
|
+
async function collectEnvFiles(root, ignorePaths) {
|
|
107
108
|
/** @type {string[]} */
|
|
108
109
|
const out = []
|
|
109
|
-
await walkDir(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
await walkDir(
|
|
111
|
+
root,
|
|
112
|
+
absPath => {
|
|
113
|
+
const rel = relative(root, absPath).split('\\').join('/')
|
|
114
|
+
if (isEnvFile(rel)) {
|
|
115
|
+
out.push(rel)
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
ignorePaths
|
|
119
|
+
)
|
|
115
120
|
return out.toSorted((a, b) => a.localeCompare(b))
|
|
116
121
|
}
|
|
117
122
|
|
|
@@ -206,7 +211,8 @@ export async function check() {
|
|
|
206
211
|
namespace: await readYamlMetadataName(join(root, HASURA_NAMESPACE_FILE), 'Namespace')
|
|
207
212
|
}
|
|
208
213
|
|
|
209
|
-
const
|
|
214
|
+
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
215
|
+
const envFiles = await collectEnvFiles(root, ignorePaths)
|
|
210
216
|
if (envFiles.length === 0) {
|
|
211
217
|
pass('Не знайдено жодного *.env файла — нічого перевіряти')
|
|
212
218
|
return reporter.getExitCode()
|
package/scripts/check-image.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
25
25
|
import { join, relative } from 'node:path'
|
|
26
26
|
|
|
27
27
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
28
|
+
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
28
29
|
import { walkDir } from './utils/walkDir.mjs'
|
|
29
30
|
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
30
31
|
|
|
@@ -216,16 +217,20 @@ function packageHasAvifDisabled(pkg) {
|
|
|
216
217
|
* @param {(msg: string) => void} fail callback при помилці
|
|
217
218
|
* @returns {Promise<void>}
|
|
218
219
|
*/
|
|
219
|
-
async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, pass, fail) {
|
|
220
|
+
async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePaths, pass, fail) {
|
|
220
221
|
const absRoot = join(process.cwd(), packageRoot)
|
|
221
222
|
const label = packageRoot === '.' ? 'корінь' : packageRoot
|
|
222
223
|
/** @type {string[]} */
|
|
223
224
|
const vueFiles = []
|
|
224
|
-
await walkDir(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
await walkDir(
|
|
226
|
+
absRoot,
|
|
227
|
+
absPath => {
|
|
228
|
+
if (!absPath.endsWith('.vue')) return
|
|
229
|
+
if (otherRootsAbs.some(other => absPath.startsWith(`${other}/`))) return
|
|
230
|
+
vueFiles.push(absPath)
|
|
231
|
+
},
|
|
232
|
+
ignorePaths
|
|
233
|
+
)
|
|
229
234
|
if (vueFiles.length === 0) return
|
|
230
235
|
|
|
231
236
|
let violations = 0
|
|
@@ -262,7 +267,7 @@ async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, pass, fa
|
|
|
262
267
|
* @param {(msg: string) => void} fail callback при помилці
|
|
263
268
|
* @returns {Promise<void>}
|
|
264
269
|
*/
|
|
265
|
-
async function checkVueAvifImports(pass, fail) {
|
|
270
|
+
async function checkVueAvifImports(ignorePaths, pass, fail) {
|
|
266
271
|
const roots = await getMonorepoPackageRootDirs()
|
|
267
272
|
const absRootsByRel = new Map(roots.map(r => [r, join(process.cwd(), r)]))
|
|
268
273
|
for (const root of roots) {
|
|
@@ -274,7 +279,7 @@ async function checkVueAvifImports(pass, fail) {
|
|
|
274
279
|
continue
|
|
275
280
|
}
|
|
276
281
|
const otherRootsAbs = roots.filter(r => r !== root && r !== '.').map(r => absRootsByRel.get(r) ?? '')
|
|
277
|
-
await checkVueAvifImportsInPackage(root, otherRootsAbs, pass, fail)
|
|
282
|
+
await checkVueAvifImportsInPackage(root, otherRootsAbs, ignorePaths, pass, fail)
|
|
278
283
|
}
|
|
279
284
|
}
|
|
280
285
|
|
|
@@ -314,7 +319,8 @@ export async function check() {
|
|
|
314
319
|
await checkHashCacheNotIgnored(pass, fail)
|
|
315
320
|
await checkLegacyCacheRemoved(pass, fail)
|
|
316
321
|
}
|
|
317
|
-
await
|
|
322
|
+
const ignorePaths = await loadCursorIgnorePaths(process.cwd())
|
|
323
|
+
await checkVueAvifImports(ignorePaths, pass, fail)
|
|
318
324
|
|
|
319
325
|
return reporter.getExitCode()
|
|
320
326
|
}
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
isBunSqlScanSourceFile,
|
|
36
36
|
textHasBunSqlImport
|
|
37
37
|
} from './utils/bun-sql-scan.mjs'
|
|
38
|
+
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
38
39
|
import { walkDir } from './utils/walkDir.mjs'
|
|
39
40
|
|
|
40
41
|
/** Імена забороненої залежності у будь-якому `package.json`. */
|
|
@@ -54,14 +55,18 @@ function asObject(v) {
|
|
|
54
55
|
* @param {string} repoRoot абсолютний шлях до кореня репозиторію
|
|
55
56
|
* @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
|
|
56
57
|
*/
|
|
57
|
-
async function findAllPackageJsonPaths(repoRoot) {
|
|
58
|
+
async function findAllPackageJsonPaths(repoRoot, ignorePaths) {
|
|
58
59
|
/** @type {string[]} */
|
|
59
60
|
const paths = []
|
|
60
|
-
await walkDir(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
await walkDir(
|
|
62
|
+
repoRoot,
|
|
63
|
+
absPath => {
|
|
64
|
+
if (absPath.endsWith(`${sep}package.json`)) {
|
|
65
|
+
paths.push(absPath)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
ignorePaths
|
|
69
|
+
)
|
|
65
70
|
paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
|
|
66
71
|
return paths
|
|
67
72
|
}
|
|
@@ -71,15 +76,19 @@ async function findAllPackageJsonPaths(repoRoot) {
|
|
|
71
76
|
* @param {string} repoRoot абсолютний шлях до кореня репозиторію
|
|
72
77
|
* @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
|
|
73
78
|
*/
|
|
74
|
-
async function findAllSourcePathsForBunSqlScan(repoRoot) {
|
|
79
|
+
async function findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths) {
|
|
75
80
|
/** @type {string[]} */
|
|
76
81
|
const paths = []
|
|
77
|
-
await walkDir(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
await walkDir(
|
|
83
|
+
repoRoot,
|
|
84
|
+
absPath => {
|
|
85
|
+
const rel = relative(repoRoot, absPath).split('\\').join('/')
|
|
86
|
+
if (isBunSqlScanSourceFile(rel)) {
|
|
87
|
+
paths.push(absPath)
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
ignorePaths
|
|
91
|
+
)
|
|
83
92
|
paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
|
|
84
93
|
return paths
|
|
85
94
|
}
|
|
@@ -236,7 +245,8 @@ export async function check() {
|
|
|
236
245
|
return reporter.getExitCode()
|
|
237
246
|
}
|
|
238
247
|
|
|
239
|
-
const
|
|
248
|
+
const ignorePaths = await loadCursorIgnorePaths(repoRoot)
|
|
249
|
+
const pkgJsonPaths = await findAllPackageJsonPaths(repoRoot, ignorePaths)
|
|
240
250
|
if (pkgJsonPaths.length === 0) {
|
|
241
251
|
pass('js-bun-db: package.json не знайдено — перевірку пропущено')
|
|
242
252
|
return reporter.getExitCode()
|
|
@@ -244,7 +254,7 @@ export async function check() {
|
|
|
244
254
|
|
|
245
255
|
await checkForbiddenDependencies(pkgJsonPaths, repoRoot, reporter)
|
|
246
256
|
|
|
247
|
-
const sourcePaths = await findAllSourcePathsForBunSqlScan(repoRoot)
|
|
257
|
+
const sourcePaths = await findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths)
|
|
248
258
|
if (sourcePaths.length === 0) {
|
|
249
259
|
pass('js-bun-db: немає JS/TS файлів для скану патернів Bun SQL')
|
|
250
260
|
return reporter.getExitCode()
|