@nitra/cursor 1.8.154 → 1.8.155
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 +115 -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 +227 -0
- package/scripts/check-k8s.mjs +2 -2
- package/scripts/utils/bunyan-imports.mjs +1 -1
- package/scripts/utils/check-env-scan.mjs +179 -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,115 @@
|
|
|
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 } 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(process.env.QL, {
|
|
77
|
+
headers: {
|
|
78
|
+
'X-Hasura-Admin-Secret': process.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
|
+
// @nitra/cursor ignore-next-line checkEnv
|
|
110
|
+
console.log(process.env.OPTIONAL_ENV_VAR)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Перевірка
|
|
114
|
+
|
|
115
|
+
`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,227 @@
|
|
|
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
|
+
* - «CheckEnv»: кожне `process.env.X` (включно з `process.env['X']` і деструктуризацією
|
|
16
|
+
* `const { X } = process.env`) має бути закрите літеральним викликом `checkEnv(['X', ...])`
|
|
17
|
+
* у тому ж файлі або коментарем `// @nitra/cursor ignore-next-line checkEnv` на попередньому
|
|
18
|
+
* рядку (див. `utils/check-env-scan.mjs`).
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync } from 'node:fs'
|
|
21
|
+
import { readFile } from 'node:fs/promises'
|
|
22
|
+
import { join, relative } from 'node:path'
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
findBunyanImportsInText,
|
|
26
|
+
isBunyanScanSourceFile,
|
|
27
|
+
shouldSkipFileForBunyanScan
|
|
28
|
+
} from './utils/bunyan-imports.mjs'
|
|
29
|
+
import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from './utils/check-env-scan.mjs'
|
|
30
|
+
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
31
|
+
import {
|
|
32
|
+
findConnFactoryImportsInText,
|
|
33
|
+
isConnImportsScanSourceFile,
|
|
34
|
+
isInsideConnDir,
|
|
35
|
+
resolveConnDirFromPackageJson
|
|
36
|
+
} from './utils/conn-imports-scan.mjs'
|
|
37
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
38
|
+
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Перетворює абсолютний шлях у posix-формі відносно кореня пакета.
|
|
42
|
+
* @param {string} absPackageRoot абсолютний корінь пакета
|
|
43
|
+
* @param {string} absPath абсолютний шлях до файлу
|
|
44
|
+
* @returns {string} відносний posix-шлях
|
|
45
|
+
*/
|
|
46
|
+
function relPosix(absPackageRoot, absPath) {
|
|
47
|
+
return relative(absPackageRoot, absPath).split('\\').join('/')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Сканує джерела пакета на заборонені імпорти `@nitra/bunyan` / `bunyan`.
|
|
52
|
+
* @param {string} absPackageRoot абсолютний шлях до кореня пакета
|
|
53
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
54
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
55
|
+
* @returns {Promise<number>} кількість знайдених порушень
|
|
56
|
+
*/
|
|
57
|
+
async function checkBunyanImports(absPackageRoot, label, fail) {
|
|
58
|
+
/** @type {string[]} */
|
|
59
|
+
const sourcePaths = []
|
|
60
|
+
await walkDir(absPackageRoot, absPath => {
|
|
61
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
62
|
+
if (!shouldSkipFileForBunyanScan(rel) && isBunyanScanSourceFile(rel)) {
|
|
63
|
+
sourcePaths.push(absPath)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
let violations = 0
|
|
68
|
+
for (const absPath of sourcePaths) {
|
|
69
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
70
|
+
const content = await readFile(absPath, 'utf8')
|
|
71
|
+
for (const v of findBunyanImportsInText(content, rel)) {
|
|
72
|
+
violations++
|
|
73
|
+
fail(`${label}${rel}:${v.line} — заміни '${v.module}' на '@nitra/pino': ${v.snippet}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return violations
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Збирає всі JS/TS-файли пакета (без node_modules, dist тощо).
|
|
81
|
+
* @param {string} absPackageRoot абсолютний шлях до кореня пакета
|
|
82
|
+
* @returns {Promise<string[]>} абсолютні шляхи до файлів
|
|
83
|
+
*/
|
|
84
|
+
async function collectSourceFiles(absPackageRoot) {
|
|
85
|
+
/** @type {string[]} */
|
|
86
|
+
const out = []
|
|
87
|
+
await walkDir(absPackageRoot, absPath => {
|
|
88
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
89
|
+
if (isCheckEnvScanSourceFile(rel)) out.push(absPath)
|
|
90
|
+
})
|
|
91
|
+
return out
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Перевіряє правило «Внутрішні аліаси» для пакета.
|
|
96
|
+
* @param {string} absPackageRoot абсолютний корінь пакета
|
|
97
|
+
* @param {string[]} sourcePaths абсолютні шляхи до файлів
|
|
98
|
+
* @param {unknown} pkgJson розпарсений `package.json` пакета (або null)
|
|
99
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
100
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
101
|
+
* @returns {Promise<number>} кількість порушень
|
|
102
|
+
*/
|
|
103
|
+
async function checkConnImports(absPackageRoot, sourcePaths, pkgJson, label, fail) {
|
|
104
|
+
const connDir = resolveConnDirFromPackageJson(pkgJson)
|
|
105
|
+
let violations = 0
|
|
106
|
+
for (const absPath of sourcePaths) {
|
|
107
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
108
|
+
if (!isConnImportsScanSourceFile(rel)) continue
|
|
109
|
+
if (isInsideConnDir(rel, connDir)) continue
|
|
110
|
+
const content = await readFile(absPath, 'utf8')
|
|
111
|
+
for (const v of findConnFactoryImportsInText(content, rel)) {
|
|
112
|
+
violations++
|
|
113
|
+
const target = v.specifier === '*' ? `'${v.module}'` : `{ ${v.specifier} } from '${v.module}'`
|
|
114
|
+
fail(
|
|
115
|
+
`${label}${rel}:${v.line} — імпорт ${target} має бути в '${connDir}/' і реекспортуватися через '#conn/*': ${v.snippet}`
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return violations
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Перевіряє правило «CheckEnv» для пакета.
|
|
124
|
+
* @param {string} absPackageRoot абсолютний корінь пакета
|
|
125
|
+
* @param {string[]} sourcePaths абсолютні шляхи до файлів
|
|
126
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
127
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
128
|
+
* @returns {Promise<number>} кількість порушень
|
|
129
|
+
*/
|
|
130
|
+
async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
|
|
131
|
+
let violations = 0
|
|
132
|
+
for (const absPath of sourcePaths) {
|
|
133
|
+
const rel = relPosix(absPackageRoot, absPath)
|
|
134
|
+
const content = await readFile(absPath, 'utf8')
|
|
135
|
+
for (const v of findUncheckedProcessEnvInText(content, rel)) {
|
|
136
|
+
violations++
|
|
137
|
+
fail(
|
|
138
|
+
`${label}${rel}:${v.line} — process.env.${v.name} без checkEnv(['${v.name}']) (або '// @nitra/cursor ignore-next-line checkEnv' попереду)`
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return violations
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Перевіряє відповідність правилам js-run.mdc для одного workspace-пакета.
|
|
147
|
+
* @param {string} rootDir відносний шлях workspace (не `'.'`)
|
|
148
|
+
* @param {(msg: string) => void} fail функція зворотного виклику для реєстрації помилки перевірки
|
|
149
|
+
* @param {(msg: string) => void} passFn успішне повідомлення (як у check-reporter)
|
|
150
|
+
* @returns {Promise<void>} завершується після перевірок цього пакета
|
|
151
|
+
*/
|
|
152
|
+
async function checkWorkspacePackage(rootDir, fail, passFn) {
|
|
153
|
+
const label = `[${rootDir}] `
|
|
154
|
+
const absPackageRoot = join(process.cwd(), rootDir)
|
|
155
|
+
/** @type {unknown} */
|
|
156
|
+
let pkgJson = null
|
|
157
|
+
const pkgPath = join(rootDir, 'package.json')
|
|
158
|
+
if (existsSync(pkgPath)) {
|
|
159
|
+
pkgJson = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
160
|
+
const deps = /** @type {Record<string, unknown>} */ (pkgJson).dependencies
|
|
161
|
+
const devDeps = /** @type {Record<string, unknown>} */ (pkgJson).devDependencies
|
|
162
|
+
const allDeps = { ...(deps || {}), ...(devDeps || {}) }
|
|
163
|
+
|
|
164
|
+
if (allDeps['@nitra/bunyan']) {
|
|
165
|
+
fail(`${label}@nitra/bunyan знайдено — замінити на @nitra/pino`)
|
|
166
|
+
}
|
|
167
|
+
if (allDeps.bunyan) {
|
|
168
|
+
fail(`${label}bunyan знайдено — замінити на @nitra/pino`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const importViolations = await checkBunyanImports(absPackageRoot, label, fail)
|
|
173
|
+
if (importViolations === 0) {
|
|
174
|
+
passFn(`${label}немає імпортів '@nitra/bunyan' / 'bunyan' у джерелах`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const sourcePaths = await collectSourceFiles(absPackageRoot)
|
|
178
|
+
|
|
179
|
+
const connViolations = await checkConnImports(absPackageRoot, sourcePaths, pkgJson, label, fail)
|
|
180
|
+
if (connViolations === 0) {
|
|
181
|
+
const connDir = resolveConnDirFromPackageJson(pkgJson)
|
|
182
|
+
passFn(`${label}імпорти підключень (bun#SQL / mssql / @nitra/graphql-request#GraphQLClient) лише в '${connDir}/'`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const envViolations = await checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail)
|
|
186
|
+
if (envViolations === 0) {
|
|
187
|
+
passFn(`${label}усі process.env.* закриті checkEnv(['…']) або '// @nitra/cursor ignore-next-line checkEnv'`)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const configmapPath = join(rootDir, 'k8s', 'base', 'configmap.yaml')
|
|
191
|
+
if (existsSync(configmapPath)) {
|
|
192
|
+
const content = await readFile(configmapPath, 'utf8')
|
|
193
|
+
if (content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
|
|
194
|
+
passFn(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
|
|
195
|
+
if (content.includes('service.name=') && content.includes('service.namespace=')) {
|
|
196
|
+
passFn(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
|
|
197
|
+
} else {
|
|
198
|
+
fail(`${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`)
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
fail(`${label}k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES`)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Перевіряє відповідність проєкту правилам js-run.mdc лише для workspace-пакетів (не корінь репо).
|
|
208
|
+
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
209
|
+
*/
|
|
210
|
+
export async function check() {
|
|
211
|
+
const reporter = createCheckReporter()
|
|
212
|
+
const { pass, fail } = reporter
|
|
213
|
+
|
|
214
|
+
const roots = await getMonorepoPackageRootDirs()
|
|
215
|
+
const workspaceRoots = roots.filter(r => r !== '.')
|
|
216
|
+
|
|
217
|
+
if (workspaceRoots.length === 0) {
|
|
218
|
+
pass('js-run: немає workspace-пакетів у кореневому package.json — перевірку залежностей і k8s у пакетах пропущено')
|
|
219
|
+
return reporter.getExitCode()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const r of workspaceRoots) {
|
|
223
|
+
await checkWorkspacePackage(r, fail, pass)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return reporter.getExitCode()
|
|
227
|
+
}
|
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,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-сканер для правила CheckEnv (js-run.mdc).
|
|
3
|
+
*
|
|
4
|
+
* Кожне використання `process.env.X` у JS/TS-коді має бути «закрите» одним з двох способів:
|
|
5
|
+
* - перед використанням у тому ж файлі викликано `checkEnv(['X', ...])` з пакету `@nitra/check-env`;
|
|
6
|
+
* - на рядку безпосередньо перед `process.env.X` стоїть коментар-маркер
|
|
7
|
+
* `// @nitra/cursor ignore-next-line checkEnv` (роздільники пробілів довільні; саме слово
|
|
8
|
+
* `checkEnv` чутливе до регістру, як в усіх прикладах документа).
|
|
9
|
+
*
|
|
10
|
+
* Семантика береться з **oxc-parser** через `parseProgramOrNull`: regex по тілу файлу не
|
|
11
|
+
* використовується, лише сирий текст рядка з коментарем перевіряється на маркер. Якщо
|
|
12
|
+
* файл не парситься — повертаємо порожній результат, спочатку треба полагодити синтаксис.
|
|
13
|
+
*
|
|
14
|
+
* Покриті форми доступу до `process.env`:
|
|
15
|
+
* - `process.env.X` (звичайний MemberExpression);
|
|
16
|
+
* - `process.env['X']` (computed з рядковим літералом);
|
|
17
|
+
* - `const { X, Y } = process.env` (ObjectPattern; ім'я з ключа);
|
|
18
|
+
* - `const { X: alias } = process.env` (ім'я з ключа, не з alias).
|
|
19
|
+
*
|
|
20
|
+
* Якщо ключ обчислюваний (наприклад, `process.env[varName]`) — пропускаємо без помилки,
|
|
21
|
+
* бо за статичним AST неможливо встановити, яка саме змінна оточення використовується.
|
|
22
|
+
*/
|
|
23
|
+
import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from './ast-scan-utils.mjs'
|
|
24
|
+
|
|
25
|
+
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
26
|
+
const IGNORE_DIRECTIVE_RE = /\/\/\s*@nitra\/cursor\s+ignore-next-line\s+checkEnv\b/u
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Чи є цей вузол виразом `process.env`.
|
|
30
|
+
* @param {unknown} node AST вузол
|
|
31
|
+
* @returns {boolean} true, якщо це `MemberExpression` `process.env` (Identifier . Identifier)
|
|
32
|
+
*/
|
|
33
|
+
function isProcessEnvAccess(node) {
|
|
34
|
+
if (!node || typeof node !== 'object') return false
|
|
35
|
+
if (node.type !== 'MemberExpression' || node.computed) return false
|
|
36
|
+
const obj = node.object
|
|
37
|
+
const prop = node.property
|
|
38
|
+
return (
|
|
39
|
+
!!obj &&
|
|
40
|
+
obj.type === 'Identifier' &&
|
|
41
|
+
obj.name === 'process' &&
|
|
42
|
+
!!prop &&
|
|
43
|
+
prop.type === 'Identifier' &&
|
|
44
|
+
prop.name === 'env'
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Витягує ім'я ENV з MemberExpression-вузла `process.env.X` або `process.env['X']`.
|
|
50
|
+
* @param {Record<string, unknown>} node MemberExpression, чий object — `process.env`
|
|
51
|
+
* @returns {string | null} ім'я змінної оточення або null, якщо ключ не статичний
|
|
52
|
+
*/
|
|
53
|
+
function envNameFromMember(node) {
|
|
54
|
+
const prop = node.property
|
|
55
|
+
if (!prop || typeof prop !== 'object') return null
|
|
56
|
+
if (!node.computed && prop.type === 'Identifier' && typeof prop.name === 'string') return prop.name
|
|
57
|
+
if (node.computed && prop.type === 'Literal' && typeof prop.value === 'string') return prop.value
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Збирає всі літеральні імена з виклику `checkEnv([...])` у файлі.
|
|
63
|
+
* Якщо callee — Identifier `checkEnv` і перший аргумент — ArrayExpression, додає
|
|
64
|
+
* всі string-літерали до set. Не-літеральні елементи (Identifier, SpreadElement) ігноруються —
|
|
65
|
+
* це робить перевірку «ліберальною»: ми лише ловимо явно неперевірені змінні.
|
|
66
|
+
* @param {unknown} programNode корінь AST
|
|
67
|
+
* @returns {Set<string>} перелік закритих імен ENV
|
|
68
|
+
*/
|
|
69
|
+
function collectCheckedEnvNames(programNode) {
|
|
70
|
+
/** @type {Set<string>} */
|
|
71
|
+
const out = new Set()
|
|
72
|
+
walkAstWithAncestors(programNode, [], node => {
|
|
73
|
+
if (node.type !== 'CallExpression') return
|
|
74
|
+
const callee = node.callee
|
|
75
|
+
if (!callee || callee.type !== 'Identifier' || callee.name !== 'checkEnv') return
|
|
76
|
+
const args = node.arguments
|
|
77
|
+
if (!Array.isArray(args) || args.length === 0) return
|
|
78
|
+
const first = args[0]
|
|
79
|
+
if (!first || typeof first !== 'object' || first.type !== 'ArrayExpression') return
|
|
80
|
+
const elements = first.elements
|
|
81
|
+
if (!Array.isArray(elements)) return
|
|
82
|
+
for (const el of elements) {
|
|
83
|
+
if (!el || typeof el !== 'object') continue
|
|
84
|
+
if (el.type === 'Literal' && typeof el.value === 'string') out.add(el.value)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
return out
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Чи закритий рядок ignore-коментарем `// @nitra/cursor ignore-next-line checkEnv`.
|
|
92
|
+
* @param {string[]} lines рядки файлу (split за \n, без CR)
|
|
93
|
+
* @param {number} oneBasedLine 1-based номер рядка з `process.env.X`
|
|
94
|
+
* @returns {boolean} true, якщо попередній рядок містить маркер
|
|
95
|
+
*/
|
|
96
|
+
function hasIgnoreDirective(lines, oneBasedLine) {
|
|
97
|
+
if (oneBasedLine <= 1) return false
|
|
98
|
+
const prev = lines[oneBasedLine - 2] ?? ''
|
|
99
|
+
return IGNORE_DIRECTIVE_RE.test(prev)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Знаходить всі доступи до `process.env.<NAME>`, які не покриті ні літеральним
|
|
104
|
+
* `checkEnv([...])` у тому ж файлі, ні коментарем-маркером безпосередньо перед.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} content вихідний код
|
|
107
|
+
* @param {string} [virtualPath] шлях для вибору `lang` парсера
|
|
108
|
+
* @returns {{ line: number, name: string }[]} список порушень
|
|
109
|
+
*/
|
|
110
|
+
export function findUncheckedProcessEnvInText(content, virtualPath = 'scan.ts') {
|
|
111
|
+
const program = parseProgramOrNull(content, virtualPath)
|
|
112
|
+
if (!program) return []
|
|
113
|
+
|
|
114
|
+
const checked = collectCheckedEnvNames(program)
|
|
115
|
+
const lines = content.split('\n').map(s => (s.endsWith('\r') ? s.slice(0, -1) : s))
|
|
116
|
+
|
|
117
|
+
/** @type {{ line: number, name: string }[]} */
|
|
118
|
+
const out = []
|
|
119
|
+
/** @type {Set<string>} */
|
|
120
|
+
const reported = new Set()
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Реєструє порушення з дедуплікацією за «name@line».
|
|
124
|
+
* @param {string} name ім'я ENV
|
|
125
|
+
* @param {number} line 1-based рядок
|
|
126
|
+
*/
|
|
127
|
+
function report(name, line) {
|
|
128
|
+
if (checked.has(name)) return
|
|
129
|
+
if (hasIgnoreDirective(lines, line)) return
|
|
130
|
+
const key = `${name}@${line}`
|
|
131
|
+
if (reported.has(key)) return
|
|
132
|
+
reported.add(key)
|
|
133
|
+
out.push({ name, line })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
walkAstWithAncestors(program, [], (node, ancestors) => {
|
|
137
|
+
if (isProcessEnvAccess(node)) {
|
|
138
|
+
const parent = ancestors[ancestors.length - 1]
|
|
139
|
+
// process.env.X / process.env['X']
|
|
140
|
+
if (parent && typeof parent === 'object' && parent.type === 'MemberExpression' && parent.object === node) {
|
|
141
|
+
const envName = envNameFromMember(parent)
|
|
142
|
+
if (envName) report(envName, offsetToLine(content, parent.start))
|
|
143
|
+
}
|
|
144
|
+
// const { X, Y } = process.env → беремо імена з ObjectPattern
|
|
145
|
+
if (
|
|
146
|
+
parent &&
|
|
147
|
+
typeof parent === 'object' &&
|
|
148
|
+
parent.type === 'VariableDeclarator' &&
|
|
149
|
+
parent.init === node &&
|
|
150
|
+
parent.id &&
|
|
151
|
+
parent.id.type === 'ObjectPattern' &&
|
|
152
|
+
Array.isArray(parent.id.properties)
|
|
153
|
+
) {
|
|
154
|
+
for (const p of parent.id.properties) {
|
|
155
|
+
if (!p || typeof p !== 'object' || p.type !== 'Property') continue
|
|
156
|
+
if (p.computed) continue
|
|
157
|
+
const key = p.key
|
|
158
|
+
if (!key || typeof key !== 'object') continue
|
|
159
|
+
/** @type {string | null} */
|
|
160
|
+
let name = null
|
|
161
|
+
if (key.type === 'Identifier' && typeof key.name === 'string') name = key.name
|
|
162
|
+
else if (key.type === 'Literal' && typeof key.value === 'string') name = key.value
|
|
163
|
+
if (name) report(name, offsetToLine(content, p.start ?? parent.start))
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
return out
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Чи сканувати цей файл за розширенням (JS/TS-сім'я, без `.d.ts`).
|
|
174
|
+
* @param {string} relativePathPosix відносний шлях (posix)
|
|
175
|
+
* @returns {boolean} true, якщо розширення підходить для AST-скану
|
|
176
|
+
*/
|
|
177
|
+
export function isCheckEnvScanSourceFile(relativePathPosix) {
|
|
178
|
+
return SOURCE_FILE_RE.test(relativePathPosix) && !relativePathPosix.endsWith('.d.ts')
|
|
179
|
+
}
|
|
@@ -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
|
-
}
|