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