@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.
@@ -28,27 +28,27 @@ const TEMPLATE_DIR_NAME = '.claude-template'
28
28
 
29
29
  /**
30
30
  * @typedef {object} HookEntry
31
- * @property {string} type
32
- * @property {string} command
33
- * @property {number} [timeout]
31
+ * @property {string} type тип hook'а у форматі Claude Code (зазвичай `'command'`)
32
+ * @property {string} command команда, яку виконує Claude Code (наш маркер живе саме тут)
33
+ * @property {number} [timeout] опційний таймаут у секундах
34
34
  */
35
35
 
36
36
  /**
37
37
  * @typedef {object} HookGroup
38
- * @property {string} [matcher]
39
- * @property {HookEntry[]} hooks
38
+ * @property {string} [matcher] патерн (наприклад, `'.*'`) для звуження hook'а
39
+ * @property {HookEntry[]} hooks впорядкований список команд hook-групи
40
40
  */
41
41
 
42
42
  /**
43
43
  * @typedef {object} ClaudeSettings
44
- * @property {{ allow?: string[] }} [permissions]
45
- * @property {Record<string, HookGroup[]>} [hooks]
44
+ * @property {{ allow?: string[] }} [permissions] секція `permissions` із .claude/settings.json
45
+ * @property {Record<string, HookGroup[]>} [hooks] hooks за подіями (`Stop`, `PreToolUse`, ...)
46
46
  */
47
47
 
48
48
  /**
49
49
  * Чи hook-група містить лише наші managed-команди (за маркером).
50
- * @param {HookGroup} group
51
- * @returns {boolean}
50
+ * @param {HookGroup} group hook-група з .claude/settings.json
51
+ * @returns {boolean} `true`, якщо всі hooks мають маркер `MANAGED_HOOK_COMMAND_MARKER`
52
52
  */
