@nitra/cursor 1.8.145 → 1.8.150
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/n-cursor.js +2 -1
- package/bin/rename-yaml-extensions.mjs +0 -2
- package/mdc/docker.mdc +2 -0
- package/mdc/js-lint.mdc +6 -4
- package/mdc/js-mssql.mdc +2 -0
- package/mdc/k8s.mdc +3 -3
- package/mdc/vue.mdc +3 -3
- package/package.json +3 -3
- package/scripts/auto-rules.mjs +46 -29
- package/scripts/check-capacitor.mjs +4 -4
- package/scripts/check-ga.mjs +27 -15
- package/scripts/check-js-bun-db.mjs +3 -3
- package/scripts/check-js-lint.mjs +114 -27
- package/scripts/check-js-mssql.mjs +155 -97
- package/scripts/check-k8s.mjs +204 -42
- package/scripts/check-nginx-default-tpl.mjs +1 -1
- package/scripts/check-php.mjs +0 -1
- package/scripts/check-text.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/ast-scan-utils.mjs +154 -0
- package/scripts/utils/bun-sql-scan.mjs +10 -144
- package/scripts/utils/bunyan-imports.mjs +2 -36
- package/scripts/utils/mssql-pool-scan.mjs +76 -187
- 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/skills/lint/SKILL.md +1 -1
package/bin/n-cursor.js
CHANGED
|
@@ -561,7 +561,7 @@ function buildClaudeLintParallelismSectionLines() {
|
|
|
561
561
|
'## Лінт і ESLint (без паралельних запусків)',
|
|
562
562
|
'',
|
|
563
563
|
'Щоб не запускати **кілька** одночасних **`eslint`** (і не перевантажувати диск/CPU), **заборонено** стартувати `bun run lint` / `lint-js` / `eslint` **паралельно** в різних Bash-задачах, **фонових** shells чи **субагентах** (Task тощо). Має бути **один** послідовний прогон на сесію; команда **`/n-lint`** — **не** ділити на паралельні підзадачі. Деталі: `.cursor/skills/n-lint/SKILL.md`.',
|
|
564
|
-
''
|
|
564
|
+
''
|
|
565
565
|
]
|
|
566
566
|
}
|
|
567
567
|
|
|
@@ -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++
|
package/mdc/docker.mdc
CHANGED
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
|
@@ -17,6 +17,7 @@ version: '1.2'
|
|
|
17
17
|
## Як виконувати запити (безпечно)
|
|
18
18
|
|
|
19
19
|
tagged template треба викликати на request-обʼєкті цього пулу:
|
|
20
|
+
|
|
20
21
|
```javascript
|
|
21
22
|
javascript// db.js
|
|
22
23
|
import sql from 'mssql';
|
|
@@ -58,6 +59,7 @@ const result = await pool.request().query`
|
|
|
58
59
|
### Не робити `query(\`...\`)`
|
|
59
60
|
|
|
60
61
|
javascript// ❌ Це не tagged template — це конкатенація рядка перед викликом
|
|
62
|
+
|
|
61
63
|
```javascript
|
|
62
64
|
await pool.request().query(`SELECT * FROM users WHERE id = ${userId}`);
|
|
63
65
|
// ↑ круглі дужки замість бекті́ків = звичайна інтерполяція = SQL injection
|
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
|
|
|
@@ -314,7 +314,7 @@ data:
|
|
|
314
314
|
|
|
315
315
|
### Прод-оверрайди у `kustomization.yaml`
|
|
316
316
|
|
|
317
|
-
Оскільки `base/` (і dev-like середовища) тримають dev-значення (`1`/`1`/`0`), у **кожному** прод-накладенні `kustomization.yaml` у `patches[]` **обов'язково** має бути
|
|
317
|
+
Оскільки `base/` (і dev-like середовища) тримають dev-значення (`1`/`1`/`0`), у **кожному** прод-накладенні `kustomization.yaml` у `patches[]` **обов'язково** має бути перевизначення **лише якщо** цей оверлей наслідує base-дерево, де є **Deployment** і **HPA/PDB** (тобто base реально дає dev-like HPA/PDB, які треба підняти в проді):
|
|
318
318
|
|
|
319
319
|
- для `HorizontalPodAutoscaler`: `spec.minReplicas` **і** `spec.maxReplicas` (щоб у проді вийшло ≥2).
|
|
320
320
|
- для `PodDisruptionBudget`: `spec.minAvailable` (щоб у проді вийшло ≥1).
|
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.150",
|
|
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
|
@@ -57,12 +57,34 @@ const DEFAULT_DISABLED_LIST = Object.freeze([])
|
|
|
57
57
|
* Чи містить текст джерела імпорт імені `sql` або `SQL` з `"bun"` (після витягування `<script>` у `.vue`).
|
|
58
58
|
* @param {string} content вміст файлу
|
|
59
59
|
* @param {string} relativePath шлях posix відносно кореня
|
|
60
|
-
* @returns {boolean}
|
|
60
|
+
* @returns {boolean} true, якщо знайдено `import { sql }` або `import { SQL }` з `"bun"`
|
|
61
61
|
*/
|
|
62
62
|
function sourceContentHasBunSqlImport(content, relativePath) {
|
|
63
63
|
return textHasBunSqlImport(contentForVueImportScan(content, relativePath))
|
|
64
64
|
}
|
|
65
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
|
+
|
|
66
88
|
/**
|
|
67
89
|
* Збирає, які з переданих ключів присутні в `dependencies` хоча б одного `package.json`.
|
|
68
90
|
* @param {string} root абсолютний шлях до кореня репозиторію
|
|
@@ -74,15 +96,32 @@ async function collectDependencyKeysPresentInPackageJsonTree(root, dependencyKey
|
|
|
74
96
|
/** @type {Set<string>} */
|
|
75
97
|
const found = new Set()
|
|
76
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
|
+
}
|
|
117
|
+
|
|
77
118
|
/**
|
|
78
119
|
* Рекурсивний обхід каталогу з пропуском службових директорій.
|
|
79
120
|
* @param {string} dir абсолютний шлях каталогу
|
|
80
121
|
* @returns {Promise<void>}
|
|
81
122
|
*/
|
|
82
123
|
async function walk(dir) {
|
|
83
|
-
if (found.size === wanted.size)
|
|
84
|
-
return
|
|
85
|
-
}
|
|
124
|
+
if (found.size === wanted.size) return
|
|
86
125
|
let entries
|
|
87
126
|
try {
|
|
88
127
|
entries = await readdir(dir, { withFileTypes: true })
|
|
@@ -90,30 +129,8 @@ async function collectDependencyKeysPresentInPackageJsonTree(root, dependencyKey
|
|
|
90
129
|
return
|
|
91
130
|
}
|
|
92
131
|
for (const entry of entries) {
|
|
93
|
-
if (found.size === wanted.size)
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
const absPath = join(dir, entry.name)
|
|
97
|
-
if (entry.isDirectory()) {
|
|
98
|
-
const isIgnoredDir = IGNORED_DIR_NAMES.has(entry.name)
|
|
99
|
-
if (!isIgnoredDir) {
|
|
100
|
-
await walk(absPath)
|
|
101
|
-
}
|
|
102
|
-
} else if (entry.isFile() && entry.name === 'package.json') {
|
|
103
|
-
try {
|
|
104
|
-
const parsed = JSON.parse(await readFile(absPath, 'utf8'))
|
|
105
|
-
const deps = parsed?.dependencies
|
|
106
|
-
if (deps && typeof deps === 'object' && !Array.isArray(deps)) {
|
|
107
|
-
for (const key of wanted) {
|
|
108
|
-
if (Object.hasOwn(deps, key)) {
|
|
109
|
-
found.add(key)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
} catch {
|
|
114
|
-
/* ігноруємо пошкоджені/недоступні package.json */
|
|
115
|
-
}
|
|
116
|
-
}
|
|
132
|
+
if (found.size === wanted.size) return
|
|
133
|
+
await processEntry(entry, dir)
|
|
117
134
|
}
|
|
118
135
|
}
|
|
119
136
|
|
|
@@ -210,7 +227,7 @@ async function updateGqlFactFromFile(absPath, relPath, facts) {
|
|
|
210
227
|
* Чи сканувати файл на імпорт `sql`/`SQL` з `bun` (ті самі розширення й skip, що для gql).
|
|
211
228
|
* @param {string} relPath шлях posix відносно кореня
|
|
212
229
|
* @param {{ hasBunSqlImport: boolean }} facts агреговані факти
|
|
213
|
-
* @returns {boolean}
|
|
230
|
+
* @returns {boolean} true, якщо файл варто сканувати
|
|
214
231
|
*/
|
|
215
232
|
function shouldScanFileForBunSql(relPath, facts) {
|
|
216
233
|
return !facts.hasBunSqlImport && isGqlScanSourceFile(relPath) && !shouldSkipFileForGqlScan(relPath)
|
|
@@ -376,9 +376,7 @@ function extractNitraObjectBodySource(source) {
|
|
|
376
376
|
* @returns {boolean} **true**, якщо в тілі є **iosCocoaPods**…**:** **true**
|
|
377
377
|
*/
|
|
378
378
|
function nitraObjectBodyStringAllowsCocoaPodsExempt(objectBody) {
|
|
379
|
-
return (
|
|
380
|
-
RE_COCOAPODS_EXEMPT_SPM.test(objectBody) === true || RE_COCOAPODS_EXEMPT_ALLOW.test(objectBody) === true
|
|
381
|
-
)
|
|
379
|
+
return RE_COCOAPODS_EXEMPT_SPM.test(objectBody) === true || RE_COCOAPODS_EXEMPT_ALLOW.test(objectBody) === true
|
|
382
380
|
}
|
|
383
381
|
|
|
384
382
|
/**
|
|
@@ -443,7 +441,9 @@ export async function check() {
|
|
|
443
441
|
const { byPath, anyCapacitor } = acc
|
|
444
442
|
|
|
445
443
|
if (!isCapacitorRelevantForCheck(root, anyCapacitor)) {
|
|
446
|
-
pass(
|
|
444
|
+
pass(
|
|
445
|
+
'Capacitor не виявлено (без capacitor.config у корені, без @capacitor/ у package.json) — check capacitor пропущено'
|
|
446
|
+
)
|
|
447
447
|
return getExitCode()
|
|
448
448
|
}
|
|
449
449
|
|
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
|
/**
|
|
@@ -77,7 +77,7 @@ async function findAllSourcePathsForBunSqlScan(repoRoot) {
|
|
|
77
77
|
* Перевіряє, чи в кореневому `package.json` присутні заборонені пакети у `dependencies`.
|
|
78
78
|
* @param {string[]} pkgJsonPaths абсолютні шляхи всіх `package.json` у репо
|
|
79
79
|
* @param {string} repoRoot абсолютний шлях до кореня
|
|
80
|
-
* @param {{ pass: (m: string) => void, fail: (m: string) => void }} reporter
|
|
80
|
+
* @param {{ pass: (m: string) => void, fail: (m: string) => void }} reporter колбеки pass і fail з перевірки
|
|
81
81
|
* @returns {Promise<number>} кількість знайдених порушень
|
|
82
82
|
*/
|
|
83
83
|
async function checkForbiddenDependencies(pkgJsonPaths, repoRoot, reporter) {
|
|
@@ -114,7 +114,7 @@ async function checkForbiddenDependencies(pkgJsonPaths, repoRoot, reporter) {
|
|
|
114
114
|
* Сканує JS/TS-джерела на небезпечні патерни Bun SQL.
|
|
115
115
|
* @param {string[]} sourcePaths абсолютні шляхи джерел
|
|
116
116
|
* @param {string} repoRoot абсолютний шлях до кореня
|
|
117
|
-
* @param {{ pass: (m: string) => void, fail: (m: string) => void }} reporter
|
|
117
|
+
* @param {{ pass: (m: string) => void, fail: (m: string) => void }} reporter колбеки pass і fail з перевірки
|
|
118
118
|
* @returns {Promise<{ hasBunSqlImport: boolean, perRequest: number, unsafeCall: number, dynamicList: number }>}
|
|
119
119
|
* `hasBunSqlImport` — чи знайдено хоч один `import { sql|SQL } from 'bun'` у джерелах;
|
|
120
120
|
* решта — кількість порушень кожного типу.
|
|
@@ -203,7 +203,7 @@ export async function check() {
|
|
|
203
203
|
pass('js-bun-db: немає створення new SQL(...) всередині функцій (singleton на рівні модуля)')
|
|
204
204
|
}
|
|
205
205
|
if (unsafeCall === 0) {
|
|
206
|
-
pass('js-bun-db: немає небезпечних викликів sql.unsafe
|
|
206
|
+
pass('js-bun-db: немає небезпечних викликів sql.unsafe з інтерполяцією в шаблонному рядку')
|
|
207
207
|
}
|
|
208
208
|
if (dynamicList === 0) {
|
|
209
209
|
pass("js-bun-db: немає небезпечних динамічних SQL-списків через .join(',') у IN/VALUES")
|
|
@@ -2,18 +2,28 @@
|
|
|
2
2
|
* Перевіряє лінт JavaScript за правилом js-lint.mdc.
|
|
3
3
|
*
|
|
4
4
|
* Канонічний `lint-js`, flat ESLint з getConfig і ignore для auto-imports, рекомендації VSCode,
|
|
5
|
-
* `.oxlintrc.json` з
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/scripts/utils/oxlint-canonical.json`):
|
|
6
|
+
* plugins, jsPlugins, categories, усі правила з канону (додаткові записи в `rules` дозволені), settings, env,
|
|
7
|
+
* globals, ignorePatterns. `@nitra/eslint-config` у devDependencies мінімум **3.6.12** (транзитивний
|
|
8
|
+
* `@e18e/eslint-plugin` для oxlint), `.jscpd.json` (gitignore, exitCode, reporters, minLines), workflow
|
|
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
11
|
*/
|
|
11
12
|
import { existsSync } from 'node:fs'
|
|
12
13
|
import { readFile } from 'node:fs/promises'
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
13
16
|
|
|
14
17
|
import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './utils/gha-workflow.mjs'
|
|
15
18
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
16
19
|
|
|
20
|
+
/** Шлях до канонічного oxlint JSON у цьому пакеті (для перевірки та тестів). */
|
|
21
|
+
export const OXLINT_CANONICAL_JSON_PATH = join(
|
|
22
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
23
|
+
'utils',
|
|
24
|
+
'oxlint-canonical.json'
|
|
25
|
+
)
|
|
26
|
+
|
|
17
27
|
/** Очікуваний локальний скрипт. */
|
|
18
28
|
export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bunx jscpd .'
|
|
19
29
|
|
|
@@ -43,9 +53,9 @@ export function isCanonicalLintJs(script) {
|
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
/**
|
|
46
|
-
* Чи діапазон `@nitra/eslint-config` у `package.json` передбачає версію з транзитивним `@e18e/eslint-plugin` (>=3.
|
|
56
|
+
* Чи діапазон `@nitra/eslint-config` у `package.json` передбачає версію з транзитивним `@e18e/eslint-plugin` (>=3.6.12).
|
|
47
57
|
* @param {unknown} versionSpec значення `devDependencies['@nitra/eslint-config']`
|
|
48
|
-
* @returns {boolean} true для `workspace:*` або першої semver у рядку >= 3.
|
|
58
|
+
* @returns {boolean} true для `workspace:*` або першої semver у рядку >= 3.6.12
|
|
49
59
|
*/
|
|
50
60
|
export function nitraEslintConfigDeclaresE18eTransitive(versionSpec) {
|
|
51
61
|
const s = String(versionSpec).trim()
|
|
@@ -64,29 +74,97 @@ export function nitraEslintConfigDeclaresE18eTransitive(versionSpec) {
|
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
/**
|
|
67
|
-
*
|
|
68
|
-
* @param {unknown}
|
|
69
|
-
* @
|
|
77
|
+
* Рекурсивне порівняння фрагментів канону oxlint (масиви — порядок як у каноні; об’єкти — той самий набір ключів і вкладеність).
|
|
78
|
+
* @param {unknown} actual значення з `.oxlintrc.json`
|
|
79
|
+
* @param {unknown} expected значення з канону
|
|
80
|
+
* @returns {boolean} true, якщо значення збігаються за правилами канону
|
|
81
|
+
*/
|
|
82
|
+
function deepEqualOxlintCanonical(actual, expected) {
|
|
83
|
+
if (expected === null || typeof expected !== 'object') {
|
|
84
|
+
return actual === expected
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(expected)) {
|
|
87
|
+
return Array.isArray(actual) && JSON.stringify(actual) === JSON.stringify(expected)
|
|
88
|
+
}
|
|
89
|
+
if (typeof actual !== 'object' || actual === null || Array.isArray(actual)) {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
const exp = /** @type {Record<string, unknown>} */ (expected)
|
|
93
|
+
const act = /** @type {Record<string, unknown>} */ (actual)
|
|
94
|
+
const expKeys = Object.keys(exp)
|
|
95
|
+
const actKeys = Object.keys(act)
|
|
96
|
+
if (expKeys.length !== actKeys.length) {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
for (const k of expKeys) {
|
|
100
|
+
if (!(k in act) || !deepEqualOxlintCanonical(act[k], exp[k])) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Безпечний доступ як до plain-object запису.
|
|
109
|
+
* @param {unknown} v будь-яке значення
|
|
110
|
+
* @returns {Record<string, unknown>} запис або пустий обʼєкт, якщо `v` не plain-object
|
|
111
|
+
*/
|
|
112
|
+
function asRecordOrEmpty(v) {
|
|
113
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? /** @type {Record<string, unknown>} */ (v) : {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Звіряє блок `rules`: кожне правило з канону має точне збіжне значення в actual.
|
|
118
|
+
* @param {unknown} expected канонічне значення для `rules`
|
|
119
|
+
* @param {unknown} actual поточне значення для `rules`
|
|
120
|
+
* @param {string[]} failures буфер для помилок
|
|
121
|
+
*/
|
|
122
|
+
function compareOxlintRules(expected, actual, failures) {
|
|
123
|
+
const er = asRecordOrEmpty(expected)
|
|
124
|
+
const ar = asRecordOrEmpty(actual)
|
|
125
|
+
for (const ruleKey of Object.keys(er)) {
|
|
126
|
+
if (ar[ruleKey] !== er[ruleKey]) {
|
|
127
|
+
failures.push(
|
|
128
|
+
`.oxlintrc.json: rules["${ruleKey}"] очікується ${JSON.stringify(er[ruleKey])}, зараз ${JSON.stringify(ar[ruleKey])}`
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Перевіряє `.oxlintrc.json` проти канону пакета `@nitra/cursor` (усі правила з канону та інші поля з `oxlint-canonical.json`).
|
|
136
|
+
* Додаткові ключі лише в `rules` дозволені; інші поля мають збігатися з каноном.
|
|
137
|
+
* @param {unknown} cfg корінь JSON з `.oxlintrc.json`
|
|
138
|
+
* @param {unknown} canonical розпарений `oxlint-canonical.json`
|
|
139
|
+
* @returns {{ ok: boolean, failures: string[] }} статус і повідомлення для `fail`
|
|
70
140
|
*/
|
|
71
|
-
export function
|
|
141
|
+
export function verifyOxlintRcAgainstCanonical(cfg, canonical) {
|
|
72
142
|
const failures = []
|
|
73
143
|
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) {
|
|
74
144
|
return { ok: false, failures: ['.oxlintrc.json: корінь має бути значенням типу object'] }
|
|
75
145
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!Array.isArray(jsPlugins) || !jsPlugins.includes('@e18e/eslint-plugin')) {
|
|
79
|
-
failures.push('.oxlintrc.json: jsPlugins має містити "@e18e/eslint-plugin"')
|
|
146
|
+
if (!canonical || typeof canonical !== 'object' || Array.isArray(canonical)) {
|
|
147
|
+
return { ok: false, failures: ['внутрішня помилка: канон oxlint має бути object'] }
|
|
80
148
|
}
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
149
|
+
const o = /** @type {Record<string, unknown>} */ (cfg)
|
|
150
|
+
const c = /** @type {Record<string, unknown>} */ (canonical)
|
|
151
|
+
|
|
152
|
+
for (const key of Object.keys(c)) {
|
|
153
|
+
const expected = c[key]
|
|
154
|
+
const actual = o[key]
|
|
155
|
+
|
|
156
|
+
if (key === 'rules') {
|
|
157
|
+
compareOxlintRules(expected, actual, failures)
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!deepEqualOxlintCanonical(actual, expected)) {
|
|
162
|
+
failures.push(
|
|
163
|
+
`.oxlintrc.json: поле "${key}" має збігатися з каноном пакета @nitra/cursor (npm/scripts/utils/oxlint-canonical.json)`
|
|
164
|
+
)
|
|
88
165
|
}
|
|
89
166
|
}
|
|
167
|
+
|
|
90
168
|
return { ok: failures.length === 0, failures }
|
|
91
169
|
}
|
|
92
170
|
|
|
@@ -96,7 +174,7 @@ export function verifyOxlintRcE18e(cfg) {
|
|
|
96
174
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
97
175
|
*/
|
|
98
176
|
async function checkEslintConfig(passFn, failFn) {
|
|
99
|
-
let eslintPath
|
|
177
|
+
let eslintPath
|
|
100
178
|
if (existsSync('eslint.config.js')) {
|
|
101
179
|
eslintPath = 'eslint.config.js'
|
|
102
180
|
passFn('eslint.config.js існує')
|
|
@@ -157,10 +235,12 @@ function checkPackageJsonLintDeps(pkg, passFn, failFn) {
|
|
|
157
235
|
if (nitraEslint) {
|
|
158
236
|
passFn('@nitra/eslint-config є в devDependencies')
|
|
159
237
|
if (nitraEslintConfigDeclaresE18eTransitive(nitraEslint)) {
|
|
160
|
-
passFn(
|
|
238
|
+
passFn(
|
|
239
|
+
'@nitra/eslint-config: мінімум 3.6.12 (транзитивний @e18e/eslint-plugin для oxlint jsPlugins, js-lint.mdc)'
|
|
240
|
+
)
|
|
161
241
|
} else {
|
|
162
242
|
failFn(
|
|
163
|
-
'@nitra/eslint-config: онови до мінімум "^3.
|
|
243
|
+
'@nitra/eslint-config: онови до мінімум "^3.6.12" — з цієї версії постачається @e18e/eslint-plugin для .oxlintrc.json (js-lint.mdc)'
|
|
164
244
|
)
|
|
165
245
|
}
|
|
166
246
|
} else {
|
|
@@ -269,9 +349,16 @@ async function checkOxlintRc(passFn, failFn) {
|
|
|
269
349
|
return
|
|
270
350
|
}
|
|
271
351
|
passFn('.oxlintrc.json існує')
|
|
272
|
-
|
|
352
|
+
let canonical
|
|
353
|
+
try {
|
|
354
|
+
canonical = JSON.parse(await readFile(OXLINT_CANONICAL_JSON_PATH, 'utf8'))
|
|
355
|
+
} catch {
|
|
356
|
+
failFn('внутрішня помилка: не вдалося прочитати канон oxlint з пакета @nitra/cursor')
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
const oxV = verifyOxlintRcAgainstCanonical(oxCfg, canonical)
|
|
273
360
|
if (oxV.ok) {
|
|
274
|
-
passFn('.oxlintrc.json
|
|
361
|
+
passFn('.oxlintrc.json збігається з каноном oxlint (@nitra/cursor)')
|
|
275
362
|
} else {
|
|
276
363
|
for (const msg of oxV.failures) {
|
|
277
364
|
failFn(msg)
|