@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.
@@ -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(/\s+/gu, ' ')
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 m = s.match(/(\d+)\.(\d+)\.(\d+)/u)
51
- if (!m) {
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
- * Перевіряє відповідність проєкту правилам js-lint.mdc
89
- * @returns {Promise<number>} 0 все OK, 1 є проблеми
93
+ * Перевіряє ESLint flat config файл.
94
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
95
+ * @param {(msg: string) => void} failFn callback при помилці
90
96
  */
91
- export async function check() {
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
- pass('eslint.config.js існує')
101
+ passFn('eslint.config.js існує')
99
102
  } else if (existsSync('eslint.config.mjs')) {
100
103
  eslintPath = 'eslint.config.mjs'
101
- pass('eslint.config.mjs існує')
104
+ passFn('eslint.config.mjs існує')
102
105
  } else {
103
- fail('Відсутній eslint.config.js або eslint.config.mjs — flat config з getConfig (js-lint.mdc)')
106
+ failFn('Відсутній eslint.config.js або eslint.config.mjs — flat config з getConfig (js-lint.mdc)')
107
+ return
104
108
  }
105
-
106
- if (eslintPath) {
107
- const eslintRaw = await readFile(eslintPath, 'utf8')
108
- if (eslintRaw.includes('getConfig')) {
109
- pass(`${eslintPath}: містить getConfig`)
110
- } else {
111
- fail(`${eslintPath}: потрібен виклик getConfig (js-lint.mdc)`)
112
- }
113
- if (eslintRaw.includes('@nitra/eslint-config')) {
114
- pass(`${eslintPath}: імпорт @nitra/eslint-config`)
115
- } else {
116
- fail(`${eslintPath}: імпортуй getConfig з @nitra/eslint-config`)
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
- if (eslintRaw.includes('**/auto-imports.d.ts')) {
119
- pass(`${eslintPath}: ignores містить **/auto-imports.d.ts`)
126
+ ]
127
+ for (const { needle, ok, err } of checks) {
128
+ if (eslintRaw.includes(needle)) {
129
+ passFn(ok)
120
130
  } else {
121
- fail(`${eslintPath}: додай у ignores запис **/auto-imports.d.ts (js-lint.mdc)`)
131
+ failFn(err)
122
132
  }
123
133
  }
134
+ }
124
135
 
125
- if (existsSync('package.json')) {
126
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
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
- if (pkg.scripts?.['lint-js']) {
129
- pass('package.json містить скрипт lint-js')
130
- const lintJs = String(pkg.scripts['lint-js'])
131
- if (isCanonicalLintJs(lintJs)) {
132
- pass(`lint-js збігається з каноном: ${CANONICAL_LINT_JS}`)
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
- fail(`package.json не містить скрипт "lint-js" — додай: ${JSON.stringify(CANONICAL_LINT_JS)}`)
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
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
143
- if (allDeps.prettier) {
144
- fail('package.json: видали залежність prettier (oxfmt замість prettier, js-lint.mdc)')
145
- } else {
146
- pass('package.json не містить prettier')
147
- }
148
- if (allDeps['@nitra/prettier-config']) {
149
- fail('package.json: видали @nitra/prettier-config (js-lint.mdc)')
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
- const nitraEslint = pkg.devDependencies?.['@nitra/eslint-config']
155
- if (nitraEslint) {
156
- pass('@nitra/eslint-config є в devDependencies')
157
- if (nitraEslintConfigDeclaresE18eTransitive(nitraEslint)) {
158
- pass('@nitra/eslint-config: мінімум 3.5.0 (транзитивний @e18e/eslint-plugin для oxlint jsPlugins, js-lint.mdc)')
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
- fail('@nitra/eslint-config відсутній в devDependencies — додай: bun add -d @nitra/eslint-config')
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
- const nodeEngine = pkg.engines?.node
169
- if (nodeEngine) {
170
- const match = nodeEngine.match(/(\d+)/u)
171
- if (match && Number(match[1]) >= 24) {
172
- pass(`engines.node: "${nodeEngine}"`)
173
- } else {
174
- fail(`engines.node: "${nodeEngine}" — має бути >=24`)
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
- fail('package.json не містить engines.node — додай: "engines": { "node": ">=24" }')
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
- if (existsSync('.oxlintrc.json')) {
182
- let oxCfg
183
- try {
184
- oxCfg = JSON.parse(await readFile('.oxlintrc.json', 'utf8'))
185
- } catch {
186
- fail('.oxlintrc.json не є валідним JSON')
187
- oxCfg = null
188
- }
189
- if (oxCfg !== null) {
190
- pass('.oxlintrc.json існує')
191
- const oxV = verifyOxlintRcE18e(oxCfg)
192
- if (oxV.ok) {
193
- pass('.oxlintrc.json: jsPlugins з @e18e/eslint-plugin і e18e/prefer-includes: error')
194
- } else {
195
- for (const msg of oxV.failures) {
196
- fail(msg)
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
- fail('.oxlintrc.json не існує додай конфіг oxlint (js-lint.mdc)')
262
+ passFn('.vscode/extensions.json: є рекомендації oxlint, eslint і GitHub Actions')
202
263
  }
264
+ }
203
265
 
204
- if (existsSync('.vscode/extensions.json')) {
205
- let ext
206
- try {
207
- ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
208
- } catch {
209
- fail('.vscode/extensions.json не є валідним JSON')
210
- ext = null
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
- if (ext) {
213
- const rec = ext.recommendations
214
- if (Array.isArray(rec)) {
215
- const missing = REQUIRED_VSCODE_EXTENSIONS.filter(id => !rec.includes(id))
216
- if (missing.length > 0) {
217
- fail(`.vscode/extensions.json: додай у recommendations: ${missing.join(', ')} (мінімум для js-lint.mdc)`)
218
- } else {
219
- pass('.vscode/extensions.json: є рекомендації oxlint, eslint і GitHub Actions')
220
- }
221
- } else {
222
- fail('.vscode/extensions.json: поле recommendations має бути масивом')
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
- fail('.vscode/extensions.json не існує — додай recommendations з js-lint.mdc (див. check-js-lint.mjs)')
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
- pass('lint-js.yml існує')
232
- const root = parseWorkflowYaml(content)
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
- fail('.github/workflows/lint-js.yml не існує — створи його (див. check-js-lint.mjs / js-lint.mdc)')
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
- const looksLikeJsLint =
272
- /\bbunx\s+oxlint\b/u.test(lintYml) && /\bbunx\s+eslint\b/u.test(lintYml) && /\bjscpd\b/u.test(lintYml)
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
- pass('.github/workflows/lint.yml не дублює oxlint/eslint/jscpd з lint-js.yml')
337
+ passFn('.github/workflows/lint.yml не дублює oxlint/eslint/jscpd з lint-js.yml')
277
338
  }
278
339
  }
340
+ }
279
341
 
280
- if (existsSync('.jscpd.json')) {
281
- let jscpdCfg
282
- try {
283
- jscpdCfg = JSON.parse(await readFile('.jscpd.json', 'utf8'))
284
- } catch {
285
- fail('.jscpd.json не є валідним JSON')
286
- jscpdCfg = null
287
- }
288
- if (jscpdCfg) {
289
- pass('.jscpd.json існує')
290
- if (jscpdCfg.gitignore === true) {
291
- pass('.jscpd.json: gitignore увімкнено')
292
- } else {
293
- fail('.jscpd.json має містити "gitignore": true')
294
- }
295
- if (jscpdCfg.exitCode === 1) {
296
- pass('.jscpd.json: exitCode 1 при дублікатах')
297
- } else {
298
- fail('.jscpd.json має містити "exitCode": 1 (інакше CI не впаде на клонах)')
299
- }
300
- const reporters = jscpdCfg.reporters
301
- if (Array.isArray(reporters) && reporters.includes('console')) {
302
- pass('.jscpd.json: reporters містить console')
303
- } else {
304
- fail('.jscpd.json має містити "reporters": ["console"] (або масив із "console")')
305
- }
306
- const minLines = jscpdCfg.minLines
307
- if (typeof minLines === 'number' && minLines >= 25) {
308
- pass(`.jscpd.json: minLines ${minLines} (>=25)`)
309
- } else {
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
- fail('.jscpd.json не існує створи з полями згідно check js-lint')
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`)