@nitra/cursor 1.8.207 → 1.8.209
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 +91 -0
- package/mdc/js-run.mdc +49 -2
- package/package.json +1 -1
- package/policy/abie/health_check_policy/health_check_policy.rego +5 -1
- package/policy/abie/http_route_base/http_route_base.rego +2 -1
- package/policy/hasura/svc_hl/svc_hl.rego +2 -1
- package/policy/k8s/manifest/manifest.rego +2 -0
- package/scripts/check-adr.mjs +10 -88
- package/scripts/check-ga.mjs +14 -192
- package/scripts/check-js-lint.mjs +14 -115
- package/scripts/check-js-run.mjs +81 -83
- package/scripts/check-npm-module.mjs +17 -155
- package/scripts/utils/conn-file-rules.mjs +170 -0
|
@@ -16,7 +16,6 @@ import { readFile } from 'node:fs/promises'
|
|
|
16
16
|
import { dirname, join } from 'node:path'
|
|
17
17
|
import { fileURLToPath } from 'node:url'
|
|
18
18
|
|
|
19
|
-
import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './utils/gha-workflow.mjs'
|
|
20
19
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
21
20
|
|
|
22
21
|
/** Шлях до канонічного oxlint JSON у цьому пакеті (для перевірки та тестів). */
|
|
@@ -34,7 +33,6 @@ export const REQUIRED_VSCODE_EXTENSIONS = ['dbaeumer.vscode-eslint', 'github.vsc
|
|
|
34
33
|
|
|
35
34
|
const WHITESPACE_RE = /\s+/gu
|
|
36
35
|
const NON_DIGITS_RE = /\D+/u
|
|
37
|
-
const OXLINT_FIX_RE = /bunx\s+oxlint[^\n]*--fix/u
|
|
38
36
|
|
|
39
37
|
/**
|
|
40
38
|
* Нормалізує рядок скрипта для порівняння (зайві пробіли).
|
|
@@ -247,41 +245,11 @@ async function checkEslintConfig(passFn, failFn) {
|
|
|
247
245
|
}
|
|
248
246
|
}
|
|
249
247
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
*/
|
|
256
|
-
function checkPackageJsonLintDeps(pkg, passFn, failFn) {
|
|
257
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
258
|
-
if (allDeps.prettier) {
|
|
259
|
-
failFn('package.json: видали залежність prettier (oxfmt замість prettier, js-lint.mdc)')
|
|
260
|
-
} else {
|
|
261
|
-
passFn('package.json не містить prettier')
|
|
262
|
-
}
|
|
263
|
-
if (allDeps['@nitra/prettier-config']) {
|
|
264
|
-
failFn('package.json: видали @nitra/prettier-config (js-lint.mdc)')
|
|
265
|
-
} else {
|
|
266
|
-
passFn('package.json не містить @nitra/prettier-config')
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const nitraEslint = pkg.devDependencies?.['@nitra/eslint-config']
|
|
270
|
-
if (nitraEslint) {
|
|
271
|
-
passFn('@nitra/eslint-config є в devDependencies')
|
|
272
|
-
if (nitraEslintConfigMeetsMinVersion(nitraEslint)) {
|
|
273
|
-
passFn(
|
|
274
|
-
'@nitra/eslint-config: мінімум 3.9.2 (no-restricted-syntax для ForInStatement з 3.8.0 + вбудований ignore "**/adr/**" з 3.9.2 + @e18e/eslint-plugin транзитивно, js-lint.mdc)'
|
|
275
|
-
)
|
|
276
|
-
} else {
|
|
277
|
-
failFn(
|
|
278
|
-
'@nitra/eslint-config: онови до мінімум "^3.9.2" — з 3.9.2 у getConfig вбудовано ignore для "**/adr/**" (ADR-документи не валідуються), плюс транзитивний @e18e/eslint-plugin для oxlint і заборона for...in з 3.8.0 (js-lint.mdc)'
|
|
279
|
-
)
|
|
280
|
-
}
|
|
281
|
-
} else {
|
|
282
|
-
failFn('@nitra/eslint-config відсутній в devDependencies — додай: bun add -d @nitra/eslint-config')
|
|
283
|
-
}
|
|
284
|
-
}
|
|
248
|
+
// Перевірки `prettier` / `@nitra/prettier-config` у залежностях (text.mdc) і
|
|
249
|
+
// `@nitra/eslint-config ≥ 3.9.2` тепер у Rego: відповідно
|
|
250
|
+
// `npm/policy/text/package_json/` і `npm/policy/js_lint/package_json/`. Тут
|
|
251
|
+
// лишилася лише workspace-ітерація для `type: "module"` і engines, бо js_lint
|
|
252
|
+
// Rego запускається лише на кореневому `package.json`.
|
|
285
253
|
|
|
286
254
|
/**
|
|
287
255
|
* Перевіряє, що package.json має `"type": "module"`.
|
|
@@ -359,36 +327,18 @@ function checkEnginesBun(label, pkg, passFn, failFn) {
|
|
|
359
327
|
}
|
|
360
328
|
|
|
361
329
|
/**
|
|
362
|
-
*
|
|
330
|
+
* Workspace-ітерація: для кожного workspace `package.json` перевіряємо
|
|
331
|
+
* `type: "module"` і `engines.{node,bun}`. Кореневий `package.json` ці поля
|
|
332
|
+
* валідує `npm/policy/js_lint/package_json/`; lint-js скрипт і `@nitra/eslint-config`
|
|
333
|
+
* — теж у Rego.
|
|
363
334
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
364
335
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
365
336
|
*/
|
|
366
337
|
async function checkPackageJsonJsLint(passFn, failFn) {
|
|
367
338
|
if (!existsSync('package.json')) return
|
|
368
339
|
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
369
|
-
|
|
370
|
-
checkPackageJsonTypeModule('package.json', pkg, passFn, failFn)
|
|
371
|
-
|
|
372
340
|
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : []
|
|
373
341
|
await checkWorkspacePackages(workspaces, passFn, failFn)
|
|
374
|
-
|
|
375
|
-
const lintJs = pkg.scripts?.['lint-js']
|
|
376
|
-
if (lintJs) {
|
|
377
|
-
passFn('package.json містить скрипт lint-js')
|
|
378
|
-
if (isCanonicalLintJs(String(lintJs))) {
|
|
379
|
-
passFn(`lint-js збігається з каноном: ${CANONICAL_LINT_JS}`)
|
|
380
|
-
} else {
|
|
381
|
-
failFn(
|
|
382
|
-
`lint-js має бути рівно: "${CANONICAL_LINT_JS}" (див. js-lint.mdc / check-js-lint.mjs). Зараз: ${JSON.stringify(normalizeLintJsScript(String(lintJs)))}`
|
|
383
|
-
)
|
|
384
|
-
}
|
|
385
|
-
} else {
|
|
386
|
-
failFn(`package.json не містить скрипт "lint-js" — додай: ${JSON.stringify(CANONICAL_LINT_JS)}`)
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
checkPackageJsonLintDeps(pkg, passFn, failFn)
|
|
390
|
-
checkEnginesNode('package.json', pkg, passFn, failFn)
|
|
391
|
-
checkEnginesBun('package.json', pkg, passFn, failFn)
|
|
392
342
|
}
|
|
393
343
|
|
|
394
344
|
/**
|
|
@@ -457,67 +407,16 @@ async function checkVscodeExtensions(passFn, failFn) {
|
|
|
457
407
|
}
|
|
458
408
|
|
|
459
409
|
/**
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*/
|
|
465
|
-
function checkLintJsWorkflowFallback(content, passFn, failFn) {
|
|
466
|
-
const checks = [
|
|
467
|
-
['actions/checkout@v6', 'lint-js.yml: потрібен крок actions/checkout@v6 (ga.mdc)'],
|
|
468
|
-
['persist-credentials: false', 'lint-js.yml: checkout з persist-credentials: false'],
|
|
469
|
-
['./.github/actions/setup-bun-deps', 'lint-js.yml: потрібен uses: ./.github/actions/setup-bun-deps'],
|
|
470
|
-
['bunx oxlint', 'lint-js.yml: у run має бути bunx oxlint'],
|
|
471
|
-
['bunx eslint .', 'lint-js.yml: у run має бути bunx eslint . (без --fix у CI)'],
|
|
472
|
-
['bunx jscpd .', 'lint-js.yml: у run має бути bunx jscpd .']
|
|
473
|
-
]
|
|
474
|
-
for (const [needle, errMsg] of checks) {
|
|
475
|
-
if (content.includes(needle)) {
|
|
476
|
-
passFn(`lint-js.yml містить: ${needle}`)
|
|
477
|
-
} else {
|
|
478
|
-
failFn(errMsg)
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
if (content.includes('bunx oxlint') && OXLINT_FIX_RE.test(content)) {
|
|
482
|
-
failFn('lint-js.yml: у CI не використовуй bunx oxlint --fix (лише bunx oxlint)')
|
|
483
|
-
}
|
|
484
|
-
if (content.includes('eslint --fix')) {
|
|
485
|
-
failFn('lint-js.yml: у CI не використовуй eslint --fix (лише bunx eslint .)')
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Перевіряє вміст lint-js.yml через YAML або fallback.
|
|
491
|
-
* @param {string} content вміст файлу
|
|
492
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
493
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
494
|
-
*/
|
|
495
|
-
function checkLintJsYmlContent(content, passFn, failFn) {
|
|
496
|
-
const root = parseWorkflowYaml(content)
|
|
497
|
-
if (root) {
|
|
498
|
-
const v = verifyLintJsWorkflowStructure(root)
|
|
499
|
-
if (v.ok) {
|
|
500
|
-
passFn('lint-js.yml: кроки checkout, setup-bun-deps, oxlint/eslint/jscpd (YAML + кроки)')
|
|
501
|
-
} else {
|
|
502
|
-
for (const msg of v.failures) {
|
|
503
|
-
failFn(`lint-js.yml: ${msg}`)
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
} else {
|
|
507
|
-
checkLintJsWorkflowFallback(content, passFn, failFn)
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Перевіряє lint-js.yml і lint.yml workflow.
|
|
410
|
+
* FS-existence для `lint-js.yml` + cross-file перевірка, що `lint.yml` (якщо існує)
|
|
411
|
+
* не дублює лінт JS-кроки. Структуру `lint-js.yml` (`actions/checkout@v6`,
|
|
412
|
+
* `persist-credentials: false`, `setup-bun-deps`, `bunx oxlint/eslint/jscpd .`,
|
|
413
|
+
* заборона `--fix` у CI) валідує `npm/policy/js_lint/lint_js_yml/`.
|
|
513
414
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
514
415
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
515
416
|
*/
|
|
516
417
|
async function checkLintJsWorkflows(passFn, failFn) {
|
|
517
418
|
if (existsSync('.github/workflows/lint-js.yml')) {
|
|
518
|
-
|
|
519
|
-
passFn('lint-js.yml існує')
|
|
520
|
-
checkLintJsYmlContent(content, passFn, failFn)
|
|
419
|
+
passFn('.github/workflows/lint-js.yml є (структуру перевіряє bun run lint-conftest → js_lint.lint_js_yml)')
|
|
521
420
|
} else {
|
|
522
421
|
failFn('.github/workflows/lint-js.yml не існує — створи його (див. check-js-lint.mjs / js-lint.mdc)')
|
|
523
422
|
}
|
package/scripts/check-js-run.mjs
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* дозволені лише у каталозі conn (за замовчуванням `src/conn/`; за наявності
|
|
13
13
|
* `package.json#imports['#conn/*']` — у його цільовому каталозі); поза ним — порушення
|
|
14
14
|
* (див. `utils/conn-imports-scan.mjs`);
|
|
15
|
+
* - «Нейминг та експорти у `#conn/`»: всередині conn-каталогу basename файла має відповідати
|
|
16
|
+
* канону `ql-<id>` / `(pg|mysql)-(read|write)[-<id>]`; `export default` заборонений; має бути
|
|
17
|
+
* іменований експорт з імʼям, що дорівнює camelCase від basename файла (`pg-write-contract.js`
|
|
18
|
+
* → `export const pgWriteContract`); `index.*` як reexport-барель пропускаємо
|
|
19
|
+
* (див. `utils/conn-file-rules.mjs`);
|
|
15
20
|
* - «process.env / CheckEnv»: пряме `process.env.X` має бути замінено на `env` —
|
|
16
21
|
* з `@nitra/check-env` (для обов'язкових змінних, із `checkEnv([...])`) або з
|
|
17
22
|
* `node:process` (для опційних). Коли `env` імпортовано з `@nitra/check-env`,
|
|
@@ -40,6 +45,7 @@ import {
|
|
|
40
45
|
import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from './utils/check-env-scan.mjs'
|
|
41
46
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
42
47
|
import { findDepcheckViolationsForPackage, readAllWorkflowFiles } from './utils/depcheck-workflow.mjs'
|
|
48
|
+
import { findConnFileRuleViolations, isConnFileRulesSourceFile } from './utils/conn-file-rules.mjs'
|
|
43
49
|
import {
|
|
44
50
|
findConnFactoryImportsInText,
|
|
45
51
|
isConnImportsScanSourceFile,
|
|
@@ -51,48 +57,6 @@ import { findPromiseSetTimeoutInText, isPromiseSetTimeoutScanSourceFile } from '
|
|
|
51
57
|
import { walkDir } from './utils/walkDir.mjs'
|
|
52
58
|
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
53
59
|
|
|
54
|
-
/** Канонічний `jsconfig.json` для backend workspace-пакетів із каталогом `src/` (js-run.mdc). */
|
|
55
|
-
const CANONICAL_BACKEND_JSCONFIG = Object.freeze({
|
|
56
|
-
compilerOptions: Object.freeze({
|
|
57
|
-
lib: Object.freeze(['esnext']),
|
|
58
|
-
module: 'NodeNext',
|
|
59
|
-
moduleResolution: 'NodeNext',
|
|
60
|
-
target: 'esnext',
|
|
61
|
-
checkJs: false
|
|
62
|
-
}),
|
|
63
|
-
include: Object.freeze(['src/**/*'])
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Глибока рівність для JSON-подібних значень (масиви — порядок важливий).
|
|
68
|
-
* @param {unknown} a перше значення
|
|
69
|
-
* @param {unknown} b друге значення
|
|
70
|
-
* @returns {boolean} true, якщо значення структурно ідентичні
|
|
71
|
-
*/
|
|
72
|
-
function deepEqualJson(a, b) {
|
|
73
|
-
if (a === b) return true
|
|
74
|
-
if (a === null || b === null || typeof a !== typeof b) return false
|
|
75
|
-
if (typeof a !== 'object') return false
|
|
76
|
-
if (Array.isArray(a) !== Array.isArray(b)) return false
|
|
77
|
-
if (Array.isArray(a)) {
|
|
78
|
-
if (a.length !== b.length) return false
|
|
79
|
-
for (const [i, v] of a.entries()) {
|
|
80
|
-
if (!deepEqualJson(v, b[i])) return false
|
|
81
|
-
}
|
|
82
|
-
return true
|
|
83
|
-
}
|
|
84
|
-
const ao = /** @type {Record<string, unknown>} */ (a)
|
|
85
|
-
const bo = /** @type {Record<string, unknown>} */ (b)
|
|
86
|
-
const keysA = Object.keys(ao).toSorted()
|
|
87
|
-
const keysB = Object.keys(bo).toSorted()
|
|
88
|
-
if (keysA.length !== keysB.length) return false
|
|
89
|
-
for (const [i, k] of keysA.entries()) {
|
|
90
|
-
if (k !== keysB[i]) return false
|
|
91
|
-
if (!deepEqualJson(ao[k], bo[k])) return false
|
|
92
|
-
}
|
|
93
|
-
return true
|
|
94
|
-
}
|
|
95
|
-
|
|
96
60
|
/**
|
|
97
61
|
* Чи існує непорожній за змістом маркер каталогу `src/` (рекомендована структура js-run).
|
|
98
62
|
* @param {string} absPackageRoot абсолютний корінь пакета
|
|
@@ -108,40 +72,29 @@ function backendPackageHasSrcDir(absPackageRoot) {
|
|
|
108
72
|
}
|
|
109
73
|
|
|
110
74
|
/**
|
|
111
|
-
*
|
|
75
|
+
* FS-existence для `jsconfig.json` у backend-пакеті з каталогом `src/` (cross-file:
|
|
76
|
+
* наявність каталогу + файла). Структуру самого `jsconfig.json` (canonical
|
|
77
|
+
* compilerOptions і include) валідує `npm/policy/js_run/jsconfig/`; її прогоняє
|
|
78
|
+
* `bun run lint-conftest`.
|
|
112
79
|
* @param {string} rootDir відносний шлях workspace
|
|
113
80
|
* @param {string} absPackageRoot абсолютний корінь пакета
|
|
114
81
|
* @param {string} label префікс `[pkg] `
|
|
115
82
|
* @param {(msg: string) => void} fail callback для повідомлень про порушення
|
|
116
83
|
* @param {(msg: string) => void} passFn callback для повідомлень про успішну перевірку
|
|
117
|
-
* @returns {
|
|
84
|
+
* @returns {void}
|
|
118
85
|
*/
|
|
119
|
-
|
|
86
|
+
function checkBackendJsconfigWhenSrcPresent(rootDir, absPackageRoot, label, fail, passFn) {
|
|
120
87
|
if (!backendPackageHasSrcDir(absPackageRoot)) return
|
|
121
88
|
|
|
122
89
|
const jcPath = join(rootDir, 'jsconfig.json')
|
|
123
|
-
if (
|
|
90
|
+
if (existsSync(jcPath)) {
|
|
91
|
+
passFn(`${label}jsconfig.json є (структуру перевіряє bun run lint-conftest → js_run.jsconfig)`)
|
|
92
|
+
} else {
|
|
124
93
|
fail(
|
|
125
94
|
`${label}є каталог src/, але немає jsconfig.json — додай канонічний файл з js-run.mdc ` +
|
|
126
95
|
`(NodeNext, include: src/**/*).`
|
|
127
96
|
)
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
let parsed
|
|
131
|
-
try {
|
|
132
|
-
parsed = JSON.parse(await readFile(jcPath, 'utf8'))
|
|
133
|
-
} catch {
|
|
134
|
-
fail(`${label}jsconfig.json не вдалося розпарсити як JSON`)
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
if (!deepEqualJson(parsed, CANONICAL_BACKEND_JSCONFIG)) {
|
|
138
|
-
fail(
|
|
139
|
-
`${label}jsconfig.json не збігається з каноном js-run.mdc — заміни на шаблон з правила ` +
|
|
140
|
-
`(compilerOptions: lib esnext, module/moduleResolution NodeNext, target esnext, checkJs false; include: src/**/*).`
|
|
141
|
-
)
|
|
142
|
-
return
|
|
143
97
|
}
|
|
144
|
-
passFn(`${label}jsconfig.json узгоджено з js-run (пакет з src/)`)
|
|
145
98
|
}
|
|
146
99
|
|
|
147
100
|
/**
|
|
@@ -236,6 +189,52 @@ async function checkConnImports(absPackageRoot, sourcePaths, pkgJson, label, fai
|
|
|
236
189
|
return violations
|
|
237
190
|
}
|
|
238
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Перевіряє правила нейминга та експортів для файлів усередині `#conn/`.
|
|
194
|
+
*
|
|
195
|
+
* Канон імені: `ql-<id>` для GraphQL, `(pg|mysql)-(read|write)[-<id>]` для БД (js-run.mdc,
|
|
196
|
+
* розділ «Нейминг файлів у `src/conn/`»). Експорт у файлі — лише іменований, з імʼям, що
|
|
197
|
+
* дорівнює camelCase від basename файла (`pg-write-contract.js` → `export const pgWriteContract`).
|
|
198
|
+
* @param {string} absPackageRoot абсолютний корінь пакета
|
|
199
|
+
* @param {string[]} sourcePaths абсолютні шляхи до файлів пакета
|
|
200
|
+
* @param {unknown} pkgJson розпарсений package.json пакета (або null)
|
|
201
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
202
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
203
|
+
* @returns {Promise<number>} кількість порушень
|
|
204
|
+
*/
|
|
205
|
+
async function checkConnFileNamingAndExports(absPackageRoot, sourcePaths, pkgJson, label, fail) {
|
|
206
|
+
const connDir = resolveConnDirFromPackageJson(pkgJson)
|
|
207
|
+
let violations = 0
|
|
208
|
+
for (const absPath of sourcePaths) {
|
|
209
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
210
|
+
if (!isInsideConnDir(rel, connDir)) continue
|
|
211
|
+
if (!isConnFileRulesSourceFile(rel)) continue
|
|
212
|
+
// пропускаємо реекспортний барель `index.*` (якщо знадобиться) і прихований .d.ts
|
|
213
|
+
const base = rel.slice(rel.lastIndexOf('/') + 1)
|
|
214
|
+
if (base.startsWith('index.')) continue
|
|
215
|
+
|
|
216
|
+
const content = await readFile(absPath, 'utf8')
|
|
217
|
+
for (const v of findConnFileRuleViolations(content, rel)) {
|
|
218
|
+
violations++
|
|
219
|
+
if (v.kind === 'name') {
|
|
220
|
+
fail(
|
|
221
|
+
`${label}${rel} — назва файла в '${connDir}/' не відповідає канону js-run: ` +
|
|
222
|
+
`'ql-<id>', 'pg-{read|write}[-<id>]' або 'mysql-{read|write}[-<id>]' (kebab-case, [a-z0-9-])`
|
|
223
|
+
)
|
|
224
|
+
} else if (v.kind === 'default-export') {
|
|
225
|
+
fail(`${label}${rel} — 'export default' заборонений у '${connDir}/'; зроби іменований експорт`)
|
|
226
|
+
} else {
|
|
227
|
+
const found = v.foundNames?.length ? v.foundNames.join(', ') : '—'
|
|
228
|
+
fail(
|
|
229
|
+
`${label}${rel} — очікується іменований експорт 'export const ${v.expectedName} = …' ` +
|
|
230
|
+
`(camelCase від назви файла); знайдено: ${found}`
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return violations
|
|
236
|
+
}
|
|
237
|
+
|
|
239
238
|
/**
|
|
240
239
|
* Перевіряє правило «CheckEnv» для пакета.
|
|
241
240
|
* @param {string} absPackageRoot абсолютний корінь пакета
|
|
@@ -323,6 +322,14 @@ async function checkWorkspacePackage(rootDir, ignorePaths, workflows, fail, pass
|
|
|
323
322
|
passFn(`${label}імпорти підключень (bun#SQL / mssql / @nitra/graphql-request#GraphQLClient) лише в '${connDir}/'`)
|
|
324
323
|
}
|
|
325
324
|
|
|
325
|
+
const connFileViolations = await checkConnFileNamingAndExports(absPackageRoot, sourcePaths, pkgJson, label, fail)
|
|
326
|
+
if (connFileViolations === 0) {
|
|
327
|
+
const connDir = resolveConnDirFromPackageJson(pkgJson)
|
|
328
|
+
passFn(
|
|
329
|
+
`${label}файли в '${connDir}/' дотримують канону js-run: нейминг (ql-/pg-/mysql-…) і іменований експорт у camelCase від basename`
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
326
333
|
const envViolations = await checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail)
|
|
327
334
|
if (envViolations === 0) {
|
|
328
335
|
passFn(
|
|
@@ -390,15 +397,11 @@ async function loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail) {
|
|
|
390
397
|
const pkgPath = join(rootDir, 'package.json')
|
|
391
398
|
if (!existsSync(pkgPath)) return null
|
|
392
399
|
const pkgJson = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
if (allDeps.bunyan) {
|
|
400
|
-
fail(`${label}bunyan знайдено — замінити на @nitra/pino`)
|
|
401
|
-
}
|
|
400
|
+
// Заборону `@nitra/bunyan` / `bunyan` у dependencies/devDependencies перенесено
|
|
401
|
+
// в Rego (`npm/policy/js_run/package_json/`); `bun run lint-conftest` запускає
|
|
402
|
+
// її по всіх workspace `package.json`. Тут лишилася лише AST-перевірка імпортів.
|
|
403
|
+
void label
|
|
404
|
+
void fail
|
|
402
405
|
return pkgJson
|
|
403
406
|
}
|
|
404
407
|
|
|
@@ -411,20 +414,15 @@ async function loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail) {
|
|
|
411
414
|
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
412
415
|
* @returns {Promise<void>} завершується після перевірки configmap
|
|
413
416
|
*/
|
|
414
|
-
|
|
417
|
+
function checkOtelConfigmap(rootDir, label, fail, passFn) {
|
|
415
418
|
const configmapPath = join(rootDir, 'k8s', 'base', 'configmap.yaml')
|
|
416
419
|
if (!existsSync(configmapPath)) return
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
passFn(`${
|
|
423
|
-
if (content.includes('service.name=') && content.includes('service.namespace=')) {
|
|
424
|
-
passFn(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
|
|
425
|
-
} else {
|
|
426
|
-
fail(`${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`)
|
|
427
|
-
}
|
|
420
|
+
// Перевірку `OTEL_RESOURCE_ATTRIBUTES` має містити `service.name=` /
|
|
421
|
+
// `service.namespace=` перенесено в Rego (`npm/policy/js_run/configmap/`);
|
|
422
|
+
// `bun run lint-conftest` запускає її на всіх `k8s/base/configmap.yaml`.
|
|
423
|
+
void label
|
|
424
|
+
void fail
|
|
425
|
+
passFn(`${rootDir}/k8s/base/configmap.yaml є (OTEL — bun run lint-conftest → js_run.configmap)`)
|
|
428
426
|
}
|
|
429
427
|
|
|
430
428
|
/**
|
|
@@ -22,20 +22,11 @@ import { join } from 'node:path'
|
|
|
22
22
|
import { promisify } from 'node:util'
|
|
23
23
|
|
|
24
24
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
25
|
-
import {
|
|
26
|
-
hasIdTokenWritePermission,
|
|
27
|
-
hasNpmPublishStepWithPackage,
|
|
28
|
-
parseWorkflowYaml,
|
|
29
|
-
pushHasMainBranch,
|
|
30
|
-
pushPathsIncludeNpmGlob
|
|
31
|
-
} from './utils/gha-workflow.mjs'
|
|
32
25
|
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
33
26
|
import { walkDir } from './utils/walkDir.mjs'
|
|
34
27
|
|
|
35
28
|
const execFileAsync = promisify(execFile)
|
|
36
29
|
|
|
37
|
-
const TYPES_FILE_RE = /^\.\/types\/.+\.d\.(ts|mts)$/
|
|
38
|
-
|
|
39
30
|
/** Перший заголовок релізу у Keep a Changelog (`## [1.2.3]`). */
|
|
40
31
|
const CHANGELOG_FIRST_VERSION_RE = /^## \[([^\]]+)\]/m
|
|
41
32
|
|
|
@@ -115,39 +106,6 @@ function missingHkEmitTypesConfigFragments(hkText) {
|
|
|
115
106
|
return need.filter(s => !hkText.includes(s))
|
|
116
107
|
}
|
|
117
108
|
|
|
118
|
-
/**
|
|
119
|
-
* Перевіряє `npm/tsconfig.emit-types.json` на мінімальний набір опцій для `emitDeclarationOnly` у `types/`.
|
|
120
|
-
* @param {unknown} parsed результат `JSON.parse` конфігурації
|
|
121
|
-
* @returns {string[]} повідомлення про помилки (порожній — OK)
|
|
122
|
-
*/
|
|
123
|
-
function emitTypesConfigIssues(parsed) {
|
|
124
|
-
const issues = []
|
|
125
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
126
|
-
return ['некоректний JSON']
|
|
127
|
-
}
|
|
128
|
-
const co = /** @type {{ [k: string]: unknown }} */ (parsed).compilerOptions
|
|
129
|
-
if (!co || typeof co !== 'object') {
|
|
130
|
-
return ['відсутній compilerOptions']
|
|
131
|
-
}
|
|
132
|
-
const get = k => /** @type {{ [k: string]: unknown }} */ (co)[k]
|
|
133
|
-
if (get('allowJs') !== true) {
|
|
134
|
-
issues.push('compilerOptions.allowJs має бути true')
|
|
135
|
-
}
|
|
136
|
-
if (get('declaration') !== true) {
|
|
137
|
-
issues.push('compilerOptions.declaration має бути true')
|
|
138
|
-
}
|
|
139
|
-
if (get('emitDeclarationOnly') !== true) {
|
|
140
|
-
issues.push('compilerOptions.emitDeclarationOnly має бути true')
|
|
141
|
-
}
|
|
142
|
-
if (get('outDir') !== 'types') {
|
|
143
|
-
issues.push('compilerOptions.outDir має бути "types"')
|
|
144
|
-
}
|
|
145
|
-
if (get('skipLibCheck') !== true) {
|
|
146
|
-
issues.push('compilerOptions.skipLibCheck має бути true')
|
|
147
|
-
}
|
|
148
|
-
return issues
|
|
149
|
-
}
|
|
150
|
-
|
|
151
109
|
/**
|
|
152
110
|
* Шлях на дискі до файлу з поля `types` у `npm/package.json` (значення на кшталт `./types/bin/x.d.ts`).
|
|
153
111
|
* @param {string} typesField значення поля `types` з `package.json`
|
|
@@ -162,30 +120,9 @@ function npmTypesFileFromPackageField(typesField) {
|
|
|
162
120
|
}
|
|
163
121
|
|
|
164
122
|
/**
|
|
165
|
-
* Перевіряє
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
169
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
170
|
-
*/
|
|
171
|
-
function checkNpmTypesField(typesField, useSrcJsLayout, passFn, failFn) {
|
|
172
|
-
if (useSrcJsLayout) {
|
|
173
|
-
if (typesField === TYPES_INDEX) {
|
|
174
|
-
passFn(`npm/package.json: "types": "${TYPES_INDEX}" (layout npm/src + .js)`)
|
|
175
|
-
} else {
|
|
176
|
-
failFn(`npm/package.json: при наявності .js під npm/src очікується "types": "${TYPES_INDEX}"`)
|
|
177
|
-
}
|
|
178
|
-
} else if (typeof typesField === 'string' && TYPES_FILE_RE.test(typesField)) {
|
|
179
|
-
passFn(`npm/package.json: "types" вказує на файл під ./types/… (${typesField})`)
|
|
180
|
-
} else {
|
|
181
|
-
failFn(
|
|
182
|
-
'npm/package.json: без .js під npm/src поле types має бути рядком виду ./types/….d.ts або .d.mts (див. npm-module.mdc)'
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Перевіряє npm/package.json на типи та files.
|
|
123
|
+
* Перевіряє наявність на диску файлу зі значення `types` у `npm/package.json`
|
|
124
|
+
* (cross-file: JSON-поле + FS). Структуру самого поля валідує
|
|
125
|
+
* `npm/policy/npm_module/npm_package_json/`; тут — лише чи файл реально існує.
|
|
189
126
|
* @param {boolean} useSrcJsLayout чи використовується layout з npm/src
|
|
190
127
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
191
128
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
@@ -195,14 +132,6 @@ async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn) {
|
|
|
195
132
|
const npmPkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
|
|
196
133
|
const typesField = npmPkg.types
|
|
197
134
|
|
|
198
|
-
checkNpmTypesField(typesField, useSrcJsLayout, passFn, failFn)
|
|
199
|
-
|
|
200
|
-
if (Array.isArray(npmPkg.files) && npmPkg.files.includes('types')) {
|
|
201
|
-
passFn('npm/package.json: files містить "types"')
|
|
202
|
-
} else {
|
|
203
|
-
failFn('npm/package.json: масив files має містити "types"')
|
|
204
|
-
}
|
|
205
|
-
|
|
206
135
|
const typesPath = useSrcJsLayout ? join('npm', 'types', 'index.d.ts') : npmTypesFileFromPackageField(typesField)
|
|
207
136
|
const missingTypesMsg = useSrcJsLayout
|
|
208
137
|
? `Відсутній ${join('npm', 'types', 'index.d.ts')} (згенеруй tsc з npm-module.mdc)`
|
|
@@ -215,31 +144,19 @@ async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn) {
|
|
|
215
144
|
}
|
|
216
145
|
|
|
217
146
|
/**
|
|
218
|
-
*
|
|
147
|
+
* FS-existence для `npm/tsconfig.emit-types.json` (структуру `compilerOptions`
|
|
148
|
+
* валідує `npm/policy/npm_module/emit_types_config/`).
|
|
219
149
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
220
150
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
221
151
|
*/
|
|
222
|
-
|
|
152
|
+
function checkEmitTypesConfig(passFn, failFn) {
|
|
223
153
|
if (!existsSync(EMIT_TYPES_CONFIG)) {
|
|
224
154
|
failFn(
|
|
225
155
|
`Без .js під npm/src потрібен ${EMIT_TYPES_CONFIG} (див. npm-module.mdc: emit через tsconfig, без штучного src/index.js)`
|
|
226
156
|
)
|
|
227
157
|
return
|
|
228
158
|
}
|
|
229
|
-
passFn(`${EMIT_TYPES_CONFIG}
|
|
230
|
-
let raw
|
|
231
|
-
try {
|
|
232
|
-
raw = JSON.parse(await readFile(EMIT_TYPES_CONFIG, 'utf8'))
|
|
233
|
-
} catch {
|
|
234
|
-
failFn(`${EMIT_TYPES_CONFIG}: некоректний JSON`)
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
const issues = emitTypesConfigIssues(raw)
|
|
238
|
-
if (issues.length === 0) {
|
|
239
|
-
passFn(`${EMIT_TYPES_CONFIG}: compilerOptions придатні для emitDeclarationOnly → types/`)
|
|
240
|
-
} else {
|
|
241
|
-
failFn(`${EMIT_TYPES_CONFIG}: ${issues.join('; ')}`)
|
|
242
|
-
}
|
|
159
|
+
passFn(`${EMIT_TYPES_CONFIG} є (структуру перевіряє bun run lint-conftest → npm_module.emit_types_config)`)
|
|
243
160
|
}
|
|
244
161
|
|
|
245
162
|
/**
|
|
@@ -364,71 +281,25 @@ async function checkDirtyNpmRequiresVersionBump(passFn, failFn) {
|
|
|
364
281
|
}
|
|
365
282
|
|
|
366
283
|
/**
|
|
367
|
-
*
|
|
284
|
+
* FS-existence для `npm-publish.yml` workflow. Поля workflow (`on.push.paths`,
|
|
285
|
+
* `branches`, `id-token: write`, JS-DevTools/npm-publish step) валідує
|
|
286
|
+
* `npm/policy/npm_module/npm_publish_yml/`.
|
|
368
287
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
369
288
|
* @param {(msg: string) => void} failFn callback при виявленому порушенні
|
|
370
|
-
* @returns {Promise<void>}
|
|
371
289
|
*/
|
|
372
|
-
|
|
290
|
+
function checkPublishWorkflow(passFn, failFn) {
|
|
373
291
|
const publishWf = '.github/workflows/npm-publish.yml'
|
|
374
|
-
if (
|
|
292
|
+
if (existsSync(publishWf)) {
|
|
293
|
+
passFn(`${publishWf} є (структуру перевіряє bun run lint-conftest → npm_module.npm_publish_yml)`)
|
|
294
|
+
} else {
|
|
375
295
|
failFn(`Відсутній ${publishWf} (npm-module.mdc: npm publish)`)
|
|
376
|
-
return
|
|
377
|
-
}
|
|
378
|
-
passFn(`${publishWf} існує`)
|
|
379
|
-
const pub = await readFile(publishWf, 'utf8')
|
|
380
|
-
const root = parseWorkflowYaml(pub)
|
|
381
|
-
if (root) {
|
|
382
|
-
const checks = [
|
|
383
|
-
{
|
|
384
|
-
ok: pushPathsIncludeNpmGlob(root),
|
|
385
|
-
pass: `${publishWf}: on.push.paths містить npm/**`,
|
|
386
|
-
fail: `${publishWf}: у on.push.paths має бути npm/**`
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
ok: pushHasMainBranch(root),
|
|
390
|
-
pass: `${publishWf}: очікується branch main`,
|
|
391
|
-
fail: `${publishWf}: очікується branch main`
|
|
392
|
-
},
|
|
393
|
-
{
|
|
394
|
-
ok: hasIdTokenWritePermission(root),
|
|
395
|
-
pass: `${publishWf}: permissions містить id-token: write (OIDC)`,
|
|
396
|
-
fail: `${publishWf}: permissions має містити id-token: write (OIDC)`
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
ok: hasNpmPublishStepWithPackage(root),
|
|
400
|
-
pass: `${publishWf}: uses JS-DevTools/npm-publish та with.package npm/package.json`,
|
|
401
|
-
fail: `${publishWf}: очікується uses: JS-DevTools/npm-publish та with.package: npm/package.json`
|
|
402
|
-
}
|
|
403
|
-
]
|
|
404
|
-
for (const c of checks) {
|
|
405
|
-
if (c.ok) {
|
|
406
|
-
passFn(c.pass)
|
|
407
|
-
} else {
|
|
408
|
-
failFn(c.fail)
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
return
|
|
412
|
-
}
|
|
413
|
-
const need = [
|
|
414
|
-
{ sub: 'npm/**', msg: `${publishWf}: у on.push.paths має бути npm/**` },
|
|
415
|
-
{ sub: 'branches:', msg: `${publishWf}: очікується on.push.branches` },
|
|
416
|
-
{ sub: 'main', msg: `${publishWf}: очікується branch main` },
|
|
417
|
-
{ sub: 'id-token: write', msg: `${publishWf}: permissions має містити id-token: write (OIDC)` },
|
|
418
|
-
{ sub: 'JS-DevTools/npm-publish', msg: `${publishWf}: очікується uses: JS-DevTools/npm-publish` },
|
|
419
|
-
{ sub: 'package: npm/package.json', msg: `${publishWf}: with.package має бути npm/package.json` }
|
|
420
|
-
]
|
|
421
|
-
for (const { sub, msg } of need) {
|
|
422
|
-
if (pub.includes(sub)) {
|
|
423
|
-
passFn(`${publishWf} містить «${sub}»`)
|
|
424
|
-
} else {
|
|
425
|
-
failFn(msg)
|
|
426
|
-
}
|
|
427
296
|
}
|
|
428
297
|
}
|
|
429
298
|
|
|
430
299
|
/**
|
|
431
|
-
* Перевіряє базову структуру
|
|
300
|
+
* Перевіряє базову структуру монорепо: наявність каталогу `npm/` і
|
|
301
|
+
* `npm/package.json`. Поле `workspaces ∋ "npm"` у кореневому `package.json`
|
|
302
|
+
* валідує `npm/policy/npm_module/root_package_json/`.
|
|
432
303
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
433
304
|
* @param {(msg: string) => void} fail callback при помилці
|
|
434
305
|
*/
|
|
@@ -450,15 +321,6 @@ async function checkNpmModuleBasicStructure(pass, fail) {
|
|
|
450
321
|
fail('npm/ директорія не існує')
|
|
451
322
|
}
|
|
452
323
|
|
|
453
|
-
if (existsSync('package.json')) {
|
|
454
|
-
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
455
|
-
if (Array.isArray(pkg.workspaces) && pkg.workspaces.includes('npm')) {
|
|
456
|
-
pass('package.json workspaces містить "npm"')
|
|
457
|
-
} else {
|
|
458
|
-
fail('package.json workspaces має містити "npm"')
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
324
|
if (existsSync('npm/package.json')) {
|
|
463
325
|
pass('npm/package.json існує')
|
|
464
326
|
} else {
|