@nitra/cursor 1.8.154 → 1.8.156
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/bin/auto-rules.md +1 -1
- package/mdc/js-lint.mdc +3 -5
- package/mdc/js-run.mdc +133 -0
- package/package.json +1 -1
- package/scripts/auto-rules.mjs +65 -2
- package/scripts/check-js-lint.mjs +34 -9
- package/scripts/check-js-run.mjs +233 -0
- package/scripts/check-k8s.mjs +2 -2
- package/scripts/utils/bunyan-imports.mjs +1 -1
- package/scripts/utils/check-env-scan.mjs +292 -0
- package/scripts/utils/conn-imports-scan.mjs +144 -0
- package/mdc/js-pino.mdc +0 -15
- package/scripts/check-js-pino.mjs +0 -120
package/bin/auto-rules.md
CHANGED
|
@@ -18,7 +18,7 @@ graphql - якщо хоч в одному js або vue файлі присут
|
|
|
18
18
|
|
|
19
19
|
js-lint - якщо присутній хоч один js файл
|
|
20
20
|
|
|
21
|
-
js-
|
|
21
|
+
js-run - якщо це вкладена директорія з package.json (не в корені) та в devDependencies немає vite
|
|
22
22
|
|
|
23
23
|
js-mssql - якщо в хоч одному package.json в секції dependencies присутній пакет mssql
|
|
24
24
|
|
package/mdc/js-lint.mdc
CHANGED
|
@@ -26,9 +26,6 @@ version: '1.15'
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@nitra/eslint-config": "^3.6.12"
|
|
29
|
-
},
|
|
30
|
-
"engines": {
|
|
31
|
-
"node": ">=24"
|
|
32
29
|
}
|
|
33
30
|
}
|
|
34
31
|
```
|
|
@@ -138,11 +135,12 @@ export default [
|
|
|
138
135
|
|
|
139
136
|
## Додаткові js правила
|
|
140
137
|
|
|
141
|
-
Завжди додавай до package.json що підтримується 24+ версія node
|
|
138
|
+
Завжди додавай до package.json що підтримується 24+ версія node і Bun 1.3+:
|
|
142
139
|
|
|
143
140
|
```json title="package.json"
|
|
144
141
|
"engines": {
|
|
145
|
-
"node": ">=24"
|
|
142
|
+
"node": ">=24",
|
|
143
|
+
"bun": ">=1.3"
|
|
146
144
|
}
|
|
147
145
|
```
|
|
148
146
|
|
package/mdc/js-run.mdc
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Це правила для backend проектів на JavaScript/Node.js, сюди входять і job і WEB сервери.
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '1.1'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Структура проекту
|
|
8
|
+
|
|
9
|
+
Рекомендується використовувати таку структуру проекту:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
k8s/ # тут всі файли для деплойменту в Kubernetes, включаючи kustomize
|
|
13
|
+
src/ # тут всі файли необхідні для роботи проекту
|
|
14
|
+
Dockerfile
|
|
15
|
+
package.json
|
|
16
|
+
readme.md
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Використання @nitra/pino
|
|
21
|
+
|
|
22
|
+
Проект використовує @nitra/pino для логування.
|
|
23
|
+
Якщо в проекті присутній @nitra/bunyan, то він повинен бути замінений на @nitra/pino — як у `package.json`, так і в коді: усі `import` / `require` / динамічні `import()` з `@nitra/bunyan` (і застарілого `bunyan`) треба замінити на `@nitra/pino` і за потреби адаптувати виклики під його API.
|
|
24
|
+
|
|
25
|
+
В **/k8s/base/configmap.yaml повинен бути заданий OTEL_RESOURCE_ATTRIBUTES: 'service.name=<project_name>,service.namespace=<project_namespace>'
|
|
26
|
+
а в директоріях з kustomize повинні бути перевизначені значення OTEL_RESOURCE_ATTRIBUTES і в них service.namespace повинен відповідати namespace, в якому знаходиться дана директорія.
|
|
27
|
+
|
|
28
|
+
## Внутрішні аліаси
|
|
29
|
+
|
|
30
|
+
Якщо в проекті є підключення до баз даних, зовнішніх graphql на кшталт:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { SQL } from 'bun'
|
|
34
|
+
|
|
35
|
+
// або
|
|
36
|
+
|
|
37
|
+
import sql from 'mssql'
|
|
38
|
+
|
|
39
|
+
// або
|
|
40
|
+
|
|
41
|
+
import { GraphQLClient } from '@nitra/graphql-request'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
то ці підключення повинні бути винесені в окремий файл, наприклад `/src/conn/pg.js`, в package.json повинні бути додано аліас:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"imports": {
|
|
49
|
+
"#conn/*": "./src/conn/*"
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
так виглядатиме підключення до PostgreSQL в коді:
|
|
56
|
+
```javascript title="Приклад підключення до PostgreSQL в /src/conn/pg.js"
|
|
57
|
+
import { checkEnv, env } from '@nitra/check-env'
|
|
58
|
+
import { SQL } from 'bun'
|
|
59
|
+
|
|
60
|
+
checkEnv(['PG_CONN'])
|
|
61
|
+
|
|
62
|
+
export const pool = new SQL({ url: env.PG_CONN })
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
а так до GraphQL:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
import { checkEnv, env } from '@nitra/check-env'
|
|
70
|
+
import { GraphQLClient } from '@nitra/graphql-request'
|
|
71
|
+
|
|
72
|
+
checkEnv(['QL', 'X_HASURA_ADMIN_SECRET'])
|
|
73
|
+
|
|
74
|
+
export { gql } from '@nitra/graphql-request'
|
|
75
|
+
|
|
76
|
+
export const graphQLClientSmart = new GraphQLClient(env.QL, {
|
|
77
|
+
headers: {
|
|
78
|
+
'X-Hasura-Admin-Secret': env.X_HASURA_ADMIN_SECRET
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
а в коді повинно бути використано:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
import { pool } from '#conn/pg.js'
|
|
88
|
+
|
|
89
|
+
// або
|
|
90
|
+
|
|
91
|
+
import { gql, graphQLClient } from '@nitra/graphql-request'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
## CheckEnv
|
|
97
|
+
|
|
98
|
+
Усі змінні оточення, які використовуються в коді, повинні бути перевірені за допомогою `checkEnv` з пакету `@nitra/check-env`. Це гарантує, що всі необхідні змінні оточення встановлені перед запуском програми.
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
```javascript title="Приклад підключення до PostgreSQL в /src/conn/pg.js"
|
|
102
|
+
import { checkEnv, env } from '@nitra/check-env'
|
|
103
|
+
import { SQL } from 'bun'
|
|
104
|
+
|
|
105
|
+
checkEnv(['PG_CONN'])
|
|
106
|
+
|
|
107
|
+
export const pool = new SQL({ url: env.PG_CONN })
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## process.env
|
|
113
|
+
|
|
114
|
+
Прямий доступ до `process.env.X` у коді заборонений — його треба замінити на `env`:
|
|
115
|
+
|
|
116
|
+
- **обов'язкова змінна** — `import { checkEnv, env } from '@nitra/check-env'` плюс `checkEnv(['X'])`
|
|
117
|
+
у тому ж файлі (приклад див. вище в розділі **CheckEnv**);
|
|
118
|
+
- **опційна змінна** — `import { env } from 'node:process'`:
|
|
119
|
+
|
|
120
|
+
```javascript title="Опційна змінна — env з node:process"
|
|
121
|
+
import { env } from 'node:process'
|
|
122
|
+
|
|
123
|
+
console.log(env.OPTIONAL_ENV_VAR)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Тимчасово приглушити перевірку для конкретного рядка можна коментарем
|
|
127
|
+
`// @nitra/cursor ignore-next-line checkEnv` безпосередньо перед використанням
|
|
128
|
+
(escape-hatch для legacy-коду, не для нових файлів).
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
## Перевірка
|
|
132
|
+
|
|
133
|
+
`npx @nitra/cursor check js-run`
|
package/package.json
CHANGED
package/scripts/auto-rules.mjs
CHANGED
|
@@ -31,7 +31,7 @@ export const AUTO_RULE_ORDER = Object.freeze([
|
|
|
31
31
|
'js-lint',
|
|
32
32
|
'js-mssql',
|
|
33
33
|
'js-bun-db',
|
|
34
|
-
'js-
|
|
34
|
+
'js-run',
|
|
35
35
|
'k8s',
|
|
36
36
|
'nginx-default-tpl',
|
|
37
37
|
'npm-module',
|
|
@@ -138,6 +138,68 @@ async function collectDependencyKeysPresentInPackageJsonTree(root, dependencyKey
|
|
|
138
138
|
return found
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Перевіряє, чи існує хоча б один вкладений `package.json` (не кореневий),
|
|
143
|
+
* у якому в `devDependencies` відсутня залежність `vite`.
|
|
144
|
+
* @param {string} root абсолютний шлях до кореня репозиторію
|
|
145
|
+
* @returns {Promise<boolean>} true, якщо знайдено вкладений package.json без `vite` у devDependencies
|
|
146
|
+
*/
|
|
147
|
+
async function hasNestedPackageJsonWithoutViteDevDependency(root) {
|
|
148
|
+
let result = false
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Перевіряє один package.json: повертає true, якщо в `devDependencies` немає `vite`.
|
|
152
|
+
* @param {string} absPath абсолютний шлях до package.json
|
|
153
|
+
* @returns {Promise<boolean>} true, якщо vite відсутній у devDependencies
|
|
154
|
+
*/
|
|
155
|
+
async function packageJsonLacksViteDevDependency(absPath) {
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(await readFile(absPath, 'utf8'))
|
|
158
|
+
const devDeps = parsed?.devDependencies
|
|
159
|
+
if (!devDeps || typeof devDeps !== 'object' || Array.isArray(devDeps)) {
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
return !Object.hasOwn(devDeps, 'vite')
|
|
163
|
+
} catch {
|
|
164
|
+
return false
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Рекурсивний обхід каталогу з пропуском службових директорій.
|
|
170
|
+
* @param {string} dir абсолютний шлях каталогу
|
|
171
|
+
* @returns {Promise<void>}
|
|
172
|
+
*/
|
|
173
|
+
async function walk(dir) {
|
|
174
|
+
if (result) return
|
|
175
|
+
let entries
|
|
176
|
+
try {
|
|
177
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
178
|
+
} catch {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
if (result) return
|
|
183
|
+
const absPath = join(dir, entry.name)
|
|
184
|
+
if (entry.isDirectory()) {
|
|
185
|
+
if (!IGNORED_DIR_NAMES.has(entry.name)) {
|
|
186
|
+
await walk(absPath)
|
|
187
|
+
}
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
if (entry.isFile() && entry.name === 'package.json' && absPath !== join(root, 'package.json')) {
|
|
191
|
+
if (await packageJsonLacksViteDevDependency(absPath)) {
|
|
192
|
+
result = true
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await walk(root)
|
|
200
|
+
return result
|
|
201
|
+
}
|
|
202
|
+
|
|
141
203
|
/**
|
|
142
204
|
* Фіксує ознаки, що залежать лише від імені підкаталогу.
|
|
143
205
|
* @param {string} dirName імʼя каталогу
|
|
@@ -438,6 +500,7 @@ export async function detectAutoRulesAndSkills({
|
|
|
438
500
|
const depHits = await collectDependencyKeysPresentInPackageJsonTree(root, ['mssql', 'pg', 'pg-format', 'mysql2'])
|
|
439
501
|
const hasMssqlDependency = depHits.has('mssql')
|
|
440
502
|
const hasJsBunDbSignal = depHits.has('pg') || depHits.has('pg-format') || depHits.has('mysql2') || facts.hasBunSqlImport
|
|
503
|
+
const hasNestedNodePackage = await hasNestedPackageJsonWithoutViteDevDependency(root)
|
|
441
504
|
|
|
442
505
|
/** @type {string[]} */
|
|
443
506
|
const detectedRules = []
|
|
@@ -478,7 +541,7 @@ export async function detectAutoRulesAndSkills({
|
|
|
478
541
|
{ enabled: facts.hasJsLikeSource, id: 'js-lint' },
|
|
479
542
|
{ enabled: hasMssqlDependency, id: 'js-mssql' },
|
|
480
543
|
{ enabled: hasJsBunDbSignal, id: 'js-bun-db' },
|
|
481
|
-
{ enabled:
|
|
544
|
+
{ enabled: hasNestedNodePackage, id: 'js-run' },
|
|
482
545
|
{ enabled: facts.hasK8sDir, id: 'k8s' },
|
|
483
546
|
{ enabled: facts.hasNginxDefaultTplFile, id: 'nginx-default-tpl' },
|
|
484
547
|
{ enabled: npmDirExists, id: 'npm-module' },
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* globals, ignorePatterns. `@nitra/eslint-config` у devDependencies мінімум **3.6.12** (транзитивний
|
|
8
8
|
* `@e18e/eslint-plugin` для oxlint), `.jscpd.json` (gitignore, exitCode, reporters, minLines), workflow
|
|
9
9
|
* `lint-js.yml` (checkout@v6, setup-bun-deps, bunx без --fix), без prettier, `engines.node` >= 24,
|
|
10
|
-
* `"type": "module"` у кореневому і всіх workspace `package.json`. Дубль перевірки JS у `lint.yml` — заборонено.
|
|
10
|
+
* `engines.bun` >= 1.3, `"type": "module"` у кореневому і всіх workspace `package.json`. Дубль перевірки JS у `lint.yml` — заборонено.
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync } from 'node:fs'
|
|
13
13
|
import { readFile } from 'node:fs/promises'
|
|
@@ -264,38 +264,62 @@ function checkPackageJsonTypeModule(label, pkg, passFn, failFn) {
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
/**
|
|
267
|
-
* `"type": "module"` у
|
|
267
|
+
* `"type": "module"`, `engines.node >= 24` і `engines.bun >= 1.3` у кожному workspace `package.json`.
|
|
268
268
|
* @param {unknown[]} workspaces поле workspaces з package.json
|
|
269
269
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
270
270
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
271
271
|
*/
|
|
272
|
-
async function
|
|
272
|
+
async function checkWorkspacePackages(workspaces, passFn, failFn) {
|
|
273
273
|
for (const ws of workspaces) {
|
|
274
274
|
const wsPkgPath = `${ws}/package.json`
|
|
275
275
|
if (existsSync(wsPkgPath)) {
|
|
276
276
|
const wsPkg = JSON.parse(await readFile(wsPkgPath, 'utf8'))
|
|
277
277
|
checkPackageJsonTypeModule(wsPkgPath, wsPkg, passFn, failFn)
|
|
278
|
+
checkEnginesNode(wsPkgPath, wsPkg, passFn, failFn)
|
|
279
|
+
checkEnginesBun(wsPkgPath, wsPkg, passFn, failFn)
|
|
278
280
|
}
|
|
279
281
|
}
|
|
280
282
|
}
|
|
281
283
|
|
|
282
284
|
/**
|
|
283
285
|
* engines.node >= 24.
|
|
286
|
+
* @param {string} label шлях або назва пакета для повідомлень
|
|
284
287
|
* @param {{ engines?: { node?: string } }} pkg розпарсений package.json
|
|
285
288
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
286
289
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
287
290
|
*/
|
|
288
|
-
function checkEnginesNode(pkg, passFn, failFn) {
|
|
291
|
+
function checkEnginesNode(label, pkg, passFn, failFn) {
|
|
289
292
|
const nodeEngine = pkg.engines?.node
|
|
290
293
|
if (nodeEngine) {
|
|
291
294
|
const firstNumeric = String(nodeEngine).split(NON_DIGITS_RE).find(Boolean)
|
|
292
295
|
if (firstNumeric && Number(firstNumeric) >= 24) {
|
|
293
|
-
passFn(
|
|
296
|
+
passFn(`${label}: engines.node "${nodeEngine}"`)
|
|
297
|
+
} else {
|
|
298
|
+
failFn(`${label}: engines.node "${nodeEngine}" — має бути >=24`)
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
failFn(`${label} не містить engines.node — додай: "engines": { "node": ">=24" }`)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* engines.bun >= 1.3.
|
|
307
|
+
* @param {string} label шлях або назва пакета для повідомлень
|
|
308
|
+
* @param {{ engines?: { bun?: string } }} pkg розпарсений package.json
|
|
309
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
310
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
311
|
+
*/
|
|
312
|
+
function checkEnginesBun(label, pkg, passFn, failFn) {
|
|
313
|
+
const bunEngine = pkg.engines?.bun
|
|
314
|
+
if (bunEngine) {
|
|
315
|
+
const [major, minor] = String(bunEngine).split(NON_DIGITS_RE).filter(Boolean).map(Number)
|
|
316
|
+
if (Number.isFinite(major) && Number.isFinite(minor) && (major > 1 || (major === 1 && minor >= 3))) {
|
|
317
|
+
passFn(`${label}: engines.bun "${bunEngine}"`)
|
|
294
318
|
} else {
|
|
295
|
-
failFn(
|
|
319
|
+
failFn(`${label}: engines.bun "${bunEngine}" — має бути >=1.3`)
|
|
296
320
|
}
|
|
297
321
|
} else {
|
|
298
|
-
failFn(
|
|
322
|
+
failFn(`${label} не містить engines.bun — додай: "engines": { "bun": ">=1.3" }`)
|
|
299
323
|
}
|
|
300
324
|
}
|
|
301
325
|
|
|
@@ -311,7 +335,7 @@ async function checkPackageJsonJsLint(passFn, failFn) {
|
|
|
311
335
|
checkPackageJsonTypeModule('package.json', pkg, passFn, failFn)
|
|
312
336
|
|
|
313
337
|
const workspaces = Array.isArray(pkg.workspaces) ? pkg.workspaces : []
|
|
314
|
-
await
|
|
338
|
+
await checkWorkspacePackages(workspaces, passFn, failFn)
|
|
315
339
|
|
|
316
340
|
const lintJs = pkg.scripts?.['lint-js']
|
|
317
341
|
if (lintJs) {
|
|
@@ -328,7 +352,8 @@ async function checkPackageJsonJsLint(passFn, failFn) {
|
|
|
328
352
|
}
|
|
329
353
|
|
|
330
354
|
checkPackageJsonLintDeps(pkg, passFn, failFn)
|
|
331
|
-
checkEnginesNode(pkg, passFn, failFn)
|
|
355
|
+
checkEnginesNode('package.json', pkg, passFn, failFn)
|
|
356
|
+
checkEnginesBun('package.json', pkg, passFn, failFn)
|
|
332
357
|
}
|
|
333
358
|
|
|
334
359
|
/**
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Для кожного workspace-пакета перевіряє правило js-run.mdc.
|
|
3
|
+
*
|
|
4
|
+
* Покрито:
|
|
5
|
+
* - заборона `@nitra/bunyan` / `bunyan` як у залежностях `package.json`, так і в коді
|
|
6
|
+
* (`import` / `require` / динамічний `import()`); імпорти сканує AST через `oxc-parser`
|
|
7
|
+
* (див. `utils/bunyan-imports.mjs`);
|
|
8
|
+
* - наявність `OTEL_RESOURCE_ATTRIBUTES` зі значеннями `service.name=` та `service.namespace=`
|
|
9
|
+
* у `k8s/base/configmap.yaml`, якщо такий файл існує (відповідність імені ConfigMap імені
|
|
10
|
+
* Deployment перевіряється в `check-k8s.mjs`);
|
|
11
|
+
* - «Внутрішні аліаси» (`#conn/*`): імпорти `bun#SQL`, будь-який `mssql`, `@nitra/graphql-request#GraphQLClient`
|
|
12
|
+
* дозволені лише у каталозі conn (за замовчуванням `src/conn/`; за наявності
|
|
13
|
+
* `package.json#imports['#conn/*']` — у його цільовому каталозі); поза ним — порушення
|
|
14
|
+
* (див. `utils/conn-imports-scan.mjs`);
|
|
15
|
+
* - «process.env / CheckEnv»: пряме `process.env.X` має бути замінено на `env` —
|
|
16
|
+
* з `@nitra/check-env` (для обов'язкових змінних, із `checkEnv([...])`) або з
|
|
17
|
+
* `node:process` (для опційних). Коли `env` імпортовано з `@nitra/check-env`,
|
|
18
|
+
* кожен `env.X` має бути закритий літеральним викликом `checkEnv(['X', ...])`
|
|
19
|
+
* у тому ж файлі або коментарем `// @nitra/cursor ignore-next-line checkEnv`
|
|
20
|
+
* на попередньому рядку (див. `utils/check-env-scan.mjs`).
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync } from 'node:fs'
|
|
23
|
+
import { readFile } from 'node:fs/promises'
|
|
24
|
+
import { join, relative } from 'node:path'
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
findBunyanImportsInText,
|
|
28
|
+
isBunyanScanSourceFile,
|
|
29
|
+
shouldSkipFileForBunyanScan
|
|
30
|
+
} from './utils/bunyan-imports.mjs'
|
|
31
|
+
import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from './utils/check-env-scan.mjs'
|
|
32
|
+
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
33
|
+
import {
|
|
34
|
+
findConnFactoryImportsInText,
|
|
35
|
+
isConnImportsScanSourceFile,
|
|
36
|
+
isInsideConnDir,
|
|
37
|
+
resolveConnDirFromPackageJson
|
|
38
|
+
} from './utils/conn-imports-scan.mjs'
|
|
39
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
40
|
+
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Перетворює абсолютний шлях у posix-формі відносно кореня пакета.
|
|
44
|
+
* @param {string} absPackageRoot абсолютний корінь пакета
|
|
45
|
+
* @param {string} absPath абсолютний шлях до файлу
|
|
46
|
+
* @returns {string} відносний posix-шлях
|
|
47
|
+
*/
|
|
48
|
+
function relPosix(absPackageRoot, absPath) {
|
|
49
|
+
return relative(absPackageRoot, absPath).split('\\').join('/')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Сканує джерела пакета на заборонені імпорти `@nitra/bunyan` / `bunyan`.
|
|
54
|
+
* @param {string} absPackageRoot абсолютний шлях до кореня пакета
|
|
55
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
56
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
57
|
+
* @returns {Promise<number>} кількість знайдених порушень
|
|
58
|
+
*/
|
|
59
|
+
async function checkBunyanImports(absPackageRoot, label, fail) {
|
|
60
|
+
/** @type {string[]} */
|
|
61
|
+
const sourcePaths = []
|
|
62
|
+
await walkDir(absPackageRoot, absPath => {
|
|
63
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
64
|
+
if (!shouldSkipFileForBunyanScan(rel) && isBunyanScanSourceFile(rel)) {
|
|
65
|
+
sourcePaths.push(absPath)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
let violations = 0
|
|
70
|
+
for (const absPath of sourcePaths) {
|
|
71
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
72
|
+
const content = await readFile(absPath, 'utf8')
|
|
73
|
+
for (const v of findBunyanImportsInText(content, rel)) {
|
|
74
|
+
violations++
|
|
75
|
+
fail(`${label}${rel}:${v.line} — заміни '${v.module}' на '@nitra/pino': ${v.snippet}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return violations
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Збирає всі JS/TS-файли пакета (без node_modules, dist тощо).
|
|
83
|
+
* @param {string} absPackageRoot абсолютний шлях до кореня пакета
|
|
84
|
+
* @returns {Promise<string[]>} абсолютні шляхи до файлів
|
|
85
|
+
*/
|
|
86
|
+
async function collectSourceFiles(absPackageRoot) {
|
|
87
|
+
/** @type {string[]} */
|
|
88
|
+
const out = []
|
|
89
|
+
await walkDir(absPackageRoot, absPath => {
|
|
90
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
91
|
+
if (isCheckEnvScanSourceFile(rel)) out.push(absPath)
|
|
92
|
+
})
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Перевіряє правило «Внутрішні аліаси» для пакета.
|
|
98
|
+
* @param {string} absPackageRoot абсолютний корінь пакета
|
|
99
|
+
* @param {string[]} sourcePaths абсолютні шляхи до файлів
|
|
100
|
+
* @param {unknown} pkgJson розпарсений `package.json` пакета (або null)
|
|
101
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
102
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
103
|
+
* @returns {Promise<number>} кількість порушень
|
|
104
|
+
*/
|
|
105
|
+
async function checkConnImports(absPackageRoot, sourcePaths, pkgJson, label, fail) {
|
|
106
|
+
const connDir = resolveConnDirFromPackageJson(pkgJson)
|
|
107
|
+
let violations = 0
|
|
108
|
+
for (const absPath of sourcePaths) {
|
|
109
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
110
|
+
if (!isConnImportsScanSourceFile(rel)) continue
|
|
111
|
+
if (isInsideConnDir(rel, connDir)) continue
|
|
112
|
+
const content = await readFile(absPath, 'utf8')
|
|
113
|
+
for (const v of findConnFactoryImportsInText(content, rel)) {
|
|
114
|
+
violations++
|
|
115
|
+
const target = v.specifier === '*' ? `'${v.module}'` : `{ ${v.specifier} } from '${v.module}'`
|
|
116
|
+
fail(
|
|
117
|
+
`${label}${rel}:${v.line} — імпорт ${target} має бути в '${connDir}/' і реекспортуватися через '#conn/*': ${v.snippet}`
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return violations
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Перевіряє правило «CheckEnv» для пакета.
|
|
126
|
+
* @param {string} absPackageRoot абсолютний корінь пакета
|
|
127
|
+
* @param {string[]} sourcePaths абсолютні шляхи до файлів
|
|
128
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
129
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
130
|
+
* @returns {Promise<number>} кількість порушень
|
|
131
|
+
*/
|
|
132
|
+
async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
|
|
133
|
+
let violations = 0
|
|
134
|
+
for (const absPath of sourcePaths) {
|
|
135
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
136
|
+
const content = await readFile(absPath, 'utf8')
|
|
137
|
+
for (const v of findUncheckedProcessEnvInText(content, rel)) {
|
|
138
|
+
violations++
|
|
139
|
+
const message =
|
|
140
|
+
v.kind === 'process-env'
|
|
141
|
+
? `${label}${rel}:${v.line} — process.env.${v.name}: заміни на env з '@nitra/check-env' (обов'язкова змінна + checkEnv(['${v.name}'])) або з 'node:process' (опційна)`
|
|
142
|
+
: `${label}${rel}:${v.line} — env.${v.name} (з '@nitra/check-env') без checkEnv(['${v.name}']) (або '// @nitra/cursor ignore-next-line checkEnv' попереду)`
|
|
143
|
+
fail(message)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return violations
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Перевіряє відповідність правилам js-run.mdc для одного workspace-пакета.
|
|
151
|
+
* @param {string} rootDir відносний шлях workspace (не `'.'`)
|
|
152
|
+
* @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
|
|
153
|
+
* @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
|
|
154
|
+
* @returns {Promise<void>} завершується після перевірок цього пакета
|
|
155
|
+
*/
|
|
156
|
+
async function checkWorkspacePackage(rootDir, fail, passFn) {
|
|
157
|
+
const label = `[${rootDir}] `
|
|
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
|
+
}
|
|
175
|
+
|
|
176
|
+
const importViolations = await checkBunyanImports(absPackageRoot, label, fail)
|
|
177
|
+
if (importViolations === 0) {
|
|
178
|
+
passFn(`${label}немає імпортів '@nitra/bunyan' / 'bunyan' у джерелах`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const sourcePaths = await collectSourceFiles(absPackageRoot)
|
|
182
|
+
|
|
183
|
+
const connViolations = await checkConnImports(absPackageRoot, sourcePaths, pkgJson, label, fail)
|
|
184
|
+
if (connViolations === 0) {
|
|
185
|
+
const connDir = resolveConnDirFromPackageJson(pkgJson)
|
|
186
|
+
passFn(`${label}імпорти підключень (bun#SQL / mssql / @nitra/graphql-request#GraphQLClient) лише в '${connDir}/'`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const envViolations = await checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail)
|
|
190
|
+
if (envViolations === 0) {
|
|
191
|
+
passFn(
|
|
192
|
+
`${label}немає прямого process.env.*; усі env.* з '@nitra/check-env' закриті checkEnv(['…']) (або '// @nitra/cursor ignore-next-line checkEnv')`
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
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
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Перевіряє відповідність проєкту правилам js-run.mdc лише для workspace-пакетів (не корінь репо).
|
|
214
|
+
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
215
|
+
*/
|
|
216
|
+
export async function check() {
|
|
217
|
+
const reporter = createCheckReporter()
|
|
218
|
+
const { pass, fail } = reporter
|
|
219
|
+
|
|
220
|
+
const roots = await getMonorepoPackageRootDirs()
|
|
221
|
+
const workspaceRoots = roots.filter(r => r !== '.')
|
|
222
|
+
|
|
223
|
+
if (workspaceRoots.length === 0) {
|
|
224
|
+
pass('js-run: немає workspace-пакетів у кореневому package.json — перевірку залежностей і k8s у пакетах пропущено')
|
|
225
|
+
return reporter.getExitCode()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const r of workspaceRoots) {
|
|
229
|
+
await checkWorkspacePackage(r, fail, pass)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return reporter.getExitCode()
|
|
233
|
+
}
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -2594,7 +2594,7 @@ async function readFirstDocByKindFromFile(filePath, kind) {
|
|
|
2594
2594
|
}
|
|
2595
2595
|
|
|
2596
2596
|
/**
|
|
2597
|
-
* Знаходить перший документ **Deployment** серед YAML-файлів каталогу (для перевірки імені ConfigMap, js-
|
|
2597
|
+
* Знаходить перший документ **Deployment** серед YAML-файлів каталогу (для перевірки імені ConfigMap, js-run.mdc).
|
|
2598
2598
|
* @param {string} dirPath абсолютний шлях до каталогу
|
|
2599
2599
|
* @returns {Promise<Record<string, unknown> | null>} об'єкт Deployment або null
|
|
2600
2600
|
*/
|
|
@@ -2678,7 +2678,7 @@ function collectConfigMapRefsFromVolumes(volumes, names) {
|
|
|
2678
2678
|
/**
|
|
2679
2679
|
* Збирає унікальні імена **ConfigMap**, на які посилається **Deployment**
|
|
2680
2680
|
* через `spec.template.spec.containers[*].envFrom[*].configMapRef.name`
|
|
2681
|
-
* та `spec.template.spec.volumes[*].configMap.name` (для перевірки js-
|
|
2681
|
+
* та `spec.template.spec.volumes[*].configMap.name` (для перевірки js-run.mdc).
|
|
2682
2682
|
* @param {Record<string, unknown>} deployment об'єкт Deployment
|
|
2683
2683
|
* @returns {Set<string>} унікальні імена ConfigMap
|
|
2684
2684
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Знаходить імпорти з `@nitra/bunyan` (і застарілого `bunyan`) у джерелах — їх треба замінити
|
|
3
|
-
* на `@nitra/pino` згідно з js-
|
|
3
|
+
* на `@nitra/pino` згідно з js-run.mdc.
|
|
4
4
|
*
|
|
5
5
|
* Семантика береться з **oxc-parser** (`module.staticImports`) — без regex по тілу файлу.
|
|
6
6
|
* Додатково по AST програми ловимо `require('@nitra/bunyan')` і динамічний `import('@nitra/bunyan')`,
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-сканер правила «process.env / CheckEnv» (js-run.mdc).
|
|
3
|
+
*
|
|
4
|
+
* Правило в .mdc формулює два контракти:
|
|
5
|
+
* 1. Прямий доступ до `process.env.X` має бути замінено на `env` — з пакета
|
|
6
|
+
* `@nitra/check-env` (для обов'язкових змінних, із викликом `checkEnv([...])`)
|
|
7
|
+
* або з `node:process` (для опційних). Тому будь-яке `process.env.X` сканер
|
|
8
|
+
* завжди реєструє як порушення з порадою про конкретну заміну.
|
|
9
|
+
* 2. Якщо у файл імпортовано `env` саме з `@nitra/check-env`, то кожне `env.X`
|
|
10
|
+
* має бути закрите літеральним викликом `checkEnv(['X', ...])` у тому ж файлі
|
|
11
|
+
* (порядок не важливий, кілька викликів зливаються в один список).
|
|
12
|
+
*
|
|
13
|
+
* Обидва контракти можна точково «приглушити» коментарем-маркером
|
|
14
|
+
* `// @nitra/cursor ignore-next-line checkEnv` на рядку безпосередньо перед
|
|
15
|
+
* порушенням — це залишається сумісним escape-hatch для legacy-коду.
|
|
16
|
+
*
|
|
17
|
+
* Семантика береться з **oxc-parser** через `parseProgramOrNull`: regex по тілу
|
|
18
|
+
* файлу не використовується, лише сирий текст рядка з коментарем перевіряється
|
|
19
|
+
* на маркер. Якщо файл не парситься — повертаємо порожній результат, спочатку
|
|
20
|
+
* треба полагодити синтаксис.
|
|
21
|
+
*
|
|
22
|
+
* Покриті форми доступу:
|
|
23
|
+
* - `process.env.X` / `process.env['X']` (як MemberExpression);
|
|
24
|
+
* - `const { X, Y } = process.env` (ObjectPattern; ім'я з ключа, не з alias);
|
|
25
|
+
* - аналогічно для `env.X` / `env['X']` / `const { X } = env`,
|
|
26
|
+
* де `env` має бути імпортований з `@nitra/check-env` (інакше ігноруємо —
|
|
27
|
+
* це може бути локальна змінна чи `env` з `node:process`).
|
|
28
|
+
*
|
|
29
|
+
* Якщо ключ обчислюваний (`process.env[varName]`) — пропускаємо без помилки,
|
|
30
|
+
* бо за статичним AST неможливо встановити, яка саме змінна оточення використовується.
|
|
31
|
+
*/
|
|
32
|
+
import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from './ast-scan-utils.mjs'
|
|
33
|
+
|
|
34
|
+
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
35
|
+
const IGNORE_DIRECTIVE_RE = /\/\/\s*@nitra\/cursor\s+ignore-next-line\s+checkEnv\b/u
|
|
36
|
+
|
|
37
|
+
const CHECK_ENV_PACKAGE = '@nitra/check-env'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Чи є цей вузол виразом `process.env`.
|
|
41
|
+
* @param {unknown} node AST вузол
|
|
42
|
+
* @returns {boolean} true, якщо це `MemberExpression` `process.env` (Identifier . Identifier)
|
|
43
|
+
*/
|
|
44
|
+
function isProcessEnvAccess(node) {
|
|
45
|
+
if (!node || typeof node !== 'object') return false
|
|
46
|
+
if (node.type !== 'MemberExpression' || node.computed) return false
|
|
47
|
+
const obj = node.object
|
|
48
|
+
const prop = node.property
|
|
49
|
+
return (
|
|
50
|
+
!!obj &&
|
|
51
|
+
obj.type === 'Identifier' &&
|
|
52
|
+
obj.name === 'process' &&
|
|
53
|
+
!!prop &&
|
|
54
|
+
prop.type === 'Identifier' &&
|
|
55
|
+
prop.name === 'env'
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Витягує ім'я ENV з MemberExpression `obj.X` або `obj['X']`.
|
|
61
|
+
* @param {Record<string, unknown>} node MemberExpression, чий object — `process.env` або `env`
|
|
62
|
+
* @returns {string | null} ім'я змінної оточення або null, якщо ключ не статичний
|
|
63
|
+
*/
|
|
64
|
+
function envNameFromMember(node) {
|
|
65
|
+
const prop = node.property
|
|
66
|
+
if (!prop || typeof prop !== 'object') return null
|
|
67
|
+
if (!node.computed && prop.type === 'Identifier' && typeof prop.name === 'string') return prop.name
|
|
68
|
+
if (node.computed && prop.type === 'Literal' && typeof prop.value === 'string') return prop.value
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Збирає всі літеральні імена з виклику `checkEnv([...])` у файлі.
|
|
74
|
+
* Якщо callee — Identifier `checkEnv` і перший аргумент — ArrayExpression, додає
|
|
75
|
+
* всі string-літерали до set. Не-літеральні елементи (Identifier, SpreadElement) ігноруються —
|
|
76
|
+
* це робить перевірку «ліберальною»: ми лише ловимо явно неперевірені змінні.
|
|
77
|
+
* @param {unknown} programNode корінь AST
|
|
78
|
+
* @returns {Set<string>} перелік закритих імен ENV
|
|
79
|
+
*/
|
|
80
|
+
function collectCheckedEnvNames(programNode) {
|
|
81
|
+
/** @type {Set<string>} */
|
|
82
|
+
const out = new Set()
|
|
83
|
+
walkAstWithAncestors(programNode, [], node => {
|
|
84
|
+
if (node.type !== 'CallExpression') return
|
|
85
|
+
const callee = node.callee
|
|
86
|
+
if (!callee || callee.type !== 'Identifier' || callee.name !== 'checkEnv') return
|
|
87
|
+
const args = node.arguments
|
|
88
|
+
if (!Array.isArray(args) || args.length === 0) return
|
|
89
|
+
const first = args[0]
|
|
90
|
+
if (!first || typeof first !== 'object' || first.type !== 'ArrayExpression') return
|
|
91
|
+
const elements = first.elements
|
|
92
|
+
if (!Array.isArray(elements)) return
|
|
93
|
+
for (const el of elements) {
|
|
94
|
+
if (!el || typeof el !== 'object') continue
|
|
95
|
+
if (el.type === 'Literal' && typeof el.value === 'string') out.add(el.value)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
return out
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Чи імпортовано локальний ідентифікатор `env` саме з `@nitra/check-env`.
|
|
103
|
+
* Перевіряє ImportDeclaration на specifier {imported.name === 'env', local.name === 'env'}.
|
|
104
|
+
* Aliased-варіанти (`{ env as x }`) свідомо не підтримуються — у наших правилах
|
|
105
|
+
* приклади завжди використовують канонічне ім'я `env`.
|
|
106
|
+
* @param {unknown} programNode корінь AST
|
|
107
|
+
* @returns {boolean} true, якщо у файлі є `import { env } from '@nitra/check-env'`
|
|
108
|
+
*/
|
|
109
|
+
function hasCheckEnvImport(programNode) {
|
|
110
|
+
let found = false
|
|
111
|
+
walkAstWithAncestors(programNode, [], node => {
|
|
112
|
+
if (found) return
|
|
113
|
+
if (node.type !== 'ImportDeclaration') return
|
|
114
|
+
const source = node.source
|
|
115
|
+
if (!source || typeof source !== 'object' || source.value !== CHECK_ENV_PACKAGE) return
|
|
116
|
+
const specifiers = node.specifiers
|
|
117
|
+
if (!Array.isArray(specifiers)) return
|
|
118
|
+
for (const s of specifiers) {
|
|
119
|
+
if (!s || typeof s !== 'object' || s.type !== 'ImportSpecifier') continue
|
|
120
|
+
const imported = s.imported
|
|
121
|
+
const local = s.local
|
|
122
|
+
if (!imported || imported.name !== 'env') continue
|
|
123
|
+
if (!local || local.name !== 'env') continue
|
|
124
|
+
found = true
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
return found
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Чи закритий рядок ignore-коментарем `// @nitra/cursor ignore-next-line checkEnv`.
|
|
133
|
+
* @param {string[]} lines рядки файлу (split за \n, без CR)
|
|
134
|
+
* @param {number} oneBasedLine 1-based номер рядка з порушенням
|
|
135
|
+
* @returns {boolean} true, якщо попередній рядок містить маркер
|
|
136
|
+
*/
|
|
137
|
+
function hasIgnoreDirective(lines, oneBasedLine) {
|
|
138
|
+
if (oneBasedLine <= 1) return false
|
|
139
|
+
const prev = lines[oneBasedLine - 2] ?? ''
|
|
140
|
+
return IGNORE_DIRECTIVE_RE.test(prev)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Чи є вузол MemberExpression виду `env.X` / `env['X']`, де `env` — Identifier
|
|
145
|
+
* (в AST oxc-parser globals і локальні імпорти не розрізняються — фільтр джерела
|
|
146
|
+
* робиться на рівні `hasCheckEnvImport`).
|
|
147
|
+
* @param {unknown} node AST вузол
|
|
148
|
+
* @returns {boolean} true, якщо це `env.<...>`
|
|
149
|
+
*/
|
|
150
|
+
function isEnvIdentifierMember(node) {
|
|
151
|
+
if (!node || typeof node !== 'object' || node.type !== 'MemberExpression') return false
|
|
152
|
+
const obj = node.object
|
|
153
|
+
return !!obj && obj.type === 'Identifier' && obj.name === 'env'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @typedef {{
|
|
158
|
+
* line: number,
|
|
159
|
+
* name: string,
|
|
160
|
+
* kind: 'process-env' | 'check-env-missing-checkEnv'
|
|
161
|
+
* }} EnvViolation
|
|
162
|
+
*/
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Перебирає AST і для кожного знайденого доступу до `process.env` чи `env`
|
|
166
|
+
* (де `env` — імпорт з `@nitra/check-env`) реєструє порушення відповідного типу.
|
|
167
|
+
* @param {unknown} program корінь AST
|
|
168
|
+
* @param {string} content вихідний код (для offset → line)
|
|
169
|
+
* @param {string[]} lines split-рядки content (для ignore-маркера)
|
|
170
|
+
* @param {Set<string>} checkedNames імена, закриті літеральним `checkEnv([...])`
|
|
171
|
+
* @param {boolean} envFromCheckEnv чи імпортовано `env` саме з `@nitra/check-env`
|
|
172
|
+
* @returns {EnvViolation[]} список порушень (відсортований за порядком зустрічі в AST)
|
|
173
|
+
*/
|
|
174
|
+
function collectViolations(program, content, lines, checkedNames, envFromCheckEnv) {
|
|
175
|
+
/** @type {EnvViolation[]} */
|
|
176
|
+
const out = []
|
|
177
|
+
/** @type {Set<string>} */
|
|
178
|
+
const reported = new Set()
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Реєструє порушення з дедуплікацією за «kind|name|line» і урахуванням ignore-маркера.
|
|
182
|
+
* @param {'process-env' | 'check-env-missing-checkEnv'} kind тип порушення
|
|
183
|
+
* @param {string} name ім'я ENV
|
|
184
|
+
* @param {number} line 1-based рядок
|
|
185
|
+
*/
|
|
186
|
+
function report(kind, name, line) {
|
|
187
|
+
if (hasIgnoreDirective(lines, line)) return
|
|
188
|
+
const key = `${kind}|${name}|${line}`
|
|
189
|
+
if (reported.has(key)) return
|
|
190
|
+
reported.add(key)
|
|
191
|
+
out.push({ kind, name, line })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
walkAstWithAncestors(program, [], (node, ancestors) => {
|
|
195
|
+
// 1. process.env.X — завжди порушення (рекомендуємо замінити на env)
|
|
196
|
+
if (isProcessEnvAccess(node)) {
|
|
197
|
+
const parent = ancestors[ancestors.length - 1]
|
|
198
|
+
if (parent && typeof parent === 'object' && parent.type === 'MemberExpression' && parent.object === node) {
|
|
199
|
+
const envName = envNameFromMember(parent)
|
|
200
|
+
if (envName) report('process-env', envName, offsetToLine(content, parent.start))
|
|
201
|
+
}
|
|
202
|
+
if (
|
|
203
|
+
parent &&
|
|
204
|
+
typeof parent === 'object' &&
|
|
205
|
+
parent.type === 'VariableDeclarator' &&
|
|
206
|
+
parent.init === node &&
|
|
207
|
+
parent.id &&
|
|
208
|
+
parent.id.type === 'ObjectPattern' &&
|
|
209
|
+
Array.isArray(parent.id.properties)
|
|
210
|
+
) {
|
|
211
|
+
for (const p of parent.id.properties) {
|
|
212
|
+
const name = staticPropertyName(p)
|
|
213
|
+
if (name) report('process-env', name, offsetToLine(content, p.start ?? parent.start))
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 2. env.X — порушення лише якщо env імпортовано з @nitra/check-env і немає checkEnv
|
|
220
|
+
if (!envFromCheckEnv) return
|
|
221
|
+
|
|
222
|
+
if (isEnvIdentifierMember(node)) {
|
|
223
|
+
const envName = envNameFromMember(node)
|
|
224
|
+
if (envName && !checkedNames.has(envName)) {
|
|
225
|
+
report('check-env-missing-checkEnv', envName, offsetToLine(content, node.start))
|
|
226
|
+
}
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// const { X, Y } = env — теж потребує checkEnv для кожного імені
|
|
231
|
+
if (
|
|
232
|
+
node.type === 'VariableDeclarator' &&
|
|
233
|
+
node.init &&
|
|
234
|
+
node.init.type === 'Identifier' &&
|
|
235
|
+
node.init.name === 'env' &&
|
|
236
|
+
node.id &&
|
|
237
|
+
node.id.type === 'ObjectPattern' &&
|
|
238
|
+
Array.isArray(node.id.properties)
|
|
239
|
+
) {
|
|
240
|
+
for (const p of node.id.properties) {
|
|
241
|
+
const name = staticPropertyName(p)
|
|
242
|
+
if (name && !checkedNames.has(name)) {
|
|
243
|
+
report('check-env-missing-checkEnv', name, offsetToLine(content, p.start ?? node.start))
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
return out
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Витягує статичне ім'я з вузла Property у ObjectPattern.
|
|
254
|
+
* @param {unknown} property AST-вузол ObjectPattern.properties[i]
|
|
255
|
+
* @returns {string | null} ім'я ключа або null
|
|
256
|
+
*/
|
|
257
|
+
function staticPropertyName(property) {
|
|
258
|
+
if (!property || typeof property !== 'object' || property.type !== 'Property') return null
|
|
259
|
+
if (property.computed) return null
|
|
260
|
+
const key = property.key
|
|
261
|
+
if (!key || typeof key !== 'object') return null
|
|
262
|
+
if (key.type === 'Identifier' && typeof key.name === 'string') return key.name
|
|
263
|
+
if (key.type === 'Literal' && typeof key.value === 'string') return key.value
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Знаходить порушення правила «process.env / CheckEnv» у файлі.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} content вихідний код
|
|
271
|
+
* @param {string} [virtualPath] шлях для вибору `lang` парсера
|
|
272
|
+
* @returns {EnvViolation[]} список порушень із типом, іменем змінної та рядком
|
|
273
|
+
*/
|
|
274
|
+
export function findUncheckedProcessEnvInText(content, virtualPath = 'scan.ts') {
|
|
275
|
+
const program = parseProgramOrNull(content, virtualPath)
|
|
276
|
+
if (!program) return []
|
|
277
|
+
|
|
278
|
+
const checked = collectCheckedEnvNames(program)
|
|
279
|
+
const envFromCheckEnv = hasCheckEnvImport(program)
|
|
280
|
+
const lines = content.split('\n').map(s => (s.endsWith('\r') ? s.slice(0, -1) : s))
|
|
281
|
+
|
|
282
|
+
return collectViolations(program, content, lines, checked, envFromCheckEnv)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Чи сканувати цей файл за розширенням (JS/TS-сім'я, без `.d.ts`).
|
|
287
|
+
* @param {string} relativePathPosix відносний шлях (posix)
|
|
288
|
+
* @returns {boolean} true, якщо розширення підходить для AST-скану
|
|
289
|
+
*/
|
|
290
|
+
export function isCheckEnvScanSourceFile(relativePathPosix) {
|
|
291
|
+
return SOURCE_FILE_RE.test(relativePathPosix) && !relativePathPosix.endsWith('.d.ts')
|
|
292
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-сканер для правила «Внутрішні аліаси» (js-run.mdc).
|
|
3
|
+
*
|
|
4
|
+
* Імпорти, які створюють підключення до БД / зовнішнього GraphQL, мають жити в окремому
|
|
5
|
+
* файлі (за замовчуванням — `src/conn/`), а решта коду повинна споживати їх через
|
|
6
|
+
* pkg-import `#conn/...`. Ловимо такі імпорти в файлах поза каталогом «conn»:
|
|
7
|
+
* - `import { SQL } from 'bun'` (named специфікатор `SQL`);
|
|
8
|
+
* - `import sql from 'mssql'` або будь-який `import ... from 'mssql'`;
|
|
9
|
+
* - `import { GraphQLClient } from '@nitra/graphql-request'` (named `GraphQLClient`).
|
|
10
|
+
*
|
|
11
|
+
* Каталог «conn» визначається з поля `package.json#imports['#conn/*']` (якщо є —
|
|
12
|
+
* відрізаємо `*` і нормалізуємо), інакше дефолт — `src/conn`. Ключ `imports` у
|
|
13
|
+
* package.json — нативний для Node.js, той самий, що й у документі правила.
|
|
14
|
+
*
|
|
15
|
+
* Семантика береться з **oxc-parser** (`module.staticImports`); regex по тілу файлу не
|
|
16
|
+
* використовується. Якщо файл не парситься — повертаємо порожній результат, спочатку
|
|
17
|
+
* треба полагодити синтаксис.
|
|
18
|
+
*/
|
|
19
|
+
import { langFromPath, normalizeSnippet, offsetToLine } from './ast-scan-utils.mjs'
|
|
20
|
+
import { parseSync } from 'oxc-parser'
|
|
21
|
+
|
|
22
|
+
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
23
|
+
const TRAILING_SLASH_RE = /\/+$/u
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Нормалізує шлях до posix без хвостових слешів.
|
|
27
|
+
* @param {string} p вхідний шлях (можливо з `./` або зворотними слешами)
|
|
28
|
+
* @returns {string} нормалізований posix-шлях без хвостового `/`
|
|
29
|
+
*/
|
|
30
|
+
function toPosixDir(p) {
|
|
31
|
+
let s = String(p).replaceAll('\\', '/').trim()
|
|
32
|
+
if (s.startsWith('./')) s = s.slice(2)
|
|
33
|
+
return s.replace(TRAILING_SLASH_RE, '')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Визначає каталог «conn» за `package.json#imports['#conn/*']`. Дефолт — `src/conn`.
|
|
38
|
+
* @param {unknown} pkgJson розпарсений package.json (або null)
|
|
39
|
+
* @returns {string} відносний posix-шлях до каталогу conn (без хвостового `/`)
|
|
40
|
+
*/
|
|
41
|
+
export function resolveConnDirFromPackageJson(pkgJson) {
|
|
42
|
+
const fallback = 'src/conn'
|
|
43
|
+
if (!pkgJson || typeof pkgJson !== 'object') return fallback
|
|
44
|
+
const imports = /** @type {Record<string, unknown>} */ (pkgJson).imports
|
|
45
|
+
if (!imports || typeof imports !== 'object') return fallback
|
|
46
|
+
const target = /** @type {Record<string, unknown>} */ (imports)['#conn/*']
|
|
47
|
+
/** @type {string | null} */
|
|
48
|
+
let raw = null
|
|
49
|
+
if (typeof target === 'string') raw = target
|
|
50
|
+
else if (target && typeof target === 'object') {
|
|
51
|
+
// умовний експорт: { default: '...', import: '...' }
|
|
52
|
+
const obj = /** @type {Record<string, unknown>} */ (target)
|
|
53
|
+
if (typeof obj.default === 'string') raw = obj.default
|
|
54
|
+
else if (typeof obj.import === 'string') raw = obj.import
|
|
55
|
+
}
|
|
56
|
+
if (!raw) return fallback
|
|
57
|
+
// Прибираємо хвіст `*`, потім слеші
|
|
58
|
+
let s = toPosixDir(raw)
|
|
59
|
+
if (s.endsWith('/*')) s = s.slice(0, -2)
|
|
60
|
+
return s.replace(TRAILING_SLASH_RE, '') || fallback
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Чи перебуває файл у каталозі conn (точно або вкладено).
|
|
65
|
+
* @param {string} relPosix відносний posix-шлях до файлу
|
|
66
|
+
* @param {string} connDir posix-шлях каталогу conn (без хвостового `/`)
|
|
67
|
+
* @returns {boolean} true, якщо файл у каталозі conn
|
|
68
|
+
*/
|
|
69
|
+
export function isInsideConnDir(relPosix, connDir) {
|
|
70
|
+
if (!connDir) return false
|
|
71
|
+
return relPosix === connDir || relPosix.startsWith(`${connDir}/`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Чи це порушення правила «Внутрішні аліаси» — імпорт зі стороннього модуля, що створює
|
|
76
|
+
* підключення (`bun` зі специфікатором `SQL`, будь-який імпорт з `mssql`, або
|
|
77
|
+
* `@nitra/graphql-request` зі специфікатором `GraphQLClient`).
|
|
78
|
+
* @param {Record<string, unknown>} staticImport елемент `module.staticImports` з oxc-parser
|
|
79
|
+
* @returns {{ module: string, specifier: string } | null} опис порушення або null
|
|
80
|
+
*/
|
|
81
|
+
function classifyConnImport(staticImport) {
|
|
82
|
+
const mod = staticImport.moduleRequest?.value
|
|
83
|
+
if (typeof mod !== 'string') return null
|
|
84
|
+
const entries = Array.isArray(staticImport.entries) ? staticImport.entries : []
|
|
85
|
+
|
|
86
|
+
if (mod === 'bun') {
|
|
87
|
+
for (const e of entries) {
|
|
88
|
+
const name = e?.importName?.name
|
|
89
|
+
if (name === 'SQL') return { module: mod, specifier: 'SQL' }
|
|
90
|
+
}
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
if (mod === 'mssql') {
|
|
94
|
+
return { module: mod, specifier: '*' }
|
|
95
|
+
}
|
|
96
|
+
if (mod === '@nitra/graphql-request') {
|
|
97
|
+
for (const e of entries) {
|
|
98
|
+
const name = e?.importName?.name
|
|
99
|
+
if (name === 'GraphQLClient') return { module: mod, specifier: 'GraphQLClient' }
|
|
100
|
+
}
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Знаходить імпорти-«фабрики підключень» у тексті файлу.
|
|
108
|
+
* @param {string} content вихідний код
|
|
109
|
+
* @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/index.ts`)
|
|
110
|
+
* @returns {{ line: number, snippet: string, module: string, specifier: string }[]} список порушень
|
|
111
|
+
*/
|
|
112
|
+
export function findConnFactoryImportsInText(content, virtualPath = 'scan.ts') {
|
|
113
|
+
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
114
|
+
let result
|
|
115
|
+
try {
|
|
116
|
+
result = parseSync(virtualPath || 'scan.ts', content, { lang, sourceType: 'module' })
|
|
117
|
+
} catch {
|
|
118
|
+
return []
|
|
119
|
+
}
|
|
120
|
+
if (result.errors?.length) return []
|
|
121
|
+
|
|
122
|
+
/** @type {{ line: number, snippet: string, module: string, specifier: string }[]} */
|
|
123
|
+
const out = []
|
|
124
|
+
for (const imp of result.module?.staticImports ?? []) {
|
|
125
|
+
const hit = classifyConnImport(imp)
|
|
126
|
+
if (!hit) continue
|
|
127
|
+
out.push({
|
|
128
|
+
line: offsetToLine(content, imp.start),
|
|
129
|
+
snippet: normalizeSnippet(content.slice(imp.start, imp.end)),
|
|
130
|
+
module: hit.module,
|
|
131
|
+
specifier: hit.specifier
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
return out
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Чи сканувати цей файл за розширенням (JS/TS-сім'я, без `.d.ts`).
|
|
139
|
+
* @param {string} relativePathPosix відносний шлях (posix)
|
|
140
|
+
* @returns {boolean} true, якщо розширення підходить для AST-скану
|
|
141
|
+
*/
|
|
142
|
+
export function isConnImportsScanSourceFile(relativePathPosix) {
|
|
143
|
+
return SOURCE_FILE_RE.test(relativePathPosix) && !relativePathPosix.endsWith('.d.ts')
|
|
144
|
+
}
|
package/mdc/js-pino.mdc
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Використання @nitra/pino
|
|
3
|
-
alwaysApply: true
|
|
4
|
-
version: '1.1'
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Проект використовує @nitra/pino для логування.
|
|
8
|
-
Якщо в проекті присутній @nitra/bunyan, то він повинен бути замінений на @nitra/pino — як у `package.json`, так і в коді: усі `import` / `require` / динамічні `import()` з `@nitra/bunyan` (і застарілого `bunyan`) треба замінити на `@nitra/pino` і за потреби адаптувати виклики під його API.
|
|
9
|
-
|
|
10
|
-
В **/k8s/base/configmap.yaml повинен бути заданий OTEL_RESOURCE_ATTRIBUTES: 'service.name=<project_name>,service.namespace=<project_namespace>'
|
|
11
|
-
а в директоріях з kustomize повинні бути перевизначені значення OTEL_RESOURCE_ATTRIBUTES і в них service.namespace повинен відповідати namespace, в якому знаходиться дана директорія.
|
|
12
|
-
|
|
13
|
-
## Перевірка
|
|
14
|
-
|
|
15
|
-
`npx @nitra/cursor check js-pino`
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Для кожного workspace-пакета перевіряє правило js-pino.mdc.
|
|
3
|
-
*
|
|
4
|
-
* Заборона `@nitra/bunyan` / `bunyan` як у залежностях `package.json`, так і в коді
|
|
5
|
-
* (`import` / `require` / динамічний `import()`); наявність `OTEL_RESOURCE_ATTRIBUTES`
|
|
6
|
-
* у `k8s/base/configmap.yaml`, якщо такий файл існує.
|
|
7
|
-
*
|
|
8
|
-
* Перевірка відповідності імені ConfigMap імені Deployment — у `check-k8s.mjs` (k8s.mdc).
|
|
9
|
-
*
|
|
10
|
-
* Імпорти в джерелах сканує AST через `oxc-parser` (див. `utils/bunyan-imports.mjs`),
|
|
11
|
-
* щоб виявити випадки на кшталт `import log from '@nitra/bunyan'`, які лишаються в коді
|
|
12
|
-
* після підміни залежності.
|
|
13
|
-
*/
|
|
14
|
-
import { existsSync } from 'node:fs'
|
|
15
|
-
import { readFile } from 'node:fs/promises'
|
|
16
|
-
import { join, relative } from 'node:path'
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
findBunyanImportsInText,
|
|
20
|
-
isBunyanScanSourceFile,
|
|
21
|
-
shouldSkipFileForBunyanScan
|
|
22
|
-
} from './utils/bunyan-imports.mjs'
|
|
23
|
-
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
24
|
-
import { walkDir } from './utils/walkDir.mjs'
|
|
25
|
-
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Сканує джерела пакета на заборонені імпорти `@nitra/bunyan` / `bunyan`.
|
|
29
|
-
* @param {string} absPackageRoot абсолютний шлях до кореня пакета
|
|
30
|
-
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
31
|
-
* @param {(msg: string) => void} fail callback при помилці
|
|
32
|
-
* @returns {Promise<number>} кількість знайдених порушень
|
|
33
|
-
*/
|
|
34
|
-
async function checkBunyanImports(absPackageRoot, label, fail) {
|
|
35
|
-
/** @type {string[]} */
|
|
36
|
-
const sourcePaths = []
|
|
37
|
-
await walkDir(absPackageRoot, absPath => {
|
|
38
|
-
const rel = relative(absPackageRoot, absPath).split('\\').join('/')
|
|
39
|
-
if (!shouldSkipFileForBunyanScan(rel) && isBunyanScanSourceFile(rel)) {
|
|
40
|
-
sourcePaths.push(absPath)
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
let violations = 0
|
|
45
|
-
for (const absPath of sourcePaths) {
|
|
46
|
-
const rel = relative(absPackageRoot, absPath).split('\\').join('/')
|
|
47
|
-
const content = await readFile(absPath, 'utf8')
|
|
48
|
-
for (const v of findBunyanImportsInText(content, rel)) {
|
|
49
|
-
violations++
|
|
50
|
-
fail(`${label}${rel}:${v.line} — заміни '${v.module}' на '@nitra/pino': ${v.snippet}`)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return violations
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Перевіряє відповідність правилам js-pino.mdc для одного workspace-пакета.
|
|
58
|
-
* @param {string} rootDir відносний шлях workspace (не `'.'`)
|
|
59
|
-
* @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
|
|
60
|
-
* @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
|
|
61
|
-
* @returns {Promise<void>} завершується після перевірок цього пакета
|
|
62
|
-
*/
|
|
63
|
-
async function checkWorkspacePackage(rootDir, fail, passFn) {
|
|
64
|
-
const label = `[${rootDir}] `
|
|
65
|
-
const pkgPath = join(rootDir, 'package.json')
|
|
66
|
-
if (existsSync(pkgPath)) {
|
|
67
|
-
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
68
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
69
|
-
|
|
70
|
-
if (allDeps['@nitra/bunyan']) {
|
|
71
|
-
fail(`${label}@nitra/bunyan знайдено — замінити на @nitra/pino`)
|
|
72
|
-
}
|
|
73
|
-
if (allDeps.bunyan) {
|
|
74
|
-
fail(`${label}bunyan знайдено — замінити на @nitra/pino`)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const importViolations = await checkBunyanImports(join(process.cwd(), rootDir), label, fail)
|
|
79
|
-
if (importViolations === 0) {
|
|
80
|
-
passFn(`${label}немає імпортів '@nitra/bunyan' / 'bunyan' у джерелах`)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const configmapPath = join(rootDir, 'k8s', 'base', 'configmap.yaml')
|
|
84
|
-
if (existsSync(configmapPath)) {
|
|
85
|
-
const content = await readFile(configmapPath, 'utf8')
|
|
86
|
-
if (content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
|
|
87
|
-
passFn(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
|
|
88
|
-
if (content.includes('service.name=') && content.includes('service.namespace=')) {
|
|
89
|
-
passFn(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
|
|
90
|
-
} else {
|
|
91
|
-
fail(`${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`)
|
|
92
|
-
}
|
|
93
|
-
} else {
|
|
94
|
-
fail(`${label}k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES`)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Перевіряє відповідність проєкту правилам js-pino.mdc лише для workspace-пакетів (не корінь репо).
|
|
101
|
-
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
102
|
-
*/
|
|
103
|
-
export async function check() {
|
|
104
|
-
const reporter = createCheckReporter()
|
|
105
|
-
const { pass, fail } = reporter
|
|
106
|
-
|
|
107
|
-
const roots = await getMonorepoPackageRootDirs()
|
|
108
|
-
const workspaceRoots = roots.filter(r => r !== '.')
|
|
109
|
-
|
|
110
|
-
if (workspaceRoots.length === 0) {
|
|
111
|
-
pass('js-pino: немає workspace-пакетів у кореневому package.json — перевірку залежностей і k8s у пакетах пропущено')
|
|
112
|
-
return reporter.getExitCode()
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
for (const r of workspaceRoots) {
|
|
116
|
-
await checkWorkspacePackage(r, fail, pass)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return reporter.getExitCode()
|
|
120
|
-
}
|