@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.
@@ -22,6 +22,7 @@ import {
22
22
  findUnsafeMssqlInListMissingEmptyGuardInText,
23
23
  isMssqlScanSourceFile
24
24
  } from './utils/mssql-pool-scan.mjs'
25
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
25
26
  import { walkDir } from './utils/walkDir.mjs'
26
27
 
27
28
  const VERSION_PREFIX_RE = /^[\^~>=<]+\s*/u
@@ -35,14 +36,18 @@ const MIN_MSSQL_VERSION = { major: 12, minor: 5, patch: 0 }
35
36
  * @param {string} repoRoot абсолютний шлях до кореня репозиторію
36
37
  * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
37
38
  */
38
- async function findAllPackageJsonPaths(repoRoot) {
39
+ async function findAllPackageJsonPaths(repoRoot, ignorePaths) {
39
40
  /** @type {string[]} */
40
41
  const paths = []
41
- await walkDir(repoRoot, absPath => {
42
- if (absPath.endsWith(`${sep}package.json`)) {
43
- paths.push(absPath)
44
- }
45
- })
42
+ await walkDir(
43
+ repoRoot,
44
+ absPath => {
45
+ if (absPath.endsWith(`${sep}package.json`)) {
46
+ paths.push(absPath)
47
+ }
48
+ },
49
+ ignorePaths
50
+ )
46
51
  paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
47
52
  return paths
48
53
  }
@@ -52,15 +57,19 @@ async function findAllPackageJsonPaths(repoRoot) {
52
57
  * @param {string} repoRoot абсолютний шлях до кореня репозиторію
53
58
  * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
54
59
  */
55
- async function findAllSourcePathsForMssqlScan(repoRoot) {
60
+ async function findAllSourcePathsForMssqlScan(repoRoot, ignorePaths) {
56
61
  /** @type {string[]} */
57
62
  const paths = []
58
- await walkDir(repoRoot, absPath => {
59
- const rel = relative(repoRoot, absPath).split('\\').join('/')
60
- if (isMssqlScanSourceFile(rel)) {
61
- paths.push(absPath)
62
- }
63
- })
63
+ await walkDir(
64
+ repoRoot,
65
+ absPath => {
66
+ const rel = relative(repoRoot, absPath).split('\\').join('/')
67
+ if (isMssqlScanSourceFile(rel)) {
68
+ paths.push(absPath)
69
+ }
70
+ },
71
+ ignorePaths
72
+ )
64
73
  paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
65
74
  return paths
66
75
  }
@@ -253,8 +262,8 @@ function reportZeroMssqlSourceViolations(counters, pass) {
253
262
  * @param {(msg: string) => void} fail fail callback
254
263
  * @returns {Promise<void>}
255
264
  */
256
- async function auditMssqlSources(repoRoot, pass, fail) {
257
- const sourcePaths = await findAllSourcePathsForMssqlScan(repoRoot)
265
+ async function auditMssqlSources(repoRoot, ignorePaths, pass, fail) {
266
+ const sourcePaths = await findAllSourcePathsForMssqlScan(repoRoot, ignorePaths)
258
267
  if (sourcePaths.length === 0) {
259
268
  pass('js-mssql: немає JS/TS файлів для скану singleton ConnectionPool')
260
269
  return
@@ -291,7 +300,8 @@ export async function check() {
291
300
  return reporter.getExitCode()
292
301
  }
293
302
 
294
- const pkgJsonPaths = await findAllPackageJsonPaths(repoRoot)
303
+ const ignorePaths = await loadCursorIgnorePaths(repoRoot)
304
+ const pkgJsonPaths = await findAllPackageJsonPaths(repoRoot, ignorePaths)
295
305
  if (pkgJsonPaths.length === 0) {
296
306
  pass('js-mssql: package.json не знайдено — перевірку пропущено')
297
307
  return reporter.getExitCode()
@@ -307,7 +317,7 @@ export async function check() {
307
317
  pass(`js-mssql: всі знайдені dependencies.mssql відповідають мінімальній версії 12.5.0 (${found})`)
308
318
  }
309
319
 
310
- await auditMssqlSources(repoRoot, pass, fail)
320
+ await auditMssqlSources(repoRoot, ignorePaths, pass, fail)
311
321
 
312
322
  return reporter.getExitCode()
313
323
  }
@@ -36,6 +36,7 @@ import {
36
36
  isInsideConnDir,
37
37
  resolveConnDirFromPackageJson
38
38
  } from './utils/conn-imports-scan.mjs'
39
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
39
40
  import { walkDir } from './utils/walkDir.mjs'
40
41
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
41
42
 
@@ -56,15 +57,19 @@ function relPosix(absPackageRoot, absPath) {
56
57
  * @param {(msg: string) => void} fail callback при помилці
57
58
  * @returns {Promise<number>} кількість знайдених порушень
58
59
  */
59
- async function checkBunyanImports(absPackageRoot, label, fail) {
60
+ async function checkBunyanImports(absPackageRoot, ignorePaths, label, fail) {
60
61
  /** @type {string[]} */
61
62
  const sourcePaths = []
62
- await walkDir(absPackageRoot, absPath => {
63
- const rel = relPosix(absPackageRoot, absPath)
64
- if (!shouldSkipFileForBunyanScan(rel) && isBunyanScanSourceFile(rel)) {
65
- sourcePaths.push(absPath)
66
- }
67
- })
63
+ await walkDir(
64
+ absPackageRoot,
65
+ absPath => {
66
+ const rel = relPosix(absPackageRoot, absPath)
67
+ if (!shouldSkipFileForBunyanScan(rel) && isBunyanScanSourceFile(rel)) {
68
+ sourcePaths.push(absPath)
69
+ }
70
+ },
71
+ ignorePaths
72
+ )
68
73
 
69
74
  let violations = 0
70
75
  for (const absPath of sourcePaths) {
@@ -83,13 +88,17 @@ async function checkBunyanImports(absPackageRoot, label, fail) {
83
88
  * @param {string} absPackageRoot абсолютний шлях до кореня пакета
84
89
  * @returns {Promise<string[]>} абсолютні шляхи до файлів
85
90
  */
86
- async function collectSourceFiles(absPackageRoot) {
91
+ async function collectSourceFiles(absPackageRoot, ignorePaths) {
87
92
  /** @type {string[]} */
88
93
  const out = []
89
- await walkDir(absPackageRoot, absPath => {
90
- const rel = relPosix(absPackageRoot, absPath)
91
- if (isCheckEnvScanSourceFile(rel)) out.push(absPath)
92
- })
94
+ await walkDir(
95
+ absPackageRoot,
96
+ absPath => {
97
+ const rel = relPosix(absPackageRoot, absPath)
98
+ if (isCheckEnvScanSourceFile(rel)) out.push(absPath)
99
+ },
100
+ ignorePaths
101
+ )
93
102
  return out
94
103
  }
95
104
 
@@ -153,17 +162,17 @@ async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
153
162
  * @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
154
163
  * @returns {Promise<void>} завершується після перевірок цього пакета
155
164
  */
156
- async function checkWorkspacePackage(rootDir, fail, passFn) {
165
+ async function checkWorkspacePackage(rootDir, ignorePaths, fail, passFn) {
157
166
  const label = `[${rootDir}] `
158
167
  const absPackageRoot = join(process.cwd(), rootDir)
159
168
  const pkgJson = await loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail)
160
169
 
161
- const importViolations = await checkBunyanImports(absPackageRoot, label, fail)
170
+ const importViolations = await checkBunyanImports(absPackageRoot, ignorePaths, label, fail)
162
171
  if (importViolations === 0) {
163
172
  passFn(`${label}немає імпортів '@nitra/bunyan' / 'bunyan' у джерелах`)
164
173
  }
165
174
 
166
- const sourcePaths = await collectSourceFiles(absPackageRoot)
175
+ const sourcePaths = await collectSourceFiles(absPackageRoot, ignorePaths)
167
176
 
168
177
  const connViolations = await checkConnImports(absPackageRoot, sourcePaths, pkgJson, label, fail)
169
178
  if (connViolations === 0) {
@@ -245,8 +254,9 @@ export async function check() {
245
254
  return reporter.getExitCode()
246
255
  }
247
256
 
257
+ const ignorePaths = await loadCursorIgnorePaths(process.cwd())
248
258
  for (const r of workspaceRoots) {
249
- await checkWorkspacePackage(r, fail, pass)
259
+ await checkWorkspacePackage(r, ignorePaths, fail, pass)
250
260
  }
251
261
 
252
262
  return reporter.getExitCode()
@@ -38,6 +38,8 @@
38
38
  * кожен **Service** — **`spec.clusterIP: None`** та ім’я на **`-hl`**. У маршрутах **Gateway API**
39
39
  * (**`HTTPRoute`**, **`GRPCRoute`**, **`TCPRoute`**, **`TLSRoute`**, **`UDPRoute`**, група **`gateway.networking.k8s.io`**)
40
40
  * посилання **`backendRefs` / `backendRef`** на **Service** мають вказувати лише сервіси з суфіксом **`-hl`** у **`name`**.
41
+ * Поле **`namespace`** у **`backendRef`**, що збігається з **`metadata.namespace`** самого маршруту, — надлишкове:
42
+ * прибери його, бо за замовчуванням Gateway API резолвить backend у тому ж namespace, що й маршрут (див. k8s.mdc).
41
43
  * **HealthCheckPolicy** (**`networking.gke.io/v1`**, GKE): **`spec.targetRef`** на **Service** — **`name`** з суфіксом **`-hl`** (див. k8s.mdc).
42
44
  * Якщо **`kustomization.yaml`** посилається на **`svc.yaml`** (**`resources`**, **`bases`**, **`components`**, **`crds`**,
43
45
  * **`patches[].path`**, **`patchesStrategicMerge`**), у **тому ж** файлі має бути посилання на відповідний **`svc-hl.yaml`**
@@ -55,6 +57,11 @@
55
57
  * Для **`patchesStrategicMerge`** і для **`patches[].path`** без **`target`** і без inline **`patch`** (зовнішній strategic-merge)
56
58
  * кожен YAML-документ з кореневим **`kind`** і **`metadata.name`** також звіряється з цим каталогом.
57
59
  *
60
+ * **Зайві `group` / `version` у `patches[].target` / `patchesJson6902[].target`:** якщо в інвентарі **`resources`** /
61
+ * **`bases`** / **`components`** / **`crds`** (рекурсивно) за **`kind`** + **`name`** немає колізії між різними
62
+ * API-групами/версіями, поля **`group`** і **`version`** у **`target`** треба прибрати — Kustomize резолвить ціль
63
+ * за **GVK + name**, а зайві поля ламаються мовчки під час змін API (k8s.mdc «patches[].target: лише kind і name»).
64
+ *
58
65
  * Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
59
66
  * **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
60
67
  * компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
@@ -109,6 +116,7 @@ import { basename, dirname, join, relative, resolve } from 'node:path'
109
116
  import { isSeq, parseAllDocuments, parseDocument } from 'yaml'
110
117
 
111
118
  import { createCheckReporter } from './utils/check-reporter.mjs'
119
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
112
120
  import { walkDir } from './utils/walkDir.mjs'
113
121
 
114
122
  /** Версія набору схем yannh — узгоджено з k8s.mdc */
@@ -1322,6 +1330,52 @@ function failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail) {
1322
1330
  }
1323
1331
  }
1324
1332
 
1333
+ /**
1334
+ * Зайві **`group`** / **`version`** у **`patches[].target`** / **`patchesJson6902[].target`**: якщо в інвентарі за **`kind`** + **`name`** немає колізії між різними API-групами/версіями, ці поля треба прибрати (k8s.mdc «patches[].target: лише kind і name»).
1335
+ * @param {string} rel відносний шлях до kustomization.yaml
1336
+ * @param {Record<string, unknown>} first корінь Kustomization
1337
+ * @param {KustomizeResourceDescriptor[]} catalog інвентар resources/bases/…
1338
+ * @param {(msg: string) => void} fail реєстрація помилки
1339
+ * @returns {void}
1340
+ */
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
+ }
1373
+ fail(
1374
+ `${rel}: ${section}[${index}].target — прибери зайві поля ${redundant.join(', ')}; для kind=${kind}, name=${name} в інвентарі немає колізії між різними API-групами/версіями (див. k8s.mdc «patches[].target: лише kind і name»)`
1375
+ )
1376
+ }
1377
+ }
1378
+
1325
1379
  /**
1326
1380
  * Документи з YAML-файлу мають мати дескриптор у **catalog** (інвентар resources).
1327
1381
  * @param {string} rel відносний шлях до kustomization.yaml
@@ -1524,6 +1578,7 @@ async function validatePatchTargetsOneKustomizationFile(root, kustAbs, rootNorm,
1524
1578
  const kustDir = dirname(resolve(kustAbs))
1525
1579
  const kustNs = typeof rec.namespace === 'string' && rec.namespace.trim() !== '' ? rec.namespace.trim() : ''
1526
1580
  failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail)
1581
+ failIfExplicitPatchTargetsHaveRedundantGroupVersion(rel, first, catalog, fail)
1527
1582
  await failIfPathOnlyPatchesNotInCatalog(rel, rec.patches, kustDir, rootNorm, root, catalog, kustNs, fail)
1528
1583
  await failIfStrategicMergePatchesNotInCatalog(
1529
1584
  rel,
@@ -1581,16 +1636,21 @@ export function baseKustomizationNamespaceViolation(obj) {
1581
1636
  /**
1582
1637
  * Збирає всі `*.yaml` та `*.yml` під деревом від кореня cwd, якщо шлях містить сегмент `k8s` (для `.yml` далі — помилка перейменування).
1583
1638
  * @param {string} root корінь репозиторію (cwd)
1639
+ * @param {string[]} [ignorePaths=[]] шляхи каталогів, повністю виключених з обходу
1584
1640
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи до файлів
1585
1641
  */
1586
- async function findK8sYamlFiles(root) {
1642
+ async function findK8sYamlFiles(root, ignorePaths = []) {
1587
1643
  /** @type {string[]} */
1588
1644
  const out = []
1589
- await walkDir(root, p => {
1590
- if (!pathHasK8sSegment(p)) return
1591
- if (!YAML_EXTENSION_RE.test(p)) return
1592
- out.push(p)
1593
- })
1645
+ await walkDir(
1646
+ root,
1647
+ p => {
1648
+ if (!pathHasK8sSegment(p)) return
1649
+ if (!YAML_EXTENSION_RE.test(p)) return
1650
+ out.push(p)
1651
+ },
1652
+ ignorePaths
1653
+ )
1594
1654
 
1595
1655
  return out.toSorted((a, b) => a.localeCompare(b))
1596
1656
  }
@@ -1659,12 +1719,13 @@ export function classifyBackendConfigManifestPresence(body) {
1659
1719
  /**
1660
1720
  * Видаляє під **`k8s`** YAML-файли, що містять **лише** ресурси **BackendConfig**; змішані файли — `fail`.
1661
1721
  * @param {string} root корінь репозиторію
1722
+ * @param {string[]} ignorePaths шляхи каталогів, повністю виключених з обходу
1662
1723
  * @param {(msg: string) => void} fail реєстрація порушення
1663
1724
  * @param {(msg: string) => void} pass реєстрація успіху
1664
1725
  * @returns {Promise<void>}
1665
1726
  */
1666
- async function removeBackendConfigOnlyK8sYamlFiles(root, fail, pass) {
1667
- const yamlFiles = await findK8sYamlFiles(root)
1727
+ async function removeBackendConfigOnlyK8sYamlFiles(root, ignorePaths, fail, pass) {
1728
+ const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
1668
1729
  for (const abs of yamlFiles) {
1669
1730
  const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
1670
1731
  try {
@@ -1736,12 +1797,13 @@ export function replaceBatchV1beta1ApiVersionInYamlText(raw) {
1736
1797
  /**
1737
1798
  * Проходить усі `*.yaml` / `*.yml` під сегментом `k8s` і на диску застосовує **`replaceBatchV1beta1ApiVersionInYamlText`**.
1738
1799
  * @param {string} root корінь репозиторію
1800
+ * @param {string[]} ignorePaths шляхи каталогів, повністю виключених з обходу
1739
1801
  * @param {(msg: string) => void} fail колбек повідомлення про помилку
1740
1802
  * @param {(msg: string) => void} pass колбек успішного повідомлення
1741
1803
  * @returns {Promise<void>}
1742
1804
  */
1743
- async function rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, fail, pass) {
1744
- const yamlFiles = await findK8sYamlFiles(root)
1805
+ async function rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, ignorePaths, fail, pass) {
1806
+ const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
1745
1807
  for (const abs of yamlFiles) {
1746
1808
  const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
1747
1809
  try {
@@ -2872,7 +2934,49 @@ export function collectGatewayApiRouteBackendServiceNames(spec) {
2872
2934
  }
2873
2935
 
2874
2936
  /**
2875
- * Один документ: маршрут Gateway API має посилатися на **Service** з суфіксом **`-hl`**.
2937
+ * Збирає **`backendRef`** до **Service** з полем **`namespace`**, що збігається з namespace маршруту.
2938
+ *
2939
+ * Поле **`namespace`** у такому **`backendRef`** надлишкове: за замовчуванням Gateway API резолвить backend
2940
+ * у тому ж namespace, що й сам маршрут (див. k8s.mdc). Зайві поля у YAML — джерело розсинхрону між середовищами.
2941
+ * @param {unknown} spec значення **`spec`** маршруту
2942
+ * @param {string} routeNs **`metadata.namespace`** маршруту (непорожній рядок)
2943
+ * @returns {string[]} імена backend-сервісів з надлишковим **`namespace`** (можливі дублікати)
2944
+ */
2945
+ export function collectGatewayApiRouteBackendRefsWithRedundantNamespace(spec, routeNs) {
2946
+ /** @type {string[]} */
2947
+ const out = []
2948
+
2949
+ /**
2950
+ * @param {unknown} node вузол для обходу
2951
+ * @returns {void}
2952
+ */
2953
+ function walk(node) {
2954
+ if (node === null || node === undefined) return
2955
+ if (Array.isArray(node)) {
2956
+ for (const x of node) {
2957
+ walk(x)
2958
+ }
2959
+ return
2960
+ }
2961
+ if (typeof node !== 'object') return
2962
+ if (isGatewayApiBackendRefToService(node)) {
2963
+ const o = /** @type {Record<string, unknown>} */ (node)
2964
+ if (typeof o.namespace === 'string' && o.namespace === routeNs) {
2965
+ out.push(String(o.name))
2966
+ }
2967
+ }
2968
+ for (const v of Object.values(node)) {
2969
+ walk(v)
2970
+ }
2971
+ }
2972
+
2973
+ walk(spec)
2974
+ return out
2975
+ }
2976
+
2977
+ /**
2978
+ * Один документ: маршрут Gateway API має посилатися на **Service** з суфіксом **`-hl`**;
2979
+ * у **`backendRef`** не має дублюватися **`namespace`**, що збігається з **`metadata.namespace`** маршруту.
2876
2980
  * @param {string} rel відносний шлях до файлу
2877
2981
  * @param {number} docIndex 1-based індекс документа
2878
2982
  * @param {Record<string, unknown>} rec корінь маніфесту
@@ -2898,6 +3002,18 @@ function failIfGatewayRouteUsesNonHeadlessService(rel, docIndex, rec, fail) {
2898
3002
  )
2899
3003
  }
2900
3004
  }
3005
+ const meta = rec.metadata
3006
+ if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
3007
+ const routeNs = /** @type {Record<string, unknown>} */ (meta).namespace
3008
+ if (typeof routeNs === 'string' && routeNs !== '') {
3009
+ const redundant = collectGatewayApiRouteBackendRefsWithRedundantNamespace(rec.spec, routeNs)
3010
+ for (const svcName of redundant) {
3011
+ fail(
3012
+ `${rel}: Gateway API ${kind} (документ ${docIndex}): backendRef «${svcName}» має namespace «${routeNs}», що збігається з metadata.namespace маршруту — прибери поле namespace з backendRef (див. k8s.mdc)`
3013
+ )
3014
+ }
3015
+ }
3016
+ }
2901
3017
  }
2902
3018
 
2903
3019
  /**
@@ -5762,12 +5878,13 @@ export async function check() {
5762
5878
  const { pass, fail } = reporter
5763
5879
 
5764
5880
  const root = process.cwd()
5881
+ const ignorePaths = await loadCursorIgnorePaths(root)
5765
5882
 
5766
- await rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, fail, pass)
5883
+ await rewriteBatchV1beta1ApiVersionInK8sYamlFiles(root, ignorePaths, fail, pass)
5767
5884
 
5768
- await removeBackendConfigOnlyK8sYamlFiles(root, fail, pass)
5885
+ await removeBackendConfigOnlyK8sYamlFiles(root, ignorePaths, fail, pass)
5769
5886
 
5770
- const yamlFiles = await findK8sYamlFiles(root)
5887
+ const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
5771
5888
 
5772
5889
  if (yamlFiles.length === 0) {
5773
5890
  pass('Немає *.yaml під k8s — перевірку $schema пропущено')
@@ -19,6 +19,7 @@ import { basename, dirname, join, relative } from 'node:path'
19
19
 
20
20
  import { findDockerfilePaths } from './check-docker.mjs'
21
21
  import { createCheckReporter } from './utils/check-reporter.mjs'
22
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
22
23
  import { walkDir } from './utils/walkDir.mjs'
23
24
 
24
25
  const LINE_SPLIT_RE = /\r?\n/u
@@ -36,15 +37,19 @@ const GZIP_EXTENSION_RE = /\*\.(?:js|css)/u
36
37
  * @param {string} root корінь cwd
37
38
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи до шаблонів
38
39
  */
39
- export async function findDefaultConfTemplatePaths(root) {
40
+ export async function findDefaultConfTemplatePaths(root, ignorePaths = []) {
40
41
  /** @type {string[]} */
41
42
  const out = []
42
- await walkDir(root, p => {
43
- if (basename(p) !== 'default.conf.template') return
44
- const rel = relative(root, p).replaceAll('\\', '/')
45
- if (rel.includes('tests/fixtures/')) return
46
- out.push(p)
47
- })
43
+ await walkDir(
44
+ root,
45
+ p => {
46
+ if (basename(p) !== 'default.conf.template') return
47
+ const rel = relative(root, p).replaceAll('\\', '/')
48
+ if (rel.includes('tests/fixtures/')) return
49
+ out.push(p)
50
+ },
51
+ ignorePaths
52
+ )
48
53
  return out.toSorted((a, b) => a.localeCompare(b))
49
54
  }
50
55
 
@@ -54,12 +59,16 @@ export async function findDefaultConfTemplatePaths(root) {
54
59
  * @param {string} root корінь обходу (зазвичай cwd репозиторію)
55
60
  * @returns {Promise<{ renamed: string[], overwritten: string[] }>} відносні шляхи до обробленого **default.tpl.conf** (для звіту)
56
61
  */
57
- export async function migrateDefaultTplConfFiles(root) {
62
+ export async function migrateDefaultTplConfFiles(root, ignorePaths = []) {
58
63
  /** @type {string[]} */
59
64
  const oldPaths = []
60
- await walkDir(root, p => {
61
- if (basename(p) === 'default.tpl.conf') oldPaths.push(p)
62
- })
65
+ await walkDir(
66
+ root,
67
+ p => {
68
+ if (basename(p) === 'default.tpl.conf') oldPaths.push(p)
69
+ },
70
+ ignorePaths
71
+ )
63
72
  oldPaths.sort((a, b) => a.localeCompare(b))
64
73
 
65
74
  /** @type {string[]} */
@@ -316,8 +325,8 @@ async function checkTemplateFile(abs, root, passFn, failFn) {
316
325
  * @param {(msg: string) => void} passFn callback при успішній перевірці
317
326
  * @param {(msg: string) => void} failFn callback при помилці
318
327
  */
319
- async function checkDockerfiles(root, passFn, failFn) {
320
- const dockerPaths = await findDockerfilePaths(root)
328
+ async function checkDockerfiles(root, ignorePaths, passFn, failFn) {
329
+ const dockerPaths = await findDockerfilePaths(root, ignorePaths)
321
330
  if (dockerPaths.length === 0) {
322
331
  failFn(
323
332
  'Є default.conf.template, але немає Dockerfile / Containerfile — додай gzip для статики та envsubst (див. nginx-default-tpl.mdc)'
@@ -380,8 +389,9 @@ export async function check() {
380
389
  const { pass, fail } = reporter
381
390
 
382
391
  const root = process.cwd()
392
+ const ignorePaths = await loadCursorIgnorePaths(root)
383
393
 
384
- const { renamed: tplRenamed, overwritten: tplOverwritten } = await migrateDefaultTplConfFiles(root)
394
+ const { renamed: tplRenamed, overwritten: tplOverwritten } = await migrateDefaultTplConfFiles(root, ignorePaths)
385
395
  for (const rel of tplRenamed) {
386
396
  pass(`Перейменовано default.tpl.conf → default.conf.template: ${rel}`)
387
397
  }
@@ -389,7 +399,7 @@ export async function check() {
389
399
  pass(`Перезаписано default.conf.template змістом default.tpl.conf: ${rel}`)
390
400
  }
391
401
 
392
- const templates = await findDefaultConfTemplatePaths(root)
402
+ const templates = await findDefaultConfTemplatePaths(root, ignorePaths)
393
403
 
394
404
  if (templates.length === 0) {
395
405
  pass('Немає default.conf.template — перевірку nginx-default-tpl пропущено')
@@ -402,7 +412,7 @@ export async function check() {
402
412
  await checkTemplateFile(abs, root, pass, fail)
403
413
  }
404
414
 
405
- await checkDockerfiles(root, pass, fail)
415
+ await checkDockerfiles(root, ignorePaths, pass, fail)
406
416
  await checkVscodeNginx(pass, fail)
407
417
 
408
418
  return reporter.getExitCode()
@@ -9,9 +9,6 @@
9
9
  * Якщо таких файлів немає — layout через `npm/tsconfig.emit-types.json`: поле `types` має вказувати на існуючий
10
10
  * файл під `./types/…`, у hk — `tsc -p tsconfig.emit-types.json`, у JSON-конфігу — потрібні compilerOptions для emit.
11
11
  *
12
- * Окремо перевіряється `npm/CHANGELOG.md`: файл існує, є в `files` у `npm/package.json` і містить запис
13
- * для поточної версії (формат `## [X.Y.Z] - YYYY-MM-DD`, Keep a Changelog).
14
- *
15
12
  * Поля workflow перевіряються після **YAML parse**, щоб не плутати з коментарями.
16
13
  */
17
14
  import { existsSync } from 'node:fs'
@@ -26,6 +23,7 @@ import {
26
23
  pushHasMainBranch,
27
24
  pushPathsIncludeNpmGlob
28
25
  } from './utils/gha-workflow.mjs'
26
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
29
27
  import { walkDir } from './utils/walkDir.mjs'
30
28
 
31
29
  const TYPES_FILE_RE = /^\.\/types\/.+\.d\.(ts|mts)$/
@@ -36,24 +34,25 @@ const TYPES_INDEX = './types/index.d.ts'
36
34
  /** Файл проєкту TypeScript для emit без каталогу `src` (див. npm-module.mdc) */
37
35
  const EMIT_TYPES_CONFIG = 'npm/tsconfig.emit-types.json'
38
36
 
39
- /** Шлях до `CHANGELOG.md` в каталозі npm-модуля */
40
- const CHANGELOG_PATH = 'npm/CHANGELOG.md'
41
-
42
37
  /**
43
38
  * Чи є під `npm/src` хоча б один `.js` (рекурсивно).
44
39
  * @returns {Promise<boolean>} `true`, якщо знайдено хоча б один `.js`
45
40
  */
46
- async function npmSrcTreeHasJsFile() {
41
+ async function npmSrcTreeHasJsFile(ignorePaths = []) {
47
42
  const root = 'npm/src'
48
43
  if (!existsSync(root)) {
49
44
  return false
50
45
  }
51
46
  let found = false
52
- await walkDir(root, p => {
53
- if (p.endsWith('.js')) {
54
- found = true
55
- }
56
- })
47
+ await walkDir(
48
+ root,
49
+ p => {
50
+ if (p.endsWith('.js')) {
51
+ found = true
52
+ }
53
+ },
54
+ ignorePaths
55
+ )
57
56
  return found
58
57
  }
59
58
 
@@ -200,53 +199,6 @@ async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn) {
200
199
  }
201
200
  }
202
201
 
203
- /**
204
- * Чи містить текст `CHANGELOG.md` запис заголовка для конкретної версії
205
- * у форматі Keep a Changelog: `## [X.Y.Z]` (з опційним `- YYYY-MM-DD`).
206
- * @param {string} text вміст `CHANGELOG.md`
207
- * @param {string} version номер версії з `npm/package.json`
208
- * @returns {boolean} `true`, якщо знайдено заголовок версії
209
- */
210
- function changelogHasVersionEntry(text, version) {
211
- const escaped = version.replaceAll(/[.+*?^$()[\]{}|\\]/g, String.raw`\$&`)
212
- const re = new RegExp(String.raw`^##\s+\[${escaped}\]`, 'm')
213
- return re.test(text)
214
- }
215
-
216
- /**
217
- * Перевіряє наявність і вміст `npm/CHANGELOG.md`, а також що він є в `files` у `npm/package.json`.
218
- * @param {(msg: string) => void} passFn callback при успішній перевірці
219
- * @param {(msg: string) => void} failFn callback при помилці
220
- */
221
- async function checkChangelog(passFn, failFn) {
222
- if (!existsSync(CHANGELOG_PATH)) {
223
- failFn(`Відсутній ${CHANGELOG_PATH} (npm-module.mdc: CHANGELOG)`)
224
- return
225
- }
226
- passFn(`${CHANGELOG_PATH} існує`)
227
-
228
- if (existsSync('npm/package.json')) {
229
- const npmPkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
230
- if (Array.isArray(npmPkg.files) && npmPkg.files.includes('CHANGELOG.md')) {
231
- passFn('npm/package.json: files містить "CHANGELOG.md"')
232
- } else {
233
- failFn('npm/package.json: масив files має містити "CHANGELOG.md"')
234
- }
235
-
236
- const version = typeof npmPkg.version === 'string' ? npmPkg.version : null
237
- if (version) {
238
- const text = await readFile(CHANGELOG_PATH, 'utf8')
239
- if (changelogHasVersionEntry(text, version)) {
240
- passFn(`${CHANGELOG_PATH}: знайдено запис для версії ${version}`)
241
- } else {
242
- failFn(
243
- `${CHANGELOG_PATH}: відсутній запис для поточної версії ${version} (формат Keep a Changelog: "## [${version}] - YYYY-MM-DD")`
244
- )
245
- }
246
- }
247
- }
248
- }
249
-
250
202
  /**
251
203
  * Перевіряє npm/tsconfig.emit-types.json.
252
204
  * @param {(msg: string) => void} passFn callback при успішній перевірці
@@ -387,12 +339,11 @@ export async function check() {
387
339
 
388
340
  await checkNpmModuleBasicStructure(pass, fail)
389
341
 
390
- const useSrcJsLayout = await npmSrcTreeHasJsFile()
342
+ const ignorePaths = await loadCursorIgnorePaths(process.cwd())
343
+ const useSrcJsLayout = await npmSrcTreeHasJsFile(ignorePaths)
391
344
 
392
345
  await checkNpmPackageJson(useSrcJsLayout, pass, fail)
393
346
 
394
- await checkChangelog(pass, fail)
395
-
396
347
  if (!useSrcJsLayout) {
397
348
  await checkEmitTypesConfig(pass, fail)
398
349
  }