@nitra/cursor 1.8.16 → 1.8.18
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/mdc/js-lint.mdc +3 -5
- package/package.json +4 -4
- package/scripts/check-js-lint.mjs +131 -40
- package/scripts/check-k8s.mjs +5 -3
- package/scripts/check-nginx-default-tpl.mjs +5 -3
package/mdc/js-lint.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Перевірка JavaScript коду
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.9'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
**oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js`:** `oxlint` (без `bunx`), **`bunx eslint`**, **`bunx jscpd`**; у CI — `bunx oxlint` / `bunx eslint` / `bunx jscpd`. Без **prettier** і **@nitra/prettier-config**. Достатньо **`@nitra/eslint-config`** у devDependencies; пакети oxlint/eslint/jscpd не додавай без потреби монорепо.
|
|
@@ -38,7 +38,7 @@ version: '1.8'
|
|
|
38
38
|
"exitCode": 1,
|
|
39
39
|
"reporters": ["console"],
|
|
40
40
|
"minLines": 25,
|
|
41
|
-
"ignore": []
|
|
41
|
+
"ignore": ["**/dist/**",]
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
44
|
|
|
@@ -132,9 +132,7 @@ export default [
|
|
|
132
132
|
|
|
133
133
|
## Тести
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
**Код:** синтаксис Node **24+**, **top level await**.
|
|
135
|
+
Проєкт має бути покритий unit-тестами (**Bun test**). Код: синтаксис Node **24+**, **top level await** (узгоджено з `engines.node` у `package.json`).
|
|
138
136
|
|
|
139
137
|
## Перевірка
|
|
140
138
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/cursor",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.18",
|
|
4
4
|
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"test": "bun test tests"
|
|
37
37
|
},
|
|
38
|
-
"engines": {
|
|
39
|
-
"node": ">=24"
|
|
40
|
-
},
|
|
41
38
|
"dependencies": {
|
|
42
39
|
"yaml": "^2.8.3"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=24"
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Перевіряє лінт JavaScript за правилом js-lint.mdc.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Канонічний `lint-js`, flat ESLint з getConfig і ignore для auto-imports, рекомендації VSCode,
|
|
5
|
+
* `.jscpd.json` (gitignore, exitCode, reporters, minLines), workflow `lint-js.yml` (checkout@v6,
|
|
6
|
+
* setup-bun-deps, bunx без --fix), без prettier, `engines.node` >= 24. Дубль перевірки JS у `lint.yml` —
|
|
7
|
+
* заборонено.
|
|
6
8
|
*/
|
|
7
9
|
import { existsSync } from 'node:fs'
|
|
8
10
|
import { readFile } from 'node:fs/promises'
|
|
9
11
|
|
|
10
12
|
import { pass } from './utils/pass.mjs'
|
|
11
13
|
|
|
14
|
+
/** Очікуваний локальний скрипт (oxlint без bunx; eslint/jscpd через bunx). */
|
|
15
|
+
export const CANONICAL_LINT_JS = 'oxlint --fix && bunx eslint --fix . && bunx jscpd .'
|
|
16
|
+
|
|
17
|
+
/** Мінімальні рекомендації розширень редактора з js-lint.mdc (eslint, oxlint, GA). */
|
|
18
|
+
export const REQUIRED_VSCODE_EXTENSIONS = ['dbaeumer.vscode-eslint', 'github.vscode-github-actions', 'oxc.oxc-vscode']
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Нормалізує рядок скрипта для порівняння (зайві пробіли).
|
|
22
|
+
* @param {string} s вихідний рядок скрипта `lint-js`
|
|
23
|
+
* @returns {string} рядок без зайвих пробілів на краях і з одиничними пробілами всередині
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeLintJsScript(s) {
|
|
26
|
+
return String(s).trim().replaceAll(/\s+/gu, ' ')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Чи рядок `lint-js` збігається з каноном і без `bunx oxlint`.
|
|
31
|
+
* @param {string} script значення `scripts.lint-js` з package.json
|
|
32
|
+
* @returns {boolean} true, якщо рядок канонічний
|
|
33
|
+
*/
|
|
34
|
+
export function isCanonicalLintJs(script) {
|
|
35
|
+
const n = normalizeLintJsScript(script)
|
|
36
|
+
if (n.includes('bunx oxlint')) return false
|
|
37
|
+
return n === CANONICAL_LINT_JS
|
|
38
|
+
}
|
|
39
|
+
|
|
12
40
|
/**
|
|
13
41
|
* Перевіряє відповідність проєкту правилам js-lint.mdc
|
|
14
42
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -20,12 +48,34 @@ export async function check() {
|
|
|
20
48
|
exitCode = 1
|
|
21
49
|
}
|
|
22
50
|
|
|
51
|
+
let eslintPath = ''
|
|
23
52
|
if (existsSync('eslint.config.js')) {
|
|
53
|
+
eslintPath = 'eslint.config.js'
|
|
24
54
|
pass('eslint.config.js існує')
|
|
25
55
|
} else if (existsSync('eslint.config.mjs')) {
|
|
56
|
+
eslintPath = 'eslint.config.mjs'
|
|
26
57
|
pass('eslint.config.mjs існує')
|
|
27
58
|
} else {
|
|
28
|
-
fail('Відсутній eslint.config.js —
|
|
59
|
+
fail('Відсутній eslint.config.js або eslint.config.mjs — flat config з getConfig (js-lint.mdc)')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (eslintPath) {
|
|
63
|
+
const eslintRaw = await readFile(eslintPath, 'utf8')
|
|
64
|
+
if (eslintRaw.includes('getConfig')) {
|
|
65
|
+
pass(`${eslintPath}: містить getConfig`)
|
|
66
|
+
} else {
|
|
67
|
+
fail(`${eslintPath}: потрібен виклик getConfig (js-lint.mdc)`)
|
|
68
|
+
}
|
|
69
|
+
if (eslintRaw.includes('@nitra/eslint-config')) {
|
|
70
|
+
pass(`${eslintPath}: імпорт @nitra/eslint-config`)
|
|
71
|
+
} else {
|
|
72
|
+
fail(`${eslintPath}: імпортуй getConfig з @nitra/eslint-config`)
|
|
73
|
+
}
|
|
74
|
+
if (eslintRaw.includes('**/auto-imports.d.ts')) {
|
|
75
|
+
pass(`${eslintPath}: ignores містить **/auto-imports.d.ts`)
|
|
76
|
+
} else {
|
|
77
|
+
fail(`${eslintPath}: додай у ignores запис **/auto-imports.d.ts (js-lint.mdc)`)
|
|
78
|
+
}
|
|
29
79
|
}
|
|
30
80
|
|
|
31
81
|
if (existsSync('package.json')) {
|
|
@@ -34,38 +84,25 @@ export async function check() {
|
|
|
34
84
|
if (pkg.scripts?.['lint-js']) {
|
|
35
85
|
pass('package.json містить скрипт lint-js')
|
|
36
86
|
const lintJs = String(pkg.scripts['lint-js'])
|
|
37
|
-
if (lintJs
|
|
38
|
-
pass(
|
|
39
|
-
} else {
|
|
40
|
-
fail('lint-js має викликати jscpd — додай "&& bunx jscpd ." у кінець скрипта')
|
|
41
|
-
}
|
|
42
|
-
if (lintJs.includes('bunx eslint')) {
|
|
43
|
-
pass('lint-js викликає bunx eslint')
|
|
44
|
-
} else {
|
|
45
|
-
fail('lint-js має містити bunx eslint (n-js-lint.mdc)')
|
|
46
|
-
}
|
|
47
|
-
if (lintJs.includes('bunx jscpd')) {
|
|
48
|
-
pass('lint-js викликає bunx jscpd')
|
|
49
|
-
} else {
|
|
50
|
-
fail('lint-js має містити bunx jscpd (n-js-lint.mdc)')
|
|
51
|
-
}
|
|
52
|
-
if (lintJs.includes('oxlint')) {
|
|
53
|
-
pass('lint-js містить oxlint')
|
|
87
|
+
if (isCanonicalLintJs(lintJs)) {
|
|
88
|
+
pass(`lint-js збігається з каноном: ${CANONICAL_LINT_JS}`)
|
|
54
89
|
} else {
|
|
55
|
-
fail(
|
|
90
|
+
fail(
|
|
91
|
+
`lint-js має бути рівно: "${CANONICAL_LINT_JS}" (oxlint без bunx; див. js-lint.mdc / check-js-lint.mjs). Зараз: ${JSON.stringify(normalizeLintJsScript(lintJs))}`
|
|
92
|
+
)
|
|
56
93
|
}
|
|
57
94
|
} else {
|
|
58
|
-
fail(
|
|
95
|
+
fail(`package.json не містить скрипт "lint-js" — додай: ${JSON.stringify(CANONICAL_LINT_JS)}`)
|
|
59
96
|
}
|
|
60
97
|
|
|
61
98
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
62
99
|
if (allDeps.prettier) {
|
|
63
|
-
fail('package.json: видали залежність prettier (oxfmt замість prettier,
|
|
100
|
+
fail('package.json: видали залежність prettier (oxfmt замість prettier, js-lint.mdc)')
|
|
64
101
|
} else {
|
|
65
102
|
pass('package.json не містить prettier')
|
|
66
103
|
}
|
|
67
104
|
if (allDeps['@nitra/prettier-config']) {
|
|
68
|
-
fail('package.json: видали @nitra/prettier-config (
|
|
105
|
+
fail('package.json: видали @nitra/prettier-config (js-lint.mdc)')
|
|
69
106
|
} else {
|
|
70
107
|
pass('package.json не містить @nitra/prettier-config')
|
|
71
108
|
}
|
|
@@ -78,7 +115,7 @@ export async function check() {
|
|
|
78
115
|
|
|
79
116
|
const nodeEngine = pkg.engines?.node
|
|
80
117
|
if (nodeEngine) {
|
|
81
|
-
const match = nodeEngine.match(/(\d+)/)
|
|
118
|
+
const match = nodeEngine.match(/(\d+)/u)
|
|
82
119
|
if (match && Number(match[1]) >= 24) {
|
|
83
120
|
pass(`engines.node: "${nodeEngine}"`)
|
|
84
121
|
} else {
|
|
@@ -89,26 +126,68 @@ export async function check() {
|
|
|
89
126
|
}
|
|
90
127
|
}
|
|
91
128
|
|
|
129
|
+
if (existsSync('.vscode/extensions.json')) {
|
|
130
|
+
let ext
|
|
131
|
+
try {
|
|
132
|
+
ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
|
|
133
|
+
} catch {
|
|
134
|
+
fail('.vscode/extensions.json не є валідним JSON')
|
|
135
|
+
ext = null
|
|
136
|
+
}
|
|
137
|
+
if (ext) {
|
|
138
|
+
const rec = ext.recommendations
|
|
139
|
+
if (Array.isArray(rec)) {
|
|
140
|
+
const missing = REQUIRED_VSCODE_EXTENSIONS.filter(id => !rec.includes(id))
|
|
141
|
+
if (missing.length > 0) {
|
|
142
|
+
fail(`.vscode/extensions.json: додай у recommendations: ${missing.join(', ')} (мінімум для js-lint.mdc)`)
|
|
143
|
+
} else {
|
|
144
|
+
pass('.vscode/extensions.json: є рекомендації oxlint, eslint і GitHub Actions')
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
fail('.vscode/extensions.json: поле recommendations має бути масивом')
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
fail('.vscode/extensions.json не існує — додай recommendations з js-lint.mdc (див. check-js-lint.mjs)')
|
|
152
|
+
}
|
|
153
|
+
|
|
92
154
|
if (existsSync('.github/workflows/lint-js.yml')) {
|
|
93
155
|
const content = await readFile('.github/workflows/lint-js.yml', 'utf8')
|
|
94
156
|
pass('lint-js.yml існує')
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
157
|
+
const checks = [
|
|
158
|
+
['actions/checkout@v6', 'lint-js.yml: потрібен крок actions/checkout@v6 (ga.mdc)'],
|
|
159
|
+
['persist-credentials: false', 'lint-js.yml: checkout з persist-credentials: false'],
|
|
160
|
+
['./.github/actions/setup-bun-deps', 'lint-js.yml: потрібен uses: ./.github/actions/setup-bun-deps'],
|
|
161
|
+
['bunx oxlint', 'lint-js.yml: у run має бути bunx oxlint'],
|
|
162
|
+
['bunx eslint .', 'lint-js.yml: у run має бути bunx eslint . (без --fix у CI)'],
|
|
163
|
+
['bunx jscpd .', 'lint-js.yml: у run має бути bunx jscpd .']
|
|
164
|
+
]
|
|
165
|
+
for (const [needle, errMsg] of checks) {
|
|
166
|
+
if (content.includes(needle)) {
|
|
167
|
+
pass(`lint-js.yml містить: ${needle}`)
|
|
168
|
+
} else {
|
|
169
|
+
fail(errMsg)
|
|
170
|
+
}
|
|
99
171
|
}
|
|
100
|
-
if (content.includes('
|
|
101
|
-
|
|
102
|
-
} else {
|
|
103
|
-
fail('lint-js.yml не містить eslint')
|
|
172
|
+
if (content.includes('bunx oxlint') && /bunx\s+oxlint[^\n]*--fix/u.test(content)) {
|
|
173
|
+
fail('lint-js.yml: у CI не використовуй oxlint --fix (лише bunx oxlint)')
|
|
104
174
|
}
|
|
105
|
-
if (content.includes('
|
|
106
|
-
|
|
107
|
-
} else {
|
|
108
|
-
fail('lint-js.yml не містить jscpd — додай крок bunx jscpd .')
|
|
175
|
+
if (content.includes('eslint --fix')) {
|
|
176
|
+
fail('lint-js.yml: у CI не використовуй eslint --fix (лише bunx eslint .)')
|
|
109
177
|
}
|
|
110
178
|
} else {
|
|
111
|
-
fail('.github/workflows/lint-js.yml не існує — створи його')
|
|
179
|
+
fail('.github/workflows/lint-js.yml не існує — створи його (див. check-js-lint.mjs / js-lint.mdc)')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (existsSync('.github/workflows/lint.yml')) {
|
|
183
|
+
const lintYml = await readFile('.github/workflows/lint.yml', 'utf8')
|
|
184
|
+
const looksLikeJsLint =
|
|
185
|
+
/\bbunx\s+oxlint\b/u.test(lintYml) && /\bbunx\s+eslint\b/u.test(lintYml) && /\bjscpd\b/u.test(lintYml)
|
|
186
|
+
if (looksLikeJsLint) {
|
|
187
|
+
fail('.github/workflows/lint.yml дублює кроки lint-js.yml — залиш один workflow на лінт JS (js-lint.mdc)')
|
|
188
|
+
} else {
|
|
189
|
+
pass('.github/workflows/lint.yml не дублює oxlint/eslint/jscpd з lint-js.yml')
|
|
190
|
+
}
|
|
112
191
|
}
|
|
113
192
|
|
|
114
193
|
if (existsSync('.jscpd.json')) {
|
|
@@ -131,13 +210,25 @@ export async function check() {
|
|
|
131
210
|
} else {
|
|
132
211
|
fail('.jscpd.json має містити "exitCode": 1 (інакше CI не впаде на клонах)')
|
|
133
212
|
}
|
|
213
|
+
const reporters = jscpdCfg.reporters
|
|
214
|
+
if (Array.isArray(reporters) && reporters.includes('console')) {
|
|
215
|
+
pass('.jscpd.json: reporters містить console')
|
|
216
|
+
} else {
|
|
217
|
+
fail('.jscpd.json має містити "reporters": ["console"] (або масив із "console")')
|
|
218
|
+
}
|
|
219
|
+
const minLines = jscpdCfg.minLines
|
|
220
|
+
if (typeof minLines === 'number' && minLines >= 25) {
|
|
221
|
+
pass(`.jscpd.json: minLines ${minLines} (>=25)`)
|
|
222
|
+
} else {
|
|
223
|
+
fail('.jscpd.json має містити "minLines" як число >= 25')
|
|
224
|
+
}
|
|
134
225
|
}
|
|
135
226
|
} else {
|
|
136
|
-
fail('.jscpd.json не існує — створи з
|
|
227
|
+
fail('.jscpd.json не існує — створи з полями згідно check js-lint')
|
|
137
228
|
}
|
|
138
229
|
|
|
139
230
|
for (const dup of ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml']) {
|
|
140
|
-
if (existsSync(dup)) fail(`Знайдено застарілий конфіг ESLint: ${dup} — видали, використовуй
|
|
231
|
+
if (existsSync(dup)) fail(`Знайдено застарілий конфіг ESLint: ${dup} — видали, використовуй flat config`)
|
|
141
232
|
}
|
|
142
233
|
|
|
143
234
|
return exitCode
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -207,17 +207,19 @@ function extractApiVersionAndKind(doc) {
|
|
|
207
207
|
|
|
208
208
|
/**
|
|
209
209
|
* Чи порушує маніфест вимогу **`Deployment.spec.template.spec.containers[].resources`** (див. k8s.mdc).
|
|
210
|
-
* @param {unknown} manifest корінь
|
|
210
|
+
* @param {unknown} manifest корінь YAML-документа як об'єкт JavaScript
|
|
211
211
|
* @returns {string | null} текст порушення для `fail` або null, якщо перевірка не застосовується / ок
|
|
212
212
|
*/
|
|
213
213
|
export function deploymentResourcesViolation(manifest) {
|
|
214
|
-
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
214
|
+
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
215
|
+
return null
|
|
215
216
|
const rec = /** @type {Record<string, unknown>} */ (manifest)
|
|
216
217
|
if (rec.kind !== 'Deployment') return null
|
|
217
218
|
const spec = rec.spec
|
|
218
219
|
if (spec === null || spec === undefined || typeof spec !== 'object' || Array.isArray(spec)) return null
|
|
219
220
|
const template = /** @type {Record<string, unknown>} */ (spec).template
|
|
220
|
-
if (template === null || template === undefined || typeof template !== 'object' || Array.isArray(template))
|
|
221
|
+
if (template === null || template === undefined || typeof template !== 'object' || Array.isArray(template))
|
|
222
|
+
return null
|
|
221
223
|
const podSpec = /** @type {Record<string, unknown>} */ (template).spec
|
|
222
224
|
if (podSpec === null || podSpec === undefined || typeof podSpec !== 'object' || Array.isArray(podSpec)) return null
|
|
223
225
|
const containers = /** @type {Record<string, unknown>} */ (podSpec).containers
|
|
@@ -143,10 +143,11 @@ export function nginxTemplateViolations(content) {
|
|
|
143
143
|
if (!ok(content)) return msg
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// cspell:ignore fastcgi uwsgi
|
|
146
147
|
const proxyLike =
|
|
147
148
|
/\b(proxy_pass|proxy_redirect|proxy_set_header|proxy_http_version|fastcgi_pass|grpc_pass|uwsgi_pass)\b/u
|
|
148
149
|
if (proxyLike.test(content)) {
|
|
149
|
-
return 'знайдено proxy
|
|
150
|
+
return 'знайдено proxy, gRPC або інший *_pass до бекенду — прибери з шаблону, логіку винеси в HTTPRoute (k8s) (див. nginx-default-tpl.mdc)'
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
return null
|
|
@@ -158,7 +159,8 @@ export function nginxTemplateViolations(content) {
|
|
|
158
159
|
* @returns {boolean} true, якщо структура збігається з прикладом у nginx-default-tpl.mdc
|
|
159
160
|
*/
|
|
160
161
|
export function httpRouteMatchesNginxDefaultTpl(manifest) {
|
|
161
|
-
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
162
|
+
if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
|
|
163
|
+
return false
|
|
162
164
|
const m = /** @type {Record<string, unknown>} */ (manifest)
|
|
163
165
|
if (m.kind !== 'HTTPRoute') return false
|
|
164
166
|
const spec = m.spec
|
|
@@ -226,7 +228,7 @@ export function httpRouteMatchesNginxDefaultTpl(manifest) {
|
|
|
226
228
|
export function iniKeysMissingInTemplate(keys, template) {
|
|
227
229
|
for (const k of keys) {
|
|
228
230
|
if (!template.includes(`$${k}`)) {
|
|
229
|
-
return `змінна "${k}" з *.ini не використовується в шаблоні — вилучи її з ini або додай
|
|
231
|
+
return `змінна "${k}" з *.ini не використовується в шаблоні — вилучи її з ini або додай у шаблон $${k} (див. nginx-default-tpl.mdc)`
|
|
230
232
|
}
|
|
231
233
|
}
|
|
232
234
|
return null
|