@nitra/cursor 1.8.156 → 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 +3 -9
- 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 +49 -29
- 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 +89 -44
- package/scripts/utils/conn-imports-scan.mjs +13 -3
- package/scripts/utils/mssql-pool-scan.mjs +57 -38
|
@@ -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
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* з `@nitra/check-env` (для обов'язкових змінних, із `checkEnv([...])`) або з
|
|
17
17
|
* `node:process` (для опційних). Коли `env` імпортовано з `@nitra/check-env`,
|
|
18
18
|
* кожен `env.X` має бути закритий літеральним викликом `checkEnv(['X', ...])`
|
|
19
|
-
* у тому ж файлі або коментарем `//
|
|
19
|
+
* у тому ж файлі або коментарем `// \@nitra/cursor ignore-next-line checkEnv`
|
|
20
20
|
* на попередньому рядку (див. `utils/check-env-scan.mjs`).
|
|
21
21
|
*/
|
|
22
22
|
import { existsSync } from 'node:fs'
|
|
@@ -156,22 +156,7 @@ async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
|
|
|
156
156
|
async function checkWorkspacePackage(rootDir, fail, passFn) {
|
|
157
157
|
const label = `[${rootDir}] `
|
|
158
158
|
const absPackageRoot = join(process.cwd(), rootDir)
|
|
159
|
-
|
|
160
|
-
let pkgJson = null
|
|
161
|
-
const pkgPath = join(rootDir, 'package.json')
|
|
162
|
-
if (existsSync(pkgPath)) {
|
|
163
|
-
pkgJson = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
164
|
-
const deps = /** @type {Record<string, unknown>} */ (pkgJson).dependencies
|
|
165
|
-
const devDeps = /** @type {Record<string, unknown>} */ (pkgJson).devDependencies
|
|
166
|
-
const allDeps = { ...(deps || {}), ...(devDeps || {}) }
|
|
167
|
-
|
|
168
|
-
if (allDeps['@nitra/bunyan']) {
|
|
169
|
-
fail(`${label}@nitra/bunyan знайдено — замінити на @nitra/pino`)
|
|
170
|
-
}
|
|
171
|
-
if (allDeps.bunyan) {
|
|
172
|
-
fail(`${label}bunyan знайдено — замінити на @nitra/pino`)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
159
|
+
const pkgJson = await loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail)
|
|
175
160
|
|
|
176
161
|
const importViolations = await checkBunyanImports(absPackageRoot, label, fail)
|
|
177
162
|
if (importViolations === 0) {
|
|
@@ -193,19 +178,54 @@ async function checkWorkspacePackage(rootDir, fail, passFn) {
|
|
|
193
178
|
)
|
|
194
179
|
}
|
|
195
180
|
|
|
181
|
+
await checkOtelConfigmap(rootDir, label, fail, passFn)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Завантажує `package.json` пакета (якщо є) і реєструє порушення для bunyan-залежностей.
|
|
186
|
+
* @param {string} rootDir відносний шлях workspace
|
|
187
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
188
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
189
|
+
* @returns {Promise<unknown>} розпарсений package.json або null
|
|
190
|
+
*/
|
|
191
|
+
async function loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail) {
|
|
192
|
+
const pkgPath = join(rootDir, 'package.json')
|
|
193
|
+
if (!existsSync(pkgPath)) return null
|
|
194
|
+
const pkgJson = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
195
|
+
const deps = /** @type {Record<string, unknown>} */ (pkgJson).dependencies
|
|
196
|
+
const devDeps = /** @type {Record<string, unknown>} */ (pkgJson).devDependencies
|
|
197
|
+
const allDeps = { ...deps, ...devDeps }
|
|
198
|
+
if (allDeps['@nitra/bunyan']) {
|
|
199
|
+
fail(`${label}@nitra/bunyan знайдено — замінити на @nitra/pino`)
|
|
200
|
+
}
|
|
201
|
+
if (allDeps.bunyan) {
|
|
202
|
+
fail(`${label}bunyan знайдено — замінити на @nitra/pino`)
|
|
203
|
+
}
|
|
204
|
+
return pkgJson
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Перевіряє вміст `k8s/base/configmap.yaml` пакета на наявність OTEL_RESOURCE_ATTRIBUTES
|
|
209
|
+
* з обов'язковими `service.name=` та `service.namespace=` всередині.
|
|
210
|
+
* @param {string} rootDir відносний шлях workspace
|
|
211
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
212
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
213
|
+
* @param {(msg: string) => void} passFn успішне повідомлення
|
|
214
|
+
* @returns {Promise<void>} завершується після перевірки configmap
|
|
215
|
+
*/
|
|
216
|
+
async function checkOtelConfigmap(rootDir, label, fail, passFn) {
|
|
196
217
|
const configmapPath = join(rootDir, 'k8s', 'base', 'configmap.yaml')
|
|
197
|
-
if (existsSync(configmapPath))
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
218
|
+
if (!existsSync(configmapPath)) return
|
|
219
|
+
const content = await readFile(configmapPath, 'utf8')
|
|
220
|
+
if (!content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
|
|
221
|
+
fail(`${label}k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES`)
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
passFn(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
|
|
225
|
+
if (content.includes('service.name=') && content.includes('service.namespace=')) {
|
|
226
|
+
passFn(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
|
|
227
|
+
} else {
|
|
228
|
+
fail(`${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`)
|
|
209
229
|
}
|
|
210
230
|
}
|
|
211
231
|
|