@nitra/cursor 1.27.9 → 1.28.1
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/CHANGELOG.md +25 -0
- package/package.json +1 -1
- package/rules/abie/js/applies.mjs +3 -2
- package/rules/abie/js/env_dns.mjs +4 -2
- package/rules/abie/js/firebase_hosting.mjs +3 -2
- package/rules/abie/js/hc_pairing.mjs +3 -2
- package/rules/abie/js/ua_http_route.mjs +4 -2
- package/rules/abie/js/ua_node_selector.mjs +4 -2
- package/rules/adr/js/hooks.mjs +36 -28
- package/rules/bun/js/layout.mjs +16 -11
- package/rules/capacitor/js/platforms.mjs +3 -2
- package/rules/changelog/js/consistency.mjs +85 -63
- package/rules/changelog/lib/package-manifest.mjs +5 -4
- package/rules/docker/js/lint.mjs +3 -2
- package/rules/ga/js/workflows.mjs +41 -32
- package/rules/graphql/js/tooling.mjs +15 -11
- package/rules/hasura/js/internal_urls.mjs +14 -10
- package/rules/image-avif/js/avif_generation.mjs +36 -23
- package/rules/image-compress/js/package_setup.mjs +18 -12
- package/rules/js-bun-db/js/safety.mjs +3 -2
- package/rules/js-lint/js/tooling.mjs +45 -32
- package/rules/js-run/js/runtime.mjs +21 -15
- package/rules/k8s/js/manifests.mjs +3 -2
- package/rules/nginx-default-tpl/js/template.mjs +7 -6
- package/rules/npm-module/js/package_structure.mjs +82 -57
- package/rules/rego/js/applies.mjs +4 -4
- package/rules/rust/js/applies.mjs +5 -3
- package/rules/security/js/sample_secret.mjs +2 -2
- package/rules/security/js/trufflehog.mjs +6 -4
- package/rules/style-lint/js/tooling.mjs +15 -8
- package/rules/test/coverage/coverage.mjs +1 -1
- package/rules/test/js/data/vitest_config/vitest.config.baseline.js +7 -0
- package/rules/test/js/location.mjs +3 -2
- package/rules/test/js/no-process-chdir.mjs +89 -0
- package/rules/test/js/no-relative-fs-path.mjs +259 -0
- package/rules/test/js/vitest-config-pool-forks.mjs +52 -0
- package/rules/test/test.mdc +21 -0
- package/rules/text/js/forbidden-prettier.mjs +4 -2
- package/rules/text/js/formatting.mjs +25 -16
- package/rules/vue/js/packages.mjs +33 -25
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* Версія та CHANGELOG: перший заголовок `## [version]` у `npm/CHANGELOG.md` має збігатися з `version` у
|
|
24
24
|
* `npm/package.json` (найсвіжіший реліз зверху). Якщо в git є незакомічені зміни під `npm/`, `version` у робочому
|
|
25
25
|
* файлі має відрізнятися від `HEAD` — інакше типовий пропуск bump після правок у пакеті.
|
|
26
|
+
* @param {string} cwd корінь репозиторію
|
|
26
27
|
*/
|
|
27
28
|
import { execFile } from 'node:child_process'
|
|
28
29
|
import { existsSync } from 'node:fs'
|
|
@@ -61,6 +62,7 @@ const TEST_DIR_NAMES = new Set(['tests', '__tests__', 'fixtures', '__fixtures__'
|
|
|
61
62
|
* (`*_test.rego`) свідомо не входить: за конвенцією conftest юніт-тест лежить
|
|
62
63
|
* поруч з полісі у тому самому `package` — і це дозволений виняток усередині
|
|
63
64
|
* опублікованого `policy/`-каталогу (npm-module.mdc).
|
|
65
|
+
* @param {string} cwd корінь репозиторію
|
|
64
66
|
*/
|
|
65
67
|
const TEST_FILE_PATTERNS = [/^.+\.(test|spec)\.[cm]?[jt]sx?$/iu]
|
|
66
68
|
|
|
@@ -91,9 +93,10 @@ const GLOBSTAR_TRAILING_RE = /\/__GLOBSTAR__$/u
|
|
|
91
93
|
* Чи є під `npm/src` хоча б один `.js` (рекурсивно).
|
|
92
94
|
* @param {string[]} [ignorePaths] абсолютні шляхи каталогів, повністю виключених з обходу
|
|
93
95
|
* @returns {Promise<boolean>} `true`, якщо знайдено хоча б один `.js`
|
|
96
|
+
* @param {string} cwd корінь репозиторію
|
|
94
97
|
*/
|
|
95
|
-
async function npmSrcTreeHasJsFile(ignorePaths = []) {
|
|
96
|
-
const root = 'npm/src'
|
|
98
|
+
async function npmSrcTreeHasJsFile(cwd, ignorePaths = []) {
|
|
99
|
+
const root = join(cwd, 'npm/src')
|
|
97
100
|
if (!existsSync(root)) {
|
|
98
101
|
return false
|
|
99
102
|
}
|
|
@@ -113,12 +116,14 @@ async function npmSrcTreeHasJsFile(ignorePaths = []) {
|
|
|
113
116
|
/**
|
|
114
117
|
* Знаходить текстовий вміст конфігурації hk для перевірки npm-module.
|
|
115
118
|
* @returns {Promise<{ path: string, text: string } | null>} знайдений файл або `null`
|
|
119
|
+
* @param {string} cwd корінь репозиторію
|
|
116
120
|
*/
|
|
117
|
-
async function readHkConfig() {
|
|
121
|
+
async function readHkConfig(cwd) {
|
|
118
122
|
const candidates = ['hk.pkl', '.config/hk.pkl']
|
|
119
123
|
for (const p of candidates) {
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
const abs = join(cwd, p)
|
|
125
|
+
if (existsSync(abs)) {
|
|
126
|
+
const text = await readFile(abs, 'utf8')
|
|
122
127
|
return { path: p, text }
|
|
123
128
|
}
|
|
124
129
|
}
|
|
@@ -174,18 +179,20 @@ function npmTypesFileFromPackageField(typesField) {
|
|
|
174
179
|
* @param {boolean} useSrcJsLayout чи використовується layout з npm/src
|
|
175
180
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
176
181
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
182
|
+
* @param {string} cwd корінь репозиторію
|
|
177
183
|
*/
|
|
178
|
-
async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn) {
|
|
179
|
-
|
|
180
|
-
|
|
184
|
+
async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn, cwd) {
|
|
185
|
+
const npmPkgPath = join(cwd, 'npm/package.json')
|
|
186
|
+
if (!existsSync(npmPkgPath)) return
|
|
187
|
+
const npmPkg = JSON.parse(await readFile(npmPkgPath, 'utf8'))
|
|
181
188
|
const typesField = npmPkg.types
|
|
182
189
|
|
|
183
|
-
const
|
|
190
|
+
const typesRel = useSrcJsLayout ? join('npm', 'types', 'index.d.ts') : npmTypesFileFromPackageField(typesField)
|
|
184
191
|
const missingTypesMsg = useSrcJsLayout
|
|
185
192
|
? `Відсутній ${join('npm', 'types', 'index.d.ts')} (згенеруй tsc з npm-module.mdc)`
|
|
186
193
|
: `Файл для поля types не знайдено або шлях не під ./types/ — ${String(typesField)}`
|
|
187
|
-
if (
|
|
188
|
-
passFn(`${
|
|
194
|
+
if (typesRel && existsSync(join(cwd, typesRel))) {
|
|
195
|
+
passFn(`${typesRel} існує`)
|
|
189
196
|
} else {
|
|
190
197
|
failFn(missingTypesMsg)
|
|
191
198
|
}
|
|
@@ -196,9 +203,10 @@ async function checkNpmPackageJson(useSrcJsLayout, passFn, failFn) {
|
|
|
196
203
|
* валідує `npm/policy/npm_module/emit_types_config/`).
|
|
197
204
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
198
205
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
206
|
+
* @param {string} cwd корінь репозиторію
|
|
199
207
|
*/
|
|
200
|
-
function checkEmitTypesConfig(passFn, failFn) {
|
|
201
|
-
if (!existsSync(EMIT_TYPES_CONFIG)) {
|
|
208
|
+
function checkEmitTypesConfig(passFn, failFn, cwd) {
|
|
209
|
+
if (!existsSync(join(cwd, EMIT_TYPES_CONFIG))) {
|
|
202
210
|
failFn(
|
|
203
211
|
`Без .js під npm/src потрібен ${EMIT_TYPES_CONFIG} (див. npm-module.mdc: emit через tsconfig, без штучного src/index.js)`
|
|
204
212
|
)
|
|
@@ -211,14 +219,16 @@ function checkEmitTypesConfig(passFn, failFn) {
|
|
|
211
219
|
* Перевіряє npm-publish.yml workflow.
|
|
212
220
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
213
221
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
222
|
+
* @param {string} cwd корінь репозиторію
|
|
214
223
|
*/
|
|
215
224
|
/**
|
|
216
225
|
* Чи виконано `git` у корені робочого дерева.
|
|
217
226
|
* @returns {Promise<boolean>} true, якщо процес запущено в межах git work tree
|
|
227
|
+
* @param {string} cwd корінь репозиторію
|
|
218
228
|
*/
|
|
219
|
-
async function gitInsideWorkTree() {
|
|
229
|
+
async function gitInsideWorkTree(cwd) {
|
|
220
230
|
try {
|
|
221
|
-
const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { encoding: 'utf8' })
|
|
231
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { encoding: 'utf8', cwd })
|
|
222
232
|
return stdout.trim() === 'true'
|
|
223
233
|
} catch {
|
|
224
234
|
return false
|
|
@@ -227,11 +237,15 @@ async function gitInsideWorkTree() {
|
|
|
227
237
|
|
|
228
238
|
/**
|
|
229
239
|
* Список незакомічених шляхів під `npm/` відносно `HEAD`.
|
|
240
|
+
* @param {string} cwd корінь репозиторію
|
|
230
241
|
* @returns {Promise<string[] | null>} шляхи або `null`, якщо `git` недоступний
|
|
231
242
|
*/
|
|
232
|
-
async function gitDiffNameOnlyNpm() {
|
|
243
|
+
async function gitDiffNameOnlyNpm(cwd) {
|
|
233
244
|
try {
|
|
234
|
-
const { stdout } = await execFileAsync('git', ['diff', '--name-only', 'HEAD', '--', 'npm'], {
|
|
245
|
+
const { stdout } = await execFileAsync('git', ['diff', '--name-only', 'HEAD', '--', 'npm'], {
|
|
246
|
+
encoding: 'utf8',
|
|
247
|
+
cwd
|
|
248
|
+
})
|
|
235
249
|
return stdout.trim().split('\n').filter(Boolean)
|
|
236
250
|
} catch {
|
|
237
251
|
return null
|
|
@@ -241,11 +255,12 @@ async function gitDiffNameOnlyNpm() {
|
|
|
241
255
|
/**
|
|
242
256
|
* Поле `version` з `npm/package.json` на заданому git-ref (`HEAD:npm/package.json`).
|
|
243
257
|
* @param {string} refPath на кшталт `HEAD:npm/package.json`
|
|
258
|
+
* @param {string} cwd корінь репозиторію
|
|
244
259
|
* @returns {Promise<string | null>} значення поля `version` або `null`, якщо ref недоступний
|
|
245
260
|
*/
|
|
246
|
-
async function gitShowNpmPackageVersionAt(refPath) {
|
|
261
|
+
async function gitShowNpmPackageVersionAt(refPath, cwd) {
|
|
247
262
|
try {
|
|
248
|
-
const { stdout } = await execFileAsync('git', ['show', refPath], { encoding: 'utf8' })
|
|
263
|
+
const { stdout } = await execFileAsync('git', ['show', refPath], { encoding: 'utf8', cwd })
|
|
249
264
|
const m = stdout.match(PACKAGE_JSON_VERSION_RE)
|
|
250
265
|
return m ? m[1] : null
|
|
251
266
|
} catch {
|
|
@@ -268,16 +283,17 @@ function firstChangelogSectionVersion(changelogText) {
|
|
|
268
283
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
269
284
|
* @param {(msg: string) => void} failFn callback при виявленому порушенні
|
|
270
285
|
* @returns {Promise<void>}
|
|
286
|
+
* @param {string} cwd корінь репозиторію
|
|
271
287
|
*/
|
|
272
|
-
async function checkChangelogTopMatchesPackageVersion(passFn, failFn) {
|
|
273
|
-
if (!existsSync('npm/CHANGELOG.md') || !existsSync('npm/package.json')) return
|
|
274
|
-
const pkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
|
|
288
|
+
async function checkChangelogTopMatchesPackageVersion(passFn, failFn, cwd) {
|
|
289
|
+
if (!existsSync(join(cwd, 'npm/CHANGELOG.md')) || !existsSync(join(cwd, 'npm/package.json'))) return
|
|
290
|
+
const pkg = JSON.parse(await readFile(join(cwd, 'npm/package.json'), 'utf8'))
|
|
275
291
|
const ver = typeof pkg.version === 'string' ? pkg.version : null
|
|
276
292
|
if (!ver) {
|
|
277
293
|
failFn('npm/package.json: відсутнє поле version')
|
|
278
294
|
return
|
|
279
295
|
}
|
|
280
|
-
const cl = await readFile('npm/CHANGELOG.md', 'utf8')
|
|
296
|
+
const cl = await readFile(join(cwd, 'npm/CHANGELOG.md'), 'utf8')
|
|
281
297
|
const first = firstChangelogSectionVersion(cl)
|
|
282
298
|
if (!first) {
|
|
283
299
|
failFn('npm/CHANGELOG.md: не знайдено жодного заголовка ## [version]')
|
|
@@ -298,23 +314,24 @@ async function checkChangelogTopMatchesPackageVersion(passFn, failFn) {
|
|
|
298
314
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
299
315
|
* @param {(msg: string) => void} failFn callback при виявленому порушенні
|
|
300
316
|
* @returns {Promise<void>}
|
|
317
|
+
* @param {string} cwd корінь репозиторію
|
|
301
318
|
*/
|
|
302
|
-
async function checkDirtyNpmRequiresVersionBump(passFn, failFn) {
|
|
303
|
-
if (!(await gitInsideWorkTree())) {
|
|
319
|
+
async function checkDirtyNpmRequiresVersionBump(passFn, failFn, cwd) {
|
|
320
|
+
if (!(await gitInsideWorkTree(cwd))) {
|
|
304
321
|
passFn('npm-module: git недоступний або поза work tree — перевірку незакоміченого bump пропущено')
|
|
305
322
|
return
|
|
306
323
|
}
|
|
307
|
-
const changed = await gitDiffNameOnlyNpm()
|
|
324
|
+
const changed = await gitDiffNameOnlyNpm(cwd)
|
|
308
325
|
if (changed === null) {
|
|
309
326
|
passFn('npm-module: git diff під npm/ недоступний — пропущено')
|
|
310
327
|
return
|
|
311
328
|
}
|
|
312
329
|
if (changed.length === 0) return
|
|
313
330
|
|
|
314
|
-
const headVer = await gitShowNpmPackageVersionAt('HEAD:npm/package.json')
|
|
331
|
+
const headVer = await gitShowNpmPackageVersionAt('HEAD:npm/package.json', cwd)
|
|
315
332
|
if (headVer === null) return
|
|
316
333
|
|
|
317
|
-
const pkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
|
|
334
|
+
const pkg = JSON.parse(await readFile(join(cwd, 'npm/package.json'), 'utf8'))
|
|
318
335
|
const cur = typeof pkg.version === 'string' ? pkg.version : null
|
|
319
336
|
if (!cur) return
|
|
320
337
|
|
|
@@ -334,10 +351,11 @@ async function checkDirtyNpmRequiresVersionBump(passFn, failFn) {
|
|
|
334
351
|
* `npm/policy/npm_module/npm_publish_yml/`.
|
|
335
352
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
336
353
|
* @param {(msg: string) => void} failFn callback при виявленому порушенні
|
|
354
|
+
* @param {string} cwd корінь репозиторію
|
|
337
355
|
*/
|
|
338
|
-
function checkPublishWorkflow(passFn, failFn) {
|
|
356
|
+
function checkPublishWorkflow(passFn, failFn, cwd) {
|
|
339
357
|
const publishWf = '.github/workflows/npm-publish.yml'
|
|
340
|
-
if (existsSync(publishWf)) {
|
|
358
|
+
if (existsSync(join(cwd, publishWf))) {
|
|
341
359
|
passFn(`${publishWf} є (структуру перевіряє npx @nitra/cursor fix → npm_module.npm_publish_yml)`)
|
|
342
360
|
} else {
|
|
343
361
|
failFn(`Відсутній ${publishWf} (npm-module.mdc: npm publish)`)
|
|
@@ -386,14 +404,16 @@ export function globToRegex(glob) {
|
|
|
386
404
|
* простір імен `files`, бо саме його сканує check.
|
|
387
405
|
* @param {string[]} filesField значення поля `files`
|
|
388
406
|
* @returns {Promise<string[]>} відсортовані posix-шляхи без `npm/` префікса
|
|
407
|
+
* @param {string} cwd корінь репозиторію
|
|
389
408
|
*/
|
|
390
|
-
async function collectPublishedFiles(filesField) {
|
|
409
|
+
async function collectPublishedFiles(filesField, cwd) {
|
|
391
410
|
const positives = filesField.filter(p => typeof p === 'string' && !p.startsWith('!'))
|
|
392
411
|
const negatives = filesField.filter(p => typeof p === 'string' && p.startsWith('!')).map(p => globToRegex(p.slice(1)))
|
|
393
412
|
/** @type {Set<string>} */
|
|
394
413
|
const collected = new Set()
|
|
414
|
+
const npmRoot = join(cwd, 'npm')
|
|
395
415
|
for (const entry of positives) {
|
|
396
|
-
const fullPath = join(
|
|
416
|
+
const fullPath = join(npmRoot, entry)
|
|
397
417
|
if (!existsSync(fullPath)) continue
|
|
398
418
|
const s = await stat(fullPath)
|
|
399
419
|
if (s.isFile()) {
|
|
@@ -402,7 +422,7 @@ async function collectPublishedFiles(filesField) {
|
|
|
402
422
|
}
|
|
403
423
|
if (!s.isDirectory()) continue
|
|
404
424
|
await walkDir(fullPath, p => {
|
|
405
|
-
const rel = p.slice(
|
|
425
|
+
const rel = p.slice(npmRoot.length + 1).split(sep).join('/')
|
|
406
426
|
collected.add(rel)
|
|
407
427
|
})
|
|
408
428
|
}
|
|
@@ -460,8 +480,9 @@ export function findTestFrameworkImport(content, virtualPath) {
|
|
|
460
480
|
* Подальші сегменти (наприклад, `rules/<r>/js/<c>/tests/`) продовжують перевірятись.
|
|
461
481
|
* @param {string} relPath posix-шлях відносно `npm/`
|
|
462
482
|
* @returns {Promise<string | null>} причина порушення або `null`
|
|
483
|
+
* @param {string} [cwd] корінь репозиторію
|
|
463
484
|
*/
|
|
464
|
-
export async function classifyPublishedFileAsTest(relPath) {
|
|
485
|
+
export async function classifyPublishedFileAsTest(relPath, cwd = process.cwd()) {
|
|
465
486
|
const segments = relPath.split('/')
|
|
466
487
|
const base = segments.at(-1)
|
|
467
488
|
const dirs = segments.slice(0, -1)
|
|
@@ -474,7 +495,7 @@ export async function classifyPublishedFileAsTest(relPath) {
|
|
|
474
495
|
if (testDir) return `test-style каталог "${testDir}/"`
|
|
475
496
|
if (TEST_FILE_PATTERNS.some(re => re.test(base))) return `test-style ім'я файлу`
|
|
476
497
|
if (JS_LIKE_EXT_RE.test(base)) {
|
|
477
|
-
const content = await readFile(join('npm', relPath), 'utf8')
|
|
498
|
+
const content = await readFile(join(cwd, 'npm', relPath), 'utf8')
|
|
478
499
|
const mod = findTestFrameworkImport(content, relPath)
|
|
479
500
|
if (mod) return `імпорт test-фреймворку "${mod}"`
|
|
480
501
|
}
|
|
@@ -488,16 +509,17 @@ export async function classifyPublishedFileAsTest(relPath) {
|
|
|
488
509
|
* @param {(msg: string) => void} pass callback при успіху
|
|
489
510
|
* @param {(msg: string) => void} fail callback при порушенні
|
|
490
511
|
* @returns {Promise<void>}
|
|
512
|
+
* @param {string} cwd корінь репозиторію
|
|
491
513
|
*/
|
|
492
|
-
async function checkNoTestsInPublishedFiles(pass, fail) {
|
|
493
|
-
if (!existsSync('npm/package.json')) return
|
|
494
|
-
const pkg = JSON.parse(await readFile('npm/package.json', 'utf8'))
|
|
514
|
+
async function checkNoTestsInPublishedFiles(pass, fail, cwd) {
|
|
515
|
+
if (!existsSync(join(cwd, 'npm/package.json'))) return
|
|
516
|
+
const pkg = JSON.parse(await readFile(join(cwd, 'npm/package.json'), 'utf8'))
|
|
495
517
|
if (!Array.isArray(pkg.files)) return
|
|
496
|
-
const files = await collectPublishedFiles(pkg.files)
|
|
518
|
+
const files = await collectPublishedFiles(pkg.files, cwd)
|
|
497
519
|
/** @type {{ file: string, reason: string }[]} */
|
|
498
520
|
const violations = []
|
|
499
521
|
for (const rel of files) {
|
|
500
|
-
const reason = await classifyPublishedFileAsTest(rel)
|
|
522
|
+
const reason = await classifyPublishedFileAsTest(rel, cwd)
|
|
501
523
|
if (reason) violations.push({ file: rel, reason })
|
|
502
524
|
}
|
|
503
525
|
if (violations.length === 0) {
|
|
@@ -518,16 +540,18 @@ async function checkNoTestsInPublishedFiles(pass, fail) {
|
|
|
518
540
|
* валідує `npm/policy/npm_module/root_package_json/`.
|
|
519
541
|
* @param {(msg: string) => void} pass callback при успішній перевірці
|
|
520
542
|
* @param {(msg: string) => void} fail callback при помилці
|
|
543
|
+
* @param {string} cwd корінь репозиторію
|
|
521
544
|
*/
|
|
522
|
-
async function checkNpmModuleBasicStructure(pass, fail) {
|
|
523
|
-
if (existsSync('package.json')) {
|
|
545
|
+
async function checkNpmModuleBasicStructure(pass, fail, cwd) {
|
|
546
|
+
if (existsSync(join(cwd, 'package.json'))) {
|
|
524
547
|
pass('package.json існує')
|
|
525
548
|
} else {
|
|
526
549
|
fail('package.json не існує')
|
|
527
550
|
}
|
|
528
551
|
|
|
529
|
-
|
|
530
|
-
|
|
552
|
+
const npmDir = join(cwd, 'npm')
|
|
553
|
+
if (existsSync(npmDir)) {
|
|
554
|
+
const s = await stat(npmDir)
|
|
531
555
|
if (s.isDirectory()) {
|
|
532
556
|
pass('npm/ директорія існує')
|
|
533
557
|
} else {
|
|
@@ -537,7 +561,7 @@ async function checkNpmModuleBasicStructure(pass, fail) {
|
|
|
537
561
|
fail('npm/ директорія не існує')
|
|
538
562
|
}
|
|
539
563
|
|
|
540
|
-
if (existsSync('npm/package.json')) {
|
|
564
|
+
if (existsSync(join(cwd, 'npm/package.json'))) {
|
|
541
565
|
pass('npm/package.json існує')
|
|
542
566
|
} else {
|
|
543
567
|
fail('npm/package.json не існує — створи package.json для npm модуля')
|
|
@@ -546,26 +570,27 @@ async function checkNpmModuleBasicStructure(pass, fail) {
|
|
|
546
570
|
|
|
547
571
|
/**
|
|
548
572
|
* Перевіряє відповідність проєкту правилам npm-module.mdc
|
|
573
|
+
* @param {string} [cwd] корінь репозиторію
|
|
549
574
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
550
575
|
*/
|
|
551
|
-
export async function check() {
|
|
576
|
+
export async function check(cwd = process.cwd()) {
|
|
552
577
|
const reporter = createCheckReporter()
|
|
553
578
|
const { pass, fail } = reporter
|
|
554
579
|
|
|
555
|
-
await checkNpmModuleBasicStructure(pass, fail)
|
|
556
|
-
await checkNoTestsInPublishedFiles(pass, fail)
|
|
580
|
+
await checkNpmModuleBasicStructure(pass, fail, cwd)
|
|
581
|
+
await checkNoTestsInPublishedFiles(pass, fail, cwd)
|
|
557
582
|
|
|
558
|
-
const ignorePaths = await loadCursorIgnorePaths(
|
|
559
|
-
const useSrcJsLayout = await npmSrcTreeHasJsFile(ignorePaths)
|
|
583
|
+
const ignorePaths = await loadCursorIgnorePaths(cwd)
|
|
584
|
+
const useSrcJsLayout = await npmSrcTreeHasJsFile(cwd, ignorePaths)
|
|
560
585
|
|
|
561
|
-
await checkNpmPackageJson(useSrcJsLayout, pass, fail)
|
|
586
|
+
await checkNpmPackageJson(useSrcJsLayout, pass, fail, cwd)
|
|
562
587
|
|
|
563
588
|
if (!useSrcJsLayout) {
|
|
564
|
-
await checkEmitTypesConfig(pass, fail)
|
|
589
|
+
await checkEmitTypesConfig(pass, fail, cwd)
|
|
565
590
|
}
|
|
566
591
|
|
|
567
592
|
const layoutLabel = useSrcJsLayout ? 'layout src' : 'tsconfig emit-types'
|
|
568
|
-
const hk = await readHkConfig()
|
|
593
|
+
const hk = await readHkConfig(cwd)
|
|
569
594
|
if (hk) {
|
|
570
595
|
pass(`${hk.path} існує`)
|
|
571
596
|
const missing = useSrcJsLayout ? missingHkSrcLayoutFragments(hk.text) : missingHkEmitTypesConfigFragments(hk.text)
|
|
@@ -578,16 +603,16 @@ export async function check() {
|
|
|
578
603
|
fail('Очікується hk.pkl або .config/hk.pkl з pre-commit і tsc (npm-module.mdc)')
|
|
579
604
|
}
|
|
580
605
|
|
|
581
|
-
if (existsSync('.github/workflows')) {
|
|
606
|
+
if (existsSync(join(cwd, '.github/workflows'))) {
|
|
582
607
|
pass('.github/workflows/ існує')
|
|
583
608
|
} else {
|
|
584
609
|
fail('.github/workflows/ не існує')
|
|
585
610
|
}
|
|
586
611
|
|
|
587
|
-
await checkPublishWorkflow(pass, fail)
|
|
612
|
+
await checkPublishWorkflow(pass, fail, cwd)
|
|
588
613
|
|
|
589
|
-
await checkChangelogTopMatchesPackageVersion(pass, fail)
|
|
590
|
-
await checkDirtyNpmRequiresVersionBump(pass, fail)
|
|
614
|
+
await checkChangelogTopMatchesPackageVersion(pass, fail, cwd)
|
|
615
|
+
await checkDirtyNpmRequiresVersionBump(pass, fail, cwd)
|
|
591
616
|
|
|
592
617
|
return reporter.getExitCode()
|
|
593
618
|
}
|
|
@@ -35,12 +35,12 @@ async function projectHasRegoFiles(root, ignorePaths) {
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Rule-level applies-гейт: CLI пропускає правило, якщо в репо немає `.rego` файлів.
|
|
38
|
+
* @param {string} [cwd] корінь репозиторію
|
|
38
39
|
* @returns {Promise<boolean>} `true`, якщо правило застосовне
|
|
39
40
|
*/
|
|
40
|
-
export async function applies() {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
return projectHasRegoFiles(root, ignorePaths)
|
|
41
|
+
export async function applies(cwd = process.cwd()) {
|
|
42
|
+
const ignorePaths = await loadCursorIgnorePaths(cwd)
|
|
43
|
+
return projectHasRegoFiles(cwd, ignorePaths)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* `check()` друкує тільки context-pass; реальна робота — у policy-концернах.
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
9
10
|
|
|
10
11
|
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
11
12
|
|
|
@@ -14,11 +15,12 @@ import { hasCargoTomlInTree } from '../lib/has-cargo-toml.mjs'
|
|
|
14
15
|
const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo'])
|
|
15
16
|
|
|
16
17
|
/**
|
|
18
|
+
* @param {string} [cwd] корінь репозиторію
|
|
17
19
|
* @returns {Promise<boolean>} `true` — правило застосовне; `false` — пропустити
|
|
18
20
|
*/
|
|
19
|
-
export function applies() {
|
|
20
|
-
if (existsSync('Cargo.toml')) return Promise.resolve(true)
|
|
21
|
-
return Promise.resolve(hasCargoTomlInTree(
|
|
21
|
+
export function applies(cwd = process.cwd()) {
|
|
22
|
+
if (existsSync(join(cwd, 'Cargo.toml'))) return Promise.resolve(true)
|
|
23
|
+
return Promise.resolve(hasCargoTomlInTree(cwd, IGNORED_DIR_NAMES))
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -61,12 +61,12 @@ function isExampleFile(relPosix) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
+
* @param {string} [cwd] корінь репозиторію
|
|
64
65
|
* @returns {Promise<number>} exit-код перевірки (0 — OK, 1 — є bare `secret`)
|
|
65
66
|
*/
|
|
66
|
-
export async function check() {
|
|
67
|
+
export async function check(cwd = process.cwd()) {
|
|
67
68
|
const reporter = createCheckReporter()
|
|
68
69
|
const { pass, fail } = reporter
|
|
69
|
-
const cwd = process.cwd()
|
|
70
70
|
|
|
71
71
|
/** @type {Array<{ abs: string, rel: string }>} */
|
|
72
72
|
const examples = []
|
|
@@ -18,24 +18,26 @@ const HERE = dirname(fileURLToPath(import.meta.url))
|
|
|
18
18
|
const SNIPPET_PATH = join(HERE, 'templates', 'trufflehog', '.trufflehog-exclude.snippet.txt')
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
+
* @param {string} [cwd] корінь репозиторію
|
|
21
22
|
* @returns {Promise<number>} exit-код перевірки
|
|
22
23
|
*/
|
|
23
|
-
export async function check() {
|
|
24
|
+
export async function check(cwd = process.cwd()) {
|
|
24
25
|
const reporter = createCheckReporter()
|
|
25
26
|
const { pass, fail } = reporter
|
|
26
27
|
|
|
27
|
-
if (!existsSync('package.json')) {
|
|
28
|
+
if (!existsSync(join(cwd, 'package.json'))) {
|
|
28
29
|
fail('package.json не знайдено в корені — додай (security.mdc)')
|
|
29
30
|
return reporter.getExitCode()
|
|
30
31
|
}
|
|
31
32
|
pass('package.json є (структуру перевіряє Rego)')
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
const trufflePath = join(cwd, '.trufflehog-exclude')
|
|
35
|
+
if (!existsSync(trufflePath)) {
|
|
34
36
|
fail('.trufflehog-exclude не знайдено в корені — додай за каноном (security.mdc)')
|
|
35
37
|
return reporter.getExitCode()
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
const actual = await readFile(
|
|
40
|
+
const actual = await readFile(trufflePath, 'utf8')
|
|
39
41
|
const template = await readFile(SNIPPET_PATH, 'utf8')
|
|
40
42
|
const errors = checkTextSubset(actual, template, {
|
|
41
43
|
targetPath: '.trufflehog-exclude',
|
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
* у `recommendations` `.vscode/extensions.json`;
|
|
16
16
|
* - `npm/policy/style_lint/vscode_settings/` — `css.validate`/`scss.validate`/
|
|
17
17
|
* `less.validate: false` у `.vscode/settings.json`.
|
|
18
|
+
* @param {string} cwd корінь репозиторію
|
|
18
19
|
*/
|
|
19
20
|
import { existsSync } from 'node:fs'
|
|
20
21
|
import { readFile } from 'node:fs/promises'
|
|
22
|
+
import { join } from 'node:path'
|
|
21
23
|
|
|
22
24
|
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
23
25
|
|
|
@@ -26,14 +28,18 @@ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
|
26
28
|
* поля немає і файлу немає, фейлимося; якщо є хоч щось — пропускаємо. Поле
|
|
27
29
|
* `stylelint.extends == "@nitra/stylelint-config"` сам формат — у Rego.
|
|
28
30
|
* @param {import('../../../scripts/lib/check-reporter.mjs').CheckReporter} reporter репортер
|
|
31
|
+
* @param {string} cwd корінь репозиторію
|
|
29
32
|
*/
|
|
30
|
-
async function checkStylelintConfigPresence(reporter) {
|
|
33
|
+
async function checkStylelintConfigPresence(reporter, cwd) {
|
|
31
34
|
const { pass, fail } = reporter
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
const pkgPath = join(cwd, 'package.json')
|
|
36
|
+
if (!existsSync(pkgPath)) return
|
|
37
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
34
38
|
const hasField = pkg.stylelint && typeof pkg.stylelint === 'object'
|
|
35
39
|
const hasExternalCfg =
|
|
36
|
-
existsSync('.stylelintrc.json')
|
|
40
|
+
existsSync(join(cwd, '.stylelintrc.json')) ||
|
|
41
|
+
existsSync(join(cwd, '.stylelintrc.js')) ||
|
|
42
|
+
existsSync(join(cwd, 'stylelint.config.js'))
|
|
37
43
|
if (hasField || hasExternalCfg) {
|
|
38
44
|
pass('Конфіг stylelint є — у package.json або окремим файлом')
|
|
39
45
|
} else {
|
|
@@ -48,22 +54,23 @@ async function checkStylelintConfigPresence(reporter) {
|
|
|
48
54
|
|
|
49
55
|
/**
|
|
50
56
|
* Перевіряє відповідність проєкту правилам style-lint.mdc
|
|
57
|
+
* @param {string} [cwd] корінь репозиторію
|
|
51
58
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
52
59
|
*/
|
|
53
|
-
export async function check() {
|
|
60
|
+
export async function check(cwd = process.cwd()) {
|
|
54
61
|
const reporter = createCheckReporter()
|
|
55
62
|
const { pass, fail } = reporter
|
|
56
63
|
|
|
57
|
-
await checkStylelintConfigPresence(reporter)
|
|
64
|
+
await checkStylelintConfigPresence(reporter, cwd)
|
|
58
65
|
|
|
59
|
-
if (existsSync('.stylelintignore')) {
|
|
66
|
+
if (existsSync(join(cwd, '.stylelintignore'))) {
|
|
60
67
|
pass('.stylelintignore існує')
|
|
61
68
|
} else {
|
|
62
69
|
fail('.stylelintignore не існує — створи з вмістом: dist/')
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
const wfPath = '.github/workflows/lint-style.yml'
|
|
66
|
-
if (existsSync(wfPath)) {
|
|
73
|
+
if (existsSync(join(cwd, wfPath))) {
|
|
67
74
|
pass(`${wfPath} є (структуру перевіряє npx @nitra/cursor fix → style_lint.lint_style_yml)`)
|
|
68
75
|
} else {
|
|
69
76
|
fail(`${wfPath} не існує — створи його`)
|
|
@@ -189,7 +189,7 @@ export async function runCoverageSteps(opts = {}) {
|
|
|
189
189
|
if (opts.fix) {
|
|
190
190
|
const allSurvived = rows.flatMap(r => r.survived ?? [])
|
|
191
191
|
// eslint-disable-next-line no-unsanitized/method -- шлях відносний до пакету, не user-input
|
|
192
|
-
const { fixSurvivedMutants } = await import(new URL('
|
|
192
|
+
const { fixSurvivedMutants } = await import(new URL('../../../scripts/coverage-fix.mjs', import.meta.url).href)
|
|
193
193
|
await fixSurvivedMutants(allSurvived, cwd)
|
|
194
194
|
}
|
|
195
195
|
|
|
@@ -6,6 +6,13 @@ export default defineConfig({
|
|
|
6
6
|
// у піддиректоріях `tests/`) і top-level integration suites у `<root>/tests/`.
|
|
7
7
|
include: ['**/*.test.{js,mjs}', 'tests/**/*.test.{js,mjs}'],
|
|
8
8
|
environment: 'node',
|
|
9
|
+
// `pool: 'forks'` — defense-in-depth ізоляція процесів між test-файлами.
|
|
10
|
+
// У default `pool: 'threads'` усі workers ділять один процес → паралельний
|
|
11
|
+
// `process.chdir(dir)` у тестовій фікстурі перехоплює cwd сусіда посеред
|
|
12
|
+
// FS- або `git`-операції. Реальний інцидент: `git init`+`git commit` із
|
|
13
|
+
// tmp-фікстури потрапив у реальний робочий репозиторій. Forks гарантують
|
|
14
|
+
// ізоляцію. Канон тестів — `withTmpDir(async dir => ...)` (test.mdc).
|
|
15
|
+
pool: 'forks',
|
|
9
16
|
coverage: { provider: 'v8', reporter: ['lcov', 'text-summary'] }
|
|
10
17
|
}
|
|
11
18
|
})
|
|
@@ -35,13 +35,14 @@ function isInsideTestsDir(absPath) {
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Перевіряє розміщення тестових файлів у каталозі `tests/` (test.mdc).
|
|
38
|
+
* @param {string} [cwdParam] корінь репозиторію
|
|
38
39
|
* @returns {Promise<number>} 0 — всі тести у `tests/`, 1 — є порушення
|
|
39
40
|
*/
|
|
40
|
-
export async function check() {
|
|
41
|
+
export async function check(cwdParam = process.cwd()) {
|
|
41
42
|
const reporter = createCheckReporter()
|
|
42
43
|
const { pass, fail } = reporter
|
|
43
44
|
|
|
44
|
-
const cwd =
|
|
45
|
+
const cwd = cwdParam
|
|
45
46
|
const ignorePaths = await loadCursorIgnorePaths(cwd)
|
|
46
47
|
|
|
47
48
|
/** @type {string[]} */
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Заборона `process.chdir(...)` у тестах.
|
|
3
|
+
*
|
|
4
|
+
* Контекст (test.mdc, секція "Заборона `process.chdir` у тестах"):
|
|
5
|
+
* `process.chdir` — process-wide мутація. Vitest за замовчуванням ставить
|
|
6
|
+
* `pool: 'threads'`, де workers ділять один процес: паралельний test file
|
|
7
|
+
* може перехопити cwd сусіда посеред FS- або `git`-операції. Реальний
|
|
8
|
+
* інцидент — `git init`+`git commit` із tmp-фікстури `withTmpCwd` потрапив у
|
|
9
|
+
* реальний робочий репозиторій і створив rogue commit з автором `test
|
|
10
|
+
* <test@test>`. Тому канон: `withTmpDir(async dir => ...)` зі
|
|
11
|
+
* `scripts/utils/test-helpers.mjs` (без `chdir`) + явні `cwd: dir` у child-
|
|
12
|
+
* процесах + `await check(dir)` для concern-функцій.
|
|
13
|
+
*
|
|
14
|
+
* Цей concern сканує `**\/*.test.{js,mjs}` і падає на будь-яке вживання
|
|
15
|
+
* `process.chdir(`. Виняток — коментарі/документація: regex знаходить лише
|
|
16
|
+
* викликний паттерн (відкривна дужка), тож згадки у JSDoc типу
|
|
17
|
+
* "не використовуй `process.chdir`" не тригерять.
|
|
18
|
+
*
|
|
19
|
+
* Скіпи: `node_modules`, `.git`, `dist`, `build`, `.venv`, `venv` (через
|
|
20
|
+
* `walkDir`) і шляхи з `.n-cursor.json:ignore`.
|
|
21
|
+
*/
|
|
22
|
+
import { readFile } from 'node:fs/promises'
|
|
23
|
+
import { basename, relative } from 'node:path'
|
|
24
|
+
|
|
25
|
+
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
26
|
+
import { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.mjs'
|
|
27
|
+
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
28
|
+
|
|
29
|
+
/** Шукаємо викликний паттерн з відкривною дужкою — не зачепить згадку у docstring. */
|
|
30
|
+
const CHDIR_CALL_RE = /process\.chdir\s*\(/u
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Чи файл — JS-тест (`*.test.mjs` або `*.test.js`).
|
|
34
|
+
* @param {string} absPath абсолютний шлях
|
|
35
|
+
* @returns {boolean} `true` для імен з `.test.{mjs,js}` суфіксом
|
|
36
|
+
*/
|
|
37
|
+
function isTestFile(absPath) {
|
|
38
|
+
const name = basename(absPath)
|
|
39
|
+
return name.endsWith('.test.mjs') || name.endsWith('.test.js')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Перевіряє, що жоден `*.test.{mjs,js}` файл не викликає `process.chdir(`.
|
|
44
|
+
* @param {string} [cwdParam] корінь репозиторію
|
|
45
|
+
* @returns {Promise<number>} 0 — чисто, 1 — знайдено `process.chdir(` у тесті
|
|
46
|
+
*/
|
|
47
|
+
export async function check(cwdParam = process.cwd()) {
|
|
48
|
+
const reporter = createCheckReporter()
|
|
49
|
+
const { pass, fail } = reporter
|
|
50
|
+
|
|
51
|
+
const cwd = cwdParam
|
|
52
|
+
const ignorePaths = await loadCursorIgnorePaths(cwd)
|
|
53
|
+
|
|
54
|
+
/** @type {string[]} */
|
|
55
|
+
const testFiles = []
|
|
56
|
+
await walkDir(
|
|
57
|
+
cwd,
|
|
58
|
+
absPath => {
|
|
59
|
+
if (isTestFile(absPath)) testFiles.push(absPath)
|
|
60
|
+
},
|
|
61
|
+
ignorePaths
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
/** @type {Array<{file: string, line: number}>} */
|
|
65
|
+
const offenders = []
|
|
66
|
+
for (const absPath of testFiles) {
|
|
67
|
+
const body = await readFile(absPath, 'utf8')
|
|
68
|
+
if (!CHDIR_CALL_RE.test(body)) continue
|
|
69
|
+
const lines = body.split('\n')
|
|
70
|
+
for (const [i, line] of lines.entries()) {
|
|
71
|
+
if (CHDIR_CALL_RE.test(line)) {
|
|
72
|
+
offenders.push({ file: relative(cwd, absPath), line: i + 1 })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (offenders.length === 0) {
|
|
78
|
+
pass(`Жоден з ${testFiles.length} тестових файлів не викликає process.chdir() (test.mdc)`)
|
|
79
|
+
return reporter.getExitCode()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const { file, line } of offenders) {
|
|
83
|
+
fail(
|
|
84
|
+
`${file}:${line}: process.chdir() у тесті заборонений — використовуй withTmpDir(async dir => …) + явні join(dir, …) + cwd: dir (test.mdc)`
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return reporter.getExitCode()
|
|
89
|
+
}
|