@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.
@@ -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
+ }
@@ -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(root, p => {
74
- if (isDockerfileName(basename(p))) out.push(p)
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 files = await findDockerfilePaths(root)
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(root, absPath => {
40
- const rel = relative(root, absPath).split('\\').join('/')
41
- if (shouldSkipFileForGqlScan(rel) || !isGqlScanSourceFile(rel)) {
42
- return
43
- }
44
- candidates.push(absPath)
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 candidates = await collectScanCandidates(root)
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) {
@@ -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(root, absPath => {
110
- const rel = relative(root, absPath).split('\\').join('/')
111
- if (isEnvFile(rel)) {
112
- out.push(rel)
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 envFiles = await collectEnvFiles(root)
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()
@@ -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(absRoot, absPath => {
225
- if (!absPath.endsWith('.vue')) return
226
- if (otherRootsAbs.some(other => absPath.startsWith(`${other}/`))) return
227
- vueFiles.push(absPath)
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 checkVueAvifImports(pass, fail)
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(repoRoot, absPath => {
61
- if (absPath.endsWith(`${sep}package.json`)) {
62
- paths.push(absPath)
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(repoRoot, absPath => {
78
- const rel = relative(repoRoot, absPath).split('\\').join('/')
79
- if (isBunSqlScanSourceFile(rel)) {
80
- paths.push(absPath)
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 pkgJsonPaths = await findAllPackageJsonPaths(repoRoot)
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()