@nitra/cursor 1.8.179 → 1.8.184

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.
@@ -187,9 +187,7 @@ async function checkLegacyCacheRemoved(pass, fail) {
187
187
  }
188
188
  const lines = await readGitignoreLines()
189
189
  if (lines && lines.includes(LEGACY_CACHE_FILENAME)) {
190
- fail(
191
- `.gitignore: прибери застарілий рядок \`${LEGACY_CACHE_FILENAME}\` — split-cache 3.2.0 його не використовує`
192
- )
190
+ fail(`.gitignore: прибери застарілий рядок \`${LEGACY_CACHE_FILENAME}\` — split-cache 3.2.0 його не використовує`)
193
191
  return
194
192
  }
195
193
  pass(`${LEGACY_CACHE_FILENAME} відсутній (міграція на split-cache завершена)`)
@@ -203,7 +201,9 @@ async function checkLegacyCacheRemoved(pass, fail) {
203
201
  */
204
202
  function packageHasAvifDisabled(pkg) {
205
203
  const cfg = pkg[PKG_CONFIG_FIELD]
206
- return Boolean(cfg && typeof cfg === 'object' && /** @type {Record<string, unknown>} */ (cfg)['disable-avif'] === true)
204
+ return Boolean(
205
+ cfg && typeof cfg === 'object' && /** @type {Record<string, unknown>} */ (cfg)['disable-avif'] === true
206
+ )
207
207
  }
208
208
 
209
209
  /**
@@ -213,9 +213,10 @@ function packageHasAvifDisabled(pkg) {
213
213
  * один раз (інакше при обході кореня `.` ми б повторно зайшли в `demo/` і подвоїли звіти).
214
214
  * @param {string} packageRoot відносний шлях до кореня пакета (наприклад `'.'` або `'demo'`)
215
215
  * @param {string[]} otherRootsAbs абсолютні шляхи інших workspace-коренів — їх піддерева пропускаємо
216
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
216
217
  * @param {(msg: string) => void} pass callback при успішній перевірці
217
218
  * @param {(msg: string) => void} fail callback при помилці
218
- * @returns {Promise<void>}
219
+ * @returns {Promise<void>} резолвиться по завершенню перевірки одного пакета
219
220
  */
220
221
  async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePaths, pass, fail) {
221
222
  const absRoot = join(process.cwd(), packageRoot)
@@ -263,9 +264,10 @@ async function checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePa
263
264
  * Сканує всі workspace-пакети: для кожного перевіряє opt-out і за потреби викликає
264
265
  * перевірку Vue-imports. Перевірка пропускається, якщо в репозиторії немає workspaces
265
266
  * або немає `.vue`-файлів — тоді `image` правило не для цього проєкту.
267
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
266
268
  * @param {(msg: string) => void} pass callback при успішній перевірці
267
269
  * @param {(msg: string) => void} fail callback при помилці
268
- * @returns {Promise<void>}
270
+ * @returns {Promise<void>} резолвиться по завершенню перевірки всіх workspace-пакетів
269
271
  */
270
272
  async function checkVueAvifImports(ignorePaths, pass, fail) {
271
273
  const roots = await getMonorepoPackageRootDirs()
@@ -275,7 +277,9 @@ async function checkVueAvifImports(ignorePaths, pass, fail) {
275
277
  if (!existsSync(pkgPath)) continue
276
278
  const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
277
279
  if (packageHasAvifDisabled(pkg)) {
278
- pass(`[${root === '.' ? 'корінь' : root}] avif-import enforcement вимкнено через "@nitra/minify-image.disable-avif"`)
280
+ pass(
281
+ `[${root === '.' ? 'корінь' : root}] avif-import enforcement вимкнено через "@nitra/minify-image.disable-avif"`
282
+ )
279
283
  continue
280
284
  }
281
285
  const otherRootsAbs = roots.filter(r => r !== root && r !== '.').map(r => absRootsByRel.get(r) ?? '')
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import { existsSync } from 'node:fs'
25
25
  import { readFile } from 'node:fs/promises'
26
- import { join, relative, sep } from 'node:path'
26
+ import { join, relative } from 'node:path'
27
27
 
28
28
  import { createCheckReporter } from './utils/check-reporter.mjs'
29
29
  import {
@@ -35,6 +35,7 @@ import {
35
35
  isBunSqlScanSourceFile,
36
36
  textHasBunSqlImport
37
37
  } from './utils/bun-sql-scan.mjs'
38
+ import { findAllPackageJsonPaths } from './utils/find-package-json-paths.mjs'
38
39
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
39
40
  import { walkDir } from './utils/walkDir.mjs'
40
41
 
@@ -50,30 +51,10 @@ function asObject(v) {
50
51
  return /** @type {Record<string, unknown>} */ (v)
51
52
  }
52
53
 
53
- /**
54
- * Знаходить всі `package.json` у репозиторії (крім пропущених директорій у walkDir).
55
- * @param {string} repoRoot абсолютний шлях до кореня репозиторію
56
- * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
57
- */
58
- async function findAllPackageJsonPaths(repoRoot, ignorePaths) {
59
- /** @type {string[]} */
60
- const paths = []
61
- await walkDir(
62
- repoRoot,
63
- absPath => {
64
- if (absPath.endsWith(`${sep}package.json`)) {
65
- paths.push(absPath)
66
- }
67
- },
68
- ignorePaths
69
- )
70
- paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
71
- return paths
72
- }
73
-
74
54
  /**
75
55
  * Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану Bun SQL патернів.
76
56
  * @param {string} repoRoot абсолютний шлях до кореня репозиторію
57
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
77
58
  * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
78
59
  */
79
60
  async function findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths) {
@@ -145,7 +145,9 @@ function compareOxlintIgnorePatterns(expected, actual, failures) {
145
145
  return
146
146
  }
147
147
  if (!Array.isArray(actual)) {
148
- failures.push('.oxlintrc.json: поле "ignorePatterns" має бути масивом (канон задає мінімум, додаткові патерни дозволені)')
148
+ failures.push(
149
+ '.oxlintrc.json: поле "ignorePatterns" має бути масивом (канон задає мінімум, додаткові патерни дозволені)'
150
+ )
149
151
  return
150
152
  }
151
153
  const set = new Set(actual)
@@ -10,9 +10,10 @@
10
10
  */
11
11
  import { existsSync } from 'node:fs'
12
12
  import { readFile } from 'node:fs/promises'
13
- import { join, relative, sep } from 'node:path'
13
+ import { join, relative } from 'node:path'
14
14
 
15
15
  import { createCheckReporter } from './utils/check-reporter.mjs'
16
+ import { findAllPackageJsonPaths } from './utils/find-package-json-paths.mjs'
16
17
  import {
17
18
  findMssqlPerRequestConnectionInText,
18
19
  findSharedMssqlRequestInText,
@@ -31,30 +32,10 @@ const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)/u
31
32
  /** Мінімальна дозволена версія mssql (js-mssql.mdc). */
32
33
  const MIN_MSSQL_VERSION = { major: 12, minor: 5, patch: 0 }
33
34
 
34
- /**
35
- * Знаходить всі `package.json` у репозиторії (крім пропущених директорій у walkDir).
36
- * @param {string} repoRoot абсолютний шлях до кореня репозиторію
37
- * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
38
- */
39
- async function findAllPackageJsonPaths(repoRoot, ignorePaths) {
40
- /** @type {string[]} */
41
- const paths = []
42
- await walkDir(
43
- repoRoot,
44
- absPath => {
45
- if (absPath.endsWith(`${sep}package.json`)) {
46
- paths.push(absPath)
47
- }
48
- },
49
- ignorePaths
50
- )
51
- paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
52
- return paths
53
- }
54
-
55
35
  /**
56
36
  * Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану mssql.
57
37
  * @param {string} repoRoot абсолютний шлях до кореня репозиторію
38
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
58
39
  * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
59
40
  */
60
41
  async function findAllSourcePathsForMssqlScan(repoRoot, ignorePaths) {
@@ -258,9 +239,10 @@ function reportZeroMssqlSourceViolations(counters, pass) {
258
239
  /**
259
240
  * Аудит усіх JS/TS-джерел репо щодо безпечного використання mssql.
260
241
  * @param {string} repoRoot корінь репозиторію
242
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
261
243
  * @param {(msg: string) => void} pass pass callback
262
244
  * @param {(msg: string) => void} fail fail callback
263
- * @returns {Promise<void>}
245
+ * @returns {Promise<void>} резолвиться по завершенню аудиту всіх знайдених джерел
264
246
  */
265
247
  async function auditMssqlSources(repoRoot, ignorePaths, pass, fail) {
266
248
  const sourcePaths = await findAllSourcePathsForMssqlScan(repoRoot, ignorePaths)
@@ -17,7 +17,11 @@
17
17
  * `node:process` (для опційних). Коли `env` імпортовано з `@nitra/check-env`,
18
18
  * кожен `env.X` має бути закритий літеральним викликом `checkEnv(['X', ...])`
19
19
  * у тому ж файлі або коментарем `// \@nitra/cursor ignore-next-line checkEnv`
20
- * на попередньому рядку (див. `utils/check-env-scan.mjs`).
20
+ * на попередньому рядку (див. `utils/check-env-scan.mjs`);
21
+ * - «depcheck у GitHub Actions з path-фільтром»: для кожного workflow з `paths:`,
22
+ * обмеженим каталогом цього пакета (`<rootDir>/...`), має бути крок
23
+ * `npx depcheck --ignores="graphql,bun"` (плюс інші, за потреби) з
24
+ * `working-directory: <rootDir>` (див. `utils/depcheck-workflow.mjs`).
21
25
  */
22
26
  import { existsSync } from 'node:fs'
23
27
  import { readFile } from 'node:fs/promises'
@@ -30,6 +34,7 @@ import {
30
34
  } from './utils/bunyan-imports.mjs'
31
35
  import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from './utils/check-env-scan.mjs'
32
36
  import { createCheckReporter } from './utils/check-reporter.mjs'
37
+ import { findDepcheckViolationsForPackage, readAllWorkflowFiles } from './utils/depcheck-workflow.mjs'
33
38
  import {
34
39
  findConnFactoryImportsInText,
35
40
  isConnImportsScanSourceFile,
@@ -53,6 +58,7 @@ function relPosix(absPackageRoot, absPath) {
53
58
  /**
54
59
  * Сканує джерела пакета на заборонені імпорти `@nitra/bunyan` / `bunyan`.
55
60
  * @param {string} absPackageRoot абсолютний шлях до кореня пакета
61
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
56
62
  * @param {string} label префікс повідомлення `[<pkg>] `
57
63
  * @param {(msg: string) => void} fail callback при помилці
58
64
  * @returns {Promise<number>} кількість знайдених порушень
@@ -86,6 +92,7 @@ async function checkBunyanImports(absPackageRoot, ignorePaths, label, fail) {
86
92
  /**
87
93
  * Збирає всі JS/TS-файли пакета (без node_modules, dist тощо).
88
94
  * @param {string} absPackageRoot абсолютний шлях до кореня пакета
95
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
89
96
  * @returns {Promise<string[]>} абсолютні шляхи до файлів
90
97
  */
91
98
  async function collectSourceFiles(absPackageRoot, ignorePaths) {
@@ -158,15 +165,26 @@ async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
158
165
  /**
159
166
  * Перевіряє відповідність правилам js-run.mdc для одного workspace-пакета.
160
167
  * @param {string} rootDir відносний шлях workspace (не `'.'`)
168
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
169
+ * @param {{ relPath: string, content: string }[]} workflows кешований список workflow-файлів репо
161
170
  * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
162
171
  * @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
163
172
  * @returns {Promise<void>} завершується після перевірок цього пакета
164
173
  */
165
- async function checkWorkspacePackage(rootDir, ignorePaths, fail, passFn) {
174
+ async function checkWorkspacePackage(rootDir, ignorePaths, workflows, fail, passFn) {
166
175
  const label = `[${rootDir}] `
167
176
  const absPackageRoot = join(process.cwd(), rootDir)
168
177
  const pkgJson = await loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail)
169
178
 
179
+ // Frontend-пакети (vite у devDependencies) виходять за межі js-run:
180
+ // браузерний бандл не має `node:process`, а `process.env.*` бандлер
181
+ // обробляє самостійно. Перевірку process.env / conn-аліасів пропускаємо,
182
+ // bunyan-залежність уже звірено в `loadPackageJsonAndCheckBunyanDeps`.
183
+ if (packageJsonHasViteDevDependency(pkgJson)) {
184
+ passFn(`${label}vite-пакет (frontend) — js-run пропущено (process.env / conn-aliases / OTEL configmap)`)
185
+ return
186
+ }
187
+
170
188
  const importViolations = await checkBunyanImports(absPackageRoot, ignorePaths, label, fail)
171
189
  if (importViolations === 0) {
172
190
  passFn(`${label}немає імпортів '@nitra/bunyan' / 'bunyan' у джерелах`)
@@ -188,6 +206,47 @@ async function checkWorkspacePackage(rootDir, ignorePaths, fail, passFn) {
188
206
  }
189
207
 
190
208
  await checkOtelConfigmap(rootDir, label, fail, passFn)
209
+
210
+ checkDepcheckInWorkflows(rootDir, workflows, label, fail, passFn)
211
+ }
212
+
213
+ /**
214
+ * Перевіряє правило «depcheck у workflow» для одного пакета.
215
+ *
216
+ * Для кожного `.github/workflows/*.yml`, чий `paths:` обмежено до `<rootDir>/...`,
217
+ * має бути крок `npx depcheck --ignores="graphql,bun"` з `working-directory: <rootDir>`.
218
+ * Якщо в репо немає каталогу `.github/workflows`, перевірка no-op.
219
+ * @param {string} rootDir відносний шлях workspace-пакета
220
+ * @param {{ relPath: string, content: string }[]} workflows кешований список workflow-файлів
221
+ * @param {string} label префікс повідомлення `[<pkg>] `
222
+ * @param {(msg: string) => void} fail callback при помилці
223
+ * @param {(msg: string) => void} passFn успішне повідомлення
224
+ * @returns {void}
225
+ */
226
+ function checkDepcheckInWorkflows(rootDir, workflows, label, fail, passFn) {
227
+ if (workflows.length === 0) return
228
+ const violations = findDepcheckViolationsForPackage(workflows, rootDir.replace(/\\/g, '/'))
229
+ if (violations.length === 0) {
230
+ passFn(`${label}depcheck у path-scoped workflow налаштовано (або відсутній path-scoped workflow для пакета)`)
231
+ return
232
+ }
233
+ for (const v of violations) {
234
+ fail(`${label}${v}`)
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Чи має пакет `vite` у `devDependencies` (маркер frontend-пакета — vite/quasar/capacitor SPA).
240
+ * Семантично ідентично `packageJsonLacksViteDevDependency` з `auto-rules.mjs`, але
241
+ * приймає вже розпарсений pkgJson.
242
+ * @param {unknown} pkgJson розпарсений `package.json` пакета (або null)
243
+ * @returns {boolean} true, якщо `vite` присутній у `devDependencies`
244
+ */
245
+ function packageJsonHasViteDevDependency(pkgJson) {
246
+ if (!pkgJson || typeof pkgJson !== 'object' || Array.isArray(pkgJson)) return false
247
+ const devDeps = /** @type {Record<string, unknown>} */ (pkgJson).devDependencies
248
+ if (!devDeps || typeof devDeps !== 'object' || Array.isArray(devDeps)) return false
249
+ return Object.hasOwn(devDeps, 'vite')
191
250
  }
192
251
 
193
252
  /**
@@ -255,8 +314,9 @@ export async function check() {
255
314
  }
256
315
 
257
316
  const ignorePaths = await loadCursorIgnorePaths(process.cwd())
317
+ const workflows = await readAllWorkflowFiles(process.cwd())
258
318
  for (const r of workspaceRoots) {
259
- await checkWorkspacePackage(r, ignorePaths, fail, pass)
319
+ await checkWorkspacePackage(r, ignorePaths, workflows, fail, pass)
260
320
  }
261
321
 
262
322
  return reporter.getExitCode()
@@ -1339,43 +1339,44 @@ function failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail) {
1339
1339
  * @returns {void}
1340
1340
  */
1341
1341
  function failIfExplicitPatchTargetsHaveRedundantGroupVersion(rel, first, catalog, fail) {
1342
- for (const { section, index, target } of extractExplicitPatchTargetsFromKustomization(first)) {
1343
- if (target === null || typeof target !== 'object' || Array.isArray(target)) {
1344
- continue
1345
- }
1346
- const t = /** @type {Record<string, unknown>} */ (target)
1347
- const kind = typeof t.kind === 'string' ? t.kind.trim() : ''
1348
- const name = typeof t.name === 'string' ? t.name.trim() : ''
1349
- if (kind === '' || name === '') {
1350
- continue
1351
- }
1352
- if (patchTargetUsesSelector(t)) {
1353
- continue
1354
- }
1355
- const tgtGroup = typeof t.group === 'string' ? t.group.trim() : ''
1356
- const tgtVersion = typeof t.version === 'string' ? t.version.trim() : ''
1357
- if (tgtGroup === '' && tgtVersion === '') {
1358
- continue
1359
- }
1360
- const matchingByKindName = catalog.filter(r => r.kind === kind && r.name === name)
1361
- const distinctGvk = new Set(matchingByKindName.map(r => `${r.group}/${r.version}`))
1362
- if (distinctGvk.size > 1) {
1363
- continue
1364
- }
1365
- /** @type {string[]} */
1366
- const redundant = []
1367
- if (tgtGroup !== '') {
1368
- redundant.push('group')
1369
- }
1370
- if (tgtVersion !== '') {
1371
- redundant.push('version')
1372
- }
1342
+ for (const entry of extractExplicitPatchTargetsFromKustomization(first)) {
1343
+ const violation = describePatchTargetRedundancy(entry, catalog)
1344
+ if (violation === null) continue
1345
+ const { section, index, kind, name, redundant } = violation
1373
1346
  fail(
1374
1347
  `${rel}: ${section}[${index}].target — прибери зайві поля ${redundant.join(', ')}; для kind=${kind}, name=${name} в інвентарі немає колізії між різними API-групами/версіями (див. k8s.mdc «patches[].target: лише kind і name»)`
1375
1348
  )
1376
1349
  }
1377
1350
  }
1378
1351
 
1352
+ /**
1353
+ * Аналізує один patch.target: повертає опис надлишкових полів `group`/`version`,
1354
+ * якщо в інвентарі для пари (kind, name) немає колізії GVK; інакше `null`.
1355
+ * @param {{ section: string, index: number, target: unknown }} entry елемент із `extractExplicitPatchTargetsFromKustomization`
1356
+ * @param {KustomizeResourceDescriptor[]} catalog інвентар resources/bases/…
1357
+ * @returns {{ section: string, index: number, kind: string, name: string, redundant: string[] } | null} опис порушення або `null`
1358
+ */
1359
+ function describePatchTargetRedundancy(entry, catalog) {
1360
+ const { section, index, target } = entry
1361
+ if (target === null || typeof target !== 'object' || Array.isArray(target)) return null
1362
+ const t = /** @type {Record<string, unknown>} */ (target)
1363
+ const kind = typeof t.kind === 'string' ? t.kind.trim() : ''
1364
+ const name = typeof t.name === 'string' ? t.name.trim() : ''
1365
+ if (kind === '' || name === '') return null
1366
+ if (patchTargetUsesSelector(t)) return null
1367
+ const tgtGroup = typeof t.group === 'string' ? t.group.trim() : ''
1368
+ const tgtVersion = typeof t.version === 'string' ? t.version.trim() : ''
1369
+ if (tgtGroup === '' && tgtVersion === '') return null
1370
+ const matchingByKindName = catalog.filter(r => r.kind === kind && r.name === name)
1371
+ const distinctGvk = new Set(matchingByKindName.map(r => `${r.group}/${r.version}`))
1372
+ if (distinctGvk.size > 1) return null
1373
+ /** @type {string[]} */
1374
+ const redundant = []
1375
+ if (tgtGroup !== '') redundant.push('group')
1376
+ if (tgtVersion !== '') redundant.push('version')
1377
+ return { section, index, kind, name, redundant }
1378
+ }
1379
+
1379
1380
  /**
1380
1381
  * Документи з YAML-файлу мають мати дескриптор у **catalog** (інвентар resources).
1381
1382
  * @param {string} rel відносний шлях до kustomization.yaml
@@ -1636,7 +1637,7 @@ export function baseKustomizationNamespaceViolation(obj) {
1636
1637
  /**
1637
1638
  * Збирає всі `*.yaml` та `*.yml` під деревом від кореня cwd, якщо шлях містить сегмент `k8s` (для `.yml` далі — помилка перейменування).
1638
1639
  * @param {string} root корінь репозиторію (cwd)
1639
- * @param {string[]} [ignorePaths=[]] шляхи каталогів, повністю виключених з обходу
1640
+ * @param {string[]} [ignorePaths] шляхи каталогів, повністю виключених з обходу
1640
1641
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи до файлів
1641
1642
  */
1642
1643
  async function findK8sYamlFiles(root, ignorePaths = []) {
@@ -35,6 +35,7 @@ const GZIP_EXTENSION_RE = /\*\.(?:js|css)/u
35
35
  /**
36
36
  * Збирає абсолютні шляхи до **default.conf.template** у репозиторії; шлях `tests/fixtures` не обходиться як проєктний шаблон.
37
37
  * @param {string} root корінь cwd
38
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
38
39
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи до шаблонів
39
40
  */
40
41
  export async function findDefaultConfTemplatePaths(root, ignorePaths = []) {
@@ -57,6 +58,7 @@ export async function findDefaultConfTemplatePaths(root, ignorePaths = []) {
57
58
  * Знаходить у дереві від `root` усі **default.tpl.conf**. Якщо поруч немає **default.conf.template** —
58
59
  * перейменовує файл; якщо є — перезаписує **default.conf.template** вмістом **default.tpl.conf** і видаляє **default.tpl.conf**.
59
60
  * @param {string} root корінь обходу (зазвичай cwd репозиторію)
61
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
60
62
  * @returns {Promise<{ renamed: string[], overwritten: string[] }>} відносні шляхи до обробленого **default.tpl.conf** (для звіту)
61
63
  */
62
64
  export async function migrateDefaultTplConfFiles(root, ignorePaths = []) {
@@ -322,6 +324,7 @@ async function checkTemplateFile(abs, root, passFn, failFn) {
322
324
  /**
323
325
  * Перевіряє Dockerfile на наявність gzip та envsubst.
324
326
  * @param {string} root корінь репозиторію
327
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
325
328
  * @param {(msg: string) => void} passFn callback при успішній перевірці
326
329
  * @param {(msg: string) => void} failFn callback при помилці
327
330
  */
@@ -36,6 +36,7 @@ const EMIT_TYPES_CONFIG = 'npm/tsconfig.emit-types.json'
36
36
 
37
37
  /**
38
38
  * Чи є під `npm/src` хоча б один `.js` (рекурсивно).
39
+ * @param {string[]} [ignorePaths] абсолютні шляхи каталогів, повністю виключених з обходу
39
40
  * @returns {Promise<boolean>} `true`, якщо знайдено хоча б один `.js`
40
41
  */
41
42
  async function npmSrcTreeHasJsFile(ignorePaths = []) {
@@ -115,6 +115,7 @@ async function collectEsbuildMatchesInFiles(absPackageRoot, files, maxMatches) {
115
115
  * Сканує дерево пакета на згадки `esbuild` і підказує заміну на `rolldown`.
116
116
  * @param {string} rootDir відносний шлях до пакета
117
117
  * @param {string} absPackageRoot абсолютний шлях до кореня пакета
118
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
118
119
  * @param {string} prefix параметр prefix
119
120
  * @param {(msg: string) => void} passFn callback при успішній перевірці
120
121
  * @param {(msg: string) => void} fail callback при помилці
@@ -342,9 +343,7 @@ async function checkVueNodeImportViolations(rootDir, absPackageRoot, ignorePaths
342
343
  }
343
344
  }
344
345
  if (nodeImportViolations === 0) {
345
- passFn(
346
- `${prefix}немає імпортів Node-нативних модулів у .vue (проскановано ${ukFilesCountPhrase(vuePaths.length)})`
347
- )
346
+ passFn(`${prefix}немає імпортів Node-нативних модулів у .vue (проскановано ${ukFilesCountPhrase(vuePaths.length)})`)
348
347
  }
349
348
  }
350
349
 
@@ -354,18 +353,17 @@ async function checkVueNodeImportViolations(rootDir, absPackageRoot, ignorePaths
354
353
  * Якщо `unplugin-auto-import` не сконфігурований на `'vue'` у `vite.config`, явні value-імпорти
355
354
  * формально не заборонені — їх видалення зламає код. У цьому випадку перевірка пропускається,
356
355
  * а fail про відсутній `'vue'` у `AutoImport.imports` уже зареєстровано в `checkViteConfig`.
357
- * @param {string} rootDir параметр rootDir
358
- * @param {string} absPackageRoot параметр absPackageRoot
359
- * @param {string} prefix параметр prefix
356
+ * @param {string} rootDir відносний шлях до пакета
357
+ * @param {string} absPackageRoot абсолютний шлях до кореня пакета
358
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
360
359
  * @param {boolean} hasVueAutoImport чи `AutoImport({ imports: [..., 'vue', ...] })` сконфігуровано
360
+ * @param {string} prefix префікс повідомлення `[<pkg>] `
361
361
  * @param {(msg: string) => void} passFn callback при успішній перевірці
362
362
  * @param {(msg: string) => void} fail callback при помилці
363
363
  */
364
364
  async function checkVueImportViolations(rootDir, absPackageRoot, ignorePaths, hasVueAutoImport, prefix, passFn, fail) {
365
365
  if (!hasVueAutoImport) {
366
- passFn(
367
- `${prefix}value-імпорти з 'vue' не заборонені — спершу додай 'vue' до AutoImport.imports у vite.config`
368
- )
366
+ passFn(`${prefix}value-імпорти з 'vue' не заборонені — спершу додай 'vue' до AutoImport.imports у vite.config`)
369
367
  return
370
368
  }
371
369
  /** @type {string[]} */
@@ -400,6 +398,7 @@ async function checkVueImportViolations(rootDir, absPackageRoot, ignorePaths, ha
400
398
  /**
401
399
  * Перевіряє залежності та vite.config одного Vue-пакета.
402
400
  * @param {string} rootDir відносний шлях до пакета
401
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
403
402
  * @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
404
403
  * @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
405
404
  * @returns {Promise<void>} завершується після перевірок залежностей, `vite.config` і сканування джерел на імпорти з `vue`
@@ -444,7 +443,15 @@ async function checkVuePackage(rootDir, ignorePaths, fail, passFn) {
444
443
  )
445
444
 
446
445
  const { hasVueAutoImport } = await checkViteConfig(rootDir, prefix, passFn, fail)
447
- await checkVueImportViolations(rootDir, join(process.cwd(), rootDir), ignorePaths, hasVueAutoImport, prefix, passFn, fail)
446
+ await checkVueImportViolations(
447
+ rootDir,
448
+ join(process.cwd(), rootDir),
449
+ ignorePaths,
450
+ hasVueAutoImport,
451
+ prefix,
452
+ passFn,
453
+ fail
454
+ )
448
455
  await checkVueNodeImportViolations(rootDir, join(process.cwd(), rootDir), ignorePaths, prefix, passFn, fail)
449
456
  await checkEsbuildMentions(rootDir, join(process.cwd(), rootDir), ignorePaths, prefix, passFn, fail)
450
457
  }
@@ -11,25 +11,27 @@
11
11
  * `npx --no @nitra/cursor stop-hook`
12
12
  */
13
13
  import { spawn } from 'node:child_process'
14
+ import { once } from 'node:events'
14
15
 
15
16
  /**
16
17
  * Зчитує stdin до EOF як utf8 рядок. Якщо stdin порожній (TTY) — повертає '' одразу.
17
18
  * @returns {Promise<string>} вміст stdin
18
19
  */
19
- function readStdin() {
20
- return new Promise(resolve => {
21
- if (process.stdin.isTTY) {
22
- resolve('')
23
- return
24
- }
25
- let data = ''
26
- process.stdin.setEncoding('utf8')
27
- process.stdin.on('data', chunk => {
28
- data += chunk
29
- })
30
- process.stdin.on('end', () => resolve(data))
31
- process.stdin.on('error', () => resolve(data))
20
+ async function readStdin() {
21
+ if (process.stdin.isTTY) {
22
+ return ''
23
+ }
24
+ process.stdin.setEncoding('utf8')
25
+ const chunks = []
26
+ process.stdin.on('data', chunk => {
27
+ chunks.push(chunk)
32
28
  })
29
+ try {
30
+ await once(process.stdin, 'end')
31
+ } catch {
32
+ // 'error' на stdin — повертаємо те, що встигли зібрати
33
+ }
34
+ return chunks.join('')
33
35
  }
34
36
 
35
37
  /**
@@ -60,12 +62,13 @@ export async function runStopHookCli() {
60
62
  if (isRecursiveStopHookCall(stdin)) {
61
63
  return 0
62
64
  }
63
- return new Promise(resolve => {
64
- const child = spawn('npx', ['--no', '@nitra/cursor', 'check'], { stdio: 'inherit' })
65
- child.on('exit', code => resolve(code ?? 1))
66
- child.on('error', err => {
67
- process.stderr.write(`stop-hook: не вдалося запустити npx @nitra/cursor check — ${err.message}\n`)
68
- resolve(1)
69
- })
70
- })
65
+ // eslint-disable-next-line sonarjs/no-os-command-from-path -- npx як стандартне dev-середовище через PATH; альтернативи (хардкод шляху) непортативні
66
+ const child = spawn('npx', ['--no', '@nitra/cursor', 'check'], { stdio: 'inherit' })
67
+ try {
68
+ const [code] = await once(child, 'exit')
69
+ return code ?? 1
70
+ } catch (error) {
71
+ process.stderr.write(`stop-hook: не вдалося запустити npx @nitra/cursor check — ${error.message}\n`)
72
+ return 1
73
+ }
71
74
  }
@@ -18,7 +18,6 @@ import { resolveCmd } from './utils/resolve-cmd.mjs'
18
18
 
19
19
  /**
20
20
  * Опис залежності preflight-ом: бінарник, для чого потрібен, і команди встановлення.
21
- *
22
21
  * @typedef {object} PreflightDep
23
22
  * @property {string} bin ім'я виконуваного файлу (на Windows додається `.exe` за потреби)
24
23
  * @property {string[]} winBins альтернативні імена на Windows (`shellcheck.exe`); якщо нема — fallback на `bin`
@@ -68,6 +68,7 @@ export function replaceExtension(relPosix, newExt) {
68
68
  /**
69
69
  * Збирає операції перейменування (без виконання).
70
70
  * @param {string} rootAbs абсолютний корінь репозиторію
71
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
71
72
  * @returns {Promise<Array<{ kind: 'k8s' | 'github', fromAbs: string, toAbs: string, relFrom: string, relTo: string }>>} відсортовані операції перейменування без запису на диск
72
73
  */
73
74
  async function collectRenameOps(rootAbs, ignorePaths) {
@@ -29,6 +29,7 @@ export function isLintDockerfileName(name) {
29
29
  /**
30
30
  * Збирає абсолютні шляхи для lint-docker.
31
31
  * @param {string} root корінь репозиторію
32
+ * @param {string[]} [ignorePaths] абсолютні шляхи каталогів, повністю виключених з обходу
32
33
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи
33
34
  */
34
35
  export async function findLintDockerfilePaths(root, ignorePaths = []) {
@@ -59,6 +59,7 @@ export function k8sRootFromFile(absFile) {
59
59
  /**
60
60
  * Унікальні корені `k8s` за наявності `*.yaml` під деревом cwd.
61
61
  * @param {string} root корінь репозиторію
62
+ * @param {string[]} [ignorePaths] абсолютні шляхи каталогів, повністю виключених з обходу
62
63
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи до каталогів `k8s`
63
64
  */
64
65
  export async function findK8sRoots(root, ignorePaths = []) {