@nitra/cursor 1.8.180 → 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 +37 -5
- package/mdc/ga.mdc +23 -1
- package/mdc/js-run.mdc +21 -1
- package/package.json +4 -4
- package/scripts/check-abie.mjs +1 -0
- package/scripts/check-changelog.mjs +49 -50
- package/scripts/check-ga.mjs +69 -8
- 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 +37 -3
- package/scripts/check-k8s.mjs +32 -31
- package/scripts/check-vue.mjs +17 -10
- package/scripts/claude-stop-hook.mjs +1 -0
- package/scripts/lint-ga.mjs +0 -1
- package/scripts/run-docker.mjs +1 -0
- package/scripts/run-k8s.mjs +1 -0
- 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 +4 -2
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { existsSync } from 'node:fs'
|
|
25
25
|
import { readFile } from 'node:fs/promises'
|
|
26
|
-
import { join, relative
|
|
26
|
+
import { join, relative } from 'node:path'
|
|
27
27
|
|
|
28
28
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
29
29
|
import {
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
isBunSqlScanSourceFile,
|
|
36
36
|
textHasBunSqlImport
|
|
37
37
|
} from './utils/bun-sql-scan.mjs'
|
|
38
|
+
import { findAllPackageJsonPaths } from './utils/find-package-json-paths.mjs'
|
|
38
39
|
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
39
40
|
import { walkDir } from './utils/walkDir.mjs'
|
|
40
41
|
|
|
@@ -50,30 +51,10 @@ function asObject(v) {
|
|
|
50
51
|
return /** @type {Record<string, unknown>} */ (v)
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
/**
|
|
54
|
-
* Знаходить всі `package.json` у репозиторії (крім пропущених директорій у walkDir).
|
|
55
|
-
* @param {string} repoRoot абсолютний шлях до кореня репозиторію
|
|
56
|
-
* @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
|
|
57
|
-
*/
|
|
58
|
-
async function findAllPackageJsonPaths(repoRoot, ignorePaths) {
|
|
59
|
-
/** @type {string[]} */
|
|
60
|
-
const paths = []
|
|
61
|
-
await walkDir(
|
|
62
|
-
repoRoot,
|
|
63
|
-
absPath => {
|
|
64
|
-
if (absPath.endsWith(`${sep}package.json`)) {
|
|
65
|
-
paths.push(absPath)
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
ignorePaths
|
|
69
|
-
)
|
|
70
|
-
paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
|
|
71
|
-
return paths
|
|
72
|
-
}
|
|
73
|
-
|
|
74
54
|
/**
|
|
75
55
|
* Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану Bun SQL патернів.
|
|
76
56
|
* @param {string} repoRoot абсолютний шлях до кореня репозиторію
|
|
57
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
77
58
|
* @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
|
|
78
59
|
*/
|
|
79
60
|
async function findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths) {
|
|
@@ -145,7 +145,9 @@ function compareOxlintIgnorePatterns(expected, actual, failures) {
|
|
|
145
145
|
return
|
|
146
146
|
}
|
|
147
147
|
if (!Array.isArray(actual)) {
|
|
148
|
-
failures.push(
|
|
148
|
+
failures.push(
|
|
149
|
+
'.oxlintrc.json: поле "ignorePatterns" має бути масивом (канон задає мінімум, додаткові патерни дозволені)'
|
|
150
|
+
)
|
|
149
151
|
return
|
|
150
152
|
}
|
|
151
153
|
const set = new Set(actual)
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { existsSync } from 'node:fs'
|
|
12
12
|
import { readFile } from 'node:fs/promises'
|
|
13
|
-
import { join, relative
|
|
13
|
+
import { join, relative } from 'node:path'
|
|
14
14
|
|
|
15
15
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
16
|
+
import { findAllPackageJsonPaths } from './utils/find-package-json-paths.mjs'
|
|
16
17
|
import {
|
|
17
18
|
findMssqlPerRequestConnectionInText,
|
|
18
19
|
findSharedMssqlRequestInText,
|
|
@@ -31,30 +32,10 @@ const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)/u
|
|
|
31
32
|
/** Мінімальна дозволена версія mssql (js-mssql.mdc). */
|
|
32
33
|
const MIN_MSSQL_VERSION = { major: 12, minor: 5, patch: 0 }
|
|
33
34
|
|
|
34
|
-
/**
|
|
35
|
-
* Знаходить всі `package.json` у репозиторії (крім пропущених директорій у walkDir).
|
|
36
|
-
* @param {string} repoRoot абсолютний шлях до кореня репозиторію
|
|
37
|
-
* @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
|
|
38
|
-
*/
|
|
39
|
-
async function findAllPackageJsonPaths(repoRoot, ignorePaths) {
|
|
40
|
-
/** @type {string[]} */
|
|
41
|
-
const paths = []
|
|
42
|
-
await walkDir(
|
|
43
|
-
repoRoot,
|
|
44
|
-
absPath => {
|
|
45
|
-
if (absPath.endsWith(`${sep}package.json`)) {
|
|
46
|
-
paths.push(absPath)
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
ignorePaths
|
|
50
|
-
)
|
|
51
|
-
paths.sort((a, b) => relative(repoRoot, a).localeCompare(relative(repoRoot, b)))
|
|
52
|
-
return paths
|
|
53
|
-
}
|
|
54
|
-
|
|
55
35
|
/**
|
|
56
36
|
* Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану mssql.
|
|
57
37
|
* @param {string} repoRoot абсолютний шлях до кореня репозиторію
|
|
38
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
58
39
|
* @returns {Promise<string[]>} абсолютні шляхи, відсортовані за відносним шляхом
|
|
59
40
|
*/
|
|
60
41
|
async function findAllSourcePathsForMssqlScan(repoRoot, ignorePaths) {
|
|
@@ -258,9 +239,10 @@ function reportZeroMssqlSourceViolations(counters, pass) {
|
|
|
258
239
|
/**
|
|
259
240
|
* Аудит усіх JS/TS-джерел репо щодо безпечного використання mssql.
|
|
260
241
|
* @param {string} repoRoot корінь репозиторію
|
|
242
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
261
243
|
* @param {(msg: string) => void} pass pass callback
|
|
262
244
|
* @param {(msg: string) => void} fail fail callback
|
|
263
|
-
* @returns {Promise<void>}
|
|
245
|
+
* @returns {Promise<void>} резолвиться по завершенню аудиту всіх знайдених джерел
|
|
264
246
|
*/
|
|
265
247
|
async function auditMssqlSources(repoRoot, ignorePaths, pass, fail) {
|
|
266
248
|
const sourcePaths = await findAllSourcePathsForMssqlScan(repoRoot, ignorePaths)
|
package/scripts/check-js-run.mjs
CHANGED
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
* `node:process` (для опційних). Коли `env` імпортовано з `@nitra/check-env`,
|
|
18
18
|
* кожен `env.X` має бути закритий літеральним викликом `checkEnv(['X', ...])`
|
|
19
19
|
* у тому ж файлі або коментарем `// \@nitra/cursor ignore-next-line checkEnv`
|
|
20
|
-
* на попередньому рядку (див. `utils/check-env-scan.mjs`)
|
|
20
|
+
* на попередньому рядку (див. `utils/check-env-scan.mjs`);
|
|
21
|
+
* - «depcheck у GitHub Actions з path-фільтром»: для кожного workflow з `paths:`,
|
|
22
|
+
* обмеженим каталогом цього пакета (`<rootDir>/...`), має бути крок
|
|
23
|
+
* `npx depcheck --ignores="graphql,bun"` (плюс інші, за потреби) з
|
|
24
|
+
* `working-directory: <rootDir>` (див. `utils/depcheck-workflow.mjs`).
|
|
21
25
|
*/
|
|
22
26
|
import { existsSync } from 'node:fs'
|
|
23
27
|
import { readFile } from 'node:fs/promises'
|
|
@@ -30,6 +34,7 @@ import {
|
|
|
30
34
|
} from './utils/bunyan-imports.mjs'
|
|
31
35
|
import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from './utils/check-env-scan.mjs'
|
|
32
36
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
37
|
+
import { findDepcheckViolationsForPackage, readAllWorkflowFiles } from './utils/depcheck-workflow.mjs'
|
|
33
38
|
import {
|
|
34
39
|
findConnFactoryImportsInText,
|
|
35
40
|
isConnImportsScanSourceFile,
|
|
@@ -161,11 +166,12 @@ async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
|
|
|
161
166
|
* Перевіряє відповідність правилам js-run.mdc для одного workspace-пакета.
|
|
162
167
|
* @param {string} rootDir відносний шлях workspace (не `'.'`)
|
|
163
168
|
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
169
|
+
* @param {{ relPath: string, content: string }[]} workflows кешований список workflow-файлів репо
|
|
164
170
|
* @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
|
|
165
171
|
* @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
|
|
166
172
|
* @returns {Promise<void>} завершується після перевірок цього пакета
|
|
167
173
|
*/
|
|
168
|
-
async function checkWorkspacePackage(rootDir, ignorePaths, fail, passFn) {
|
|
174
|
+
async function checkWorkspacePackage(rootDir, ignorePaths, workflows, fail, passFn) {
|
|
169
175
|
const label = `[${rootDir}] `
|
|
170
176
|
const absPackageRoot = join(process.cwd(), rootDir)
|
|
171
177
|
const pkgJson = await loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail)
|
|
@@ -200,6 +206,33 @@ async function checkWorkspacePackage(rootDir, ignorePaths, fail, passFn) {
|
|
|
200
206
|
}
|
|
201
207
|
|
|
202
208
|
await checkOtelConfigmap(rootDir, label, fail, passFn)
|
|
209
|
+
|
|
210
|
+
checkDepcheckInWorkflows(rootDir, workflows, label, fail, passFn)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Перевіряє правило «depcheck у workflow» для одного пакета.
|
|
215
|
+
*
|
|
216
|
+
* Для кожного `.github/workflows/*.yml`, чий `paths:` обмежено до `<rootDir>/...`,
|
|
217
|
+
* має бути крок `npx depcheck --ignores="graphql,bun"` з `working-directory: <rootDir>`.
|
|
218
|
+
* Якщо в репо немає каталогу `.github/workflows`, перевірка no-op.
|
|
219
|
+
* @param {string} rootDir відносний шлях workspace-пакета
|
|
220
|
+
* @param {{ relPath: string, content: string }[]} workflows кешований список workflow-файлів
|
|
221
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
222
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
223
|
+
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
224
|
+
* @returns {void}
|
|
225
|
+
*/
|
|
226
|
+
function checkDepcheckInWorkflows(rootDir, workflows, label, fail, passFn) {
|
|
227
|
+
if (workflows.length === 0) return
|
|
228
|
+
const violations = findDepcheckViolationsForPackage(workflows, rootDir.replace(/\\/g, '/'))
|
|
229
|
+
if (violations.length === 0) {
|
|
230
|
+
passFn(`${label}depcheck у path-scoped workflow налаштовано (або відсутній path-scoped workflow для пакета)`)
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
for (const v of violations) {
|
|
234
|
+
fail(`${label}${v}`)
|
|
235
|
+
}
|
|
203
236
|
}
|
|
204
237
|
|
|
205
238
|
/**
|
|
@@ -281,8 +314,9 @@ export async function check() {
|
|
|
281
314
|
}
|
|
282
315
|
|
|
283
316
|
const ignorePaths = await loadCursorIgnorePaths(process.cwd())
|
|
317
|
+
const workflows = await readAllWorkflowFiles(process.cwd())
|
|
284
318
|
for (const r of workspaceRoots) {
|
|
285
|
-
await checkWorkspacePackage(r, ignorePaths, fail, pass)
|
|
319
|
+
await checkWorkspacePackage(r, ignorePaths, workflows, fail, pass)
|
|
286
320
|
}
|
|
287
321
|
|
|
288
322
|
return reporter.getExitCode()
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -1339,43 +1339,44 @@ function failIfExplicitPatchTargetsNotInCatalog(rel, first, catalog, fail) {
|
|
|
1339
1339
|
* @returns {void}
|
|
1340
1340
|
*/
|
|
1341
1341
|
function failIfExplicitPatchTargetsHaveRedundantGroupVersion(rel, first, catalog, fail) {
|
|
1342
|
-
for (const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
}
|
|
1346
|
-
const t = /** @type {Record<string, unknown>} */ (target)
|
|
1347
|
-
const kind = typeof t.kind === 'string' ? t.kind.trim() : ''
|
|
1348
|
-
const name = typeof t.name === 'string' ? t.name.trim() : ''
|
|
1349
|
-
if (kind === '' || name === '') {
|
|
1350
|
-
continue
|
|
1351
|
-
}
|
|
1352
|
-
if (patchTargetUsesSelector(t)) {
|
|
1353
|
-
continue
|
|
1354
|
-
}
|
|
1355
|
-
const tgtGroup = typeof t.group === 'string' ? t.group.trim() : ''
|
|
1356
|
-
const tgtVersion = typeof t.version === 'string' ? t.version.trim() : ''
|
|
1357
|
-
if (tgtGroup === '' && tgtVersion === '') {
|
|
1358
|
-
continue
|
|
1359
|
-
}
|
|
1360
|
-
const matchingByKindName = catalog.filter(r => r.kind === kind && r.name === name)
|
|
1361
|
-
const distinctGvk = new Set(matchingByKindName.map(r => `${r.group}/${r.version}`))
|
|
1362
|
-
if (distinctGvk.size > 1) {
|
|
1363
|
-
continue
|
|
1364
|
-
}
|
|
1365
|
-
/** @type {string[]} */
|
|
1366
|
-
const redundant = []
|
|
1367
|
-
if (tgtGroup !== '') {
|
|
1368
|
-
redundant.push('group')
|
|
1369
|
-
}
|
|
1370
|
-
if (tgtVersion !== '') {
|
|
1371
|
-
redundant.push('version')
|
|
1372
|
-
}
|
|
1342
|
+
for (const entry of extractExplicitPatchTargetsFromKustomization(first)) {
|
|
1343
|
+
const violation = describePatchTargetRedundancy(entry, catalog)
|
|
1344
|
+
if (violation === null) continue
|
|
1345
|
+
const { section, index, kind, name, redundant } = violation
|
|
1373
1346
|
fail(
|
|
1374
1347
|
`${rel}: ${section}[${index}].target — прибери зайві поля ${redundant.join(', ')}; для kind=${kind}, name=${name} в інвентарі немає колізії між різними API-групами/версіями (див. k8s.mdc «patches[].target: лише kind і name»)`
|
|
1375
1348
|
)
|
|
1376
1349
|
}
|
|
1377
1350
|
}
|
|
1378
1351
|
|
|
1352
|
+
/**
|
|
1353
|
+
* Аналізує один patch.target: повертає опис надлишкових полів `group`/`version`,
|
|
1354
|
+
* якщо в інвентарі для пари (kind, name) немає колізії GVK; інакше `null`.
|
|
1355
|
+
* @param {{ section: string, index: number, target: unknown }} entry елемент із `extractExplicitPatchTargetsFromKustomization`
|
|
1356
|
+
* @param {KustomizeResourceDescriptor[]} catalog інвентар resources/bases/…
|
|
1357
|
+
* @returns {{ section: string, index: number, kind: string, name: string, redundant: string[] } | null} опис порушення або `null`
|
|
1358
|
+
*/
|
|
1359
|
+
function describePatchTargetRedundancy(entry, catalog) {
|
|
1360
|
+
const { section, index, target } = entry
|
|
1361
|
+
if (target === null || typeof target !== 'object' || Array.isArray(target)) return null
|
|
1362
|
+
const t = /** @type {Record<string, unknown>} */ (target)
|
|
1363
|
+
const kind = typeof t.kind === 'string' ? t.kind.trim() : ''
|
|
1364
|
+
const name = typeof t.name === 'string' ? t.name.trim() : ''
|
|
1365
|
+
if (kind === '' || name === '') return null
|
|
1366
|
+
if (patchTargetUsesSelector(t)) return null
|
|
1367
|
+
const tgtGroup = typeof t.group === 'string' ? t.group.trim() : ''
|
|
1368
|
+
const tgtVersion = typeof t.version === 'string' ? t.version.trim() : ''
|
|
1369
|
+
if (tgtGroup === '' && tgtVersion === '') return null
|
|
1370
|
+
const matchingByKindName = catalog.filter(r => r.kind === kind && r.name === name)
|
|
1371
|
+
const distinctGvk = new Set(matchingByKindName.map(r => `${r.group}/${r.version}`))
|
|
1372
|
+
if (distinctGvk.size > 1) return null
|
|
1373
|
+
/** @type {string[]} */
|
|
1374
|
+
const redundant = []
|
|
1375
|
+
if (tgtGroup !== '') redundant.push('group')
|
|
1376
|
+
if (tgtVersion !== '') redundant.push('version')
|
|
1377
|
+
return { section, index, kind, name, redundant }
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1379
1380
|
/**
|
|
1380
1381
|
* Документи з YAML-файлу мають мати дескриптор у **catalog** (інвентар resources).
|
|
1381
1382
|
* @param {string} rel відносний шлях до kustomization.yaml
|
package/scripts/check-vue.mjs
CHANGED
|
@@ -115,6 +115,7 @@ async function collectEsbuildMatchesInFiles(absPackageRoot, files, maxMatches) {
|
|
|
115
115
|
* Сканує дерево пакета на згадки `esbuild` і підказує заміну на `rolldown`.
|
|
116
116
|
* @param {string} rootDir відносний шлях до пакета
|
|
117
117
|
* @param {string} absPackageRoot абсолютний шлях до кореня пакета
|
|
118
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
118
119
|
* @param {string} prefix параметр prefix
|
|
119
120
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
120
121
|
* @param {(msg: string) => void} fail callback при помилці
|
|
@@ -342,9 +343,7 @@ async function checkVueNodeImportViolations(rootDir, absPackageRoot, ignorePaths
|
|
|
342
343
|
}
|
|
343
344
|
}
|
|
344
345
|
if (nodeImportViolations === 0) {
|
|
345
|
-
passFn(
|
|
346
|
-
`${prefix}немає імпортів Node-нативних модулів у .vue (проскановано ${ukFilesCountPhrase(vuePaths.length)})`
|
|
347
|
-
)
|
|
346
|
+
passFn(`${prefix}немає імпортів Node-нативних модулів у .vue (проскановано ${ukFilesCountPhrase(vuePaths.length)})`)
|
|
348
347
|
}
|
|
349
348
|
}
|
|
350
349
|
|
|
@@ -354,18 +353,17 @@ async function checkVueNodeImportViolations(rootDir, absPackageRoot, ignorePaths
|
|
|
354
353
|
* Якщо `unplugin-auto-import` не сконфігурований на `'vue'` у `vite.config`, явні value-імпорти
|
|
355
354
|
* формально не заборонені — їх видалення зламає код. У цьому випадку перевірка пропускається,
|
|
356
355
|
* а fail про відсутній `'vue'` у `AutoImport.imports` уже зареєстровано в `checkViteConfig`.
|
|
357
|
-
* @param {string} rootDir
|
|
358
|
-
* @param {string} absPackageRoot
|
|
359
|
-
* @param {string}
|
|
356
|
+
* @param {string} rootDir відносний шлях до пакета
|
|
357
|
+
* @param {string} absPackageRoot абсолютний шлях до кореня пакета
|
|
358
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
360
359
|
* @param {boolean} hasVueAutoImport чи `AutoImport({ imports: [..., 'vue', ...] })` сконфігуровано
|
|
360
|
+
* @param {string} prefix префікс повідомлення `[<pkg>] `
|
|
361
361
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
362
362
|
* @param {(msg: string) => void} fail callback при помилці
|
|
363
363
|
*/
|
|
364
364
|
async function checkVueImportViolations(rootDir, absPackageRoot, ignorePaths, hasVueAutoImport, prefix, passFn, fail) {
|
|
365
365
|
if (!hasVueAutoImport) {
|
|
366
|
-
passFn(
|
|
367
|
-
`${prefix}value-імпорти з 'vue' не заборонені — спершу додай 'vue' до AutoImport.imports у vite.config`
|
|
368
|
-
)
|
|
366
|
+
passFn(`${prefix}value-імпорти з 'vue' не заборонені — спершу додай 'vue' до AutoImport.imports у vite.config`)
|
|
369
367
|
return
|
|
370
368
|
}
|
|
371
369
|
/** @type {string[]} */
|
|
@@ -400,6 +398,7 @@ async function checkVueImportViolations(rootDir, absPackageRoot, ignorePaths, ha
|
|
|
400
398
|
/**
|
|
401
399
|
* Перевіряє залежності та vite.config одного Vue-пакета.
|
|
402
400
|
* @param {string} rootDir відносний шлях до пакета
|
|
401
|
+
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
403
402
|
* @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
|
|
404
403
|
* @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
|
|
405
404
|
* @returns {Promise<void>} завершується після перевірок залежностей, `vite.config` і сканування джерел на імпорти з `vue`
|
|
@@ -444,7 +443,15 @@ async function checkVuePackage(rootDir, ignorePaths, fail, passFn) {
|
|
|
444
443
|
)
|
|
445
444
|
|
|
446
445
|
const { hasVueAutoImport } = await checkViteConfig(rootDir, prefix, passFn, fail)
|
|
447
|
-
await checkVueImportViolations(
|
|
446
|
+
await checkVueImportViolations(
|
|
447
|
+
rootDir,
|
|
448
|
+
join(process.cwd(), rootDir),
|
|
449
|
+
ignorePaths,
|
|
450
|
+
hasVueAutoImport,
|
|
451
|
+
prefix,
|
|
452
|
+
passFn,
|
|
453
|
+
fail
|
|
454
|
+
)
|
|
448
455
|
await checkVueNodeImportViolations(rootDir, join(process.cwd(), rootDir), ignorePaths, prefix, passFn, fail)
|
|
449
456
|
await checkEsbuildMentions(rootDir, join(process.cwd(), rootDir), ignorePaths, prefix, passFn, fail)
|
|
450
457
|
}
|
|
@@ -62,6 +62,7 @@ export async function runStopHookCli() {
|
|
|
62
62
|
if (isRecursiveStopHookCall(stdin)) {
|
|
63
63
|
return 0
|
|
64
64
|
}
|
|
65
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path -- npx як стандартне dev-середовище через PATH; альтернативи (хардкод шляху) непортативні
|
|
65
66
|
const child = spawn('npx', ['--no', '@nitra/cursor', 'check'], { stdio: 'inherit' })
|
|
66
67
|
try {
|
|
67
68
|
const [code] = await once(child, 'exit')
|
package/scripts/lint-ga.mjs
CHANGED
|
@@ -18,7 +18,6 @@ import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Опис залежності preflight-ом: бінарник, для чого потрібен, і команди встановлення.
|
|
21
|
-
*
|
|
22
21
|
* @typedef {object} PreflightDep
|
|
23
22
|
* @property {string} bin ім'я виконуваного файлу (на Windows додається `.exe` за потреби)
|
|
24
23
|
* @property {string[]} winBins альтернативні імена на Windows (`shellcheck.exe`); якщо нема — fallback на `bin`
|
package/scripts/run-docker.mjs
CHANGED
|
@@ -29,6 +29,7 @@ export function isLintDockerfileName(name) {
|
|
|
29
29
|
/**
|
|
30
30
|
* Збирає абсолютні шляхи для lint-docker.
|
|
31
31
|
* @param {string} root корінь репозиторію
|
|
32
|
+
* @param {string[]} [ignorePaths] абсолютні шляхи каталогів, повністю виключених з обходу
|
|
32
33
|
* @returns {Promise<string[]>} відсортовані абсолютні шляхи
|
|
33
34
|
*/
|
|
34
35
|
export async function findLintDockerfilePaths(root, ignorePaths = []) {
|
package/scripts/run-k8s.mjs
CHANGED
|
@@ -59,6 +59,7 @@ export function k8sRootFromFile(absFile) {
|
|
|
59
59
|
/**
|
|
60
60
|
* Унікальні корені `k8s` за наявності `*.yaml` під деревом cwd.
|
|
61
61
|
* @param {string} root корінь репозиторію
|
|
62
|
+
* @param {string[]} [ignorePaths] абсолютні шляхи каталогів, повністю виключених з обходу
|
|
62
63
|
* @returns {Promise<string[]>} відсортовані абсолютні шляхи до каталогів `k8s`
|
|
63
64
|
*/
|
|
64
65
|
export async function findK8sRoots(root, ignorePaths = []) {
|
|
@@ -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
|
}
|