@nitra/cursor 1.8.156 → 1.8.158

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.
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Перевіряє правило hasura.mdc для проєктів **nitra** і **abie**: значення
3
+ * `HASURA_GRAPHQL_ENDPOINT` у `*.env` має бути **внутрішнім** кластерним URL,
4
+ * а не публічним доменом.
5
+ *
6
+ * Запускається лише якщо в кореневому `package.json` поле `repository`
7
+ * вказує на `https://github.com/nitra/...` або `https://github.com/abinbevefes/...`
8
+ * (інші репозиторії пропускаються без помилок — як у check-abie).
9
+ *
10
+ * Очікуваний формат URL:
11
+ * `http://<service>.<namespace>.svc.<cluster>.internal:<port>`
12
+ *
13
+ * приклад: `http://contract-h.ua-contract.svc.abie-ua.internal:8080`
14
+ *
15
+ * Сегменти беруться з `hasura/k8s/base/svc-hl.yaml` (`metadata.name` —
16
+ * має закінчуватись на `-h`, headless-сервіс) і `hasura/k8s/base/namespace.yaml`
17
+ * (`metadata.name` — namespace). Якщо ці YAML є в репозиторії, у URL додатково
18
+ * звіряються конкретні `<service>` і `<namespace>` з ними.
19
+ *
20
+ * Скануються всі файли `*.env` (наприклад `dev.env`, `production.env`); файл
21
+ * `.env` без імені — виключення з правила (локальний файл розробника), його
22
+ * не перевіряємо. Пропускаються `node_modules`, `.git`, `dist`, `coverage`,
23
+ * `.turbo`, `.next` (як у `walkDir`).
24
+ */
25
+ import { existsSync } from 'node:fs'
26
+ import { readFile } from 'node:fs/promises'
27
+ import { basename, join, relative } from 'node:path'
28
+
29
+ import { parseAllDocuments } from 'yaml'
30
+
31
+ import { getRepositoryUrl } from './auto-rules.mjs'
32
+ import { createCheckReporter } from './utils/check-reporter.mjs'
33
+ import { walkDir } from './utils/walkDir.mjs'
34
+
35
+ const NITRA_REPOSITORY_URL_MARKER = 'https://github.com/nitra/'
36
+ const ABIE_REPOSITORY_URL_MARKER = 'https://github.com/abinbevefes/'
37
+
38
+ const HASURA_BASE_DIR = 'hasura/k8s/base'
39
+ const HASURA_SVC_HL_FILE = `${HASURA_BASE_DIR}/svc-hl.yaml`
40
+ const HASURA_NAMESPACE_FILE = `${HASURA_BASE_DIR}/namespace.yaml`
41
+
42
+ const ENV_FILE_RE = /\.env$/u
43
+ const HASURA_ENDPOINT_LINE_RE = /^[ \t]*(?:export[ \t]+)?HASURA_GRAPHQL_ENDPOINT[ \t]*=[ \t]*['"]?([^'"\r\n#]+)/mu
44
+ const INTERNAL_HASURA_URL_RE = /^http:\/\/([^./]+)\.([^./]+)\.svc\.([^./]+)\.internal:(\d+)\/?$/u
45
+
46
+ /**
47
+ * Розбір значення `HASURA_GRAPHQL_ENDPOINT` як внутрішнього кластерного URL.
48
+ * Дозволяє лише `http://` (TLS усередині кластера зайвий), вимагає сегментів
49
+ * `<service>.<namespace>.svc.<cluster>.internal` та явного порту.
50
+ * @param {string} url значення з `.env` (без огорнутих лапок)
51
+ * @returns {{ ok: true, service: string, namespace: string, cluster: string, port: string } | { ok: false }}
52
+ * розібрані сегменти або `{ ok: false }`, якщо формат не відповідає внутрішньому кластерному URL
53
+ */
54
+ export function parseInternalHasuraEndpoint(url) {
55
+ const m = url.trim().match(INTERNAL_HASURA_URL_RE)
56
+ if (!m) {
57
+ return { ok: false }
58
+ }
59
+ return { ok: true, service: m[1], namespace: m[2], cluster: m[3], port: m[4] }
60
+ }
61
+
62
+ /**
63
+ * Зчитує `metadata.name` з першого документа YAML, який має заданий `kind`.
64
+ * @param {string} absPath абсолютний шлях до YAML
65
+ * @param {string} kind очікуваний `kind` (наприклад `Service`, `Namespace`)
66
+ * @returns {Promise<string | null>} ім'я ресурсу або null, якщо файл/документ відсутній
67
+ */
68
+ async function readYamlMetadataName(absPath, kind) {
69
+ if (!existsSync(absPath)) {
70
+ return null
71
+ }
72
+ let docs
73
+ try {
74
+ docs = parseAllDocuments(await readFile(absPath, 'utf8'))
75
+ } catch {
76
+ return null
77
+ }
78
+ for (const doc of docs) {
79
+ const obj = doc.toJS()
80
+ if (obj && typeof obj === 'object' && obj.kind === kind && obj.metadata?.name) {
81
+ return String(obj.metadata.name)
82
+ }
83
+ }
84
+ return null
85
+ }
86
+
87
+ /**
88
+ * Чи відносний шлях вказує на `*.env`, який треба перевіряти hasura.mdc.
89
+ * Файл рівно `.env` (без імені) — виключення з правила (локальний файл
90
+ * розробника, hasura.mdc його не зачіпає), тому повертає false.
91
+ * @param {string} relPath posix-шлях відносно кореня
92
+ * @returns {boolean} true для `dev.env`, `nitra.env`; false для `.env`
93
+ */
94
+ export function isEnvFile(relPath) {
95
+ if (!ENV_FILE_RE.test(relPath)) {
96
+ return false
97
+ }
98
+ return basename(relPath) !== '.env'
99
+ }
100
+
101
+ /**
102
+ * Збирає всі `*.env` файли в дереві, окрім службових каталогів.
103
+ * @param {string} root абсолютний шлях кореня
104
+ * @returns {Promise<string[]>} відсортовані posix-шляхи відносно кореня
105
+ */
106
+ async function collectEnvFiles(root) {
107
+ /** @type {string[]} */
108
+ const out = []
109
+ await walkDir(root, absPath => {
110
+ const rel = relative(root, absPath).split('\\').join('/')
111
+ if (isEnvFile(rel)) {
112
+ out.push(rel)
113
+ }
114
+ })
115
+ return out.toSorted((a, b) => a.localeCompare(b))
116
+ }
117
+
118
+ /**
119
+ * Перевіряє один `.env` файл на коректність `HASURA_GRAPHQL_ENDPOINT`.
120
+ * Якщо в файлі немає змінної — вважаємо OK.
121
+ * @param {string} relPath відносний шлях файла
122
+ * @param {{ service: string | null, namespace: string | null }} expected очікувані сегменти з YAML
123
+ * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер
124
+ * @returns {Promise<void>}
125
+ */
126
+ async function checkEnvFile(relPath, expected, reporter) {
127
+ const { pass, fail } = reporter
128
+ const content = await readFile(relPath, 'utf8')
129
+ const m = content.match(HASURA_ENDPOINT_LINE_RE)
130
+ if (!m) {
131
+ return
132
+ }
133
+ const value = m[1].trim()
134
+ const parsed = parseInternalHasuraEndpoint(value)
135
+ if (!parsed.ok) {
136
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url, sonarjs/no-clear-text-protocols -- hasura.mdc вимагає саме http:// для кластерного URL (TLS не використовується)
137
+ const example = 'http://<service>.<namespace>.svc.<cluster>.internal:<port>'
138
+ fail(
139
+ `${relPath}: HASURA_GRAPHQL_ENDPOINT="${value}" — потрібен внутрішній кластерний URL виду ${example} (hasura.mdc)`
140
+ )
141
+ return
142
+ }
143
+ if (expected.service && parsed.service !== expected.service) {
144
+ fail(
145
+ `${relPath}: HASURA_GRAPHQL_ENDPOINT — сервіс "${parsed.service}" не збігається з ` +
146
+ `metadata.name "${expected.service}" із ${HASURA_SVC_HL_FILE} (hasura.mdc)`
147
+ )
148
+ return
149
+ }
150
+ if (expected.namespace && parsed.namespace !== expected.namespace) {
151
+ fail(
152
+ `${relPath}: HASURA_GRAPHQL_ENDPOINT — namespace "${parsed.namespace}" не збігається з ` +
153
+ `metadata.name "${expected.namespace}" із ${HASURA_NAMESPACE_FILE} (hasura.mdc)`
154
+ )
155
+ return
156
+ }
157
+ pass(`${relPath}: HASURA_GRAPHQL_ENDPOINT — внутрішній кластерний URL`)
158
+ }
159
+
160
+ /**
161
+ * Зчитує URL репозиторію з кореневого `package.json` (або null, якщо файла немає / не валідний).
162
+ * @returns {Promise<string | null>} URL з поля `repository`
163
+ */
164
+ async function readRootRepositoryUrl() {
165
+ if (!existsSync('package.json')) {
166
+ return null
167
+ }
168
+ try {
169
+ const pkg = JSON.parse(await readFile('package.json', 'utf8'))
170
+ return getRepositoryUrl(pkg?.repository)
171
+ } catch {
172
+ return null
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Чи URL репозиторію вказує на nitra або abie (за маркерами hasura.mdc).
178
+ * @param {string | null} url значення з `package.json` `repository`
179
+ * @returns {boolean} true для nitra/abie проєктів
180
+ */
181
+ export function isNitraOrAbieRepository(url) {
182
+ if (typeof url !== 'string') {
183
+ return false
184
+ }
185
+ const lc = url.toLowerCase()
186
+ return lc.includes(NITRA_REPOSITORY_URL_MARKER) || lc.includes(ABIE_REPOSITORY_URL_MARKER)
187
+ }
188
+
189
+ /**
190
+ * Перевіряє hasura.mdc для поточного робочого каталогу.
191
+ * @returns {Promise<number>} 0 — OK / правило не застосовується, 1 — порушення
192
+ */
193
+ export async function check() {
194
+ const reporter = createCheckReporter()
195
+ const { pass } = reporter
196
+
197
+ const repositoryUrl = await readRootRepositoryUrl()
198
+ if (!isNitraOrAbieRepository(repositoryUrl)) {
199
+ pass('Пропущено: репозиторій не nitra і не abie (hasura.mdc застосовується лише до них)')
200
+ return reporter.getExitCode()
201
+ }
202
+
203
+ const root = process.cwd()
204
+ const expected = {
205
+ service: await readYamlMetadataName(join(root, HASURA_SVC_HL_FILE), 'Service'),
206
+ namespace: await readYamlMetadataName(join(root, HASURA_NAMESPACE_FILE), 'Namespace')
207
+ }
208
+
209
+ const envFiles = await collectEnvFiles(root)
210
+ if (envFiles.length === 0) {
211
+ pass('Не знайдено жодного *.env файла — нічого перевіряти')
212
+ return reporter.getExitCode()
213
+ }
214
+
215
+ for (const rel of envFiles) {
216
+ await checkEnvFile(rel, expected, reporter)
217
+ }
218
+
219
+ // Якщо у файлах не було жодної згадки HASURA_GRAPHQL_ENDPOINT — повідом про це.
220
+ const exit = reporter.getExitCode()
221
+ if (exit === 0) {
222
+ const names = envFiles.map(p => basename(p)).join(', ')
223
+ pass(`Перевірено ${envFiles.length} *.env файл(ів): ${names}`)
224
+ }
225
+ return exit
226
+ }
@@ -124,11 +124,8 @@ async function checkForbiddenDependencies(pkgJsonPaths, repoRoot, reporter) {
124
124
  */
125
125
  async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
126
126
  const { fail } = reporter
127
+ const counts = { perRequest: 0, unsafeCall: 0, dynamicList: 0, inListGuard: 0 }
127
128
  let hasBunSqlImport = false
128
- let perRequest = 0
129
- let unsafeCall = 0
130
- let dynamicList = 0
131
- let inListGuard = 0
132
129
 
133
130
  for (const absPath of sourcePaths) {
134
131
  const rel = relative(repoRoot, absPath).split('\\').join('/')
@@ -136,50 +133,72 @@ async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
136
133
  if (!hasBunSqlImport && textHasBunSqlImport(content)) {
137
134
  hasBunSqlImport = true
138
135
  }
136
+ scanFileForBunSqlPatterns(content, rel, fail, counts)
137
+ }
139
138
 
140
- for (const v of findBunSqlPerRequestConnectionInText(content, rel)) {
141
- perRequest++
142
- fail(
143
- `js-bun-db: ${rel}:${v.line} — не створюй new SQL(...) всередині функцій; ` +
144
- `тримай singleton на рівні модуля (js-bun-db.mdc): ${v.snippet}`
145
- )
146
- }
147
- for (const v of findUnsafeBunSqlUnsafeCallInText(content, rel)) {
148
- unsafeCall++
149
- fail(
150
- `js-bun-db: ${rel}:${v.line} — sql.unsafe(\`...\${...}...\`) недопустимо: ` +
151
- `використовуй tagged template sql\`...\${value}...\` або sql.unsafe('static', [params]) (js-bun-db.mdc): ${v.snippet}`
152
- )
153
- }
154
- for (const v of findUnsafeBunSqlDynamicSqlListInText(content, rel)) {
155
- dynamicList++
156
- fail(
157
- `js-bun-db: ${rel}:${v.line} — заборонено підставляти у SQL динамічні списки через .join(',') ` +
158
- `у IN (...) / VALUES (...); використовуй sql([...]) (js-bun-db.mdc): ${v.snippet}`
159
- )
160
- }
161
- for (const v of findUnsafeBunSqlInListMissingEmptyGuardInText(content, rel)) {
162
- inListGuard++
163
- if (v.reason === 'missing_guard') {
164
- fail(
165
- `js-bun-db: ${rel}:${v.line} — перед IN-списком ${JSON.stringify(v.name)} потрібна перевірка на пустоту ` +
166
- throw (наприклад if (!${v.name}.length) throw ...), інакше можливі некоректні запити (js-bun-db.mdc): ${v.snippet}`
167
- )
168
- } else if (v.reason === 'sql_helper_not_var') {
169
- fail(
170
- `js-bun-db: ${rel}:${v.line} — IN-список у \${sql(...)} має підставлятись зі змінної (Identifier) ` +
171
- `після валідації на пустоту + throw (js-bun-db.mdc): ${v.snippet}`
172
- )
173
- } else {
174
- fail(
175
- `js-bun-db: ${rel}:${v.line} — значення для IN (...) у template literal треба винести в окрему змінну ` +
176
- `і перевірити на пустоту (throw), не підставляти вираз напряму (js-bun-db.mdc): ${v.snippet}`
177
- )
178
- }
179
- }
139
+ return { hasBunSqlImport, ...counts }
140
+ }
141
+
142
+ /**
143
+ * Сканує один файл усіма AST-сканерами bun-sql і реєструє знайдені порушення.
144
+ * @param {string} content вміст файлу
145
+ * @param {string} rel posix-шлях відносно `repoRoot`
146
+ * @param {(msg: string) => void} fail callback при помилці
147
+ * @param {{ perRequest: number, unsafeCall: number, dynamicList: number, inListGuard: number }} counts акумулятори
148
+ * @returns {void}
149
+ */
150
+ function scanFileForBunSqlPatterns(content, rel, fail, counts) {
151
+ for (const v of findBunSqlPerRequestConnectionInText(content, rel)) {
152
+ counts.perRequest++
153
+ fail(
154
+ `js-bun-db: ${rel}:${v.line} — не створюй new SQL(...) всередині функцій; ` +
155
+ `тримай singleton на рівні модуля (js-bun-db.mdc): ${v.snippet}`
156
+ )
157
+ }
158
+ for (const v of findUnsafeBunSqlUnsafeCallInText(content, rel)) {
159
+ counts.unsafeCall++
160
+ fail(
161
+ `js-bun-db: ${rel}:${v.line} — sql.unsafe(\`...\${...}...\`) недопустимо: ` +
162
+ `використовуй tagged template sql\`...\${value}...\` або sql.unsafe('static', [params]) (js-bun-db.mdc): ${v.snippet}`
163
+ )
164
+ }
165
+ for (const v of findUnsafeBunSqlDynamicSqlListInText(content, rel)) {
166
+ counts.dynamicList++
167
+ fail(
168
+ `js-bun-db: ${rel}:${v.line} — заборонено підставляти у SQL динамічні списки через .join(',') ` +
169
+ IN (...) / VALUES (...); використовуй sql([...]) (js-bun-db.mdc): ${v.snippet}`
170
+ )
171
+ }
172
+ for (const v of findUnsafeBunSqlInListMissingEmptyGuardInText(content, rel)) {
173
+ counts.inListGuard++
174
+ fail(messageForBunSqlInListGuard(rel, v))
180
175
  }
176
+ }
181
177
 
182
- return { hasBunSqlImport, perRequest, unsafeCall, dynamicList, inListGuard }
178
+ /**
179
+ * Будує повідомлення `fail` для порушення `findUnsafeBunSqlInListMissingEmptyGuardInText`
180
+ * залежно від `reason` (різні діагностики однакового сімейства).
181
+ * @param {string} rel posix-шлях відносно кореня репо
182
+ * @param {{ line: number, snippet: string, name?: string, reason: string }} v порушення
183
+ * @returns {string} готове повідомлення для `fail`
184
+ */
185
+ function messageForBunSqlInListGuard(rel, v) {
186
+ if (v.reason === 'missing_guard') {
187
+ return (
188
+ `js-bun-db: ${rel}:${v.line} — перед IN-списком ${JSON.stringify(v.name)} потрібна перевірка на пустоту ` +
189
+ `з throw (наприклад if (!${v.name}.length) throw ...), інакше можливі некоректні запити (js-bun-db.mdc): ${v.snippet}`
190
+ )
191
+ }
192
+ if (v.reason === 'sql_helper_not_var') {
193
+ return (
194
+ `js-bun-db: ${rel}:${v.line} — IN-список у \${sql(...)} має підставлятись зі змінної (Identifier) ` +
195
+ `після валідації на пустоту + throw (js-bun-db.mdc): ${v.snippet}`
196
+ )
197
+ }
198
+ return (
199
+ `js-bun-db: ${rel}:${v.line} — значення для IN (...) у template literal треба винести в окрему змінну ` +
200
+ `і перевірити на пустоту (throw), не підставляти вираз напряму (js-bun-db.mdc): ${v.snippet}`
201
+ )
183
202
  }
184
203
 
185
204
  /**
@@ -4,7 +4,8 @@
4
4
  * Канонічний `lint-js`, flat ESLint з getConfig і ignore для auto-imports, рекомендації VSCode,
5
5
  * `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/scripts/utils/oxlint-canonical.json`):
6
6
  * plugins, jsPlugins, categories, усі правила з канону (додаткові записи в `rules` дозволені), settings, env,
7
- * globals, ignorePatterns. `@nitra/eslint-config` у devDependencies мінімум **3.6.12** (транзитивний
7
+ * globals, ignorePatterns. `@nitra/eslint-config` у devDependencies мінімум **3.8.0** (з цієї версії
8
+ * правило `no-restricted-syntax` для `ForInStatement` забороняє `for...in`; також тягне транзитивний
8
9
  * `@e18e/eslint-plugin` для oxlint), `.jscpd.json` (gitignore, exitCode, reporters, minLines), workflow
9
10
  * `lint-js.yml` (checkout@v6, setup-bun-deps, bunx без --fix), без prettier, `engines.node` >= 24,
10
11
  * `engines.bun` >= 1.3, `"type": "module"` у кореневому і всіх workspace `package.json`. Дубль перевірки JS у `lint.yml` — заборонено.
@@ -53,11 +54,12 @@ export function isCanonicalLintJs(script) {
53
54
  }
54
55
 
55
56
  /**
56
- * Чи діапазон `@nitra/eslint-config` у `package.json` передбачає версію з транзитивним `@e18e/eslint-plugin` (>=3.6.12).
57
+ * Чи діапазон `@nitra/eslint-config` у `package.json` задовольняє мінімум `>= 3.8.0`
58
+ * (заборона `for...in` через `no-restricted-syntax` + транзитивний `@e18e/eslint-plugin` для oxlint).
57
59
  * @param {unknown} versionSpec значення `devDependencies['@nitra/eslint-config']`
58
- * @returns {boolean} true для `workspace:*` або першої semver у рядку >= 3.6.12
60
+ * @returns {boolean} true для `workspace:*` або першої semver у рядку >= 3.8.0
59
61
  */
60
- export function nitraEslintConfigDeclaresE18eTransitive(versionSpec) {
62
+ export function nitraEslintConfigMeetsMinVersion(versionSpec) {
61
63
  const s = String(versionSpec).trim()
62
64
  if (s.startsWith('workspace:')) {
63
65
  return true
@@ -70,7 +72,7 @@ export function nitraEslintConfigDeclaresE18eTransitive(versionSpec) {
70
72
  if ([major, minor, patch].some(n => Number.isNaN(n))) {
71
73
  return false
72
74
  }
73
- return major > 3 || (major === 3 && minor > 5) || (major === 3 && minor === 5 && patch >= 0)
75
+ return major > 3 || (major === 3 && minor >= 8 && patch >= 0)
74
76
  }
75
77
 
76
78
  /**
@@ -234,13 +236,13 @@ function checkPackageJsonLintDeps(pkg, passFn, failFn) {
234
236
  const nitraEslint = pkg.devDependencies?.['@nitra/eslint-config']
235
237
  if (nitraEslint) {
236
238
  passFn('@nitra/eslint-config є в devDependencies')
237
- if (nitraEslintConfigDeclaresE18eTransitive(nitraEslint)) {
239
+ if (nitraEslintConfigMeetsMinVersion(nitraEslint)) {
238
240
  passFn(
239
- '@nitra/eslint-config: мінімум 3.6.12 (транзитивний @e18e/eslint-plugin для oxlint jsPlugins, js-lint.mdc)'
241
+ '@nitra/eslint-config: мінімум 3.8.0 (no-restricted-syntax для ForInStatement + @e18e/eslint-plugin транзитивно, js-lint.mdc)'
240
242
  )
241
243
  } else {
242
244
  failFn(
243
- '@nitra/eslint-config: онови до мінімум "^3.6.12" — з цієї версії постачається @e18e/eslint-plugin для .oxlintrc.json (js-lint.mdc)'
245
+ '@nitra/eslint-config: онови до мінімум "^3.8.0" — з цієї версії правило no-restricted-syntax забороняє for...in (плюс транзитивний @e18e/eslint-plugin для oxlint, js-lint.mdc)'
244
246
  )
245
247
  }
246
248
  } else {
@@ -16,7 +16,7 @@
16
16
  * з `@nitra/check-env` (для обов'язкових змінних, із `checkEnv([...])`) або з
17
17
  * `node:process` (для опційних). Коли `env` імпортовано з `@nitra/check-env`,
18
18
  * кожен `env.X` має бути закритий літеральним викликом `checkEnv(['X', ...])`
19
- * у тому ж файлі або коментарем `// @nitra/cursor ignore-next-line checkEnv`
19
+ * у тому ж файлі або коментарем `// \@nitra/cursor ignore-next-line checkEnv`
20
20
  * на попередньому рядку (див. `utils/check-env-scan.mjs`).
21
21
  */
22
22
  import { existsSync } from 'node:fs'
@@ -156,22 +156,7 @@ async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
156
156
  async function checkWorkspacePackage(rootDir, fail, passFn) {
157
157
  const label = `[${rootDir}] `
158
158
  const absPackageRoot = join(process.cwd(), rootDir)
159
- /** @type {unknown} */
160
- let pkgJson = null
161
- const pkgPath = join(rootDir, 'package.json')
162
- if (existsSync(pkgPath)) {
163
- pkgJson = JSON.parse(await readFile(pkgPath, 'utf8'))
164
- const deps = /** @type {Record<string, unknown>} */ (pkgJson).dependencies
165
- const devDeps = /** @type {Record<string, unknown>} */ (pkgJson).devDependencies
166
- const allDeps = { ...(deps || {}), ...(devDeps || {}) }
167
-
168
- if (allDeps['@nitra/bunyan']) {
169
- fail(`${label}@nitra/bunyan знайдено — замінити на @nitra/pino`)
170
- }
171
- if (allDeps.bunyan) {
172
- fail(`${label}bunyan знайдено — замінити на @nitra/pino`)
173
- }
174
- }
159
+ const pkgJson = await loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail)
175
160
 
176
161
  const importViolations = await checkBunyanImports(absPackageRoot, label, fail)
177
162
  if (importViolations === 0) {
@@ -193,19 +178,54 @@ async function checkWorkspacePackage(rootDir, fail, passFn) {
193
178
  )
194
179
  }
195
180
 
181
+ await checkOtelConfigmap(rootDir, label, fail, passFn)
182
+ }
183
+
184
+ /**
185
+ * Завантажує `package.json` пакета (якщо є) і реєструє порушення для bunyan-залежностей.
186
+ * @param {string} rootDir відносний шлях workspace
187
+ * @param {string} label префікс повідомлення `[<pkg>] `
188
+ * @param {(msg: string) => void} fail callback при помилці
189
+ * @returns {Promise<unknown>} розпарсений package.json або null
190
+ */
191
+ async function loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail) {
192
+ const pkgPath = join(rootDir, 'package.json')
193
+ if (!existsSync(pkgPath)) return null
194
+ const pkgJson = JSON.parse(await readFile(pkgPath, 'utf8'))
195
+ const deps = /** @type {Record<string, unknown>} */ (pkgJson).dependencies
196
+ const devDeps = /** @type {Record<string, unknown>} */ (pkgJson).devDependencies
197
+ const allDeps = { ...deps, ...devDeps }
198
+ if (allDeps['@nitra/bunyan']) {
199
+ fail(`${label}@nitra/bunyan знайдено — замінити на @nitra/pino`)
200
+ }
201
+ if (allDeps.bunyan) {
202
+ fail(`${label}bunyan знайдено — замінити на @nitra/pino`)
203
+ }
204
+ return pkgJson
205
+ }
206
+
207
+ /**
208
+ * Перевіряє вміст `k8s/base/configmap.yaml` пакета на наявність OTEL_RESOURCE_ATTRIBUTES
209
+ * з обов'язковими `service.name=` та `service.namespace=` всередині.
210
+ * @param {string} rootDir відносний шлях workspace
211
+ * @param {string} label префікс повідомлення `[<pkg>] `
212
+ * @param {(msg: string) => void} fail callback при помилці
213
+ * @param {(msg: string) => void} passFn успішне повідомлення
214
+ * @returns {Promise<void>} завершується після перевірки configmap
215
+ */
216
+ async function checkOtelConfigmap(rootDir, label, fail, passFn) {
196
217
  const configmapPath = join(rootDir, 'k8s', 'base', 'configmap.yaml')
197
- if (existsSync(configmapPath)) {
198
- const content = await readFile(configmapPath, 'utf8')
199
- if (content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
200
- passFn(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
201
- if (content.includes('service.name=') && content.includes('service.namespace=')) {
202
- passFn(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
203
- } else {
204
- fail(`${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`)
205
- }
206
- } else {
207
- fail(`${label}k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES`)
208
- }
218
+ if (!existsSync(configmapPath)) return
219
+ const content = await readFile(configmapPath, 'utf8')
220
+ if (!content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
221
+ fail(`${label}k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES`)
222
+ return
223
+ }
224
+ passFn(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
225
+ if (content.includes('service.name=') && content.includes('service.namespace=')) {
226
+ passFn(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
227
+ } else {
228
+ fail(`${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`)
209
229
  }
210
230
  }
211
231