@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.
@@ -20,6 +20,7 @@ import {
20
20
  isVueImportScanSourceFile,
21
21
  shouldSkipFileForVueImportScan
22
22
  } from './utils/vue-forbidden-imports.mjs'
23
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
23
24
  import { walkDir } from './utils/walkDir.mjs'
24
25
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
25
26
 
@@ -113,14 +114,18 @@ async function collectEsbuildMatchesInFiles(absPackageRoot, files, maxMatches) {
113
114
  * @param {(msg: string) => void} passFn callback при успішній перевірці
114
115
  * @param {(msg: string) => void} fail callback при помилці
115
116
  */
116
- async function checkEsbuildMentions(rootDir, absPackageRoot, prefix, passFn, fail) {
117
+ async function checkEsbuildMentions(rootDir, absPackageRoot, ignorePaths, prefix, passFn, fail) {
117
118
  /** @type {{ rel: string }[]} */
118
119
  const candidates = []
119
- await walkDir(absPackageRoot, absPath => {
120
- const rel = relative(absPackageRoot, absPath).split('\\').join('/')
121
- if (!isEsbuildScanFile(rel)) return
122
- candidates.push({ rel })
123
- })
120
+ await walkDir(
121
+ absPackageRoot,
122
+ absPath => {
123
+ const rel = relative(absPackageRoot, absPath).split('\\').join('/')
124
+ if (!isEsbuildScanFile(rel)) return
125
+ candidates.push({ rel })
126
+ },
127
+ ignorePaths
128
+ )
124
129
 
125
130
  const maxMatches = 30
126
131
  const matches = await collectEsbuildMatchesInFiles(absPackageRoot, candidates, maxMatches)
@@ -308,7 +313,7 @@ async function checkViteConfig(rootDir, prefix, passFn, fail) {
308
313
  * @param {(msg: string) => void} passFn callback при успішній перевірці
309
314
  * @param {(msg: string) => void} fail callback при помилці
310
315
  */
311
- async function checkVueImportViolations(rootDir, absPackageRoot, hasVueAutoImport, prefix, passFn, fail) {
316
+ async function checkVueImportViolations(rootDir, absPackageRoot, ignorePaths, hasVueAutoImport, prefix, passFn, fail) {
312
317
  if (!hasVueAutoImport) {
313
318
  passFn(
314
319
  `${prefix}value-імпорти з 'vue' не заборонені — спершу додай 'vue' до AutoImport.imports у vite.config`
@@ -317,12 +322,16 @@ async function checkVueImportViolations(rootDir, absPackageRoot, hasVueAutoImpor
317
322
  }
318
323
  /** @type {string[]} */
319
324
  const sourcePaths = []
320
- await walkDir(absPackageRoot, absPath => {
321
- const rel = relative(absPackageRoot, absPath).split('\\').join('/')
322
- if (!shouldSkipFileForVueImportScan(rel) && isVueImportScanSourceFile(rel)) {
323
- sourcePaths.push(absPath)
324
- }
325
- })
325
+ await walkDir(
326
+ absPackageRoot,
327
+ absPath => {
328
+ const rel = relative(absPackageRoot, absPath).split('\\').join('/')
329
+ if (!shouldSkipFileForVueImportScan(rel) && isVueImportScanSourceFile(rel)) {
330
+ sourcePaths.push(absPath)
331
+ }
332
+ },
333
+ ignorePaths
334
+ )
326
335
 
327
336
  let importViolations = 0
328
337
  for (const absPath of sourcePaths) {
@@ -347,7 +356,7 @@ async function checkVueImportViolations(rootDir, absPackageRoot, hasVueAutoImpor
347
356
  * @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
348
357
  * @returns {Promise<void>} завершується після перевірок залежностей, `vite.config` і сканування джерел на імпорти з `vue`
349
358
  */
350
- async function checkVuePackage(rootDir, fail, passFn) {
359
+ async function checkVuePackage(rootDir, ignorePaths, fail, passFn) {
351
360
  const prefix = `[${packageLabel(rootDir)}] `
352
361
  const pkg = JSON.parse(await readFile(join(rootDir, 'package.json'), 'utf8'))
353
362
  const deps = pkg.dependencies || {}
@@ -387,8 +396,8 @@ async function checkVuePackage(rootDir, fail, passFn) {
387
396
  )
388
397
 
389
398
  const { hasVueAutoImport } = await checkViteConfig(rootDir, prefix, passFn, fail)
390
- await checkVueImportViolations(rootDir, join(process.cwd(), rootDir), hasVueAutoImport, prefix, passFn, fail)
391
- await checkEsbuildMentions(rootDir, join(process.cwd(), rootDir), prefix, passFn, fail)
399
+ await checkVueImportViolations(rootDir, join(process.cwd(), rootDir), ignorePaths, hasVueAutoImport, prefix, passFn, fail)
400
+ await checkEsbuildMentions(rootDir, join(process.cwd(), rootDir), ignorePaths, prefix, passFn, fail)
392
401
  }
393
402
 
394
403
  /**
@@ -446,8 +455,9 @@ export async function check() {
446
455
 
447
456
  await checkVueVolarRecommendation(pass, fail)
448
457
 
458
+ const ignorePaths = await loadCursorIgnorePaths(process.cwd())
449
459
  for (const r of vueRoots) {
450
- await checkVuePackage(r, fail, pass)
460
+ await checkVuePackage(r, ignorePaths, fail, pass)
451
461
  }
452
462
 
453
463
  return reporter.getExitCode()
@@ -14,6 +14,7 @@ import { rename } from 'node:fs/promises'
14
14
  import { cwd } from 'node:process'
15
15
  import { relative, resolve } from 'node:path'
16
16
 
17
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
17
18
  import { walkDir } from './utils/walkDir.mjs'
18
19
 
19
20
  const K8S_YML_RE = /\.yml$/iu
@@ -69,37 +70,41 @@ export function replaceExtension(relPosix, newExt) {
69
70
  * @param {string} rootAbs абсолютний корінь репозиторію
70
71
  * @returns {Promise<Array<{ kind: 'k8s' | 'github', fromAbs: string, toAbs: string, relFrom: string, relTo: string }>>} відсортовані операції перейменування без запису на диск
71
72
  */
72
- async function collectRenameOps(rootAbs) {
73
+ async function collectRenameOps(rootAbs, ignorePaths) {
73
74
  /** @type {Array<{ kind: 'k8s' | 'github', fromAbs: string, toAbs: string, relFrom: string, relTo: string }>} */
74
75
  const ops = []
75
76
 
76
- await walkDir(rootAbs, fileAbs => {
77
- const rel = posixRelFromRoot(rootAbs, fileAbs)
78
- if (rel === null) return
79
- if (pathMatchesK8sYml(rel)) {
80
- const relTo = replaceExtension(rel, '.yaml')
81
- if (relTo === rel) return
82
- ops.push({
83
- kind: 'k8s',
84
- fromAbs: resolve(rootAbs, rel),
85
- toAbs: resolve(rootAbs, relTo),
86
- relFrom: rel,
87
- relTo
88
- })
89
- return
90
- }
91
- if (pathMatchesGithubYaml(rel)) {
92
- const relTo = replaceExtension(rel, '.yml')
93
- if (relTo === rel) return
94
- ops.push({
95
- kind: 'github',
96
- fromAbs: resolve(rootAbs, rel),
97
- toAbs: resolve(rootAbs, relTo),
98
- relFrom: rel,
99
- relTo
100
- })
101
- }
102
- })
77
+ await walkDir(
78
+ rootAbs,
79
+ fileAbs => {
80
+ const rel = posixRelFromRoot(rootAbs, fileAbs)
81
+ if (rel === null) return
82
+ if (pathMatchesK8sYml(rel)) {
83
+ const relTo = replaceExtension(rel, '.yaml')
84
+ if (relTo === rel) return
85
+ ops.push({
86
+ kind: 'k8s',
87
+ fromAbs: resolve(rootAbs, rel),
88
+ toAbs: resolve(rootAbs, relTo),
89
+ relFrom: rel,
90
+ relTo
91
+ })
92
+ return
93
+ }
94
+ if (pathMatchesGithubYaml(rel)) {
95
+ const relTo = replaceExtension(rel, '.yml')
96
+ if (relTo === rel) return
97
+ ops.push({
98
+ kind: 'github',
99
+ fromAbs: resolve(rootAbs, rel),
100
+ toAbs: resolve(rootAbs, relTo),
101
+ relFrom: rel,
102
+ relTo
103
+ })
104
+ }
105
+ },
106
+ ignorePaths
107
+ )
103
108
 
104
109
  ops.sort((a, b) => {
105
110
  const ko = (a.kind === 'k8s' ? 0 : 1) - (b.kind === 'k8s' ? 0 : 1)
@@ -119,7 +124,8 @@ async function collectRenameOps(rootAbs) {
119
124
  export async function renameYamlExtensions(root, options = {}) {
120
125
  const dryRun = options.dryRun === true
121
126
  const rootAbs = resolve(root)
122
- const ops = await collectRenameOps(rootAbs)
127
+ const ignorePaths = await loadCursorIgnorePaths(rootAbs)
128
+ const ops = await collectRenameOps(rootAbs, ignorePaths)
123
129
 
124
130
  /** @type { { relFrom: string, relTo: string }[]} */
125
131
  const renamed = []
@@ -12,6 +12,7 @@ import { basename } from 'node:path'
12
12
  import { isRunAsCli } from './cli-entry.mjs'
13
13
  import { lintDockerfileWithHadolint, posixRel } from './utils/docker-hadolint.mjs'
14
14
  import { createCheckReporter } from './utils/check-reporter.mjs'
15
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
15
16
  import { walkDir } from './utils/walkDir.mjs'
16
17
 
17
18
  /**
@@ -30,12 +31,16 @@ export function isLintDockerfileName(name) {
30
31
  * @param {string} root корінь репозиторію
31
32
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи
32
33
  */
33
- export async function findLintDockerfilePaths(root) {
34
+ export async function findLintDockerfilePaths(root, ignorePaths = []) {
34
35
  /** @type {string[]} */
35
36
  const out = []
36
- await walkDir(root, p => {
37
- if (isLintDockerfileName(basename(p))) out.push(p)
38
- })
37
+ await walkDir(
38
+ root,
39
+ p => {
40
+ if (isLintDockerfileName(basename(p))) out.push(p)
41
+ },
42
+ ignorePaths
43
+ )
39
44
  return out.toSorted((a, b) => a.localeCompare(b))
40
45
  }
41
46
 
@@ -48,7 +53,8 @@ async function main() {
48
53
  const { pass, fail } = reporter
49
54
 
50
55
  const root = process.cwd()
51
- const files = await findLintDockerfilePaths(root)
56
+ const ignorePaths = await loadCursorIgnorePaths(root)
57
+ const files = await findLintDockerfilePaths(root, ignorePaths)
52
58
 
53
59
  if (files.length === 0) {
54
60
  pass('lint-docker: немає Dockerfile / *.Dockerfile — hadolint пропущено')
@@ -16,6 +16,7 @@ import { spawnSync } from 'node:child_process'
16
16
  import { basename, dirname } from 'node:path'
17
17
 
18
18
  import { isRunAsCli } from './cli-entry.mjs'
19
+ import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
19
20
  import { resolveCmd } from './utils/resolve-cmd.mjs'
20
21
  import { walkDir } from './utils/walkDir.mjs'
21
22
 
@@ -60,15 +61,19 @@ export function k8sRootFromFile(absFile) {
60
61
  * @param {string} root корінь репозиторію
61
62
  * @returns {Promise<string[]>} відсортовані абсолютні шляхи до каталогів `k8s`
62
63
  */
63
- export async function findK8sRoots(root) {
64
+ export async function findK8sRoots(root, ignorePaths = []) {
64
65
  /** @type {Set<string>} */
65
66
  const roots = new Set()
66
- await walkDir(root, p => {
67
- if (!pathHasK8sSegment(p)) return
68
- if (!YAML_EXT_RE.test(p)) return
69
- const k8sRoot = k8sRootFromFile(p)
70
- if (k8sRoot) roots.add(k8sRoot)
71
- })
67
+ await walkDir(
68
+ root,
69
+ p => {
70
+ if (!pathHasK8sSegment(p)) return
71
+ if (!YAML_EXT_RE.test(p)) return
72
+ const k8sRoot = k8sRootFromFile(p)
73
+ if (k8sRoot) roots.add(k8sRoot)
74
+ },
75
+ ignorePaths
76
+ )
72
77
  return [...roots].toSorted((a, b) => a.localeCompare(b))
73
78
  }
74
79
 
@@ -134,7 +139,8 @@ function runKubescape(dirs) {
134
139
  */
135
140
  async function main() {
136
141
  const root = process.cwd()
137
- const dirs = await findK8sRoots(root)
142
+ const ignorePaths = await loadCursorIgnorePaths(root)
143
+ const dirs = await findK8sRoots(root, ignorePaths)
138
144
 
139
145
  if (dirs.length === 0) {
140
146
  console.log('run-k8s: немає *.yaml під k8s — kubeconform і kubescape пропущено')
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Утиліта читання `.n-cursor.json` у корені репозиторію.
3
+ *
4
+ * Зараз експортує `loadCursorIgnorePaths(root)` — список абсолютних posix-шляхів каталогів,
5
+ * які check-скрипти повністю виключають з обходу (поле `ignore` у конфізі).
6
+ */
7
+ import { existsSync } from 'node:fs'
8
+ import { readFile } from 'node:fs/promises'
9
+ import { isAbsolute, join, resolve, sep } from 'node:path'
10
+
11
+ const CONFIG_FILE = '.n-cursor.json'
12
+
13
+ /**
14
+ * Нормалізує шлях до абсолютного posix-формату без trailing-slash.
15
+ * Відносні шляхи розв'язуються від `root`.
16
+ * @param {string} root абсолютний корінь репозиторію
17
+ * @param {string} p шлях з конфігу (відносний або абсолютний)
18
+ * @returns {string} абсолютний posix-шлях
19
+ */
20
+ function toAbsPosix(root, p) {
21
+ const trimmed = String(p).trim()
22
+ const abs = isAbsolute(trimmed) ? trimmed : resolve(root, trimmed)
23
+ return abs.split(sep).join('/').replace(/\/+$/, '')
24
+ }
25
+
26
+ /**
27
+ * Читає `.n-cursor.json` з кореня та повертає нормалізовані ignore-шляхи.
28
+ * Якщо файлу нема, поле `ignore` відсутнє чи має невалідний формат — повертає порожній масив.
29
+ * Сам конфіг не валідується (це робить v8r/окрема перевірка) — лише поле `ignore`.
30
+ * @param {string} root абсолютний корінь репозиторію
31
+ * @returns {Promise<string[]>} абсолютні posix-шляхи без trailing-slash
32
+ */
33
+ export async function loadCursorIgnorePaths(root) {
34
+ const file = join(root, CONFIG_FILE)
35
+ if (!existsSync(file)) return []
36
+ let raw
37
+ try {
38
+ raw = JSON.parse(await readFile(file, 'utf8'))
39
+ } catch {
40
+ return []
41
+ }
42
+ const list = raw?.ignore
43
+ if (!Array.isArray(list)) return []
44
+ /** @type {string[]} */
45
+ const out = []
46
+ for (const item of list) {
47
+ if (typeof item !== 'string') continue
48
+ const v = item.trim()
49
+ if (v.length === 0) continue
50
+ out.push(toAbsPosix(root, v))
51
+ }
52
+ return out
53
+ }
@@ -2,19 +2,60 @@
2
2
  * Рекурсивний обхід каталогів для скриптів перевірки (Dockerfile, k8s YAML тощо).
3
3
  *
4
4
  * Обходить дерево від заданого кореня; для кожного звичайного файлу викликає переданий callback.
5
- * Каталоги node_modules, .git, dist, coverage, .turbo, .next не заходяться. Якщо readdir для
6
- * каталогу не вдаєтьсятихо виходить без throw.
5
+ * Каталоги node_modules, .git, dist, coverage, .turbo, .next не заходяться.
6
+ * Додатково можна передати `ignorePaths` повні шляхи каталогів (абсолютні posix), які слід
7
+ * пропускати разом з усім вмістом (поле `ignore` у `.n-cursor.json`). Якщо readdir для каталогу
8
+ * не вдається — тихо виходить без throw.
7
9
  */
8
10
  import { readdir } from 'node:fs/promises'
9
- import { join } from 'node:path'
11
+ import { isAbsolute, join, resolve, sep } from 'node:path'
10
12
 
11
13
  /**
12
- * Рекурсивно обходить каталог, пропускає типові артефакти збірки та залежностей.
14
+ * Перетворює довільний шлях у абсолютний posix-формат без trailing-slash.
15
+ * @param {string} p шлях
16
+ * @returns {string} абсолютний posix-шлях
17
+ */
18
+ function toAbsPosix(p) {
19
+ const abs = isAbsolute(p) ? p : resolve(p)
20
+ return abs.split(sep).join('/').replace(/\/+$/, '')
21
+ }
22
+
23
+ /**
24
+ * Чи каталог `dirAbsPosix` входить у список ignore (точний збіг або префікс з '/').
25
+ * Часткові збіги басенейму не враховуються (postgres-master-test ≠ postgres-master).
26
+ * @param {string} dirAbsPosix абсолютний posix-шлях каталогу
27
+ * @param {string[]} ignorePosix вже нормалізовані ignore-шляхи
28
+ * @returns {boolean}
29
+ */
30
+ function isIgnoredDir(dirAbsPosix, ignorePosix) {
31
+ for (const ig of ignorePosix) {
32
+ if (dirAbsPosix === ig) return true
33
+ if (dirAbsPosix.startsWith(`${ig}/`)) return true
34
+ }
35
+ return false
36
+ }
37
+
38
+ /**
39
+ * Рекурсивно обходить каталог, пропускає типові артефакти збірки/залежностей та `ignorePaths`.
13
40
  * @param {string} dir абсолютний шлях
14
41
  * @param {(filePath: string) => void} onFile виклик для кожного файлу
42
+ * @param {string[]} [ignorePaths=[]] шляхи каталогів (відносні від cwd або абсолютні), що повністю виключаються з обходу
43
+ * @returns {Promise<void>}
44
+ */
45
+ export async function walkDir(dir, onFile, ignorePaths = []) {
46
+ const ignorePosix = ignorePaths.map(toAbsPosix)
47
+ await walkDirInner(dir, onFile, ignorePosix)
48
+ }
49
+
50
+ /**
51
+ * Внутрішній рекурсор. ignorePosix вже нормалізовано — не нормалізуємо повторно на кожному рівні.
52
+ * @param {string} dir
53
+ * @param {(filePath: string) => void} onFile
54
+ * @param {string[]} ignorePosix
15
55
  * @returns {Promise<void>}
16
56
  */
17
- export async function walkDir(dir, onFile) {
57
+ async function walkDirInner(dir, onFile, ignorePosix) {
58
+ if (ignorePosix.length > 0 && isIgnoredDir(toAbsPosix(dir), ignorePosix)) return
18
59
  let entries
19
60
  try {
20
61
  entries = await readdir(dir, { withFileTypes: true })
@@ -31,9 +72,9 @@ export async function walkDir(dir, onFile) {
31
72
  e.name === 'coverage' ||
32
73
  e.name === '.turbo' ||
33
74
  e.name === '.next'
34
- if (!skipDir) {
35
- await walkDir(p, onFile)
36
- }
75
+ if (skipDir) continue
76
+ if (ignorePosix.length > 0 && isIgnoredDir(toAbsPosix(p), ignorePosix)) continue
77
+ await walkDirInner(p, onFile, ignorePosix)
37
78
  } else if (e.isFile()) {
38
79
  onFile(p)
39
80
  }