@nitra/cursor 1.8.104 → 1.8.106
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 +45 -0
- package/bin/n-cursor.js +270 -149
- package/mdc/graphql.mdc +15 -1
- package/mdc/k8s.mdc +11 -10
- package/package.json +1 -1
- package/schemas/n-cursor.json +16 -0
- package/scripts/auto-rules.mjs +404 -0
- package/scripts/check-abie.mjs +558 -553
- package/scripts/check-bun.mjs +106 -82
- package/scripts/check-ga.mjs +151 -119
- package/scripts/check-graphql.mjs +112 -34
- package/scripts/check-js-lint.mjs +267 -186
- package/scripts/check-k8s.mjs +1148 -673
- package/scripts/check-nginx-default-tpl.mjs +125 -100
- package/scripts/check-npm-module.mjs +165 -118
- package/scripts/check-style-lint.mjs +74 -61
- package/scripts/check-text.mjs +288 -210
- package/scripts/check-vue.mjs +110 -69
- package/scripts/utils/docker-hadolint.mjs +9 -5
- package/scripts/utils/gha-workflow.mjs +92 -72
- package/scripts/utils/workspaces.mjs +39 -16
|
@@ -19,13 +19,17 @@ export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bu
|
|
|
19
19
|
/** Мінімальні рекомендації розширень редактора з js-lint.mdc (eslint, oxlint, GA). */
|
|
20
20
|
export const REQUIRED_VSCODE_EXTENSIONS = ['dbaeumer.vscode-eslint', 'github.vscode-github-actions', 'oxc.oxc-vscode']
|
|
21
21
|
|
|
22
|
+
const WHITESPACE_RE = /\s+/gu
|
|
23
|
+
const NON_DIGITS_RE = /\D+/u
|
|
24
|
+
const OXLINT_FIX_RE = /bunx\s+oxlint[^\n]*--fix/u
|
|
25
|
+
|
|
22
26
|
/**
|
|
23
27
|
* Нормалізує рядок скрипта для порівняння (зайві пробіли).
|
|
24
28
|
* @param {string} s вихідний рядок скрипта `lint-js`
|
|
25
29
|
* @returns {string} рядок без зайвих пробілів на краях і з одиничними пробілами всередині
|
|
26
30
|
*/
|
|
27
31
|
export function normalizeLintJsScript(s) {
|
|
28
|
-
return String(s).trim().replaceAll(
|
|
32
|
+
return String(s).trim().replaceAll(WHITESPACE_RE, ' ')
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
/**
|
|
@@ -47,13 +51,14 @@ export function nitraEslintConfigDeclaresE18eTransitive(versionSpec) {
|
|
|
47
51
|
if (s.startsWith('workspace:')) {
|
|
48
52
|
return true
|
|
49
53
|
}
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
54
|
+
const parts = s.split(NON_DIGITS_RE).filter(Boolean)
|
|
55
|
+
if (parts.length < 3) {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
const [major, minor, patch] = parts.slice(0, 3).map(Number)
|
|
59
|
+
if ([major, minor, patch].some(n => Number.isNaN(n))) {
|
|
52
60
|
return false
|
|
53
61
|
}
|
|
54
|
-
const major = Number(m[1])
|
|
55
|
-
const minor = Number(m[2])
|
|
56
|
-
const patch = Number(m[3])
|
|
57
62
|
return major > 3 || (major === 3 && minor > 5) || (major === 3 && minor === 5 && patch >= 0)
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -85,234 +90,310 @@ export function verifyOxlintRcE18e(cfg) {
|
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
/**
|
|
88
|
-
* Перевіряє
|
|
89
|
-
* @
|
|
93
|
+
* Перевіряє ESLint flat config файл.
|
|
94
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
95
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
90
96
|
*/
|
|
91
|
-
|
|
92
|
-
const reporter = createCheckReporter()
|
|
93
|
-
const { pass, fail } = reporter
|
|
94
|
-
|
|
97
|
+
async function checkEslintConfig(passFn, failFn) {
|
|
95
98
|
let eslintPath = ''
|
|
96
99
|
if (existsSync('eslint.config.js')) {
|
|
97
100
|
eslintPath = 'eslint.config.js'
|
|
98
|
-
|
|
101
|
+
passFn('eslint.config.js існує')
|
|
99
102
|
} else if (existsSync('eslint.config.mjs')) {
|
|
100
103
|
eslintPath = 'eslint.config.mjs'
|
|
101
|
-
|
|
104
|
+
passFn('eslint.config.mjs існує')
|
|
102
105
|
} else {
|
|
103
|
-
|
|
106
|
+
failFn('Відсутній eslint.config.js або eslint.config.mjs — flat config з getConfig (js-lint.mdc)')
|
|
107
|
+
return
|
|
104
108
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
const eslintRaw = await readFile(eslintPath, 'utf8')
|
|
110
|
+
const checks = [
|
|
111
|
+
{
|
|
112
|
+
needle: 'getConfig',
|
|
113
|
+
ok: `${eslintPath}: містить getConfig`,
|
|
114
|
+
err: `${eslintPath}: потрібен виклик getConfig (js-lint.mdc)`
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
needle: '@nitra/eslint-config',
|
|
118
|
+
ok: `${eslintPath}: імпорт @nitra/eslint-config`,
|
|
119
|
+
err: `${eslintPath}: імпортуй getConfig з @nitra/eslint-config`
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
needle: '**/auto-imports.d.ts',
|
|
123
|
+
ok: `${eslintPath}: ignores містить **/auto-imports.d.ts`,
|
|
124
|
+
err: `${eslintPath}: додай у ignores запис **/auto-imports.d.ts (js-lint.mdc)`
|
|
117
125
|
}
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
]
|
|
127
|
+
for (const { needle, ok, err } of checks) {
|
|
128
|
+
if (eslintRaw.includes(needle)) {
|
|
129
|
+
passFn(ok)
|
|
120
130
|
} else {
|
|
121
|
-
|
|
131
|
+
failFn(err)
|
|
122
132
|
}
|
|
123
133
|
}
|
|
134
|
+
}
|
|
124
135
|
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Перевіряє залежності lint-js у package.json (prettier, `@nitra/eslint-config`).
|
|
138
|
+
* @param {{ dependencies?: Record<string, string>, devDependencies?: Record<string, string> }} pkg parsed package.json
|
|
139
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
140
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
141
|
+
*/
|
|
142
|
+
function checkPackageJsonLintDeps(pkg, passFn, failFn) {
|
|
143
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
144
|
+
if (allDeps.prettier) {
|
|
145
|
+
failFn('package.json: видали залежність prettier (oxfmt замість prettier, js-lint.mdc)')
|
|
146
|
+
} else {
|
|
147
|
+
passFn('package.json не містить prettier')
|
|
148
|
+
}
|
|
149
|
+
if (allDeps['@nitra/prettier-config']) {
|
|
150
|
+
failFn('package.json: видали @nitra/prettier-config (js-lint.mdc)')
|
|
151
|
+
} else {
|
|
152
|
+
passFn('package.json не містить @nitra/prettier-config')
|
|
153
|
+
}
|
|
127
154
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
} else {
|
|
134
|
-
fail(
|
|
135
|
-
`lint-js має бути рівно: "${CANONICAL_LINT_JS}" (див. js-lint.mdc / check-js-lint.mjs). Зараз: ${JSON.stringify(normalizeLintJsScript(lintJs))}`
|
|
136
|
-
)
|
|
137
|
-
}
|
|
155
|
+
const nitraEslint = pkg.devDependencies?.['@nitra/eslint-config']
|
|
156
|
+
if (nitraEslint) {
|
|
157
|
+
passFn('@nitra/eslint-config є в devDependencies')
|
|
158
|
+
if (nitraEslintConfigDeclaresE18eTransitive(nitraEslint)) {
|
|
159
|
+
passFn('@nitra/eslint-config: мінімум 3.5.0 (транзитивний @e18e/eslint-plugin для oxlint jsPlugins, js-lint.mdc)')
|
|
138
160
|
} else {
|
|
139
|
-
|
|
161
|
+
failFn(
|
|
162
|
+
'@nitra/eslint-config: онови до мінімум "^3.5.0" — з цієї версії постачається @e18e/eslint-plugin для .oxlintrc.json (js-lint.mdc)'
|
|
163
|
+
)
|
|
140
164
|
}
|
|
165
|
+
} else {
|
|
166
|
+
failFn('@nitra/eslint-config відсутній в devDependencies — додай: bun add -d @nitra/eslint-config')
|
|
167
|
+
}
|
|
168
|
+
}
|
|
141
169
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
} else {
|
|
151
|
-
pass('package.json не містить @nitra/prettier-config')
|
|
152
|
-
}
|
|
170
|
+
/**
|
|
171
|
+
* Перевіряє package.json на lint-js, prettier, eslint-config, engines.node.
|
|
172
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
173
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
174
|
+
*/
|
|
175
|
+
async function checkPackageJsonJsLint(passFn, failFn) {
|
|
176
|
+
if (!existsSync('package.json')) return
|
|
177
|
+
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
153
178
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
} else {
|
|
160
|
-
fail(
|
|
161
|
-
'@nitra/eslint-config: онови до мінімум "^3.5.0" — з цієї версії постачається @e18e/eslint-plugin для .oxlintrc.json (js-lint.mdc)'
|
|
162
|
-
)
|
|
163
|
-
}
|
|
179
|
+
const lintJs = pkg.scripts?.['lint-js']
|
|
180
|
+
if (lintJs) {
|
|
181
|
+
passFn('package.json містить скрипт lint-js')
|
|
182
|
+
if (isCanonicalLintJs(String(lintJs))) {
|
|
183
|
+
passFn(`lint-js збігається з каноном: ${CANONICAL_LINT_JS}`)
|
|
164
184
|
} else {
|
|
165
|
-
|
|
185
|
+
failFn(
|
|
186
|
+
`lint-js має бути рівно: "${CANONICAL_LINT_JS}" (див. js-lint.mdc / check-js-lint.mjs). Зараз: ${JSON.stringify(normalizeLintJsScript(String(lintJs)))}`
|
|
187
|
+
)
|
|
166
188
|
}
|
|
189
|
+
} else {
|
|
190
|
+
failFn(`package.json не містить скрипт "lint-js" — додай: ${JSON.stringify(CANONICAL_LINT_JS)}`)
|
|
191
|
+
}
|
|
167
192
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
193
|
+
checkPackageJsonLintDeps(pkg, passFn, failFn)
|
|
194
|
+
|
|
195
|
+
const nodeEngine = pkg.engines?.node
|
|
196
|
+
if (nodeEngine) {
|
|
197
|
+
const firstNumeric = String(nodeEngine).split(NON_DIGITS_RE).find(Boolean)
|
|
198
|
+
if (firstNumeric && Number(firstNumeric) >= 24) {
|
|
199
|
+
passFn(`engines.node: "${nodeEngine}"`)
|
|
176
200
|
} else {
|
|
177
|
-
|
|
201
|
+
failFn(`engines.node: "${nodeEngine}" — має бути >=24`)
|
|
178
202
|
}
|
|
203
|
+
} else {
|
|
204
|
+
failFn('package.json не містить engines.node — додай: "engines": { "node": ">=24" }')
|
|
179
205
|
}
|
|
206
|
+
}
|
|
180
207
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Перевіряє .oxlintrc.json.
|
|
210
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
211
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
212
|
+
*/
|
|
213
|
+
async function checkOxlintRc(passFn, failFn) {
|
|
214
|
+
if (!existsSync('.oxlintrc.json')) {
|
|
215
|
+
failFn('.oxlintrc.json не існує — додай конфіг oxlint (js-lint.mdc)')
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
let oxCfg
|
|
219
|
+
try {
|
|
220
|
+
oxCfg = JSON.parse(await readFile('.oxlintrc.json', 'utf8'))
|
|
221
|
+
} catch {
|
|
222
|
+
failFn('.oxlintrc.json не є валідним JSON')
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
passFn('.oxlintrc.json існує')
|
|
226
|
+
const oxV = verifyOxlintRcE18e(oxCfg)
|
|
227
|
+
if (oxV.ok) {
|
|
228
|
+
passFn('.oxlintrc.json: jsPlugins з @e18e/eslint-plugin і e18e/prefer-includes: error')
|
|
229
|
+
} else {
|
|
230
|
+
for (const msg of oxV.failures) {
|
|
231
|
+
failFn(msg)
|
|
199
232
|
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Перевіряє .vscode/extensions.json на потрібні розширення.
|
|
238
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
239
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
240
|
+
*/
|
|
241
|
+
async function checkVscodeExtensions(passFn, failFn) {
|
|
242
|
+
if (!existsSync('.vscode/extensions.json')) {
|
|
243
|
+
failFn('.vscode/extensions.json не існує — додай recommendations з js-lint.mdc (див. check-js-lint.mjs)')
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
let ext
|
|
247
|
+
try {
|
|
248
|
+
ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
|
|
249
|
+
} catch {
|
|
250
|
+
failFn('.vscode/extensions.json не є валідним JSON')
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
const rec = ext.recommendations
|
|
254
|
+
if (!Array.isArray(rec)) {
|
|
255
|
+
failFn('.vscode/extensions.json: поле recommendations має бути масивом')
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
const missing = REQUIRED_VSCODE_EXTENSIONS.filter(id => !rec.includes(id))
|
|
259
|
+
if (missing.length > 0) {
|
|
260
|
+
failFn(`.vscode/extensions.json: додай у recommendations: ${missing.join(', ')} (мінімум для js-lint.mdc)`)
|
|
200
261
|
} else {
|
|
201
|
-
|
|
262
|
+
passFn('.vscode/extensions.json: є рекомендації oxlint, eslint і GitHub Actions')
|
|
202
263
|
}
|
|
264
|
+
}
|
|
203
265
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Перевіряє lint-js.yml workflow (fallback — текстовий пошук).
|
|
268
|
+
* @param {string} content вміст workflow файлу
|
|
269
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
270
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
271
|
+
*/
|
|
272
|
+
function checkLintJsWorkflowFallback(content, passFn, failFn) {
|
|
273
|
+
const checks = [
|
|
274
|
+
['actions/checkout@v6', 'lint-js.yml: потрібен крок actions/checkout@v6 (ga.mdc)'],
|
|
275
|
+
['persist-credentials: false', 'lint-js.yml: checkout з persist-credentials: false'],
|
|
276
|
+
['./.github/actions/setup-bun-deps', 'lint-js.yml: потрібен uses: ./.github/actions/setup-bun-deps'],
|
|
277
|
+
['bunx oxlint', 'lint-js.yml: у run має бути bunx oxlint'],
|
|
278
|
+
['bunx eslint .', 'lint-js.yml: у run має бути bunx eslint . (без --fix у CI)'],
|
|
279
|
+
['bunx jscpd .', 'lint-js.yml: у run має бути bunx jscpd .']
|
|
280
|
+
]
|
|
281
|
+
for (const [needle, errMsg] of checks) {
|
|
282
|
+
if (content.includes(needle)) {
|
|
283
|
+
passFn(`lint-js.yml містить: ${needle}`)
|
|
284
|
+
} else {
|
|
285
|
+
failFn(errMsg)
|
|
211
286
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
287
|
+
}
|
|
288
|
+
if (content.includes('bunx oxlint') && OXLINT_FIX_RE.test(content)) {
|
|
289
|
+
failFn('lint-js.yml: у CI не використовуй bunx oxlint --fix (лише bunx oxlint)')
|
|
290
|
+
}
|
|
291
|
+
if (content.includes('eslint --fix')) {
|
|
292
|
+
failFn('lint-js.yml: у CI не використовуй eslint --fix (лише bunx eslint .)')
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Перевіряє вміст lint-js.yml через YAML або fallback.
|
|
298
|
+
* @param {string} content вміст файлу
|
|
299
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
300
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
301
|
+
*/
|
|
302
|
+
function checkLintJsYmlContent(content, passFn, failFn) {
|
|
303
|
+
const root = parseWorkflowYaml(content)
|
|
304
|
+
if (root) {
|
|
305
|
+
const v = verifyLintJsWorkflowStructure(root)
|
|
306
|
+
if (v.ok) {
|
|
307
|
+
passFn('lint-js.yml: кроки checkout, setup-bun-deps, oxlint/eslint/jscpd (YAML + кроки)')
|
|
308
|
+
} else {
|
|
309
|
+
for (const msg of v.failures) {
|
|
310
|
+
failFn(`lint-js.yml: ${msg}`)
|
|
223
311
|
}
|
|
224
312
|
}
|
|
225
313
|
} else {
|
|
226
|
-
|
|
314
|
+
checkLintJsWorkflowFallback(content, passFn, failFn)
|
|
227
315
|
}
|
|
316
|
+
}
|
|
228
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Перевіряє lint-js.yml і lint.yml workflow.
|
|
320
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
321
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
322
|
+
*/
|
|
323
|
+
async function checkLintJsWorkflows(passFn, failFn) {
|
|
229
324
|
if (existsSync('.github/workflows/lint-js.yml')) {
|
|
230
325
|
const content = await readFile('.github/workflows/lint-js.yml', 'utf8')
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (root) {
|
|
234
|
-
const v = verifyLintJsWorkflowStructure(root)
|
|
235
|
-
if (v.ok) {
|
|
236
|
-
pass('lint-js.yml: кроки checkout, setup-bun-deps, oxlint/eslint/jscpd (YAML + кроки)')
|
|
237
|
-
} else {
|
|
238
|
-
for (const msg of v.failures) {
|
|
239
|
-
fail(`lint-js.yml: ${msg}`)
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
} else {
|
|
243
|
-
const checks = [
|
|
244
|
-
['actions/checkout@v6', 'lint-js.yml: потрібен крок actions/checkout@v6 (ga.mdc)'],
|
|
245
|
-
['persist-credentials: false', 'lint-js.yml: checkout з persist-credentials: false'],
|
|
246
|
-
['./.github/actions/setup-bun-deps', 'lint-js.yml: потрібен uses: ./.github/actions/setup-bun-deps'],
|
|
247
|
-
['bunx oxlint', 'lint-js.yml: у run має бути bunx oxlint'],
|
|
248
|
-
['bunx eslint .', 'lint-js.yml: у run має бути bunx eslint . (без --fix у CI)'],
|
|
249
|
-
['bunx jscpd .', 'lint-js.yml: у run має бути bunx jscpd .']
|
|
250
|
-
]
|
|
251
|
-
for (const [needle, errMsg] of checks) {
|
|
252
|
-
if (content.includes(needle)) {
|
|
253
|
-
pass(`lint-js.yml містить: ${needle}`)
|
|
254
|
-
} else {
|
|
255
|
-
fail(errMsg)
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
if (content.includes('bunx oxlint') && /bunx\s+oxlint[^\n]*--fix/u.test(content)) {
|
|
259
|
-
fail('lint-js.yml: у CI не використовуй bunx oxlint --fix (лише bunx oxlint)')
|
|
260
|
-
}
|
|
261
|
-
if (content.includes('eslint --fix')) {
|
|
262
|
-
fail('lint-js.yml: у CI не використовуй eslint --fix (лише bunx eslint .)')
|
|
263
|
-
}
|
|
264
|
-
}
|
|
326
|
+
passFn('lint-js.yml існує')
|
|
327
|
+
checkLintJsYmlContent(content, passFn, failFn)
|
|
265
328
|
} else {
|
|
266
|
-
|
|
329
|
+
failFn('.github/workflows/lint-js.yml не існує — створи його (див. check-js-lint.mjs / js-lint.mdc)')
|
|
267
330
|
}
|
|
268
331
|
|
|
269
332
|
if (existsSync('.github/workflows/lint.yml')) {
|
|
270
333
|
const lintYml = await readFile('.github/workflows/lint.yml', 'utf8')
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (looksLikeJsLint) {
|
|
274
|
-
fail('.github/workflows/lint.yml дублює кроки lint-js.yml — залиш один workflow на лінт JS (js-lint.mdc)')
|
|
334
|
+
if (lintYml.includes('bunx oxlint') && lintYml.includes('bunx eslint') && lintYml.includes('jscpd')) {
|
|
335
|
+
failFn('.github/workflows/lint.yml дублює кроки lint-js.yml — залиш один workflow на лінт JS (js-lint.mdc)')
|
|
275
336
|
} else {
|
|
276
|
-
|
|
337
|
+
passFn('.github/workflows/lint.yml не дублює oxlint/eslint/jscpd з lint-js.yml')
|
|
277
338
|
}
|
|
278
339
|
}
|
|
340
|
+
}
|
|
279
341
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
fail('.jscpd.json має містити "minLines" як число >= 25')
|
|
311
|
-
}
|
|
312
|
-
}
|
|
342
|
+
/**
|
|
343
|
+
* Перевіряє .jscpd.json.
|
|
344
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
345
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
346
|
+
*/
|
|
347
|
+
async function checkJscpdConfig(passFn, failFn) {
|
|
348
|
+
if (!existsSync('.jscpd.json')) {
|
|
349
|
+
failFn('.jscpd.json не існує — створи з полями згідно check js-lint')
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
let jscpdCfg
|
|
353
|
+
try {
|
|
354
|
+
jscpdCfg = JSON.parse(await readFile('.jscpd.json', 'utf8'))
|
|
355
|
+
} catch {
|
|
356
|
+
failFn('.jscpd.json не є валідним JSON')
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
passFn('.jscpd.json існує')
|
|
360
|
+
if (jscpdCfg.gitignore === true) {
|
|
361
|
+
passFn('.jscpd.json: gitignore увімкнено')
|
|
362
|
+
} else {
|
|
363
|
+
failFn('.jscpd.json має містити "gitignore": true')
|
|
364
|
+
}
|
|
365
|
+
if (jscpdCfg.exitCode === 1) {
|
|
366
|
+
passFn('.jscpd.json: exitCode 1 при дублікатах')
|
|
367
|
+
} else {
|
|
368
|
+
failFn('.jscpd.json має містити "exitCode": 1 (інакше CI не впаде на клонах)')
|
|
369
|
+
}
|
|
370
|
+
if (Array.isArray(jscpdCfg.reporters) && jscpdCfg.reporters.includes('console')) {
|
|
371
|
+
passFn('.jscpd.json: reporters містить console')
|
|
313
372
|
} else {
|
|
314
|
-
|
|
373
|
+
failFn('.jscpd.json має містити "reporters": ["console"] (або масив із "console")')
|
|
315
374
|
}
|
|
375
|
+
const minLines = jscpdCfg.minLines
|
|
376
|
+
if (typeof minLines === 'number' && minLines >= 25) {
|
|
377
|
+
passFn(`.jscpd.json: minLines ${minLines} (>=25)`)
|
|
378
|
+
} else {
|
|
379
|
+
failFn('.jscpd.json має містити "minLines" як число >= 25')
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Перевіряє відповідність проєкту правилам js-lint.mdc
|
|
385
|
+
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
386
|
+
*/
|
|
387
|
+
export async function check() {
|
|
388
|
+
const reporter = createCheckReporter()
|
|
389
|
+
const { pass, fail } = reporter
|
|
390
|
+
|
|
391
|
+
await checkEslintConfig(pass, fail)
|
|
392
|
+
await checkPackageJsonJsLint(pass, fail)
|
|
393
|
+
await checkOxlintRc(pass, fail)
|
|
394
|
+
await checkVscodeExtensions(pass, fail)
|
|
395
|
+
await checkLintJsWorkflows(pass, fail)
|
|
396
|
+
await checkJscpdConfig(pass, fail)
|
|
316
397
|
|
|
317
398
|
for (const dup of ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml']) {
|
|
318
399
|
if (existsSync(dup)) fail(`Знайдено застарілий конфіг ESLint: ${dup} — видали, використовуй flat config`)
|