@nitra/cursor 1.8.144 → 1.8.147
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 +2 -0
- package/bin/n-cursor.js +1 -0
- package/bin/rename-yaml-extensions.mjs +0 -1
- package/mdc/js-bun-db.mdc +118 -0
- package/mdc/js-lint.mdc +6 -4
- package/mdc/js-mssql.mdc +27 -1
- package/mdc/k8s.mdc +2 -2
- package/mdc/vue.mdc +3 -3
- package/package.json +3 -3
- package/scripts/auto-rules.mjs +104 -26
- package/scripts/check-ga.mjs +27 -15
- package/scripts/check-js-bun-db.mjs +213 -0
- package/scripts/check-js-lint.mjs +110 -27
- package/scripts/check-js-mssql.mjs +156 -86
- package/scripts/check-k8s.mjs +161 -32
- package/scripts/check-nginx-default-tpl.mjs +1 -1
- package/scripts/check-vue.mjs +82 -45
- package/scripts/cli-entry.mjs +2 -5
- package/scripts/upgrade-nitra-cursor-and-install.mjs +1 -1
- package/scripts/utils/bun-sql-scan.mjs +294 -0
- package/scripts/utils/mssql-pool-scan.mjs +188 -1
- package/scripts/utils/oxlint-canonical-skeleton.json +27 -0
- package/scripts/utils/oxlint-canonical.json +387 -0
- package/scripts/utils/oxlint-rules.tsv +359 -0
- package/scripts/utils/rebuild-oxlint-canonical.mjs +29 -0
package/bin/auto-rules.md
CHANGED
|
@@ -22,6 +22,8 @@ js-pino - якщо присутній хоч один js файл, не в мо
|
|
|
22
22
|
|
|
23
23
|
js-mssql - якщо в хоч одному package.json в секції dependencies присутній пакет mssql
|
|
24
24
|
|
|
25
|
+
js-bun-db - якщо в хоч одному package.json в секції dependencies присутній пакет pg або mysql2 або є імпорт sql/SQL з Bun (приклад: import { sql } from "bun")
|
|
26
|
+
|
|
25
27
|
k8s - якщо присутня хоч одна директорія k8s
|
|
26
28
|
|
|
27
29
|
nginx-default-tpl - якщо присутній хоч один файл з переліку - default.conf.template, default.conf, nginx.conf
|
package/bin/n-cursor.js
CHANGED
|
@@ -1036,6 +1036,7 @@ async function runChecks(requestedRules) {
|
|
|
1036
1036
|
const scriptPath = join(BUNDLED_SCRIPTS_DIR, `check-${rule}.mjs`)
|
|
1037
1037
|
console.log(`📋 ${rule}:`)
|
|
1038
1038
|
try {
|
|
1039
|
+
// eslint-disable-next-line no-unsanitized/method -- rule валідовано проти available, scriptPath будується з фіксованої BUNDLED_SCRIPTS_DIR
|
|
1039
1040
|
const { check } = await import(scriptPath)
|
|
1040
1041
|
const code = await check()
|
|
1041
1042
|
if (code !== 0) totalFailed++
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Використання pg / mysql2 / Bun SQL у Node.js та Bun
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '1.1'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Підтримувані версії баз даних
|
|
8
|
+
|
|
9
|
+
PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом, підключаємось як `mysql://`).
|
|
10
|
+
|
|
11
|
+
## Заміна на Bun native SQL
|
|
12
|
+
|
|
13
|
+
Якщо в проєкті використовуються бібліотеки `pg` або `mysql2`, їх потрібно замінити на Bun native SQL: <https://bun.com/docs/runtime/sql>.
|
|
14
|
+
|
|
15
|
+
- Видалити з `dependencies`: `pg`, `pg-pool`, `pg-native`, `mysql`, `mysql2`.
|
|
16
|
+
- Видалити з коду: усі `import` / `require` цих пакетів та власні обгортки над ними.
|
|
17
|
+
- Замінити на `import { sql, SQL } from 'bun'` — Bun має вбудований клієнт із пулом, prepared statements та tagged templates.
|
|
18
|
+
|
|
19
|
+
## Підключення (singleton + env)
|
|
20
|
+
|
|
21
|
+
Дефолтний експорт `sql` з `'bun'` сам читає змінні середовища (`DATABASE_URL`, `POSTGRES_URL`, `MYSQL_URL`, `PGHOST`/`PGUSER`/... та `MYSQL_HOST`/`MYSQL_USER`/...) і керує пулом — окремий `Pool` як у `pg` створювати не треба.
|
|
22
|
+
|
|
23
|
+
Для явного конфігу — `new SQL(...)` як **singleton** на рівні модуля, а не на кожен запит:
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
// db.js
|
|
27
|
+
import { SQL } from 'bun'
|
|
28
|
+
|
|
29
|
+
export const sql = new SQL({
|
|
30
|
+
url: process.env.DATABASE_URL,
|
|
31
|
+
max: 20,
|
|
32
|
+
idleTimeout: 30,
|
|
33
|
+
connectionTimeout: 10
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Connection string обирає адаптер автоматично:
|
|
38
|
+
|
|
39
|
+
- `postgres://...` / `postgresql://...` → PostgreSQL
|
|
40
|
+
- `mysql://...` / `mysql2://...` → MySQL/MariaDB
|
|
41
|
+
- `sqlite://...` / `file://...` / `:memory:` → SQLite
|
|
42
|
+
|
|
43
|
+
## Як виконувати запити (безпечно)
|
|
44
|
+
|
|
45
|
+
Тільки **tagged template** з `${...}` — Bun сам біндить позиційні параметри й захищає від SQL injection:
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
import { sql } from 'bun'
|
|
49
|
+
|
|
50
|
+
const userId = 42
|
|
51
|
+
const status = 'active'
|
|
52
|
+
|
|
53
|
+
const users = await sql`
|
|
54
|
+
SELECT * FROM users
|
|
55
|
+
WHERE id = ${userId} AND status = ${status}
|
|
56
|
+
`
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Об'єктний INSERT/UPDATE та `IN (...)` — через helper `sql(...)`:
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
const user = { name: 'Alice', email: 'a@example.com' }
|
|
63
|
+
|
|
64
|
+
const [created] = await sql`
|
|
65
|
+
INSERT INTO users ${sql(user)}
|
|
66
|
+
RETURNING *
|
|
67
|
+
`
|
|
68
|
+
|
|
69
|
+
await sql`UPDATE users SET ${sql(user, 'name', 'email')} WHERE id = ${created.id}`
|
|
70
|
+
|
|
71
|
+
const ids = [1, 2, 3]
|
|
72
|
+
await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Транзакції — через `sql.begin` (auto-commit/rollback), вкладені — через `tx.savepoint`:
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
await sql.begin(async tx => {
|
|
79
|
+
await tx`INSERT INTO users ${sql(user)}`
|
|
80
|
+
await tx`UPDATE accounts SET balance = balance - ${100} WHERE user_id = ${user.id}`
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Що НЕ робити
|
|
85
|
+
|
|
86
|
+
### Не використовувати `sql.unsafe(...)` з конкатенацією
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
// ❌ конкатенація даних у SQL — SQL injection
|
|
90
|
+
await sql.unsafe(`SELECT * FROM users WHERE id = ${userId}`)
|
|
91
|
+
|
|
92
|
+
// ❌ навіть у tagged template — динамічний список через .join(',')
|
|
93
|
+
await sql`SELECT * FROM users WHERE id IN (${ids.join(',')})`
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`sql.unsafe(text, params)` допустимий лише для **статичного** SQL без даних від користувача (наприклад, разовий DDL-скрипт), і обов'язково з масивом параметрів — ніяких `${...}` у самому рядку.
|
|
97
|
+
|
|
98
|
+
Для динамічних списків — `sql([...])` або `sql(rows, 'colA', 'colB')`, **не** `.join(',')`.
|
|
99
|
+
|
|
100
|
+
### Не створювати підключення на кожен запит
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
// ❌ нове підключення/інстанс на кожен виклик
|
|
104
|
+
function getUser(id) {
|
|
105
|
+
const sql = new SQL(process.env.DATABASE_URL)
|
|
106
|
+
return sql`SELECT * FROM users WHERE id = ${id}`
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`new SQL(...)` має створюватись **один раз** на рівні модуля. Bun сам тримає пул (`max`, `idleTimeout`, `maxLifetime`) — окремих `Pool`/`Client` як у `pg` не потрібно.
|
|
111
|
+
|
|
112
|
+
### Не лишати `pg` / `mysql2` поряд із Bun SQL
|
|
113
|
+
|
|
114
|
+
Якщо в коді з'явився `import { sql } from 'bun'`, то `pg` та `mysql2` мають бути прибрані і з `dependencies`, і з імпортів — щоб не лишалось двох паралельних шляхів до БД.
|
|
115
|
+
|
|
116
|
+
## Перевірка
|
|
117
|
+
|
|
118
|
+
`npx @nitra/cursor check js-bun-db`.
|
package/mdc/js-lint.mdc
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Перевірка JavaScript коду
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.15'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
**oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config` мінімум `^3.
|
|
7
|
+
**oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. У **`devDependencies`** має бути **`@nitra/eslint-config` мінімум `^3.6.12`** (з ним транзитивно йде **`@e18e/eslint-plugin`** для oxlint); пакет **`@e18e/eslint-plugin`** окремо не додавай. Пакети oxlint/eslint/jscpd не додавай без потреби монорепо.
|
|
8
8
|
|
|
9
9
|
```json title=".vscode/extensions.json"
|
|
10
10
|
{
|
|
@@ -25,7 +25,7 @@ version: '1.14'
|
|
|
25
25
|
"lint-js": "bunx oxlint --fix && bunx eslint --fix . && bunx jscpd ."
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@nitra/eslint-config": "^3.
|
|
28
|
+
"@nitra/eslint-config": "^3.6.12"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=24"
|
|
@@ -33,7 +33,9 @@ version: '1.14'
|
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
У корені має бути **`.oxlintrc.json
|
|
36
|
+
У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/scripts/utils/oxlint-canonical.json`** (plugins, jsPlugins з **`@e18e/eslint-plugin`**, categories, повний набір **rules** із канону — додаткові записи в **`rules`** дозволені; також **`settings`**, **`env`**, **`globals`**, **`ignorePatterns`**). Оновити можна з репозиторію пакета або скопіювавши файл після **`bun ./scripts/utils/rebuild-oxlint-canonical.mjs`** (джерело правил — **`oxlint-rules.tsv`** + скелет **`oxlint-canonical-skeleton.json`**). Модуль **`@e18e/eslint-plugin`** не оголошуй окремо в **`package.json`** — він уже в залежностях **`@nitra/eslint-config`** (з **3.6.12**), oxlint підвантажує його з **`node_modules`**.
|
|
37
|
+
|
|
38
|
+
Мінімум для розуміння структури (реальний корінь конфігу має збігатися з каноном повністю):
|
|
37
39
|
|
|
38
40
|
```json title=".oxlintrc.json (фрагмент)"
|
|
39
41
|
{
|
package/mdc/js-mssql.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Використання mssql в nodejs
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.2'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Підтримувана версія SQL Server
|
|
@@ -160,4 +160,30 @@ WHERE NOT EXISTS (
|
|
|
160
160
|
- ліміт на кількість елементів (наприклад 5k/10k — залежить від вашого потоку).
|
|
161
161
|
- `supplierId`/ID: число/BigInt, валідне та скінченне.
|
|
162
162
|
|
|
163
|
+
## Парсинг значень для `IN (${...})`
|
|
164
|
+
|
|
165
|
+
Якщо `IN (...)` все ж використовується (а не `JOIN` на TVP), значення в `${...}` **обовʼязково** мають бути попередньо приведені числовим парсером і відфільтровані від `NaN`. Це знімає будь-яку можливість SQL injection: SQL-метасимволи в `Number`/`parseInt(...)` перетворюються на `NaN` і відсіюються.
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
// ❌ НЕ МОЖНА: значення з req.body / зовнішнього джерела без парсингу
|
|
169
|
+
const outIds = pgQ.rows.flatMap(x => x.req_body.Orders.map(o => o.OutletId))
|
|
170
|
+
await pool.query(/* sql */ String.raw`
|
|
171
|
+
SELECT ... WHERE so.OutletId IN (${outIds})
|
|
172
|
+
`)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
// ✅ МОЖНА: parseInt + filter(!isNaN) гарантує, що в SQL потраплять лише числа
|
|
177
|
+
const outIds = pgQ.rows
|
|
178
|
+
.flatMap(x => x.req_body.Orders.map(o => parseInt(o.OutletId)))
|
|
179
|
+
.filter(n => !isNaN(n))
|
|
180
|
+
await pool.request().query`
|
|
181
|
+
SELECT ... WHERE so.OutletId IN (${outIds})
|
|
182
|
+
`
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Допустимі парсери: `parseInt(...)`, `parseFloat(...)`, `Number(...)`, `BigInt(...)` або унарний `+x`. Літеральні масиви чисел (`[1, 2, 3]`) теж безпечні — без парсера, але без жодних рядків.
|
|
186
|
+
|
|
187
|
+
Це правило діє і для безпечного `pool.request().query\`...\`` (де mssql сам параметризує масив), і поготів для `pool.query(String.raw\`...\`)` чи `pool.query(\`...\`)`, де такий парсинг — єдиний бар'єр.
|
|
188
|
+
|
|
163
189
|
Перевірка: `npx @nitra/cursor check js-mssql`.
|
package/mdc/k8s.mdc
CHANGED
|
@@ -288,13 +288,13 @@ data:
|
|
|
288
288
|
|
|
289
289
|
## Deployment: обов'язкові `hpa.yaml`, `pdb.yaml`, `topologySpreadConstraints`
|
|
290
290
|
|
|
291
|
-
Для **кожного** `kind: Deployment`
|
|
291
|
+
Для **кожного** `kind: Deployment` у каталозі **`…/k8s/…/base/`** (у будь-якому файлі `.yaml`, наприклад **`deploy.yaml`**, **`deployment.yaml`**) у **тому ж каталозі** мають бути **`hpa.yaml`** (HPA) і **`pdb.yaml`** (PDB), а сам Deployment — канонічні **`spec.template.spec.topologySpreadConstraints`**. Інші workload-и (**CronJob**, **Job** тощо) або каталоги без шару **`base`** цими вимогами не охоплюються — **`check k8s`** їх не змушує додавати HPA/PDB поруч. Скрипт звіряє прив’язку за іменами:
|
|
292
292
|
|
|
293
293
|
- **`hpa.yaml`** — `autoscaling/v2`, `HorizontalPodAutoscaler`, `spec.scaleTargetRef.name` **= `metadata.name`** Deployment.
|
|
294
294
|
- **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment.
|
|
295
295
|
- **`topologySpreadConstraints`** — запис з `maxSkew: 1`, `topologyKey: kubernetes.io/hostname`, `whenUnsatisfiable: ScheduleAnyway`, `labelSelector.matchLabels.app` рівне тій самій мітці `app`.
|
|
296
296
|
|
|
297
|
-
**Kustomize `base` і overlay:** у дереві, зібраному з `k8s/…/base/kustomization.yaml` (`resources` / `bases` / `components` / `crds`, рекурсивно), **HorizontalPodAutoscaler** і **PodDisruptionBudget** допустимі **лише якщо** в тому ж дереві kustomize є
|
|
297
|
+
**Kustomize `base` і overlay:** у дереві, зібраному з `k8s/…/base/kustomization.yaml` (`resources` / `bases` / `components` / `crds`, рекурсивно), **HorizontalPodAutoscaler** і **PodDisruptionBudget** допустимі **лише якщо** в тому ж дереві kustomize є хоча б один **`Deployment`** у YAML під **`…/k8s/…/base/`**. У `kustomization.yaml` overlay, який підключає цей `base` (наприклад, `../base` у `resources`), не додавай окремі YAML-файли з HPA / PDB, доки в наслідуваному `base` у дереві не з’явиться такий Deployment. Перевіряє **`check-k8s.mjs`**.
|
|
298
298
|
|
|
299
299
|
**Локальні шляхи в `kustomization.yaml`:** кожен запис без `://` (remote) з `resources` / `bases` / `components` / `crds`, `patchesStrategicMerge`, `patches[].path`, `patchesJson6902[].path`, `configurations[]`, `replacements[].path` має вказувати на **існуючий** у репозиторії файл (`.yaml` / `.yml`) або **каталог**; биті посилання — помилка **`check k8s`**.
|
|
300
300
|
|
package/mdc/vue.mdc
CHANGED
|
@@ -156,7 +156,7 @@ export default {
|
|
|
156
156
|
"private": true,
|
|
157
157
|
"type": "module",
|
|
158
158
|
"dependencies": {
|
|
159
|
-
"vue": "^3.
|
|
159
|
+
"vue": "^3.6.12"
|
|
160
160
|
},
|
|
161
161
|
"devDependencies": {
|
|
162
162
|
"vite": "^8.0.0",
|
|
@@ -224,7 +224,7 @@ export default defineConfig({
|
|
|
224
224
|
|
|
225
225
|
## npm_lifecycle_event
|
|
226
226
|
|
|
227
|
-
у більшості проектів в файлі vite.config.js
|
|
227
|
+
у більшості проектів в файлі vite.config.js
|
|
228
228
|
є конструкція виду
|
|
229
229
|
|
|
230
230
|
switch (process.env.npm_lifecycle_event) {
|
|
@@ -253,7 +253,7 @@ function getProxy(mode) {
|
|
|
253
253
|
}
|
|
254
254
|
```
|
|
255
255
|
|
|
256
|
-
і викликати всередині
|
|
256
|
+
і викликати всередині
|
|
257
257
|
|
|
258
258
|
```javascript title="vite.config.js"
|
|
259
259
|
export default defineConfig(({ mode, command }) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/cursor",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.147",
|
|
4
4
|
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"rename-yaml-extensions": "bun ./bin/n-cursor.js rename-yaml-extensions"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"oxc-parser": "^0.
|
|
42
|
+
"oxc-parser": "^0.128.0",
|
|
43
43
|
"yaml": "^2.8.3"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
|
46
|
-
"node": ">=
|
|
46
|
+
"node": ">=25"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/scripts/auto-rules.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Автовизначення правил і skills для `.n-cursor.json` за умовами з `npm/bin/auto-rules.md`.
|
|
3
3
|
*
|
|
4
|
-
* Модуль аналізує дерево проєкту (наявність файлів/директорій, `gql\`...\`` у source,
|
|
4
|
+
* Модуль аналізує дерево проєкту (наявність файлів/директорій, `gql\`...\`` у source,
|
|
5
|
+
* залежності `mssql` / `pg` / `mysql2` у `package.json`, імпорт `sql`/`SQL` з `bun`, кореневий
|
|
5
6
|
* `package.json`) та повертає ідентифікатори правил і skills, які потрібно автододати.
|
|
6
7
|
*
|
|
7
8
|
* Також враховує винятки `disable-rules` і `disable-skills`: елементи з цих списків не
|
|
@@ -11,11 +12,13 @@ import { existsSync } from 'node:fs'
|
|
|
11
12
|
import { readdir, readFile } from 'node:fs/promises'
|
|
12
13
|
import { basename, join, relative } from 'node:path'
|
|
13
14
|
|
|
15
|
+
import { textHasBunSqlImport } from './utils/bun-sql-scan.mjs'
|
|
14
16
|
import {
|
|
15
17
|
isGqlScanSourceFile,
|
|
16
18
|
shouldSkipFileForGqlScan,
|
|
17
19
|
sourceFileHasGqlTaggedTemplate
|
|
18
20
|
} from './utils/graphql-gql-scan.mjs'
|
|
21
|
+
import { contentForVueImportScan } from './utils/vue-forbidden-imports.mjs'
|
|
19
22
|
|
|
20
23
|
/** Порядок автододавання правил відповідно до `auto-rules.md`. */
|
|
21
24
|
export const AUTO_RULE_ORDER = Object.freeze([
|
|
@@ -27,6 +30,7 @@ export const AUTO_RULE_ORDER = Object.freeze([
|
|
|
27
30
|
'graphql',
|
|
28
31
|
'js-lint',
|
|
29
32
|
'js-mssql',
|
|
33
|
+
'js-bun-db',
|
|
30
34
|
'js-pino',
|
|
31
35
|
'k8s',
|
|
32
36
|
'nginx-default-tpl',
|
|
@@ -50,12 +54,66 @@ const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo'])
|
|
|
50
54
|
const DEFAULT_DISABLED_LIST = Object.freeze([])
|
|
51
55
|
|
|
52
56
|
/**
|
|
53
|
-
* Чи
|
|
57
|
+
* Чи містить текст джерела імпорт імені `sql` або `SQL` з `"bun"` (після витягування `<script>` у `.vue`).
|
|
58
|
+
* @param {string} content вміст файлу
|
|
59
|
+
* @param {string} relativePath шлях posix відносно кореня
|
|
60
|
+
* @returns {boolean} true, якщо знайдено `import { sql }` або `import { SQL }` з `"bun"`
|
|
61
|
+
*/
|
|
62
|
+
function sourceContentHasBunSqlImport(content, relativePath) {
|
|
63
|
+
return textHasBunSqlImport(contentForVueImportScan(content, relativePath))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Зчитує `package.json` і додає в `found` усі ключі з `wanted`, що присутні в `dependencies`.
|
|
68
|
+
* @param {string} absPath абсолютний шлях до package.json
|
|
69
|
+
* @param {Set<string>} wanted множина ключів-цілей
|
|
70
|
+
* @param {Set<string>} found буфер знайдених ключів
|
|
71
|
+
* @returns {Promise<void>}
|
|
72
|
+
*/
|
|
73
|
+
async function collectFoundDependencyKeysFromPackageJson(absPath, wanted, found) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(await readFile(absPath, 'utf8'))
|
|
76
|
+
const deps = parsed?.dependencies
|
|
77
|
+
if (!deps || typeof deps !== 'object' || Array.isArray(deps)) return
|
|
78
|
+
for (const key of wanted) {
|
|
79
|
+
if (Object.hasOwn(deps, key)) {
|
|
80
|
+
found.add(key)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
/* ігноруємо пошкоджені/недоступні package.json */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Збирає, які з переданих ключів присутні в `dependencies` хоча б одного `package.json`.
|
|
54
90
|
* @param {string} root абсолютний шлях до кореня репозиторію
|
|
55
|
-
* @
|
|
91
|
+
* @param {string[]} dependencyKeys імена залежностей (наприклад `mssql`, `pg`)
|
|
92
|
+
* @returns {Promise<Set<string>>} множина знайдених ключів
|
|
56
93
|
*/
|
|
57
|
-
async function
|
|
58
|
-
|
|
94
|
+
async function collectDependencyKeysPresentInPackageJsonTree(root, dependencyKeys) {
|
|
95
|
+
const wanted = new Set(dependencyKeys)
|
|
96
|
+
/** @type {Set<string>} */
|
|
97
|
+
const found = new Set()
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Обробка одного запису з readdir: рекурсія в підкаталог або зчитування package.json.
|
|
101
|
+
* @param {import('node:fs').Dirent} entry елемент readdir
|
|
102
|
+
* @param {string} dir абсолютний шлях каталогу-власника entry
|
|
103
|
+
* @returns {Promise<void>}
|
|
104
|
+
*/
|
|
105
|
+
async function processEntry(entry, dir) {
|
|
106
|
+
const absPath = join(dir, entry.name)
|
|
107
|
+
if (entry.isDirectory()) {
|
|
108
|
+
if (!IGNORED_DIR_NAMES.has(entry.name)) {
|
|
109
|
+
await walk(absPath)
|
|
110
|
+
}
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (entry.isFile() && entry.name === 'package.json') {
|
|
114
|
+
await collectFoundDependencyKeysFromPackageJson(absPath, wanted, found)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
59
117
|
|
|
60
118
|
/**
|
|
61
119
|
* Рекурсивний обхід каталогу з пропуском службових директорій.
|
|
@@ -63,7 +121,7 @@ async function hasMssqlDependencyInAnyPackageJson(root) {
|
|
|
63
121
|
* @returns {Promise<void>}
|
|
64
122
|
*/
|
|
65
123
|
async function walk(dir) {
|
|
66
|
-
if (found) return
|
|
124
|
+
if (found.size === wanted.size) return
|
|
67
125
|
let entries
|
|
68
126
|
try {
|
|
69
127
|
entries = await readdir(dir, { withFileTypes: true })
|
|
@@ -71,25 +129,8 @@ async function hasMssqlDependencyInAnyPackageJson(root) {
|
|
|
71
129
|
return
|
|
72
130
|
}
|
|
73
131
|
for (const entry of entries) {
|
|
74
|
-
if (found) return
|
|
75
|
-
|
|
76
|
-
if (entry.isDirectory()) {
|
|
77
|
-
const isIgnoredDir = IGNORED_DIR_NAMES.has(entry.name)
|
|
78
|
-
if (!isIgnoredDir) {
|
|
79
|
-
await walk(absPath)
|
|
80
|
-
}
|
|
81
|
-
} else if (entry.isFile() && entry.name === 'package.json') {
|
|
82
|
-
try {
|
|
83
|
-
const parsed = JSON.parse(await readFile(absPath, 'utf8'))
|
|
84
|
-
const deps = parsed?.dependencies
|
|
85
|
-
if (deps && typeof deps === 'object' && !Array.isArray(deps) && Object.hasOwn(deps, 'mssql')) {
|
|
86
|
-
found = true
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
} catch {
|
|
90
|
-
/* ігноруємо пошкоджені/недоступні package.json */
|
|
91
|
-
}
|
|
92
|
-
}
|
|
132
|
+
if (found.size === wanted.size) return
|
|
133
|
+
await processEntry(entry, dir)
|
|
93
134
|
}
|
|
94
135
|
}
|
|
95
136
|
|
|
@@ -182,11 +223,40 @@ async function updateGqlFactFromFile(absPath, relPath, facts) {
|
|
|
182
223
|
}
|
|
183
224
|
}
|
|
184
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Чи сканувати файл на імпорт `sql`/`SQL` з `bun` (ті самі розширення й skip, що для gql).
|
|
228
|
+
* @param {string} relPath шлях posix відносно кореня
|
|
229
|
+
* @param {{ hasBunSqlImport: boolean }} facts агреговані факти
|
|
230
|
+
* @returns {boolean} true, якщо файл варто сканувати
|
|
231
|
+
*/
|
|
232
|
+
function shouldScanFileForBunSql(relPath, facts) {
|
|
233
|
+
return !facts.hasBunSqlImport && isGqlScanSourceFile(relPath) && !shouldSkipFileForGqlScan(relPath)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Оновлює ознаку `hasBunSqlImport` за вмістом файлу.
|
|
238
|
+
* @param {string} absPath абсолютний шлях до файлу
|
|
239
|
+
* @param {string} relPath шлях posix відносно кореня
|
|
240
|
+
* @param {{ hasBunSqlImport: boolean }} facts агреговані факти
|
|
241
|
+
* @returns {Promise<void>}
|
|
242
|
+
*/
|
|
243
|
+
async function updateBunSqlFactFromFile(absPath, relPath, facts) {
|
|
244
|
+
try {
|
|
245
|
+
const content = await readFile(absPath, 'utf8')
|
|
246
|
+
if (sourceContentHasBunSqlImport(content, relPath)) {
|
|
247
|
+
facts.hasBunSqlImport = true
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
/* ігноруємо пошкоджені/недоступні файли */
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
185
254
|
/**
|
|
186
255
|
* Обробляє файл під час обходу дерева.
|
|
187
256
|
* @param {string} absPath абсолютний шлях до файлу
|
|
188
257
|
* @param {string} root абсолютний шлях кореня
|
|
189
258
|
* @param {{
|
|
259
|
+
* hasBunSqlImport: boolean,
|
|
190
260
|
* hasCapacitorConfig: boolean,
|
|
191
261
|
* hasDockerfile: boolean,
|
|
192
262
|
* hasGqlTaggedTemplates: boolean,
|
|
@@ -204,6 +274,9 @@ async function processFileEntry(absPath, root, facts) {
|
|
|
204
274
|
if (shouldScanFileForGql(rel, facts)) {
|
|
205
275
|
await updateGqlFactFromFile(absPath, rel, facts)
|
|
206
276
|
}
|
|
277
|
+
if (shouldScanFileForBunSql(rel, facts)) {
|
|
278
|
+
await updateBunSqlFactFromFile(absPath, rel, facts)
|
|
279
|
+
}
|
|
207
280
|
}
|
|
208
281
|
|
|
209
282
|
/**
|
|
@@ -270,6 +343,7 @@ export function isMonorepoPackage(packageJson) {
|
|
|
270
343
|
* hasCapacitorConfig: boolean,
|
|
271
344
|
* hasDockerfile: boolean,
|
|
272
345
|
* hasGaWorkflowsDir: boolean,
|
|
346
|
+
* hasBunSqlImport: boolean,
|
|
273
347
|
* hasGqlTaggedTemplates: boolean,
|
|
274
348
|
* hasJsLikeSource: boolean,
|
|
275
349
|
* hasK8sDir: boolean,
|
|
@@ -282,6 +356,7 @@ export function isMonorepoPackage(packageJson) {
|
|
|
282
356
|
*/
|
|
283
357
|
export async function collectAutoRuleFacts(root) {
|
|
284
358
|
const facts = {
|
|
359
|
+
hasBunSqlImport: false,
|
|
285
360
|
hasCapacitorConfig: false,
|
|
286
361
|
hasDockerfile: false,
|
|
287
362
|
hasGaWorkflowsDir: existsSync(join(root, '.github', 'workflows')),
|
|
@@ -360,7 +435,9 @@ export async function detectAutoRulesAndSkills({
|
|
|
360
435
|
)
|
|
361
436
|
const isAbie = typeof repositoryUrl === 'string' && repositoryUrl.toLowerCase().includes(ABIE_REPOSITORY_URL_MARKER)
|
|
362
437
|
const isMonorepo = isMonorepoPackage(packageJsonParsed)
|
|
363
|
-
const
|
|
438
|
+
const depHits = await collectDependencyKeysPresentInPackageJsonTree(root, ['mssql', 'pg', 'mysql2'])
|
|
439
|
+
const hasMssqlDependency = depHits.has('mssql')
|
|
440
|
+
const hasJsBunDbSignal = depHits.has('pg') || depHits.has('mysql2') || facts.hasBunSqlImport
|
|
364
441
|
|
|
365
442
|
/** @type {string[]} */
|
|
366
443
|
const detectedRules = []
|
|
@@ -400,6 +477,7 @@ export async function detectAutoRulesAndSkills({
|
|
|
400
477
|
{ enabled: facts.hasGqlTaggedTemplates, id: 'graphql' },
|
|
401
478
|
{ enabled: facts.hasJsLikeSource, id: 'js-lint' },
|
|
402
479
|
{ enabled: hasMssqlDependency, id: 'js-mssql' },
|
|
480
|
+
{ enabled: hasJsBunDbSignal, id: 'js-bun-db' },
|
|
403
481
|
{ enabled: facts.hasJsLikeSource && !(isMonorepo && facts.hasVueSource && facts.hasTempoDir), id: 'js-pino' },
|
|
404
482
|
{ enabled: facts.hasK8sDir, id: 'k8s' },
|
|
405
483
|
{ enabled: facts.hasNginxDefaultTplFile, id: 'nginx-default-tpl' },
|
package/scripts/check-ga.mjs
CHANGED
|
@@ -60,7 +60,6 @@ const REQUIRED_WORKFLOWS = ['clean-ga-workflows.yml', 'clean-merged-branch.yml',
|
|
|
60
60
|
*
|
|
61
61
|
* Використовує `git ls-files` з pathspec-магiєю `:(glob)`, щоб не реалізовувати glob engine вручну
|
|
62
62
|
* і не сканувати файлову систему рекурсивно.
|
|
63
|
-
*
|
|
64
63
|
* @param {string} globPattern glob з workflow (наприклад "files/**" або "image-migration-new/**")
|
|
65
64
|
* @returns {boolean} true, якщо є хоча б один збіг
|
|
66
65
|
*/
|
|
@@ -69,6 +68,7 @@ function gitHasAnyTrackedFileMatchingGlob(globPattern) {
|
|
|
69
68
|
if (!p) return false
|
|
70
69
|
if (p.startsWith('!')) return true
|
|
71
70
|
try {
|
|
71
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path -- git як стандартне dev-середовище через PATH; альтернативи (хардкод шляху) непортативні
|
|
72
72
|
const out = execFileSync('git', ['ls-files', '-z', '--', `:(glob)${p}`], { encoding: 'utf8' })
|
|
73
73
|
return out.length > 0
|
|
74
74
|
} catch {
|
|
@@ -82,7 +82,6 @@ function gitHasAnyTrackedFileMatchingGlob(globPattern) {
|
|
|
82
82
|
* У багатьох workflow (особливо лінтерах) `paths` часто містить “широкі” шаблони по розширеннях
|
|
83
83
|
* (наприклад `*.vue`, `*.php`), які можуть бути відсутні в конкретному репозиторії й це ок.
|
|
84
84
|
* Запит цієї перевірки — ловити посилання на неіснуючі директорії/шляхи (типово `some-dir/**`).
|
|
85
|
-
*
|
|
86
85
|
* @param {string} p glob з workflow
|
|
87
86
|
* @returns {boolean} true, якщо треба валідувати наявність файлів
|
|
88
87
|
*/
|
|
@@ -96,6 +95,28 @@ function shouldValidateWorkflowPathsGlob(p) {
|
|
|
96
95
|
return true
|
|
97
96
|
}
|
|
98
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Перевіряє один glob з `on.<event>.paths` на наявність збігів у репо.
|
|
100
|
+
* @param {string} relPath шлях workflow для повідомлень
|
|
101
|
+
* @param {string} eventName назва події (push / pull_request)
|
|
102
|
+
* @param {unknown} raw сирий елемент масиву paths
|
|
103
|
+
* @param {(msg: string) => void} passFn pass
|
|
104
|
+
* @param {(msg: string) => void} failFn fail
|
|
105
|
+
*/
|
|
106
|
+
function verifyOnePathsGlob(relPath, eventName, raw, passFn, failFn) {
|
|
107
|
+
const p = String(raw ?? '').trim()
|
|
108
|
+
if (!p) return
|
|
109
|
+
if (!shouldValidateWorkflowPathsGlob(p)) {
|
|
110
|
+
passFn(`${relPath}: on.${eventName}.paths glob пропущено для перевірки існування: ${JSON.stringify(p)}`)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (gitHasAnyTrackedFileMatchingGlob(p)) {
|
|
114
|
+
passFn(`${relPath}: on.${eventName}.paths glob матчитсья: ${JSON.stringify(p)}`)
|
|
115
|
+
} else {
|
|
116
|
+
failFn(`${relPath}: on.${eventName}.paths glob не матчитсья ні на один файл: ${JSON.stringify(p)}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
/**
|
|
100
121
|
* Валідує `on.push.paths` / `on.pull_request.paths`: кожен позитивний glob має мати збіги в репозиторії.
|
|
101
122
|
* @param {string} relPath шлях workflow для повідомлень
|
|
@@ -116,17 +137,7 @@ function verifyWorkflowEventPathsGlobsExist(relPath, root, passFn, failFn) {
|
|
|
116
137
|
for (const [eventName, paths] of candidates) {
|
|
117
138
|
if (!Array.isArray(paths)) continue
|
|
118
139
|
for (const raw of paths) {
|
|
119
|
-
|
|
120
|
-
if (!p) continue
|
|
121
|
-
if (!shouldValidateWorkflowPathsGlob(p)) {
|
|
122
|
-
passFn(`${relPath}: on.${eventName}.paths glob пропущено для перевірки існування: ${JSON.stringify(p)}`)
|
|
123
|
-
continue
|
|
124
|
-
}
|
|
125
|
-
if (gitHasAnyTrackedFileMatchingGlob(p)) {
|
|
126
|
-
passFn(`${relPath}: on.${eventName}.paths glob матчитсья: ${JSON.stringify(p)}`)
|
|
127
|
-
} else {
|
|
128
|
-
failFn(`${relPath}: on.${eventName}.paths glob не матчитсья ні на один файл: ${JSON.stringify(p)}`)
|
|
129
|
-
}
|
|
140
|
+
verifyOnePathsGlob(relPath, eventName, raw, passFn, failFn)
|
|
130
141
|
}
|
|
131
142
|
}
|
|
132
143
|
}
|
|
@@ -138,8 +149,9 @@ function verifyWorkflowEventPathsGlobsExist(relPath, root, passFn, failFn) {
|
|
|
138
149
|
* @returns {unknown} значення поля або undefined
|
|
139
150
|
*/
|
|
140
151
|
function getObjKey(obj, key) {
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
return obj && typeof obj === 'object' && !Array.isArray(obj)
|
|
153
|
+
? /** @type {Record<string, unknown>} */ (obj)[key]
|
|
154
|
+
: undefined
|
|
143
155
|
}
|
|
144
156
|
|
|
145
157
|
/**
|