@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.
@@ -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
- * Перевіряє залежності lint-js у package.json (prettier, `@nitra/eslint-config`).
252
- * @param {{ dependencies?: Record<string, string>, devDependencies?: Record<string, string> }} pkg parsed package.json
253
- * @param {(msg: string) => void} passFn callback при успішній перевірці
254
- * @param {(msg: string) => void} failFn callback при помилці
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
- * Перевіряє package.json на lint-js, prettier, eslint-config, engines.node.
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
- * Перевіряє lint-js.yml workflow (fallback текстовий пошук).
461
- * @param {string} content вміст workflow файлу
462
- * @param {(msg: string) => void} passFn callback при успішній перевірці
463
- * @param {(msg: string) => void} failFn callback при помилці
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
- const content = await readFile('.github/workflows/lint-js.yml', 'utf8')
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
  }
@@ -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
- * Перевіряє `jsconfig.json` для backend-пакетів із `src/`.
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 {Promise<void>}
84
+ * @returns {void}
118
85
  */
119
- async function checkBackendJsconfigWhenSrcPresent(rootDir, absPackageRoot, label, fail, passFn) {
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 (!existsSync(jcPath)) {
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
- const deps = /** @type {Record<string, unknown>} */ (pkgJson).dependencies
394
- const devDeps = /** @type {Record<string, unknown>} */ (pkgJson).devDependencies
395
- const allDeps = { ...deps, ...devDeps }
396
- if (allDeps['@nitra/bunyan']) {
397
- fail(`${label}@nitra/bunyan знайдено — замінити на @nitra/pino`)
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
- async function checkOtelConfigmap(rootDir, label, fail, passFn) {
417
+ function checkOtelConfigmap(rootDir, label, fail, passFn) {
415
418
  const configmapPath = join(rootDir, 'k8s', 'base', 'configmap.yaml')
416
419
  if (!existsSync(configmapPath)) return
417
- const content = await readFile(configmapPath, 'utf8')
418
- if (!content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
419
- fail(`${label}k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES`)
420
- return
421
- }
422
- passFn(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
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
- * Перевіряє поле types у npm/package.json.
166
- * @param {unknown} typesField значення поля types
167
- * @param {boolean} useSrcJsLayout чи використовується layout з npm/src
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
- * Перевіряє npm/tsconfig.emit-types.json.
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
- async function checkEmitTypesConfig(passFn, failFn) {
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
- * Перевіряє npm-publish.yml workflow на наявність потрібних полів і кроків.
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
- async function checkPublishWorkflow(passFn, failFn) {
290
+ function checkPublishWorkflow(passFn, failFn) {
373
291
  const publishWf = '.github/workflows/npm-publish.yml'
374
- if (!existsSync(publishWf)) {
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
- * Перевіряє базову структуру монорепо (package.json, npm/, workspaces, npm/package.json).
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 {