53
53
  function isManagedHookGroup(group) {
54
54
  if (!group?.hooks?.length) {
@@ -60,9 +60,9 @@ function isManagedHookGroup(group) {
60
60
  /**
61
61
  * Зливає список allow-permissions: union існуючого і темплейтного без дублікатів,
62
62
  * порядок — спочатку існуючі (щоб не міняти користувацький порядок), потім нові.
63
- * @param {string[] | undefined} existing
64
- * @param {string[] | undefined} fromTemplate
65
- * @returns {string[]}
63
+ * @param {string[] | undefined} existing існуючий список з `.claude/settings.json` користувача
64
+ * @param {string[] | undefined} fromTemplate список з темплейту пакета `@nitra/cursor`
65
+ * @returns {string[]} об'єднаний список без дублікатів (порядок: існуючі, потім нові)
66
66
  */
67
67
  export function mergeAllowList(existing, fromTemplate) {
68
68
  const out = []
@@ -83,9 +83,9 @@ export function mergeAllowList(existing, fromTemplate) {
83
83
  * Зливає hooks-секцію: для кожної події в темплейті видаляємо managed-групи
84
84
  * з існуючої конфігурації і додаємо актуальні з темплейту. Немені події в
85
85
  * темплейті не чіпаються.
86
- * @param {Record<string, HookGroup[]> | undefined} existing
87
- * @param {Record<string, HookGroup[]> | undefined} fromTemplate
88
- * @returns {Record<string, HookGroup[]>}
86
+ * @param {Record<string, HookGroup[]> | undefined} existing поточна `hooks`-секція з .claude/settings.json
87
+ * @param {Record<string, HookGroup[]> | undefined} fromTemplate цільова `hooks`-секція з темплейту
88
+ * @returns {Record<string, HookGroup[]>} результат злиття (порожні події видаляються)
89
89
  */
90
90
  export function mergeHooks(existing, fromTemplate) {
91
91
  /** @type {Record<string, HookGroup[]>} */
@@ -105,9 +105,9 @@ export function mergeHooks(existing, fromTemplate) {
105
105
 
106
106
  /**
107
107
  * Повертає об'єднаний об'єкт settings.json.
108
- * @param {ClaudeSettings | undefined} existing
109
- * @param {ClaudeSettings} template
110
- * @returns {ClaudeSettings}
108
+ * @param {ClaudeSettings | undefined} existing існуючий вміст `.claude/settings.json` користувача (або undefined, якщо файла нема)
109
+ * @param {ClaudeSettings} template settings із темплейту пакета `@nitra/cursor`
110
+ * @returns {ClaudeSettings} результат merge-у (користувацькі поля збережено, наші перевизначено)
111
111
  */
112
112
  export function mergeSettings(existing, template) {
113
113
  /** @type {ClaudeSettings} */
@@ -127,8 +127,8 @@ export function mergeSettings(existing, template) {
127
127
 
128
128
  /**
129
129
  * Читає JSON-файл; якщо файл відсутній або не валідний — повертає `undefined`.
130
- * @param {string} path
131
- * @returns {Promise<ClaudeSettings | undefined>}
130
+ * @param {string} path абсолютний шлях до JSON-файлу
131
+ * @returns {Promise<ClaudeSettings | undefined>} розпарсений об'єкт або `undefined` (файл відсутній / невалідний)
132
132
  */
133
133
  async function readJsonOrUndefined(path) {
134
134
  if (!existsSync(path)) {
@@ -146,7 +146,7 @@ async function readJsonOrUndefined(path) {
146
146
  * користувацьких полів.
147
147
  * @param {string} projectRoot корінь проєкту, куди писати
148
148
  * @param {string} templateDir каталог `.claude-template/` усередині пакету
149
- * @returns {Promise<{ written: boolean, path: string }>}
149
+ * @returns {Promise<{ written: boolean, path: string }>} результат: чи писали файл, та його відносний шлях
150
150
  */
151
151
  export async function syncClaudeSettings(projectRoot, templateDir) {
152
152
  const templatePath = join(templateDir, 'settings.template.json')
@@ -164,9 +164,9 @@ export async function syncClaudeSettings(projectRoot, templateDir) {
164
164
 
165
165
  /**
166
166
  * Копіює `npm/CLAUDE.md` з темплейту, якщо в проєкті є каталог `npm/`.
167
- * @param {string} projectRoot
168
- * @param {string} templateDir
169
- * @returns {Promise<{ written: boolean, path: string }>}
167
+ * @param {string} projectRoot корінь проєкту, куди писати
168
+ * @param {string} templateDir каталог `.claude-template/` усередині пакету `@nitra/cursor`
169
+ * @returns {Promise<{ written: boolean, path: string }>} результат: чи писали файл, та його відносний шлях
170
170
  */
171
171
  export async function syncNpmClaudeMd(projectRoot, templateDir) {
172
172
  if (!existsSync(join(projectRoot, 'npm'))) {
@@ -185,8 +185,8 @@ export async function syncNpmClaudeMd(projectRoot, templateDir) {
185
185
  * Копіює всі slash-команди з `templateDir/commands/` у `.claude/commands/`.
186
186
  * Команди ідентифікуються тим, що вони лежать у темплейті — не перетинаються
187
187
  * з командами скілів (n-fix, n-lint, ...).
188
- * @param {string} projectRoot
189
- * @param {string} templateDir
188
+ * @param {string} projectRoot корінь проєкту-споживача
189
+ * @param {string} templateDir каталог `.claude-template/` усередині пакету `@nitra/cursor`
190
190
  * @returns {Promise<string[]>} масив відносних шляхів записаних файлів
191
191
  */
192
192
  export async function syncClaudeCommands(projectRoot, templateDir) {
@@ -211,11 +211,11 @@ export async function syncClaudeCommands(projectRoot, templateDir) {
211
211
  /**
212
212
  * Виконує повну синхронізацію Claude Code-конфігу з темплейту пакету в проєкт.
213
213
  * Використовується з `bin/n-cursor.js` після інших синків.
214
- * @param {object} options
214
+ * @param {object} options опції синку
215
215
  * @param {string} options.projectRoot корінь проєкту-споживача
216
216
  * @param {string} options.bundledPackageRoot корінь установленого `@nitra/cursor`
217
217
  * @param {boolean} options.enabled чи увімкнено sync (з `.n-cursor.json` `claude-config`)
218
- * @returns {Promise<{ settings: boolean, npmClaudeMd: boolean, commands: string[] }>}
218
+ * @returns {Promise<{ settings: boolean, npmClaudeMd: boolean, commands: string[] }>} прапорці записів settings/CLAUDE.md та список записаних slash-команд
219
219
  */
220
220
  export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enabled }) {
221
221
  if (!enabled) {
@@ -118,7 +118,7 @@ export function parseProgramOrNull(content, virtualPath) {
118
118
  * базовий `parseProgramOrNull` свідомо лишається без коментарів, щоб не змінювати API.
119
119
  * @param {string} content вихідний код
120
120
  * @param {string} virtualPath шлях для вибору `lang` (також для діагностики)
121
- * @returns {{ program: unknown, comments: { type: 'Line' | 'Block', value: string, start: number, end: number }[] } | null}
121
+ * @returns {{ program: unknown, comments: { type: 'Line' | 'Block', value: string, start: number, end: number }[] } | null} `program` + список коментарів, або `null` якщо парсер віддав помилки/exception
122
122
  */
123
123
  export function parseProgramAndCommentsOrNull(content, virtualPath) {
124
124
  const lang = langFromPath(virtualPath || 'scan.ts')
@@ -296,8 +296,7 @@ export function findBunSqlPerRequestConnectionInText(content, virtualPath = 'sca
296
296
  * на тому ж рядку або рядком вище. `sql.unsafe` за замовчуванням заборонено: дозволено
297
297
  * лише коли значення контролюється кодом (не user input) і потрібно підставити те, що
298
298
  * не можна параметризувати — назву таблиці/колонки або dynamic SQL/DDL. У всіх інших
299
- * випадках — переробити на tagged template `sql\`...\${value}...\``.
300
- *
299
+ * випадках — переробити на tagged template виду `sql` із інтерполяцією значень.
301
300
  * Маркер-коментар фіксує причину для ревʼюера й одночасно слугує opt-in: без нього
302
301
  * перевірка падає, навіть якщо у `unsafe` лежить статичний рядок без інтерполяції.
303
302
  * @param {string} content вихідний код
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Аналіз GitHub Actions workflow на правило «depcheck для path-scoped backend-пакета»
3
+ * (див. секцію в `npm/mdc/js-run.mdc`).
4
+ *
5
+ * Алгоритм для одного workspace-пакета (`<rootDir>`):
6
+ * 1. Шукаємо всі workflow, у яких `on.push.paths` або `on.pull_request.paths` містить
7
+ * glob, що починається з `<rootDir>/` — це означає, що workflow обмежено саме цим пакетом
8
+ * (повністю або частково).
9
+ * 2. У кожному такому workflow має бути крок, чий `run` починається з `npx depcheck …`,
10
+ * `working-directory` дорівнює `<rootDir>`, а список `--ignores="…"` містить
11
+ * щонайменше `graphql` і `bun` (інші значення допустимі).
12
+ *
13
+ * Якщо паттерн `paths:` стосується цього пакета, але крок depcheck відсутній / без потрібних
14
+ * ignores / у неправильному working-directory — фіксується порушення.
15
+ *
16
+ * Workflow без `paths:` або з глобальними патернами (`**\/*.js`, `npm/**`) ігноруються —
17
+ * вони не «належать» жодному окремому пакету і виходять за межі правила.
18
+ */
19
+ import { readdir, readFile } from 'node:fs/promises'
20
+ import { join, relative } from 'node:path'
21
+
22
+ import {
23
+ flattenWorkflowSteps,
24
+ getStepRun,
25
+ parseWorkflowYaml
26
+ } from './gha-workflow.mjs'
27
+
28
+ const WORKFLOWS_DIR_REL = '.github/workflows'
29
+ const REQUIRED_IGNORES = ['graphql', 'bun']
30
+ const DEPCHECK_RUN_RE = /(?:^|[\s;&|])npx\s+depcheck\b([^\n]*)/u
31
+ const IGNORES_FLAG_RE = /--ignores\s*=?\s*(?:"([^"]*)"|'([^']*)'|([^\s"']+))/u
32
+
33
+ /**
34
+ * Чи містить workflow.on[event].paths хоча б один patten, що починається з `<pkgRoot>/`.
35
+ * @param {Record<string, unknown>} root корінь workflow
36
+ * @param {string} pkgRoot відносний (POSIX) шлях каталогу пакета (наприклад `cron-jobs/refund-loyalty-points`)
37
+ * @returns {boolean} `true`, якщо знайдено хоча б один підходящий glob
38
+ */
39
+ export function workflowHasPathsScopedToPackage(root, pkgRoot) {
40
+ const prefix = `${pkgRoot.replace(/\\/g, '/').replace(/\/+$/, '')}/`
41
+ const on = root?.on
42
+ if (!on || typeof on !== 'object') return false
43
+ for (const event of /** @type {const} */ (['push', 'pull_request'])) {
44
+ const ev = /** @type {Record<string, unknown>} */ (on)[event]
45
+ if (!ev || typeof ev !== 'object') continue
46
+ const paths = /** @type {Record<string, unknown>} */ (ev).paths
47
+ if (!Array.isArray(paths)) continue
48
+ if (paths.some(p => typeof p === 'string' && p.startsWith(prefix))) return true
49
+ }
50
+ return false
51
+ }
52
+
53
+ /**
54
+ * Розбирає `--ignores="a,b,c"` (також `--ignores=a,b`, single-quotes тощо) з аргументів `npx depcheck`.
55
+ * @param {string} depcheckArgs частина рядка `run` після `npx depcheck`
56
+ * @returns {string[] | null} масив значень ignores або `null`, якщо прапор відсутній
57
+ */
58
+ export function parseDepcheckIgnoresArg(depcheckArgs) {
59
+ const m = IGNORES_FLAG_RE.exec(depcheckArgs)
60
+ if (!m) return null
61
+ const raw = m[1] ?? m[2] ?? m[3] ?? ''
62
+ return raw
63
+ .split(',')
64
+ .map(s => s.trim())
65
+ .filter(s => s.length > 0)
66
+ }
67
+
68
+ /**
69
+ * Шукає `npx depcheck` у `run` кроку. Повертає рядок аргументів після `npx depcheck` або `null`.
70
+ * @param {string} runText значення `run:` (можливо багаторядкове)
71
+ * @returns {string | null} текст аргументів depcheck або `null`
72
+ */
73
+ export function extractDepcheckArgs(runText) {
74
+ if (typeof runText !== 'string' || runText.length === 0) return null
75
+ const m = DEPCHECK_RUN_RE.exec(runText)
76
+ return m ? m[1] : null
77
+ }
78
+
79
+ /**
80
+ * Чи `working-directory` кроку дорівнює очікуваному pkgRoot (з нормалізацією слешів і хвостових `/`).
81
+ * @param {Record<string, unknown>} step об'єкт кроку
82
+ * @param {string} pkgRoot очікуваний шлях
83
+ * @returns {boolean} `true`, якщо збігаються
84
+ */
85
+ export function stepWorkingDirectoryEquals(step, pkgRoot) {
86
+ const wd = step['working-directory']
87
+ if (typeof wd !== 'string') return false
88
+ const norm = wd.replace(/\\/g, '/').replace(/\/+$/, '')
89
+ const expected = pkgRoot.replace(/\\/g, '/').replace(/\/+$/, '')
90
+ return norm === expected
91
+ }
92
+
93
+ /**
94
+ * Перевіряє один workflow на наявність валідного depcheck-кроку для пакета.
95
+ * @param {Record<string, unknown>} root корінь workflow
96
+ * @param {string} pkgRoot відносний шлях пакета
97
+ * @returns {{ kind: 'ok' } | { kind: 'missing' } | { kind: 'wrong-cwd', actual: string } | { kind: 'missing-ignores', missing: string[] }} результат
98
+ */
99
+ export function evaluateDepcheckStepForPackage(root, pkgRoot) {
100
+ /** @type {{ args: string, step: Record<string, unknown> }[]} */
101
+ const depcheckSteps = []
102
+ for (const { step } of flattenWorkflowSteps(root)) {
103
+ const args = extractDepcheckArgs(getStepRun(step))
104
+ if (args !== null) depcheckSteps.push({ args, step })
105
+ }
106
+ if (depcheckSteps.length === 0) return { kind: 'missing' }
107
+
108
+ // Серед усіх знайдених depcheck-кроків шукаємо хоча б один, що відповідає пакету.
109
+ const stepsForThisPackage = depcheckSteps.filter(s => stepWorkingDirectoryEquals(s.step, pkgRoot))
110
+ if (stepsForThisPackage.length === 0) {
111
+ const actual = depcheckSteps
112
+ .map(s => /** @type {string} */ (s.step['working-directory'] ?? '<repo root>'))
113
+ .join(', ')
114
+ return { kind: 'wrong-cwd', actual }
115
+ }
116
+
117
+ for (const { args } of stepsForThisPackage) {
118
+ const ignores = parseDepcheckIgnoresArg(args) ?? []
119
+ const missing = REQUIRED_IGNORES.filter(req => !ignores.includes(req))
120
+ if (missing.length === 0) return { kind: 'ok' }
121
+ }
122
+ // Усі знайдені кроки існують, але жоден не має повного списку обов'язкових ignores —
123
+ // повертаємо missing з першого, щоб дати конкретний фідбек.
124
+ const firstMissing = REQUIRED_IGNORES.filter(
125
+ req => !((parseDepcheckIgnoresArg(stepsForThisPackage[0].args) ?? []).includes(req))
126
+ )
127
+ return { kind: 'missing-ignores', missing: firstMissing }
128
+ }
129
+
130
+ /**
131
+ * Зчитує всі `.github/workflows/*.yml` (без `*.yaml` — за правилом n-ga) з коренем у `repoRoot`.
132
+ * @param {string} repoRoot абсолютний корінь репозиторію
133
+ * @returns {Promise<{ relPath: string, content: string }[]>} список workflow-файлів
134
+ */
135
+ export async function readAllWorkflowFiles(repoRoot) {
136
+ const dir = join(repoRoot, WORKFLOWS_DIR_REL)
137
+ /** @type {{ relPath: string, content: string }[]} */
138
+ const out = []
139
+ let entries
140
+ try {
141
+ entries = await readdir(dir, { withFileTypes: true })
142
+ } catch {
143
+ return out
144
+ }
145
+ for (const ent of entries) {
146
+ if (!ent.isFile() || !ent.name.endsWith('.yml')) continue
147
+ const abs = join(dir, ent.name)
148
+ const content = await readFile(abs, 'utf8')
149
+ out.push({ relPath: relative(repoRoot, abs).split('\\').join('/'), content })
150
+ }
151
+ return out
152
+ }
153
+
154
+ /**
155
+ * Знаходить порушення правила depcheck для конкретного workspace-пакета.
156
+ *
157
+ * Повертає список повідомлень про порушення (порожній — все ok). Для кожного workflow,
158
+ * чий `paths:` обмежено до цього пакета, перевіряє, що серед кроків є валідний `npx depcheck`
159
+ * з потрібним `working-directory` та `--ignores`.
160
+ * @param {{ relPath: string, content: string }[]} workflows список workflow-файлів (з `readAllWorkflowFiles`)
161
+ * @param {string} pkgRoot відносний шлях workspace-пакета
162
+ * @returns {string[]} повідомлення про порушення, по одному на workflow
163
+ */
164
+ export function findDepcheckViolationsForPackage(workflows, pkgRoot) {
165
+ /** @type {string[]} */
166
+ const violations = []
167
+ for (const { relPath, content } of workflows) {
168
+ const root = parseWorkflowYaml(content)
169
+ if (!root) continue
170
+ if (!workflowHasPathsScopedToPackage(root, pkgRoot)) continue
171
+ const result = evaluateDepcheckStepForPackage(root, pkgRoot)
172
+ if (result.kind === 'ok') continue
173
+ if (result.kind === 'missing') {
174
+ violations.push(
175
+ `${relPath}: paths обмежено до '${pkgRoot}/**', але немає кроку 'npx depcheck --ignores="graphql,bun"' з working-directory: ${pkgRoot}`
176
+ )
177
+ } else if (result.kind === 'wrong-cwd') {
178
+ violations.push(
179
+ `${relPath}: 'npx depcheck' знайдено, але working-directory не дорівнює '${pkgRoot}' (фактично: ${result.actual})`
180
+ )
181
+ } else {
182
+ violations.push(
183
+ `${relPath}: 'npx depcheck' у '${pkgRoot}' має містити --ignores з '${result.missing.join(',')}' (мінімум: graphql,bun)`
184
+ )
185
+ }
186
+ }
187
+ return violations
188
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Спільна утиліта для check-скриптів: збирає всі `package.json` у дереві (крім пропущених
3
+ * каталогів у `walkDir`), сортує за відносним шляхом. Винесена з check-js-bun-db / check-js-mssql,
4
+ * щоб уникнути дублювання (jscpd).
5
+ */
6
+ import { relative, sep } from 'node:path'
7
+
8
+ import { walkDir } from './walkDir.mjs'
9
+
10
+ /**
11
+ * Знаходить всі `package.json` у репозиторії (крім пропущених директорій у walkDir).
12
+ * @param {string} repoRoot абсолютний шлях до кореня репозиторію
13
+ * @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
14
+ * @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
15
+ */
16
+ export async function findAllPackageJsonPaths(repoRoot, ignorePaths) {
17
+ /** @type {string[]} */
18
+ const paths = []
19
+ await walkDir(
20
+ repoRoot,
21
+ absPath => {
22
+ if (absPath.endsWith(`${sep}package.json`)) {
23
+ paths.push(absPath)
24
+ }
25
+ },
26
+ ignorePaths
27
+ )
28
+ paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
29
+ return paths
30
+ }
@@ -20,7 +20,9 @@ const CONFIG_FILE = '.n-cursor.json'
20
20
  function toAbsPosix(root, p) {
21
21
  const trimmed = String(p).trim()
22
22
  const abs = isAbsolute(trimmed) ? trimmed : resolve(root, trimmed)
23
- return abs.split(sep).join('/').replace(/\/+$/, '')
23
+ let posix = abs.split(sep).join('/')
24
+ while (posix.endsWith('/')) posix = posix.slice(0, -1)
25
+ return posix
24
26
  }
25
27
 
26
28
  /**
@@ -1,17 +1,7 @@
1
1
  {
2
2
  "$schema": "./node_modules/oxlint/configuration_schema.json",
3
- "plugins": [
4
- "unicorn",
5
- "oxc",
6
- "import",
7
- "jsdoc",
8
- "promise",
9
- "node",
10
- "vue"
11
- ],
12
- "jsPlugins": [
13
- "@e18e/eslint-plugin"
14
- ],
3
+ "plugins": ["unicorn", "oxc", "import", "jsdoc", "promise", "node", "vue"],
4
+ "jsPlugins": ["@e18e/eslint-plugin"],
15
5
  "categories": {},
16
6
  "rules": {
17
7
  "e18e/prefer-includes": "error",
@@ -393,8 +383,5 @@
393
383
  "builtin": true
394
384
  },
395
385
  "globals": {},
396
- "ignorePatterns": [
397
- "**/schema.graphql",
398
- "**/auto-imports.d.ts"
399
- ]
386
+ "ignorePatterns": ["**/schema.graphql", "**/auto-imports.d.ts"]
400
387
  }
@@ -17,7 +17,9 @@ import { isAbsolute, join, resolve, sep } from 'node:path'
17
17
  */
18
18
  function toAbsPosix(p) {
19
19
  const abs = isAbsolute(p) ? p : resolve(p)
20
- return abs.split(sep).join('/').replace(/\/+$/, '')
20
+ let posix = abs.split(sep).join('/')
21
+ while (posix.endsWith('/')) posix = posix.slice(0, -1)
22
+ return posix
21
23
  }
22
24
 
23
25
  /**
@@ -25,7 +27,7 @@ function toAbsPosix(p) {
25
27
  * Часткові збіги басенейму не враховуються (postgres-master-test ≠ postgres-master).
26
28
  * @param {string} dirAbsPosix абсолютний posix-шлях каталогу
27
29
  * @param {string[]} ignorePosix вже нормалізовані ignore-шляхи
28
- * @returns {boolean}
30
+ * @returns {boolean} `true`, якщо шлях слід пропустити (точний збіг або префікс з `/`)
29
31
  */
30
32
  function isIgnoredDir(dirAbsPosix, ignorePosix) {
31
33
  for (const ig of ignorePosix) {
@@ -39,20 +41,20 @@ function isIgnoredDir(dirAbsPosix, ignorePosix) {
39
41
  * Рекурсивно обходить каталог, пропускає типові артефакти збірки/залежностей та `ignorePaths`.
40
42
  * @param {string} dir абсолютний шлях
41
43
  * @param {(filePath: string) => void} onFile виклик для кожного файлу
42
- * @param {string[]} [ignorePaths=[]] шляхи каталогів (відносні від cwd або абсолютні), що повністю виключаються з обходу
43
- * @returns {Promise<void>}
44
+ * @param {string[]} [ignorePaths] шляхи каталогів (відносні від cwd або абсолютні), що повністю виключаються з обходу
45
+ * @returns {Promise<void>} резолвиться по завершенню обходу
44
46
  */
45
47
  export async function walkDir(dir, onFile, ignorePaths = []) {
46
- const ignorePosix = ignorePaths.map(toAbsPosix)
48
+ const ignorePosix = ignorePaths.map(p => toAbsPosix(p))
47
49
  await walkDirInner(dir, onFile, ignorePosix)
48
50
  }
49
51
 
50
52
  /**
51
53
  * Внутрішній рекурсор. ignorePosix вже нормалізовано — не нормалізуємо повторно на кожному рівні.
52
- * @param {string} dir
53
- * @param {(filePath: string) => void} onFile
54
- * @param {string[]} ignorePosix
55
- * @returns {Promise<void>}
54
+ * @param {string} dir абсолютний шлях каталогу для обходу
55
+ * @param {(filePath: string) => void} onFile колбек, що викликається для кожного звичайного файлу
56
+ * @param {string[]} ignorePosix вже нормалізовані абсолютні posix-шляхи ігнорованих каталогів
57
+ * @returns {Promise<void>} резолвиться по завершенню рекурсії
56
58
  */
57
59
  async function walkDirInner(dir, onFile, ignorePosix) {
58
60
  if (ignorePosix.length > 0 && isIgnoredDir(toAbsPosix(dir), ignorePosix)) return