@nitra/cursor 1.17.1 → 1.17.3
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 +30 -0
- package/package.json +1 -1
- package/rules/js-lint/coverage/coverage.mjs +6 -18
- package/rules/rust/coverage/coverage.mjs +4 -24
- package/rules/test/js/cargo_mutants_config.mjs +65 -0
- package/rules/test/js/data/cargo_mutants_config/mutants.toml.baseline +4 -0
- package/rules/test/js/data/stryker_config/stryker.config.baseline.mjs +12 -0
- package/rules/test/js/stryker_config.mjs +75 -0
- package/rules/test/test.mdc +18 -3
- package/scripts/utils/ensure-gitignore-entries.mjs +30 -0
- package/scripts/utils/resolve-cargo-manifest.mjs +62 -0
- package/scripts/utils/resolve-js-root.mjs +46 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.17.3] - 2026-05-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Концерн `stryker_config` правила `test` тепер ідемпотентно додає у кореневий `.gitignore` патерни Stryker-output-у:
|
|
12
|
+
- `**/reports/stryker/.tmp/` — in-place backup-каталог (з baseline-у `tempDirName`).
|
|
13
|
+
- `**/reports/stryker/mutation.json` — JSON-репорт мутацій.
|
|
14
|
+
- Header-секція `# Stryker mutation testing (test.mdc)`, sectioning через `ensureGitignoreEntries`.
|
|
15
|
+
- Спільний helper `npm/scripts/utils/ensure-gitignore-entries.mjs` — append-only оновлювач `.gitignore` з header-секціями. Idempotent (точне співпадіння рядка після `trim`), створює файл якщо немає, зберігає trailing-newline. 5 unit-тестів.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `test.mdc` 2.0 → 2.1: додано параграф про gitignore-керування Stryker-output-у в секцію «Налаштування mutation-testing».
|
|
20
|
+
- `stryker_config` concern: додано виклик `ensureGitignoreEntries` після копіювання baseline-ів; репортер видає pass-повідомлення про додані патерни.
|
|
21
|
+
|
|
22
|
+
## [1.17.2] - 2026-05-24
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Правило `test`: два нових концерни — `stryker_config` і `cargo_mutants_config`. Self-gating через `.n-cursor.json#rules`: концерн активний лише якщо відповідне залежне правило (`js-lint` / `rust`) enabled. **Iterate-all-workspaces**: при відсутності цільового файлу копіює canonical baseline у КОЖЕН workspace-каталог (не лише workspaces[0]).
|
|
27
|
+
- `stryker.config.mjs` у кожному JS-root (всі workspaces з package.json, або cwd у single-package) — мінімум для роботи з `bun test`.
|
|
28
|
+
- `.cargo/mutants.toml` у каталозі КОЖНОГО Cargo.toml-маніфесту: корінь + workspaces (з підтримкою Tauri-патерну `<ws>/src-tauri/Cargo.toml`) — комент-плейсхолдер; cargo-mutants має робочі defaults.
|
|
29
|
+
- Спільні резолвери у `npm/scripts/utils/`: `resolveJsRoot` (single, для coverage-провайдера) + `resolveAllJsRoots` (plural, для test-концерну); `resolveCargoManifest` (single) + `resolveAllCargoManifests` (plural). Coverage-провайдери js-lint і rust реюзають single-варіанти.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `test.mdc` 1.2 → 2.0 (major): `alwaysApply: true → false`; явні `globs` (`.n-cursor.json`, `package.json`, `Cargo.toml`, mutation-config-цілі, `*.test.mjs`). Нова секція «Налаштування mutation-testing» з посиланнями на baselines.
|
|
34
|
+
- `js-lint/coverage/coverage.mjs`: hint при missing `mutation.json` тепер вказує на `npx @nitra/cursor fix test`. `resolveJsRoot` витягнуто у спільний модуль.
|
|
35
|
+
- `rust/coverage/coverage.mjs`: `resolveCargoManifest` витягнуто у спільний модуль (контракт `null` замість throw для missing manifest; user-facing throw зберігся на callsite).
|
|
36
|
+
|
|
7
37
|
## [1.17.1] - 2026-05-24
|
|
8
38
|
|
|
9
39
|
### Fixed
|
package/package.json
CHANGED
|
@@ -11,23 +11,7 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
|
11
11
|
import { tmpdir } from 'node:os'
|
|
12
12
|
import { join } from 'node:path'
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
* Резолвить cwd, у якому стоять JS-тести. Workspace-проєкти — перший workspace
|
|
16
|
-
* (наприклад: app/), single-package — корінь.
|
|
17
|
-
* @param {string} cwd корінь проєкту
|
|
18
|
-
* @returns {Promise<string|null>} абсолютний шлях до JS-root або null без package.json
|
|
19
|
-
*/
|
|
20
|
-
async function resolveJsRoot(cwd) {
|
|
21
|
-
const rootPkgPath = join(cwd, 'package.json')
|
|
22
|
-
if (!existsSync(rootPkgPath)) return null
|
|
23
|
-
const rootPkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
|
|
24
|
-
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : []
|
|
25
|
-
if (workspaces.length > 0) {
|
|
26
|
-
const wsPath = join(cwd, workspaces[0])
|
|
27
|
-
if (existsSync(join(wsPath, 'package.json'))) return wsPath
|
|
28
|
-
}
|
|
29
|
-
return cwd
|
|
30
|
-
}
|
|
14
|
+
import { resolveJsRoot } from '../../../scripts/utils/resolve-js-root.mjs'
|
|
31
15
|
|
|
32
16
|
/**
|
|
33
17
|
* Чи `scripts` містить coverage-сумісну команду.
|
|
@@ -140,7 +124,11 @@ export async function collect(cwd, opts = {}) {
|
|
|
140
124
|
try {
|
|
141
125
|
mutationReport = JSON.parse(await readFile(join(jsRoot, 'reports', 'stryker', 'mutation.json'), 'utf8'))
|
|
142
126
|
} catch {
|
|
143
|
-
throw new Error(
|
|
127
|
+
throw new Error(
|
|
128
|
+
'js-lint coverage: stryker не залишив mutation.json — ' +
|
|
129
|
+
'запусти `npx @nitra/cursor fix test` для встановлення canonical stryker.config.mjs, ' +
|
|
130
|
+
'або налаштуй його вручну'
|
|
131
|
+
)
|
|
144
132
|
}
|
|
145
133
|
const mutation = parseStrykerReport(mutationReport)
|
|
146
134
|
|
|
@@ -13,6 +13,7 @@ import { tmpdir } from 'node:os'
|
|
|
13
13
|
import { join } from 'node:path'
|
|
14
14
|
|
|
15
15
|
import { hasCargoTomlInTree } from '../lib/has-cargo-toml.mjs'
|
|
16
|
+
import { resolveCargoManifest } from '../../../scripts/utils/resolve-cargo-manifest.mjs'
|
|
16
17
|
|
|
17
18
|
const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo', 'target'])
|
|
18
19
|
|
|
@@ -26,30 +27,6 @@ export function detect(cwd) {
|
|
|
26
27
|
return Promise.resolve(hasCargoTomlInTree(cwd, IGNORED_DIR_NAMES))
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
/**
|
|
30
|
-
* Знайти Cargo.toml: cwd/Cargo.toml або в одному з workspace-підкаталогів.
|
|
31
|
-
* @param {string} cwd корінь проєкту
|
|
32
|
-
* @returns {Promise<string>} абсолютний шлях до Cargo.toml
|
|
33
|
-
*/
|
|
34
|
-
async function resolveCargoManifest(cwd) {
|
|
35
|
-
const rootManifest = join(cwd, 'Cargo.toml')
|
|
36
|
-
if (existsSync(rootManifest)) return rootManifest
|
|
37
|
-
|
|
38
|
-
const rootPkgPath = join(cwd, 'package.json')
|
|
39
|
-
if (existsSync(rootPkgPath)) {
|
|
40
|
-
const rootPkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
|
|
41
|
-
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : []
|
|
42
|
-
for (const ws of workspaces) {
|
|
43
|
-
const tauriManifest = join(cwd, ws, 'src-tauri', 'Cargo.toml')
|
|
44
|
-
if (existsSync(tauriManifest)) return tauriManifest
|
|
45
|
-
const flatManifest = join(cwd, ws, 'Cargo.toml')
|
|
46
|
-
if (existsSync(flatManifest)) return flatManifest
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
throw new Error('rust coverage: Cargo.toml не знайдено (cwd + workspaces)')
|
|
51
|
-
}
|
|
52
|
-
|
|
53
30
|
const defaultRunner = {
|
|
54
31
|
runLlvmCov({ manifestPath }) {
|
|
55
32
|
const r = spawnSync('cargo', ['llvm-cov', '--manifest-path', manifestPath, '--json', '--summary-only'], {
|
|
@@ -76,6 +53,9 @@ const defaultRunner = {
|
|
|
76
53
|
export async function collect(cwd, opts = {}) {
|
|
77
54
|
const runner = opts.runner ?? defaultRunner
|
|
78
55
|
const manifestPath = await resolveCargoManifest(cwd)
|
|
56
|
+
if (manifestPath === null) {
|
|
57
|
+
throw new Error('rust coverage: Cargo.toml не знайдено (cwd + workspaces)')
|
|
58
|
+
}
|
|
79
59
|
|
|
80
60
|
// 1. Coverage через cargo llvm-cov
|
|
81
61
|
const { exitCode: llvmCode, stdout: llvmJson } = await runner.runLlvmCov({ manifestPath })
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Концерн `cargo_mutants_config` правила test (test.mdc): якщо `rust` присутнє
|
|
3
|
+
* в `.n-cursor.json#rules` і не у `disable-rules` — резолвить ВСІ Cargo.toml
|
|
4
|
+
* (cwd і всі workspaces, з підтримкою Tauri-патерну) і копіює canonical
|
|
5
|
+
* baseline `.cargo/mutants.toml` у каталог кожного manifest'а, якщо файлу немає.
|
|
6
|
+
*
|
|
7
|
+
* Self-gating: концерн silently skips коли `rust` не enabled.
|
|
8
|
+
* Якщо `rust` enabled, але жодного Cargo.toml не знайдено — теж silently skip
|
|
9
|
+
* (manifest може з'явитися пізніше; це не помилка).
|
|
10
|
+
*
|
|
11
|
+
* Baseline — порожній файл з коментом; cargo-mutants має робочі defaults.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from 'node:fs'
|
|
14
|
+
import { copyFile, mkdir } from 'node:fs/promises'
|
|
15
|
+
import { dirname, join, relative } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
|
|
18
|
+
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
19
|
+
import { readNCursorConfigLite } from '../../../scripts/lib/read-n-cursor-config-lite.mjs'
|
|
20
|
+
import { resolveAllCargoManifests } from '../../../scripts/utils/resolve-cargo-manifest.mjs'
|
|
21
|
+
|
|
22
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
23
|
+
const BASELINE_PATH = join(HERE, 'data', 'cargo_mutants_config', 'mutants.toml.baseline')
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @returns {Promise<number>} 0 — OK або silently skipped, 1 — порушення
|
|
27
|
+
*/
|
|
28
|
+
export async function check() {
|
|
29
|
+
const reporter = createCheckReporter()
|
|
30
|
+
const cwd = process.cwd()
|
|
31
|
+
const config = await readNCursorConfigLite(cwd)
|
|
32
|
+
|
|
33
|
+
// Self-gate: rust має бути enabled
|
|
34
|
+
if (!config.rules.includes('rust') || config.disableRules.includes('rust')) {
|
|
35
|
+
return reporter.getExitCode()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const manifests = await resolveAllCargoManifests(cwd)
|
|
39
|
+
if (manifests.length === 0) {
|
|
40
|
+
// rust enabled, але Cargo.toml ще немає — silently skip (manifest може з'явитися пізніше)
|
|
41
|
+
return reporter.getExitCode()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!existsSync(BASELINE_PATH)) {
|
|
45
|
+
reporter.fail(
|
|
46
|
+
`.cargo/mutants.toml canonical baseline не знайдено (${BASELINE_PATH}) — перевстанови @nitra/cursor`
|
|
47
|
+
)
|
|
48
|
+
return reporter.getExitCode()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const manifestPath of manifests) {
|
|
52
|
+
const cargoDir = dirname(manifestPath)
|
|
53
|
+
const target = join(cargoDir, '.cargo', 'mutants.toml')
|
|
54
|
+
|
|
55
|
+
if (existsSync(target)) {
|
|
56
|
+
reporter.pass(`.cargo/mutants.toml існує (${relative(cwd, target)})`)
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await mkdir(dirname(target), { recursive: true })
|
|
61
|
+
await copyFile(BASELINE_PATH, target)
|
|
62
|
+
reporter.pass(`.cargo/mutants.toml створено з canonical baseline (${relative(cwd, target)}) (test.mdc)`)
|
|
63
|
+
}
|
|
64
|
+
return reporter.getExitCode()
|
|
65
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** @type {import('@stryker-mutator/core').PartialStrykerOptions} */
|
|
2
|
+
export default {
|
|
3
|
+
testRunner: 'command',
|
|
4
|
+
commandRunner: { command: 'bun test' },
|
|
5
|
+
// inPlace: уникає hoisted-node_modules issues у Bun monorepo (sandbox-копія втрачає resolution).
|
|
6
|
+
// Також тести, що читають git/fs-state (integration checks), працюють тільки in-place.
|
|
7
|
+
inPlace: true,
|
|
8
|
+
tempDirName: 'reports/stryker/.tmp',
|
|
9
|
+
reporters: ['json', 'clear-text'],
|
|
10
|
+
jsonReporter: { fileName: 'reports/stryker/mutation.json' },
|
|
11
|
+
coverageAnalysis: 'off'
|
|
12
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Концерн `stryker_config` правила test (test.mdc): якщо `js-lint` присутнє в
|
|
3
|
+
* `.n-cursor.json#rules` і не у `disable-rules` — резолвить ВСІ JS-roots
|
|
4
|
+
* (всі workspaces з package.json, або cwd у single-package) і копіює canonical
|
|
5
|
+
* baseline `stryker.config.mjs` у кожен root, де файлу немає.
|
|
6
|
+
*
|
|
7
|
+
* Self-gating: концерн silently skips коли `js-lint` не enabled — це навмисно,
|
|
8
|
+
* щоб не шуміти у single-language проєктах без JS coverage tooling.
|
|
9
|
+
*
|
|
10
|
+
* Baseline — мінімум для запуску Stryker з bun test runner; mutate-патерни
|
|
11
|
+
* лишаються на Stryker defaults (`src/**\/*.{js,mjs,ts,jsx,tsx,cjs}`).
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from 'node:fs'
|
|
14
|
+
import { copyFile } from 'node:fs/promises'
|
|
15
|
+
import { dirname, join, relative } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
|
|
18
|
+
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
19
|
+
import { readNCursorConfigLite } from '../../../scripts/lib/read-n-cursor-config-lite.mjs'
|
|
20
|
+
import { ensureGitignoreEntries } from '../../../scripts/utils/ensure-gitignore-entries.mjs'
|
|
21
|
+
import { resolveAllJsRoots } from '../../../scripts/utils/resolve-js-root.mjs'
|
|
22
|
+
|
|
23
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
24
|
+
const BASELINE_PATH = join(HERE, 'data', 'stryker_config', 'stryker.config.baseline.mjs')
|
|
25
|
+
|
|
26
|
+
// Stryker-output патерни для .gitignore: temp-каталог з backup-файлами
|
|
27
|
+
// (`tempDirName: 'reports/stryker/.tmp'` у baseline) і JSON-репорт мутацій.
|
|
28
|
+
// Канон in-place mode у baseline лишає backup'и у `reports/stryker/.tmp/backup-XXX/`,
|
|
29
|
+
// які НЕ можна комітити. Подвійний-зірочка-префікс покриває всі workspaces.
|
|
30
|
+
const STRYKER_GITIGNORE_ENTRIES = ['**/reports/stryker/.tmp/', '**/reports/stryker/mutation.json']
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @returns {Promise<number>} 0 — OK або silently skipped, 1 — порушення
|
|
34
|
+
*/
|
|
35
|
+
export async function check() {
|
|
36
|
+
const reporter = createCheckReporter()
|
|
37
|
+
const cwd = process.cwd()
|
|
38
|
+
const config = await readNCursorConfigLite(cwd)
|
|
39
|
+
|
|
40
|
+
// Self-gate: js-lint має бути enabled
|
|
41
|
+
if (!config.rules.includes('js-lint') || config.disableRules.includes('js-lint')) {
|
|
42
|
+
return reporter.getExitCode()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const jsRoots = await resolveAllJsRoots(cwd)
|
|
46
|
+
if (jsRoots.length === 0) {
|
|
47
|
+
reporter.fail('test: js-lint enabled, але кореневий package.json не знайдено (test.mdc)')
|
|
48
|
+
return reporter.getExitCode()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!existsSync(BASELINE_PATH)) {
|
|
52
|
+
reporter.fail(
|
|
53
|
+
`stryker.config.mjs canonical baseline не знайдено (${BASELINE_PATH}) — перевстанови @nitra/cursor`
|
|
54
|
+
)
|
|
55
|
+
return reporter.getExitCode()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const jsRoot of jsRoots) {
|
|
59
|
+
const target = join(jsRoot, 'stryker.config.mjs')
|
|
60
|
+
if (existsSync(target)) {
|
|
61
|
+
reporter.pass(`stryker.config.mjs існує (${relative(cwd, target)})`)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
await copyFile(BASELINE_PATH, target)
|
|
65
|
+
reporter.pass(`stryker.config.mjs створено з canonical baseline (${relative(cwd, target)}) (test.mdc)`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Гарантуємо що Stryker temp/output ніколи не комітяться. Patterns
|
|
69
|
+
// покривають усі workspaces через `**/`-префікс (єдиний root .gitignore).
|
|
70
|
+
const { added } = await ensureGitignoreEntries(cwd, STRYKER_GITIGNORE_ENTRIES, 'Stryker mutation testing (test.mdc)')
|
|
71
|
+
if (added.length > 0) {
|
|
72
|
+
reporter.pass(`.gitignore: додано Stryker-патерни (${added.join(', ')}) (test.mdc)`)
|
|
73
|
+
}
|
|
74
|
+
return reporter.getExitCode()
|
|
75
|
+
}
|
package/rules/test/test.mdc
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: JS-тести (*.test.mjs) живуть у
|
|
3
|
-
version: '1
|
|
4
|
-
|
|
2
|
+
description: JS-тести (*.test.mjs) живуть у tests/. Правило `test` керує stryker.config.mjs (якщо js-lint enabled) і .cargo/mutants.toml (якщо rust enabled).
|
|
3
|
+
version: '2.1'
|
|
4
|
+
globs: "**/{.n-cursor.json,package.json,Cargo.toml,stryker.config.mjs,.cargo/mutants.toml},**/*.test.mjs"
|
|
5
|
+
alwaysApply: false
|
|
5
6
|
---
|
|
6
7
|
|
|
7
8
|
## Конвенція розміщення тестів
|
|
@@ -68,3 +69,17 @@ Recursive globs ловлять файли всередині `tests/` так с
|
|
|
68
69
|
У `package.json` (корінь) має бути `scripts.coverage` із викликом `n-cursor coverage`:
|
|
69
70
|
|
|
70
71
|
Канон `scripts.coverage` (substring requirement): [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
|
|
72
|
+
|
|
73
|
+
## Налаштування mutation-testing
|
|
74
|
+
|
|
75
|
+
Якщо у `.n-cursor.json#rules` присутнє правило `js-lint` — правило `test` створює canonical baseline `stryker.config.mjs` у **кожному** JS-root проєкту: у кожному workspace з власним `package.json` (або в корені для single-package). У monorepo з `workspaces: ['app', 'scripts']` отримаєте `app/stryker.config.mjs` і `scripts/stryker.config.mjs`.
|
|
76
|
+
|
|
77
|
+
Канон Stryker config (мінімум для роботи з `bun test`): [stryker.config.baseline.mjs](./js/data/stryker_config/stryker.config.baseline.mjs)
|
|
78
|
+
|
|
79
|
+
Аналогічно, якщо `rust` присутнє в `rules` — створюється `.cargo/mutants.toml` у каталозі **кожного** Cargo.toml-маніфесту: кореневий `Cargo.toml`, `<workspace>/src-tauri/Cargo.toml` (Tauri-патерн) і `<workspace>/Cargo.toml` (flat workspace).
|
|
80
|
+
|
|
81
|
+
Канон cargo-mutants config: [mutants.toml.baseline](./js/data/cargo_mutants_config/mutants.toml.baseline)
|
|
82
|
+
|
|
83
|
+
Customization (mutate patterns, exclude rules, timeout) — відповідальність проєкту-споживача; концерни лише забезпечують наявність файлу як стартового baseline в кожному з виявлених workspace-каталогів.
|
|
84
|
+
|
|
85
|
+
Додатково: коли `js-lint` enabled, концерн `stryker_config` ідемпотентно додає у кореневий `.gitignore` патерни Stryker-output-у — `**/reports/stryker/.tmp/` (in-place backup-каталог з baseline-у) і `**/reports/stryker/mutation.json` (JSON-репорт). Це запобігає випадковому коміту backup-копій вихідного коду та мутаційного звіту як build-артефактів.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent append-only оновлювач `.gitignore` у корені проєкту. Перевіряє,
|
|
3
|
+
* чи задані entries уже присутні (точне співпадіння рядка після `trim`); відсутні
|
|
4
|
+
* дописує під header-комент, не порушуючи решту файлу. Якщо `.gitignore` немає —
|
|
5
|
+
* створюється з заданими entries + header.
|
|
6
|
+
*
|
|
7
|
+
* Викликається з test-концерну `stryker_config` (gitignore Stryker temp dirs).
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} cwd корінь репо (де знаходиться `.gitignore`)
|
|
15
|
+
* @param {string[]} entries патерни для .gitignore (порядок збережено)
|
|
16
|
+
* @param {string} sectionLabel header-коментар над секцією (без `#`-префікса)
|
|
17
|
+
* @returns {Promise<{added: string[]}>} перелік патернів, що були дописані
|
|
18
|
+
*/
|
|
19
|
+
export async function ensureGitignoreEntries(cwd, entries, sectionLabel) {
|
|
20
|
+
const gitignorePath = join(cwd, '.gitignore')
|
|
21
|
+
const existing = existsSync(gitignorePath) ? await readFile(gitignorePath, 'utf8') : ''
|
|
22
|
+
const existingLines = new Set(existing.split('\n').map(line => line.trim()))
|
|
23
|
+
const missing = entries.filter(entry => !existingLines.has(entry))
|
|
24
|
+
if (missing.length === 0) return { added: [] }
|
|
25
|
+
|
|
26
|
+
const prefix = existing.length === 0 || existing.endsWith('\n') ? '' : '\n'
|
|
27
|
+
const block = `${prefix}\n# ${sectionLabel}\n${missing.join('\n')}\n`
|
|
28
|
+
await writeFile(gitignorePath, existing + block)
|
|
29
|
+
return { added: missing }
|
|
30
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Резолвить шлях до Cargo.toml у проєкті: cwd/Cargo.toml або в одному з
|
|
3
|
+
* workspace-підкаталогів (з підтримкою Tauri-патерну `<workspace>/src-tauri/`).
|
|
4
|
+
* Спільна утиліта для coverage-провайдера rust і test-концерну cargo_mutants_config.
|
|
5
|
+
* Повертає null (а не throw) щоб callsite-и могли gracefully skip-нути.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from 'node:fs'
|
|
8
|
+
import { readFile } from 'node:fs/promises'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} cwd корінь проєкту
|
|
13
|
+
* @returns {Promise<string|null>} абсолютний шлях до Cargo.toml або null
|
|
14
|
+
*/
|
|
15
|
+
export async function resolveCargoManifest(cwd) {
|
|
16
|
+
const rootManifest = join(cwd, 'Cargo.toml')
|
|
17
|
+
if (existsSync(rootManifest)) return rootManifest
|
|
18
|
+
|
|
19
|
+
const rootPkgPath = join(cwd, 'package.json')
|
|
20
|
+
if (existsSync(rootPkgPath)) {
|
|
21
|
+
const rootPkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
|
|
22
|
+
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : []
|
|
23
|
+
for (const ws of workspaces) {
|
|
24
|
+
const tauri = join(cwd, ws, 'src-tauri', 'Cargo.toml')
|
|
25
|
+
if (existsSync(tauri)) return tauri
|
|
26
|
+
const flat = join(cwd, ws, 'Cargo.toml')
|
|
27
|
+
if (existsSync(flat)) return flat
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Plural-варіант: повертає всі Cargo.toml-маніфести в проєкті — корінь
|
|
35
|
+
* (`cwd/Cargo.toml`) і у workspace-підкаталогах (`<ws>/src-tauri/Cargo.toml`
|
|
36
|
+
* пріоритетніше за `<ws>/Cargo.toml`). Порожній масив якщо нічого не знайдено.
|
|
37
|
+
* Використовується test-концерном `cargo_mutants_config` для per-manifest
|
|
38
|
+
* baseline-копіювання.
|
|
39
|
+
* @param {string} cwd корінь проєкту
|
|
40
|
+
* @returns {Promise<string[]>} абсолютні шляхи до знайдених Cargo.toml
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveAllCargoManifests(cwd) {
|
|
43
|
+
const manifests = []
|
|
44
|
+
const rootManifest = join(cwd, 'Cargo.toml')
|
|
45
|
+
if (existsSync(rootManifest)) manifests.push(rootManifest)
|
|
46
|
+
|
|
47
|
+
const rootPkgPath = join(cwd, 'package.json')
|
|
48
|
+
if (existsSync(rootPkgPath)) {
|
|
49
|
+
const rootPkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
|
|
50
|
+
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : []
|
|
51
|
+
for (const ws of workspaces) {
|
|
52
|
+
const tauri = join(cwd, ws, 'src-tauri', 'Cargo.toml')
|
|
53
|
+
if (existsSync(tauri)) {
|
|
54
|
+
manifests.push(tauri)
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
const flat = join(cwd, ws, 'Cargo.toml')
|
|
58
|
+
if (existsSync(flat)) manifests.push(flat)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return manifests
|
|
62
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Резолвить корінь JS-коду в проєкті: для workspace-projects — перший workspace
|
|
3
|
+
* (наприклад `app/` у mlmail), для single-package — корінь cwd. Спільна утиліта
|
|
4
|
+
* для coverage-провайдера js-lint і test-концерну stryker_config (DRY).
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
7
|
+
import { readFile } from 'node:fs/promises'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} cwd корінь проєкту (де `.n-cursor.json` і кореневий package.json)
|
|
12
|
+
* @returns {Promise<string|null>} абсолютний шлях до JS-root або null без кореневого package.json
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveJsRoot(cwd) {
|
|
15
|
+
const rootPkgPath = join(cwd, 'package.json')
|
|
16
|
+
if (!existsSync(rootPkgPath)) return null
|
|
17
|
+
const rootPkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
|
|
18
|
+
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : []
|
|
19
|
+
if (workspaces.length > 0) {
|
|
20
|
+
const wsPath = join(cwd, workspaces[0])
|
|
21
|
+
if (existsSync(join(wsPath, 'package.json'))) return wsPath
|
|
22
|
+
}
|
|
23
|
+
return cwd
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Plural-варіант: повертає всі JS-roots проєкту. Для workspace-projects — кожен
|
|
28
|
+
* workspace з власним `package.json`; для single-package — `[cwd]`. Порожній
|
|
29
|
+
* масив без кореневого package.json. Використовується test-концерном
|
|
30
|
+
* `stryker_config` для per-workspace baseline-копіювання.
|
|
31
|
+
* @param {string} cwd корінь проєкту
|
|
32
|
+
* @returns {Promise<string[]>} абсолютні шляхи до всіх JS-roots
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveAllJsRoots(cwd) {
|
|
35
|
+
const rootPkgPath = join(cwd, 'package.json')
|
|
36
|
+
if (!existsSync(rootPkgPath)) return []
|
|
37
|
+
const rootPkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
|
|
38
|
+
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : []
|
|
39
|
+
if (workspaces.length === 0) return [cwd]
|
|
40
|
+
const roots = []
|
|
41
|
+
for (const ws of workspaces) {
|
|
42
|
+
const wsPath = join(cwd, ws)
|
|
43
|
+
if (existsSync(join(wsPath, 'package.json'))) roots.push(wsPath)
|
|
44
|
+
}
|
|
45
|
+
return roots.length > 0 ? roots : [cwd]
|
|
46
|
+
}
|