@nitra/cursor 1.8.105 → 1.8.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/auto-rules.md +2 -2
- package/bin/n-cursor.js +5 -15
- package/mdc/js-pino.mdc +2 -2
- package/mdc/k8s.mdc +22 -12
- package/mdc/text.mdc +3 -3
- package/package.json +1 -1
- package/scripts/check-abie.mjs +515 -528
- package/scripts/check-bun.mjs +106 -78
- package/scripts/check-ga.mjs +151 -119
- package/scripts/check-js-lint.mjs +256 -179
- package/scripts/check-js-pino.mjs +48 -3
- package/scripts/check-k8s.mjs +403 -34
- package/scripts/check-nginx-default-tpl.mjs +109 -91
- package/scripts/check-npm-module.mjs +163 -116
- package/scripts/check-style-lint.mjs +74 -61
- package/scripts/check-text.mjs +289 -209
- package/scripts/check-vue.mjs +108 -67
- package/scripts/utils/bunyan-imports.mjs +182 -0
- package/scripts/utils/gha-workflow.mjs +3 -1
|
@@ -90,233 +90,310 @@ export function verifyOxlintRcE18e(cfg) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
* Перевіряє
|
|
94
|
-
* @
|
|
93
|
+
* Перевіряє ESLint flat config файл.
|
|
94
|
+
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
95
|
+
* @param {(msg: string) => void} failFn callback при помилці
|
|
95
96
|
*/
|
|
96
|
-
|
|
97
|
-
const reporter = createCheckReporter()
|
|
98
|
-
const { pass, fail } = reporter
|
|
99
|
-
|
|
97
|
+
async function checkEslintConfig(passFn, failFn) {
|
|
100
98
|
let eslintPath = ''
|
|
101
99
|
if (existsSync('eslint.config.js')) {
|
|
102
100
|
eslintPath = 'eslint.config.js'
|
|
103
|
-
|
|
101
|
+
passFn('eslint.config.js існує')
|
|
104
102
|
} else if (existsSync('eslint.config.mjs')) {
|
|
105
103
|
eslintPath = 'eslint.config.mjs'
|
|
106
|
-
|
|
104
|
+
passFn('eslint.config.mjs існує')
|
|
107
105
|
} else {
|
|
108
|
-
|
|
106
|
+
failFn('Відсутній eslint.config.js або eslint.config.mjs — flat config з getConfig (js-lint.mdc)')
|
|
107
|
+
return
|
|
109
108
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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)`
|
|
122
125
|
}
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
]
|
|
127
|
+
for (const { needle, ok, err } of checks) {
|
|
128
|
+
if (eslintRaw.includes(needle)) {
|
|
129
|
+
passFn(ok)
|
|
125
130
|
} else {
|
|
126
|
-
|
|
131
|
+
failFn(err)
|
|
127
132
|
}
|
|
128
133
|
}
|
|
134
|
+
}
|
|
129
135
|
|
|
130
|
-
|
|
131
|
-
|
|
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
|
+
}
|
|
132
154
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
} else {
|
|
139
|
-
fail(
|
|
140
|
-
`lint-js має бути рівно: "${CANONICAL_LINT_JS}" (див. js-lint.mdc / check-js-lint.mjs). Зараз: ${JSON.stringify(normalizeLintJsScript(lintJs))}`
|
|
141
|
-
)
|
|
142
|
-
}
|
|
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)')
|
|
143
160
|
} else {
|
|
144
|
-
|
|
161
|
+
failFn(
|
|
162
|
+
'@nitra/eslint-config: онови до мінімум "^3.5.0" — з цієї версії постачається @e18e/eslint-plugin для .oxlintrc.json (js-lint.mdc)'
|
|
163
|
+
)
|
|
145
164
|
}
|
|
165
|
+
} else {
|
|
166
|
+
failFn('@nitra/eslint-config відсутній в devDependencies — додай: bun add -d @nitra/eslint-config')
|
|
167
|
+
}
|
|
168
|
+
}
|
|
146
169
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
} else {
|
|
156
|
-
pass('package.json не містить @nitra/prettier-config')
|
|
157
|
-
}
|
|
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'))
|
|
158
178
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
} else {
|
|
165
|
-
fail(
|
|
166
|
-
'@nitra/eslint-config: онови до мінімум "^3.5.0" — з цієї версії постачається @e18e/eslint-plugin для .oxlintrc.json (js-lint.mdc)'
|
|
167
|
-
)
|
|
168
|
-
}
|
|
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}`)
|
|
169
184
|
} else {
|
|
170
|
-
|
|
185
|
+
failFn(
|
|
186
|
+
`lint-js має бути рівно: "${CANONICAL_LINT_JS}" (див. js-lint.mdc / check-js-lint.mjs). Зараз: ${JSON.stringify(normalizeLintJsScript(String(lintJs)))}`
|
|
187
|
+
)
|
|
171
188
|
}
|
|
189
|
+
} else {
|
|
190
|
+
failFn(`package.json не містить скрипт "lint-js" — додай: ${JSON.stringify(CANONICAL_LINT_JS)}`)
|
|
191
|
+
}
|
|
172
192
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
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}"`)
|
|
181
200
|
} else {
|
|
182
|
-
|
|
201
|
+
failFn(`engines.node: "${nodeEngine}" — має бути >=24`)
|
|
183
202
|
}
|
|
203
|
+
} else {
|
|
204
|
+
failFn('package.json не містить engines.node — додай: "engines": { "node": ">=24" }')
|
|
184
205
|
}
|
|
206
|
+
}
|
|
185
207
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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)
|
|
204
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)`)
|
|
205
261
|
} else {
|
|
206
|
-
|
|
262
|
+
passFn('.vscode/extensions.json: є рекомендації oxlint, eslint і GitHub Actions')
|
|
207
263
|
}
|
|
264
|
+
}
|
|
208
265
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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)
|
|
216
286
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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}`)
|
|
228
311
|
}
|
|
229
312
|
}
|
|
230
313
|
} else {
|
|
231
|
-
|
|
314
|
+
checkLintJsWorkflowFallback(content, passFn, failFn)
|
|
232
315
|
}
|
|
316
|
+
}
|
|
233
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) {
|
|
234
324
|
if (existsSync('.github/workflows/lint-js.yml')) {
|
|
235
325
|
const content = await readFile('.github/workflows/lint-js.yml', 'utf8')
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (root) {
|
|
239
|
-
const v = verifyLintJsWorkflowStructure(root)
|
|
240
|
-
if (v.ok) {
|
|
241
|
-
pass('lint-js.yml: кроки checkout, setup-bun-deps, oxlint/eslint/jscpd (YAML + кроки)')
|
|
242
|
-
} else {
|
|
243
|
-
for (const msg of v.failures) {
|
|
244
|
-
fail(`lint-js.yml: ${msg}`)
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
} else {
|
|
248
|
-
const checks = [
|
|
249
|
-
['actions/checkout@v6', 'lint-js.yml: потрібен крок actions/checkout@v6 (ga.mdc)'],
|
|
250
|
-
['persist-credentials: false', 'lint-js.yml: checkout з persist-credentials: false'],
|
|
251
|
-
['./.github/actions/setup-bun-deps', 'lint-js.yml: потрібен uses: ./.github/actions/setup-bun-deps'],
|
|
252
|
-
['bunx oxlint', 'lint-js.yml: у run має бути bunx oxlint'],
|
|
253
|
-
['bunx eslint .', 'lint-js.yml: у run має бути bunx eslint . (без --fix у CI)'],
|
|
254
|
-
['bunx jscpd .', 'lint-js.yml: у run має бути bunx jscpd .']
|
|
255
|
-
]
|
|
256
|
-
for (const [needle, errMsg] of checks) {
|
|
257
|
-
if (content.includes(needle)) {
|
|
258
|
-
pass(`lint-js.yml містить: ${needle}`)
|
|
259
|
-
} else {
|
|
260
|
-
fail(errMsg)
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if (content.includes('bunx oxlint') && OXLINT_FIX_RE.test(content)) {
|
|
264
|
-
fail('lint-js.yml: у CI не використовуй bunx oxlint --fix (лише bunx oxlint)')
|
|
265
|
-
}
|
|
266
|
-
if (content.includes('eslint --fix')) {
|
|
267
|
-
fail('lint-js.yml: у CI не використовуй eslint --fix (лише bunx eslint .)')
|
|
268
|
-
}
|
|
269
|
-
}
|
|
326
|
+
passFn('lint-js.yml існує')
|
|
327
|
+
checkLintJsYmlContent(content, passFn, failFn)
|
|
270
328
|
} else {
|
|
271
|
-
|
|
329
|
+
failFn('.github/workflows/lint-js.yml не існує — створи його (див. check-js-lint.mjs / js-lint.mdc)')
|
|
272
330
|
}
|
|
273
331
|
|
|
274
332
|
if (existsSync('.github/workflows/lint.yml')) {
|
|
275
333
|
const lintYml = await readFile('.github/workflows/lint.yml', 'utf8')
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
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)')
|
|
279
336
|
} else {
|
|
280
|
-
|
|
337
|
+
passFn('.github/workflows/lint.yml не дублює oxlint/eslint/jscpd з lint-js.yml')
|
|
281
338
|
}
|
|
282
339
|
}
|
|
340
|
+
}
|
|
283
341
|
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
fail('.jscpd.json має містити "minLines" як число >= 25')
|
|
315
|
-
}
|
|
316
|
-
}
|
|
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')
|
|
317
372
|
} else {
|
|
318
|
-
|
|
373
|
+
failFn('.jscpd.json має містити "reporters": ["console"] (або масив із "console")')
|
|
319
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)
|
|
320
397
|
|
|
321
398
|
for (const dup of ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml']) {
|
|
322
399
|
if (existsSync(dup)) fail(`Знайдено застарілий конфіг ESLint: ${dup} — видали, використовуй flat config`)
|
|
@@ -1,16 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Для кожного workspace-пакета перевіряє правило js-pino.mdc.
|
|
3
3
|
*
|
|
4
|
-
* Заборона bunyan
|
|
5
|
-
*
|
|
4
|
+
* Заборона `@nitra/bunyan` / `bunyan` як у залежностях `package.json`, так і в коді
|
|
5
|
+
* (`import` / `require` / динамічний `import()`); наявність `OTEL_RESOURCE_ATTRIBUTES`
|
|
6
|
+
* у `k8s/base/configmap.yaml`, якщо такий файл існує.
|
|
7
|
+
*
|
|
8
|
+
* Імпорти в джерелах сканує AST через `oxc-parser` (див. `utils/bunyan-imports.mjs`),
|
|
9
|
+
* щоб виявити випадки на кшталт `import log from '@nitra/bunyan'`, які лишаються в коді
|
|
10
|
+
* після підміни залежності.
|
|
6
11
|
*/
|
|
7
12
|
import { existsSync } from 'node:fs'
|
|
8
13
|
import { readFile } from 'node:fs/promises'
|
|
9
|
-
import { join } from 'node:path'
|
|
14
|
+
import { join, relative } from 'node:path'
|
|
10
15
|
|
|
16
|
+
import {
|
|
17
|
+
findBunyanImportsInText,
|
|
18
|
+
isBunyanScanSourceFile,
|
|
19
|
+
shouldSkipFileForBunyanScan
|
|
20
|
+
} from './utils/bunyan-imports.mjs'
|
|
11
21
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
22
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
12
23
|
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
13
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Сканує джерела пакета на заборонені імпорти `@nitra/bunyan` / `bunyan`.
|
|
27
|
+
* @param {string} absPackageRoot абсолютний шлях до кореня пакета
|
|
28
|
+
* @param {string} label префікс повідомлення `[<pkg>] `
|
|
29
|
+
* @param {(msg: string) => void} fail callback при помилці
|
|
30
|
+
* @returns {Promise<number>} кількість знайдених порушень
|
|
31
|
+
*/
|
|
32
|
+
async function checkBunyanImports(absPackageRoot, label, fail) {
|
|
33
|
+
/** @type {string[]} */
|
|
34
|
+
const sourcePaths = []
|
|
35
|
+
await walkDir(absPackageRoot, absPath => {
|
|
36
|
+
const rel = relative(absPackageRoot, absPath).split('\\').join('/')
|
|
37
|
+
if (!shouldSkipFileForBunyanScan(rel) && isBunyanScanSourceFile(rel)) {
|
|
38
|
+
sourcePaths.push(absPath)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
let violations = 0
|
|
43
|
+
for (const absPath of sourcePaths) {
|
|
44
|
+
const rel = relative(absPackageRoot, absPath).split('\\').join('/')
|
|
45
|
+
const content = await readFile(absPath, 'utf8')
|
|
46
|
+
for (const v of findBunyanImportsInText(content, rel)) {
|
|
47
|
+
violations++
|
|
48
|
+
fail(`${label}${rel}:${v.line} — заміни '${v.module}' на '@nitra/pino': ${v.snippet}`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return violations
|
|
52
|
+
}
|
|
53
|
+
|
|
14
54
|
/**
|
|
15
55
|
* Перевіряє відповідність правилам js-pino.mdc для одного workspace-пакета.
|
|
16
56
|
* @param {string} rootDir відносний шлях workspace (не `'.'`)
|
|
@@ -33,6 +73,11 @@ async function checkWorkspacePackage(rootDir, fail, passFn) {
|
|
|
33
73
|
}
|
|
34
74
|
}
|
|
35
75
|
|
|
76
|
+
const importViolations = await checkBunyanImports(join(process.cwd(), rootDir), label, fail)
|
|
77
|
+
if (importViolations === 0) {
|
|
78
|
+
passFn(`${label}немає імпортів '@nitra/bunyan' / 'bunyan' у джерелах`)
|
|
79
|
+
}
|
|
80
|
+
|
|
36
81
|
const configmapPath = join(rootDir, 'k8s/base/configmap.yaml')
|
|
37
82
|
if (existsSync(configmapPath)) {
|
|
38
83
|
const content = await readFile(configmapPath, 'utf8')
|