@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.
- package/CHANGELOG.md +44 -5
- package/mdc/ga.mdc +23 -1
- package/mdc/js-run.mdc +35 -1
- package/package.json +4 -4
- package/scripts/check-abie.mjs +1 -0
- package/scripts/check-changelog.mjs +49 -50
- package/scripts/check-docker.mjs +1 -1
- package/scripts/check-ga.mjs +69 -8
- package/scripts/check-graphql.mjs +1 -0
- package/scripts/check-hasura.mjs +1 -0
- package/scripts/check-image.mjs +11 -7
- package/scripts/check-js-bun-db.mjs +3 -22
- package/scripts/check-js-lint.mjs +3 -1
- package/scripts/check-js-mssql.mjs +5 -23
- package/scripts/check-js-run.mjs +63 -3
- package/scripts/check-k8s.mjs +33 -32
- package/scripts/check-nginx-default-tpl.mjs +3 -0
- package/scripts/check-npm-module.mjs +1 -0
- package/scripts/check-vue.mjs +17 -10
- package/scripts/claude-stop-hook.mjs +24 -21
- package/scripts/lint-ga.mjs +0 -1
- package/scripts/rename-yaml-extensions.mjs +1 -0
- package/scripts/run-docker.mjs +1 -0
- package/scripts/run-k8s.mjs +1 -0
- package/scripts/sync-claude-config.mjs +28 -28
- package/scripts/utils/ast-scan-utils.mjs +1 -1
- package/scripts/utils/bun-sql-scan.mjs +1 -2
- package/scripts/utils/depcheck-workflow.mjs +188 -0
- package/scripts/utils/find-package-json-paths.mjs +30 -0
- package/scripts/utils/load-cursor-config.mjs +3 -1
- package/scripts/utils/oxlint-canonical.json +3 -16
- package/scripts/utils/walkDir.mjs +11 -9
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|