@7n/rules-lang-js 0.5.0 → 0.7.0
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 +12 -0
- package/package.json +1 -1
- package/rules/style/admin_table/admin_table.mdc +88 -0
- package/rules/style/admin_table/concern.json +7 -0
- package/rules/style/admin_table/docs/index.md +9 -0
- package/rules/style/admin_table/docs/main.md +14 -0
- package/rules/style/admin_table/main.mjs +46 -0
- package/rules/style/colors/colors.mdc +21 -0
- package/rules/style/colors/concern.json +3 -0
- package/rules/style/docs/index.md +11 -0
- package/rules/style/gap/concern.json +7 -0
- package/rules/style/gap/docs/index.md +9 -0
- package/rules/style/gap/docs/main.md +15 -0
- package/rules/style/gap/gap.mdc +22 -0
- package/rules/style/gap/main.mjs +51 -0
- package/rules/style/lint/concern.json +7 -0
- package/rules/style/lint/docs/fix-lint.md +29 -0
- package/rules/style/lint/docs/index.md +11 -0
- package/rules/style/lint/docs/main.md +29 -0
- package/rules/style/lint/fix-lint.mjs +66 -0
- package/rules/style/lint/main.mjs +68 -0
- package/rules/style/main.json +1 -0
- package/rules/style/main.mdc +12 -0
- package/rules/style/package_json/concern.json +9 -0
- package/rules/style/package_json/docs/fix-package_json.md +26 -0
- package/rules/style/package_json/docs/index.md +9 -0
- package/rules/style/package_json/fix-package_json.mjs +3 -0
- package/rules/style/package_json/package_json.mdc +18 -0
- package/rules/style/package_json/package_json.rego +31 -0
- package/rules/style/package_json/template/package.json.snippet.json +5 -0
- package/rules/style/quasar/concern.json +3 -0
- package/rules/style/quasar/quasar.mdc +7 -0
- package/rules/style/quasar_fixes/concern.json +7 -0
- package/rules/style/quasar_fixes/docs/index.md +9 -0
- package/rules/style/quasar_fixes/docs/main.md +16 -0
- package/rules/style/quasar_fixes/main.mjs +57 -0
- package/rules/style/quasar_fixes/quasar_fixes.mdc +32 -0
- package/rules/style/tooling/concern.json +14 -0
- package/rules/style/tooling/docs/fix-tooling.md +29 -0
- package/rules/style/tooling/docs/index.md +12 -0
- package/rules/style/tooling/docs/main.md +34 -0
- package/rules/style/tooling/fix-tooling.mjs +58 -0
- package/rules/style/tooling/main.mjs +73 -0
- package/rules/style/tooling/tooling.mdc +85 -0
- package/rules/style/vscode_extensions/concern.json +9 -0
- package/rules/style/vscode_extensions/docs/fix-vscode_extensions.md +24 -0
- package/rules/style/vscode_extensions/docs/index.md +11 -0
- package/rules/style/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/style/vscode_extensions/template/extensions.json.snippet.json +1 -0
- package/rules/style/vscode_extensions/vscode_extensions.mdc +13 -0
- package/rules/style/vscode_extensions/vscode_extensions.rego +13 -0
- package/rules/style/vscode_settings/concern.json +9 -0
- package/rules/style/vscode_settings/docs/fix-vscode_settings.md +26 -0
- package/rules/style/vscode_settings/docs/index.md +9 -0
- package/rules/style/vscode_settings/fix-vscode_settings.mjs +5 -0
- package/rules/style/vscode_settings/template/settings.json.snippet.json +5 -0
- package/rules/style/vscode_settings/vscode_settings.mdc +19 -0
- package/rules/style/vscode_settings/vscode_settings.rego +15 -0
- package/rules/test/docs/index.md +11 -0
- package/rules/test/lib/collect-test-file-offenders.mjs +49 -0
- package/rules/test/lib/docs/collect-test-file-offenders.md +29 -0
- package/rules/test/lib/docs/index.md +9 -0
- package/rules/test/location/concern.json +8 -0
- package/rules/test/location/docs/index.md +11 -0
- package/rules/test/location/docs/main.md +36 -0
- package/rules/test/location/location.mdc +52 -0
- package/rules/test/location/main.mjs +70 -0
- package/rules/test/main.json +1 -0
- package/rules/test/main.mdc +26 -0
- package/rules/test/no-bun-test-import/concern.json +7 -0
- package/rules/test/no-bun-test-import/docs/fix-no-bun-test-import.md +30 -0
- package/rules/test/no-bun-test-import/docs/index.md +10 -0
- package/rules/test/no-bun-test-import/docs/main.md +35 -0
- package/rules/test/no-bun-test-import/fix-no-bun-test-import.mjs +51 -0
- package/rules/test/no-bun-test-import/main.mjs +105 -0
- package/rules/test/no-bun-test-import/no-bun-test-import.mdc +9 -0
- package/rules/test/no-console-store-restore/concern.json +7 -0
- package/rules/test/no-console-store-restore/docs/index.md +11 -0
- package/rules/test/no-console-store-restore/docs/main.md +44 -0
- package/rules/test/no-console-store-restore/main.mjs +55 -0
- package/rules/test/no-console-store-restore/no-console-store-restore.mdc +11 -0
- package/rules/test/no-process-chdir/concern.json +7 -0
- package/rules/test/no-process-chdir/docs/index.md +11 -0
- package/rules/test/no-process-chdir/docs/main.md +32 -0
- package/rules/test/no-process-chdir/main.mjs +40 -0
- package/rules/test/no-process-chdir/no-process-chdir.mdc +15 -0
- package/rules/test/no-relative-fs-path/concern.json +7 -0
- package/rules/test/no-relative-fs-path/docs/index.md +11 -0
- package/rules/test/no-relative-fs-path/docs/main.md +34 -0
- package/rules/test/no-relative-fs-path/main.mjs +239 -0
- package/rules/test/no-relative-fs-path/no-relative-fs-path.mdc +22 -0
- package/rules/test/package_json/concern.json +11 -0
- package/rules/test/package_json/package_json.mdc +18 -0
- package/rules/test/package_json/package_json.rego +25 -0
- package/rules/test/package_json/template/package.json.contains.json +6 -0
- package/rules/test/sandbox-aware-test/concern.json +7 -0
- package/rules/test/sandbox-aware-test/docs/index.md +11 -0
- package/rules/test/sandbox-aware-test/docs/main.md +55 -0
- package/rules/test/sandbox-aware-test/main.mjs +89 -0
- package/rules/test/sandbox-aware-test/sandbox-aware-test.mdc +28 -0
- package/rules/test/stryker_config/concern.json +8 -0
- package/rules/test/stryker_config/data/stryker_config/docs/index.md +13 -0
- package/rules/test/stryker_config/data/stryker_config/docs/stryker-vue-macros-ignorer.md +31 -0
- package/rules/test/stryker_config/data/stryker_config/docs/stryker.config.baseline.md +31 -0
- package/rules/test/stryker_config/data/stryker_config/docs/stryker.config.vue.baseline.md +35 -0
- package/rules/test/stryker_config/data/stryker_config/stryker-vue-macros-ignorer.mjs +48 -0
- package/rules/test/stryker_config/data/stryker_config/stryker.config.baseline.mjs +18 -0
- package/rules/test/stryker_config/data/stryker_config/stryker.config.vue.baseline.mjs +23 -0
- package/rules/test/stryker_config/data/vitest_config/docs/index.md +11 -0
- package/rules/test/stryker_config/data/vitest_config/docs/vitest.config.baseline.md +35 -0
- package/rules/test/stryker_config/data/vitest_config/vitest.config.baseline.js +22 -0
- package/rules/test/stryker_config/docs/fix-stryker_config.md +41 -0
- package/rules/test/stryker_config/docs/index.md +12 -0
- package/rules/test/stryker_config/docs/main.md +38 -0
- package/rules/test/stryker_config/fix-stryker_config.mjs +77 -0
- package/rules/test/stryker_config/main.mjs +504 -0
- package/rules/test/stryker_config/stryker_config.mdc +26 -0
- package/rules/test/vitest-api-conventions/concern.json +7 -0
- package/rules/test/vitest-api-conventions/docs/index.md +9 -0
- package/rules/test/vitest-api-conventions/docs/main.md +40 -0
- package/rules/test/vitest-api-conventions/main.mjs +153 -0
- package/rules/test/vitest-api-conventions/vitest-api-conventions.mdc +129 -0
- package/rules/test/vitest-config-pool-forks/concern.json +7 -0
- package/rules/test/vitest-config-pool-forks/docs/index.md +11 -0
- package/rules/test/vitest-config-pool-forks/docs/main.md +42 -0
- package/rules/test/vitest-config-pool-forks/main.mjs +41 -0
- package/rules/test/vitest-config-pool-forks/vitest-config-pool-forks.mdc +34 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T0-autofix для `test/stryker_config` — детерміноване створення canonical
|
|
3
|
+
* stryker/vitest baseline-ів, vue-plugin-файла, augment існуючого Vue-config-а та
|
|
4
|
+
* дозапис тест-патернів у `.gitignore`. Логіку перенесено з detector-а (read-only
|
|
5
|
+
* contract: detector лише ЗВІТУЄ потрібні зміни, запис — тут).
|
|
6
|
+
*
|
|
7
|
+
* Unified lint surface: structured violations (test(violations)/apply(violations,ctx)).
|
|
8
|
+
* Дії резолвимо повторним запуском чистого планувальника `planStrykerActions(cwd)`
|
|
9
|
+
* (той самий код, що й detector) → idempotent: already-applied не повертаються.
|
|
10
|
+
*/
|
|
11
|
+
import { copyFile, readFile, writeFile } from 'node:fs/promises'
|
|
12
|
+
|
|
13
|
+
import { ensureGitignoreEntries } from '@7n/rules/scripts/utils/ensure-gitignore-entries.mjs'
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
GITIGNORE_MISSING,
|
|
17
|
+
GITIGNORE_SECTION_LABEL,
|
|
18
|
+
STRYKER_CONFIG_MISSING,
|
|
19
|
+
STRYKER_VUE_AUGMENT,
|
|
20
|
+
planStrykerActions
|
|
21
|
+
} from './main.mjs'
|
|
22
|
+
|
|
23
|
+
const TRIGGER_REASONS = new Set([STRYKER_CONFIG_MISSING, STRYKER_VUE_AUGMENT, GITIGNORE_MISSING])
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Виконує одну BaselineAction: copy as-is або string-replace baseline-тексту.
|
|
27
|
+
* @param {import('./main.mjs').StrykerPlan['baselineActions'][number]} a одна baseline-дія (copy або string-replace).
|
|
28
|
+
* @returns {Promise<void>}
|
|
29
|
+
*/
|
|
30
|
+
async function writeBaseline(a) {
|
|
31
|
+
if (a.transform) {
|
|
32
|
+
const src = await readFile(a.baselinePath, 'utf8')
|
|
33
|
+
const { replacement } = a.transform
|
|
34
|
+
await writeFile(
|
|
35
|
+
a.target,
|
|
36
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- planned canonical regex replay
|
|
37
|
+
src.replace(new RegExp(a.transform.re, 'u'), () => replacement),
|
|
38
|
+
'utf8'
|
|
39
|
+
)
|
|
40
|
+
} else {
|
|
41
|
+
await copyFile(a.baselinePath, a.target)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @type {import('@7n/rules/scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
|
|
46
|
+
export const patterns = [
|
|
47
|
+
{
|
|
48
|
+
id: 'test-stryker-config-create',
|
|
49
|
+
test: violations => violations.some(v => TRIGGER_REASONS.has(v.reason)),
|
|
50
|
+
apply: async (violations, ctx) => {
|
|
51
|
+
const cwd = ctx.cwd
|
|
52
|
+
const plan = await planStrykerActions(cwd)
|
|
53
|
+
if (plan.fatal) return { touchedFiles: [] }
|
|
54
|
+
|
|
55
|
+
const touchedFiles = []
|
|
56
|
+
for (const a of plan.baselineActions) {
|
|
57
|
+
ctx.recordWrite?.(a.target)
|
|
58
|
+
await writeBaseline(a)
|
|
59
|
+
touchedFiles.push(a.target)
|
|
60
|
+
}
|
|
61
|
+
for (const w of plan.augmentWrites) {
|
|
62
|
+
ctx.recordWrite?.(w.target)
|
|
63
|
+
await writeFile(w.target, w.content, 'utf8')
|
|
64
|
+
touchedFiles.push(w.target)
|
|
65
|
+
}
|
|
66
|
+
if (plan.gitignoreMissing.length > 0) {
|
|
67
|
+
const gitignorePath = `${cwd}/.gitignore`
|
|
68
|
+
ctx.recordWrite?.(gitignorePath)
|
|
69
|
+
const { added } = await ensureGitignoreEntries(cwd, plan.gitignoreMissing, GITIGNORE_SECTION_LABEL)
|
|
70
|
+
if (added.length > 0) touchedFiles.push(gitignorePath)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (touchedFiles.length === 0) return { touchedFiles: [] }
|
|
74
|
+
return { touchedFiles, message: `stryker/vitest config + .gitignore: ${touchedFiles.join(', ')}` }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
]
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @see ./docs/stryker_config.md
|
|
3
|
+
*
|
|
4
|
+
* Read-only detector: планує (НЕ виконує) копіювання stryker/vitest baseline-ів,
|
|
5
|
+
* vue-plugin-файла, augment існуючого Vue-config-а та `.gitignore`-entries.
|
|
6
|
+
* Кожна потрібна зміна стає violation із `data` (опис дії для T0). Запис робить
|
|
7
|
+
* окремий T0-fix (`fix-stryker_config.mjs`) — `lint --no-fix` не мутує дерево.
|
|
8
|
+
* Планувальник (`planStrykerActions`) і константи шляхів спільні для detector/T0.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from 'node:fs'
|
|
11
|
+
import { glob, readFile } from 'node:fs/promises'
|
|
12
|
+
import { dirname, join, relative } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
|
|
15
|
+
import { parseSync } from 'oxc-parser'
|
|
16
|
+
|
|
17
|
+
import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
|
|
18
|
+
import { readNRulesConfigLite } from '@7n/rules/scripts/lib/read-n-rules-config-lite.mjs'
|
|
19
|
+
import { resolveAllJsRoots } from '@7n/rules/scripts/utils/resolve-js-root.mjs'
|
|
20
|
+
|
|
21
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
22
|
+
export const STRYKER_BASELINE_PATH = join(HERE, 'data', 'stryker_config', 'stryker.config.baseline.mjs')
|
|
23
|
+
export const STRYKER_VUE_BASELINE_PATH = join(HERE, 'data', 'stryker_config', 'stryker.config.vue.baseline.mjs')
|
|
24
|
+
export const STRYKER_VUE_PLUGIN_PATH = join(HERE, 'data', 'stryker_config', 'stryker-vue-macros-ignorer.mjs')
|
|
25
|
+
const STRYKER_VUE_PLUGIN_FILENAME = 'stryker-vue-macros-ignorer.mjs'
|
|
26
|
+
export const VITEST_BASELINE_PATH = join(HERE, 'data', 'vitest_config', 'vitest.config.baseline.js')
|
|
27
|
+
|
|
28
|
+
/** Стабільні reasons. */
|
|
29
|
+
export const STRYKER_CONFIG_MISSING = 'stryker-config-missing'
|
|
30
|
+
export const STRYKER_VUE_AUGMENT = 'stryker-vue-augment'
|
|
31
|
+
export const STRYKER_VUE_AUGMENT_FAIL = 'stryker-vue-augment-fail'
|
|
32
|
+
export const GITIGNORE_MISSING = 'gitignore-missing'
|
|
33
|
+
|
|
34
|
+
// Канонічна назва vitest-конфіга — `.mjs` (нові файли, js.mdc); legacy
|
|
35
|
+
// `.js` лишається валідним. Перший знайдений виграє (.mjs пріоритетніший).
|
|
36
|
+
const VITEST_CONFIG_NAMES = ['vitest.config.mjs', 'vitest.config.js']
|
|
37
|
+
// Заміна literal `configFile` у скопійованому stryker-baseline на фактичне
|
|
38
|
+
// ім'я vitest-конфіга jsRoot-а (узгодження Stryker ↔ vitest).
|
|
39
|
+
const STRYKER_CONFIG_FILE_RE = /configFile: 'vitest\.config\.[cm]?js'/u
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Визначає ім'я vitest-конфіга для jsRoot: існуючий `.mjs`/`.js` (якщо є),
|
|
43
|
+
* інакше дефолт `vitest.config.mjs` (нові файли — `.mjs`). Існуючий
|
|
44
|
+
* `vitest.config.js` лишається валідним (backward-compat), новий не плодиться.
|
|
45
|
+
* @param {string} jsRoot абсолютний шлях до workspace-каталогу
|
|
46
|
+
* @returns {string} ім'я vitest-конфіга
|
|
47
|
+
*/
|
|
48
|
+
function resolveVitestConfigName(jsRoot) {
|
|
49
|
+
return VITEST_CONFIG_NAMES.find(name => existsSync(join(jsRoot, name))) ?? 'vitest.config.mjs'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Канонічні entries, які vue-варіант baseline тримає у `plugins`/`ignorers`.
|
|
53
|
+
// Augment-крок (augmentVueStrykerConfig) дбає, щоб саме вони були присутні в
|
|
54
|
+
// уже-існуючому `stryker.config.mjs` Vue-root-а. Нову property пишемо у
|
|
55
|
+
// canonical-порядку; у наявний масив лише дописуємо відсутні entries в кінець
|
|
56
|
+
// (Stryker нечутливий до порядку plugins/ignorers).
|
|
57
|
+
const VITEST_RUNNER_PLUGIN = '@stryker-mutator/vitest-runner'
|
|
58
|
+
const VUE_MACROS_PLUGIN = './stryker-vue-macros-ignorer.mjs'
|
|
59
|
+
const VUE_MACROS_IGNORER = 'vue-macros'
|
|
60
|
+
|
|
61
|
+
// Module-scope (prefer-static-regex): рядок-відступ цілком whitespace; leading
|
|
62
|
+
// кома (можливо з whitespace) після останньої property об'єкта.
|
|
63
|
+
const INDENT_WS_RE = /^\s*$/u
|
|
64
|
+
const LEADING_COMMA_RE = /^\s*,/u
|
|
65
|
+
|
|
66
|
+
// Тест-артефакти для .gitignore (подвійний-зірочка-префікс — для monorepo workspaces):
|
|
67
|
+
// - `**/reports/stryker/` — увесь каталог Stryker-output-у (`tempDirName` backup'и,
|
|
68
|
+
// mutation.json, HTML/dashboard-репорти якщо користувач додасть інші reporter-и).
|
|
69
|
+
// - `**/coverage/` — весь output vitest v8 coverage (`lcov.info` + HTML `lcov-report/`).
|
|
70
|
+
// Ефемерний: регенерується кожним прогоном; фінальні метрики живуть у `COVERAGE.md`.
|
|
71
|
+
// Gitignore не заважає `@7n/test coverage` читати `lcov.info` у тому ж прогоні.
|
|
72
|
+
// Покриваємо каталогами замість перелічування під-патернів.
|
|
73
|
+
const TEST_GITIGNORE_ENTRIES = ['**/reports/stryker/', '**/coverage/']
|
|
74
|
+
|
|
75
|
+
// .vue detection: scope — `<jsRoot>/src/**/*.vue` (як і Stryker mutate defaults для src/);
|
|
76
|
+
// skip build-артефактів і чужих node_modules, щоб не вмикати vue-варіант через transitive deps.
|
|
77
|
+
const VUE_GLOB_PATTERN = 'src/**/*.vue'
|
|
78
|
+
const VUE_GLOB_IGNORE = ['**/node_modules/**', '**/dist/**', '**/reports/**']
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Чи містить jsRoot хоч один `.vue` файл під `src/` (skipping node_modules/dist/reports).
|
|
82
|
+
* @param {string} jsRoot абсолютний шлях до workspace-каталогу
|
|
83
|
+
* @returns {Promise<boolean>} true якщо знайдено хоча б один `.vue`
|
|
84
|
+
*/
|
|
85
|
+
async function hasVueFiles(jsRoot) {
|
|
86
|
+
for await (const _rel of glob(VUE_GLOB_PATTERN, { cwd: jsRoot, exclude: VUE_GLOB_IGNORE })) {
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Опис однієї дії-запису baseline-файла (для T0). Читання baseline і запис робить
|
|
94
|
+
* T0; detector лише планує. `transformKey` — необов'язковий маркер, який трансформ
|
|
95
|
+
* застосувати до тексту baseline (T0 мапить ключ на функцію); `null` = copy as-is.
|
|
96
|
+
* @typedef {object} BaselineAction
|
|
97
|
+
* @property {'baseline'} kind дискримінатор виду дії (завжди `'baseline'`).
|
|
98
|
+
* @property {string} baselinePath абсолютний шлях canonical baseline
|
|
99
|
+
* @property {string} target абсолютний шлях, куди писати
|
|
100
|
+
* @property {string} label людиночитна мітка
|
|
101
|
+
* @property {{ re: string, replacement: string }} [transform] string-replace над текстом baseline
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Будує BaselineAction, якщо target ще не існує (idempotent). Read-only.
|
|
106
|
+
* @param {string} baselinePath абсолютний шлях до canonical baseline
|
|
107
|
+
* @param {string} target абсолютний шлях, куди копіювати
|
|
108
|
+
* @param {string} label мітка ("stryker.config.mjs" / "vitest.config.mjs")
|
|
109
|
+
* @param {{ re: string, replacement: string }} [transform] опційний string-replace baseline-тексту
|
|
110
|
+
* @returns {BaselineAction | null} дія або null, якщо файл уже є
|
|
111
|
+
*/
|
|
112
|
+
function planBaselineFile(baselinePath, target, label, transform) {
|
|
113
|
+
if (existsSync(target)) return null
|
|
114
|
+
/** @type {BaselineAction} */
|
|
115
|
+
const action = { kind: 'baseline', baselinePath, target, label }
|
|
116
|
+
if (transform) action.transform = transform
|
|
117
|
+
return action
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Огортає рядкове значення в single-quotes для вставки у JS-масив. Канонічні
|
|
122
|
+
* entries (`@stryker-mutator/...`, `vue-macros`, `./stryker-...`) не містять
|
|
123
|
+
* лапок, тож escaping не потрібен.
|
|
124
|
+
* @param {string} s рядкове значення
|
|
125
|
+
* @returns {string} `'<s>'`
|
|
126
|
+
*/
|
|
127
|
+
function quote(s) {
|
|
128
|
+
return `'${s}'`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Знаходить `export default { … }` як ObjectExpression. Повертає null, якщо
|
|
133
|
+
* default-export відсутній або не є object-literal (factory/функція/змінна) —
|
|
134
|
+
* augment у такому разі не чіпає файл.
|
|
135
|
+
* @param {{body: Array<{type: string, declaration?: {type: string}}>}} program oxc Program node
|
|
136
|
+
* @returns {object | null} ObjectExpression node або null
|
|
137
|
+
*/
|
|
138
|
+
function findDefaultExportObject(program) {
|
|
139
|
+
const exp = program.body.find(n => n.type === 'ExportDefaultDeclaration')
|
|
140
|
+
const decl = exp?.declaration
|
|
141
|
+
return decl && decl.type === 'ObjectExpression' ? decl : null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Аналізує property `name` об'єкта: чи присутній, чи це чистий масив рядкових
|
|
146
|
+
* літералів і які значення вже містить. `dynamic: true` сигналить, що масив —
|
|
147
|
+
* computed (spread / non-string element / не ArrayExpression), і зливати його
|
|
148
|
+
* небезпечно.
|
|
149
|
+
* @param {object} obj ObjectExpression node
|
|
150
|
+
* @param {string} name ім'я property ('plugins' | 'ignorers')
|
|
151
|
+
* @returns {{prop: object|null, array: object|null, values: string[], dynamic: boolean}} стан property
|
|
152
|
+
*/
|
|
153
|
+
function analyzeArrayProperty(obj, name) {
|
|
154
|
+
const prop = obj.properties.find(
|
|
155
|
+
p => p.type === 'Property' && !p.computed && p.key && (p.key.name === name || p.key.value === name)
|
|
156
|
+
)
|
|
157
|
+
if (!prop) return { prop: null, array: null, values: [], dynamic: false }
|
|
158
|
+
const value = prop.value
|
|
159
|
+
if (!value || value.type !== 'ArrayExpression') return { prop, array: null, values: [], dynamic: true }
|
|
160
|
+
const values = []
|
|
161
|
+
for (const el of value.elements) {
|
|
162
|
+
if (!el || el.type !== 'Literal' || typeof el.value !== 'string') {
|
|
163
|
+
return { prop, array: value, values: [], dynamic: true }
|
|
164
|
+
}
|
|
165
|
+
values.push(el.value)
|
|
166
|
+
}
|
|
167
|
+
return { prop, array: value, values, dynamic: false }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Вставка відсутніх рядкових елементів у вже існуючий масив (append перед `]`).
|
|
172
|
+
* Порожній масив → елементи між `[` `]`; непорожній → `, '<item>'` після
|
|
173
|
+
* останнього елемента (trailing comma, якщо вже є, лишається валідною).
|
|
174
|
+
* @param {object} arr ArrayExpression node
|
|
175
|
+
* @param {string[]} values поточні значення масиву
|
|
176
|
+
* @param {string[]} missing значення, яких бракує (вже у потрібному порядку)
|
|
177
|
+
* @returns {{pos: number, text: string}} одна точкова вставка
|
|
178
|
+
*/
|
|
179
|
+
function arrayAppendEdit(arr, values, missing) {
|
|
180
|
+
if (values.length === 0) {
|
|
181
|
+
return { pos: arr.end - 1, text: missing.map(v => quote(v)).join(', ') }
|
|
182
|
+
}
|
|
183
|
+
const lastEl = arr.elements.at(-1)
|
|
184
|
+
return { pos: lastEl.end, text: missing.map(v => `, ${quote(v)}`).join('') }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Визначає відступ properties об'єкта за рядком останньої property (для нових
|
|
189
|
+
* рядків `plugins`/`ignorers`). Дефолт — 2 пробіли.
|
|
190
|
+
* @param {string} src вихідний текст конфіга
|
|
191
|
+
* @param {object} obj ObjectExpression node
|
|
192
|
+
* @returns {string} рядок-відступ (whitespace)
|
|
193
|
+
*/
|
|
194
|
+
function detectIndent(src, obj) {
|
|
195
|
+
const props = obj.properties
|
|
196
|
+
if (props.length > 0) {
|
|
197
|
+
const start = props.at(-1).start
|
|
198
|
+
const lineStart = src.lastIndexOf('\n', start - 1) + 1
|
|
199
|
+
const ws = src.slice(lineStart, start)
|
|
200
|
+
if (INDENT_WS_RE.test(ws)) return ws
|
|
201
|
+
}
|
|
202
|
+
return ' '
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Вставка нових properties (`plugins`/`ignorers`) у object-literal перед його
|
|
207
|
+
* закривальною `}`. Поважає trailing comma останньої property й коректно
|
|
208
|
+
* обробляє порожній об'єкт `{}`.
|
|
209
|
+
* @param {string} src вихідний текст конфіга
|
|
210
|
+
* @param {object} obj ObjectExpression node
|
|
211
|
+
* @param {string} indent відступ properties
|
|
212
|
+
* @param {string[]} lines рядки нових properties (без відступу й коми), напр. `plugins: [...]`
|
|
213
|
+
* @returns {{pos: number, text: string}} одна точкова вставка
|
|
214
|
+
*/
|
|
215
|
+
function newPropertyEdit(src, obj, indent, lines) {
|
|
216
|
+
const block = lines.join(`,\n${indent}`)
|
|
217
|
+
const props = obj.properties
|
|
218
|
+
if (props.length === 0) {
|
|
219
|
+
return { pos: obj.start + 1, text: `\n${indent}${block}\n` }
|
|
220
|
+
}
|
|
221
|
+
const lastProp = props.at(-1)
|
|
222
|
+
const tail = src.slice(lastProp.end, obj.end - 1)
|
|
223
|
+
const commaMatch = tail.match(LEADING_COMMA_RE)
|
|
224
|
+
if (commaMatch) {
|
|
225
|
+
return { pos: lastProp.end + commaMatch[0].length, text: `\n${indent}${block}` }
|
|
226
|
+
}
|
|
227
|
+
return { pos: lastProp.end, text: `,\n${indent}${block}` }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Застосовує точкові вставки до тексту. Сортує за спаданням `pos`, щоб ранні
|
|
232
|
+
* offsets лишались валідними після вставок справа.
|
|
233
|
+
* @param {string} src вихідний текст
|
|
234
|
+
* @param {Array<{pos: number, text: string}>} edits вставки
|
|
235
|
+
* @returns {string} новий текст
|
|
236
|
+
*/
|
|
237
|
+
function applyEdits(src, edits) {
|
|
238
|
+
let out = src
|
|
239
|
+
for (const e of edits.toSorted((a, b) => b.pos - a.pos)) {
|
|
240
|
+
out = out.slice(0, e.pos) + e.text + out.slice(e.pos)
|
|
241
|
+
}
|
|
242
|
+
return out
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Augment-крок для вже-існуючого `stryker.config.mjs` у Vue JS-root:
|
|
247
|
+
* реєструє локальний `vue-macros` ignorer-плагін (`plugins`/`ignorers`), якщо
|
|
248
|
+
* його ще немає. Закриває drift-hole для проєктів, які мали non-vue config ще
|
|
249
|
+
* до 3.x Vue-підтримки — `ensureBaselineFile` такий файл idempotent-skip-ить,
|
|
250
|
+
* тож baseline-секцій `plugins`/`ignorers` він мовчки не отримує, і Stryker
|
|
251
|
+
* падає у dry-run з `defineProps()` error.
|
|
252
|
+
*
|
|
253
|
+
* Стратегія: oxc-parser — лише для **аналізу** (де у source-тексті
|
|
254
|
+
* default-export object, які properties/offsets уже є). Зміни — точкові
|
|
255
|
+
* string-splice-и у вихідному тексті (insert items), щоб НЕ переписати
|
|
256
|
+
* форматування й коментарі користувача (oxc serializer їх не зберігає). Після
|
|
257
|
+
* splice — повторний parse: якщо результат не компілюється → відкат і fail.
|
|
258
|
+
* @param {string} cwd корінь проєкту (для relative-шляхів у логах)
|
|
259
|
+
* @param {string} jsRoot абсолютний шлях до Vue workspace-каталогу
|
|
260
|
+
* @returns {Promise<{ ok: false, message: string } | { ok: true, target: string, content: string | null }>}
|
|
261
|
+
* `ok:false` — augment неможливий (fail-violation); `ok:true, content:null` — no-op;
|
|
262
|
+
* `ok:true, content:string` — обчислений новий вміст для запису T0-ом
|
|
263
|
+
*/
|
|
264
|
+
export async function planVueAugment(cwd, jsRoot) {
|
|
265
|
+
const target = join(jsRoot, 'stryker.config.mjs')
|
|
266
|
+
const rel = relative(cwd, target)
|
|
267
|
+
const src = await readFile(target, 'utf8')
|
|
268
|
+
|
|
269
|
+
let result
|
|
270
|
+
try {
|
|
271
|
+
result = parseSync(target, src, { lang: 'js', sourceType: 'module' })
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return { ok: false, message: `stryker.config.mjs не парситься (${rel}): ${error.message} — augment скіпнуто` }
|
|
274
|
+
}
|
|
275
|
+
if (result.errors?.length) {
|
|
276
|
+
const msg = result.errors[0]?.message ?? 'syntax error'
|
|
277
|
+
return { ok: false, message: `stryker.config.mjs має syntax error (${rel}): ${msg} — augment скіпнуто` }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const obj = findDefaultExportObject(result.program)
|
|
281
|
+
if (!obj) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
message:
|
|
285
|
+
`stryker.config.mjs has non-literal default export (${rel}) — augment скіпнуто, ` +
|
|
286
|
+
'додай вручну plugins/ignorers згідно stryker.config.vue.baseline.mjs'
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const plugins = analyzeArrayProperty(obj, 'plugins')
|
|
291
|
+
const ignorers = analyzeArrayProperty(obj, 'ignorers')
|
|
292
|
+
if (plugins.dynamic || ignorers.dynamic) {
|
|
293
|
+
return {
|
|
294
|
+
ok: false,
|
|
295
|
+
message:
|
|
296
|
+
`stryker.config.mjs: plugins/ignorers — динамічний вираз (spread/computed) (${rel}) — ` +
|
|
297
|
+
'augment скіпнуто, додай vue-macros ignorer вручну згідно stryker.config.vue.baseline.mjs'
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const edits = []
|
|
302
|
+
const newPropLines = []
|
|
303
|
+
for (const [name, state, required] of [
|
|
304
|
+
['plugins', plugins, [VITEST_RUNNER_PLUGIN, VUE_MACROS_PLUGIN]],
|
|
305
|
+
['ignorers', ignorers, [VUE_MACROS_IGNORER]]
|
|
306
|
+
]) {
|
|
307
|
+
const missing = required.filter(v => !state.values.includes(v))
|
|
308
|
+
if (state.array) {
|
|
309
|
+
if (missing.length > 0) edits.push(arrayAppendEdit(state.array, state.values, missing))
|
|
310
|
+
} else {
|
|
311
|
+
newPropLines.push(`${name}: [${required.map(v => quote(v)).join(', ')}]`)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (newPropLines.length > 0) {
|
|
315
|
+
edits.push(newPropertyEdit(src, obj, detectIndent(src, obj), newPropLines))
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (edits.length === 0) return { ok: true, target, content: null }
|
|
319
|
+
|
|
320
|
+
const next = applyEdits(src, edits)
|
|
321
|
+
|
|
322
|
+
// Safety: результат має компілюватися. Якщо string-splice дав невалідний JS
|
|
323
|
+
// (errors або виняток парсера на патологічному вводі) — fail (не пишемо), щоб
|
|
324
|
+
// користувач не лишився зі зламаним конфігом.
|
|
325
|
+
let recheck
|
|
326
|
+
try {
|
|
327
|
+
recheck = parseSync(target, next, { lang: 'js', sourceType: 'module' })
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
message: `stryker.config.mjs: augment дав некоректний результат (${rel}): ${error.message} — відкат, додай вручну`
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (recheck.errors?.length) {
|
|
335
|
+
return {
|
|
336
|
+
ok: false,
|
|
337
|
+
message: `stryker.config.mjs: augment дав некоректний результат (${rel}) — відкат, додай вручну`
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return { ok: true, target, content: next }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Header-коментар для секції тест-артефактів у `.gitignore`. */
|
|
345
|
+
export const GITIGNORE_SECTION_LABEL = 'Test artifacts: Stryker + coverage (test.mdc)'
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Read-only: чи відсутні якісь із `TEST_GITIGNORE_ENTRIES` у кореневому `.gitignore`.
|
|
349
|
+
* Дублює дешеву перевірку `ensureGitignoreEntries` без запису.
|
|
350
|
+
* @param {string} cwd корінь репо
|
|
351
|
+
* @returns {Promise<string[]>} відсутні entries (порожній — нічого додавати)
|
|
352
|
+
*/
|
|
353
|
+
async function missingGitignoreEntries(cwd) {
|
|
354
|
+
const gitignorePath = join(cwd, '.gitignore')
|
|
355
|
+
const existing = existsSync(gitignorePath) ? await readFile(gitignorePath, 'utf8') : ''
|
|
356
|
+
const lines = new Set(existing.split('\n').map(l => l.trim()))
|
|
357
|
+
return TEST_GITIGNORE_ENTRIES.filter(e => !lines.has(e))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @typedef {object} StrykerPlan
|
|
362
|
+
* @property {string | null} fatal fail-message, що зупиняє план (missing baseline / no root)
|
|
363
|
+
* @property {BaselineAction[]} baselineActions copy-baseline дії (stryker/vitest/vue-plugin)
|
|
364
|
+
* @property {Array<{ target: string, content: string }>} augmentWrites augment-записи (computed content)
|
|
365
|
+
* @property {string[]} augmentFails augment-fail повідомлення (read-only diagnostics)
|
|
366
|
+
* @property {string[]} gitignoreMissing відсутні `.gitignore`-entries
|
|
367
|
+
*/
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Vue-специфічні дії для одного js-root: augment існуючого stryker-конфіга (drift-hole)
|
|
371
|
+
* та baseline vue-plugin. Мутує `plan` (append до baselineActions/augmentWrites/augmentFails).
|
|
372
|
+
* @param {StrykerPlan} plan план, що накопичує дії
|
|
373
|
+
* @param {string} cwd корінь репо
|
|
374
|
+
* @param {string} jsRoot корінь js-workspace
|
|
375
|
+
* @param {boolean} wasMissing чи stryker.config.mjs був відсутній до планування
|
|
376
|
+
* @returns {Promise<void>} завершення після append
|
|
377
|
+
*/
|
|
378
|
+
async function planVueRootActions(plan, cwd, jsRoot, wasMissing) {
|
|
379
|
+
if (!wasMissing) {
|
|
380
|
+
const res = await planVueAugment(cwd, jsRoot)
|
|
381
|
+
if (!res.ok) {
|
|
382
|
+
plan.augmentFails.push(res.message)
|
|
383
|
+
} else if (res.content !== null) {
|
|
384
|
+
plan.augmentWrites.push({ target: res.target, content: res.content })
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const pluginAction = planBaselineFile(
|
|
388
|
+
STRYKER_VUE_PLUGIN_PATH,
|
|
389
|
+
join(jsRoot, STRYKER_VUE_PLUGIN_FILENAME),
|
|
390
|
+
STRYKER_VUE_PLUGIN_FILENAME
|
|
391
|
+
)
|
|
392
|
+
if (pluginAction) plan.baselineActions.push(pluginAction)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Планує baseline/augment-дії для одного js-root. Мутує `plan`.
|
|
397
|
+
* @param {StrykerPlan} plan план, що накопичує дії
|
|
398
|
+
* @param {string} cwd корінь репо
|
|
399
|
+
* @param {string} jsRoot корінь js-workspace
|
|
400
|
+
* @returns {Promise<void>} завершення після append
|
|
401
|
+
*/
|
|
402
|
+
async function planJsRootActions(plan, cwd, jsRoot) {
|
|
403
|
+
const isVueRoot = await hasVueFiles(jsRoot)
|
|
404
|
+
const strykerTarget = join(jsRoot, 'stryker.config.mjs')
|
|
405
|
+
// Чи файл уже існує (до будь-якого запису). Якщо ні — baseline (vue-варіант для
|
|
406
|
+
// Vue-root) уже містить plugins/ignorers, augment не потрібен. Якщо існував —
|
|
407
|
+
// baseline idempotent-skip, і augment закриває drift-hole.
|
|
408
|
+
const wasMissing = !existsSync(strykerTarget)
|
|
409
|
+
const strykerBaseline = isVueRoot ? STRYKER_VUE_BASELINE_PATH : STRYKER_BASELINE_PATH
|
|
410
|
+
const vitestName = resolveVitestConfigName(jsRoot)
|
|
411
|
+
const strykerAction = planBaselineFile(strykerBaseline, strykerTarget, 'stryker.config.mjs', {
|
|
412
|
+
re: STRYKER_CONFIG_FILE_RE.source,
|
|
413
|
+
replacement: `configFile: '${vitestName}'`
|
|
414
|
+
})
|
|
415
|
+
if (strykerAction) plan.baselineActions.push(strykerAction)
|
|
416
|
+
|
|
417
|
+
if (isVueRoot) {
|
|
418
|
+
await planVueRootActions(plan, cwd, jsRoot, wasMissing)
|
|
419
|
+
}
|
|
420
|
+
const vitestAction = planBaselineFile(VITEST_BASELINE_PATH, join(jsRoot, vitestName), vitestName)
|
|
421
|
+
if (vitestAction) plan.baselineActions.push(vitestAction)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Чистий планувальник (read-only): обчислює всі потрібні зміни для stryker_config
|
|
426
|
+
* без жодного запису. Спільний для detector-а (→ violations) і T0-fix (→ writes).
|
|
427
|
+
* @param {string} cwd корінь репо
|
|
428
|
+
* @returns {Promise<StrykerPlan>} план змін (baseline/augment/gitignore) без запису.
|
|
429
|
+
*/
|
|
430
|
+
export async function planStrykerActions(cwd) {
|
|
431
|
+
/** @type {StrykerPlan} */
|
|
432
|
+
const plan = { fatal: null, baselineActions: [], augmentWrites: [], augmentFails: [], gitignoreMissing: [] }
|
|
433
|
+
|
|
434
|
+
const jsRoots = await resolveAllJsRoots(cwd)
|
|
435
|
+
if (jsRoots.length === 0) {
|
|
436
|
+
plan.fatal = 'test: js enabled, але кореневий package.json не знайдено (test.mdc)'
|
|
437
|
+
return plan
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for (const baselinePath of [
|
|
441
|
+
STRYKER_BASELINE_PATH,
|
|
442
|
+
STRYKER_VUE_BASELINE_PATH,
|
|
443
|
+
STRYKER_VUE_PLUGIN_PATH,
|
|
444
|
+
VITEST_BASELINE_PATH
|
|
445
|
+
]) {
|
|
446
|
+
if (!existsSync(baselinePath)) {
|
|
447
|
+
plan.fatal = `canonical baseline не знайдено (${baselinePath}) — перевстанови @7n/rules`
|
|
448
|
+
return plan
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
for (const jsRoot of jsRoots) {
|
|
453
|
+
await planJsRootActions(plan, cwd, jsRoot)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
plan.gitignoreMissing = await missingGitignoreEntries(cwd)
|
|
457
|
+
return plan
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Виконує планувальник і транслює план у pass/fail-звіт лінту.
|
|
462
|
+
* @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту (cwd, репортер).
|
|
463
|
+
* @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат перевірки з pass/fail.
|
|
464
|
+
*/
|
|
465
|
+
export async function lint(ctx) {
|
|
466
|
+
const reporter = createViolationReporter(ctx)
|
|
467
|
+
const cwd = ctx.cwd
|
|
468
|
+
const config = await readNRulesConfigLite(cwd)
|
|
469
|
+
|
|
470
|
+
// Self-gate: js має бути enabled
|
|
471
|
+
if (!config.rules.includes('js') || config.disableRules.includes('js')) {
|
|
472
|
+
return reporter.result()
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const plan = await planStrykerActions(cwd)
|
|
476
|
+
if (plan.fatal) {
|
|
477
|
+
reporter.fail(plan.fatal)
|
|
478
|
+
return reporter.result()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
for (const a of plan.baselineActions) {
|
|
482
|
+
reporter.fail(
|
|
483
|
+
`${a.label} відсутній (${relative(cwd, a.target)}) — запусти \`npx @7n/rules lint test\` для canonical baseline (test.mdc)`,
|
|
484
|
+
{ reason: STRYKER_CONFIG_MISSING, file: relative(cwd, a.target) }
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
for (const w of plan.augmentWrites) {
|
|
488
|
+
reporter.fail(
|
|
489
|
+
`vue-macros ignorer не зареєстровано у stryker.config.mjs (${relative(cwd, w.target)}) — запусти \`npx @7n/rules lint test\` (test.mdc)`,
|
|
490
|
+
{ reason: STRYKER_VUE_AUGMENT, file: relative(cwd, w.target) }
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
for (const msg of plan.augmentFails) {
|
|
494
|
+
reporter.fail(msg, STRYKER_VUE_AUGMENT_FAIL)
|
|
495
|
+
}
|
|
496
|
+
if (plan.gitignoreMissing.length > 0) {
|
|
497
|
+
reporter.fail(
|
|
498
|
+
`.gitignore: бракує тест-патернів (${plan.gitignoreMissing.join(', ')}) — запусти \`npx @7n/rules lint test\` (test.mdc)`,
|
|
499
|
+
GITIGNORE_MISSING
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return reporter.result()
|
|
504
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
## Налаштування mutation-testing: Stryker + Vitest baseline
|
|
2
|
+
|
|
3
|
+
Якщо у `.n-rules.json#rules` присутнє правило `js` — правило `test` створює canonical baseline `stryker.config.mjs` + `vitest.config.mjs` у **кожному** JS-root проєкту: у кожному workspace з власним `package.json` (або в корені для single-package). У monorepo з `workspaces: ['app', 'scripts']` отримаєте `app/stryker.config.mjs` + `app/vitest.config.mjs` і `scripts/stryker.config.mjs` + `scripts/vitest.config.mjs`.
|
|
4
|
+
|
|
5
|
+
Якщо у JS-root уже лежить legacy `vitest.config.js` — він лишається валідним, новий `.mjs` поряд не створюється, а `vitest.configFile` у скопійованому `stryker.config.mjs` приводиться до фактичного імені.
|
|
6
|
+
|
|
7
|
+
Канон Stryker config (Vitest runner + perTest): [stryker.config.baseline.mjs](./data/stryker_config/stryker.config.baseline.mjs)
|
|
8
|
+
|
|
9
|
+
### Vue SFC (`<script setup>` macros)
|
|
10
|
+
|
|
11
|
+
Якщо у JS-root знайдено бодай один `.vue` під `src/` (skip `node_modules`/`dist`/`reports`) — концерн ставить **vue-варіант** baseline ([`stryker.config.vue.baseline.mjs`](./data/stryker_config/stryker.config.vue.baseline.mjs)) замість звичайного і додатково копіює локальний Stryker `Ignore`-плагін [`stryker-vue-macros-ignorer.mjs`](./data/stryker_config/stryker-vue-macros-ignorer.mjs) поряд із конфігом.
|
|
12
|
+
|
|
13
|
+
Плагін реєструється як `plugins: ['@stryker-mutator/vitest-runner', './stryker-vue-macros-ignorer.mjs']` + `ignorers: ['vue-macros']` і виключає з мутацій виклики `<script setup>`-макросів: **`defineProps`**, **`defineEmits`**, **`defineModel`**, **`defineSlots`**, **`defineExpose`**, **`defineOptions`**. Без плагіна Stryker огортає аргументи макроса у coverage-тернарник (`stryMutAct_9fa48(...) ? {} : (stryCov_9fa48(...), {...})`), а `@vue/compiler-sfc` падає з `defineProps() in <script setup> cannot reference locally declared variables` — макроси мають бути статично-аналізованими на етапі compile-sfc.
|
|
14
|
+
|
|
15
|
+
JS-root без `.vue` отримує дефолтний baseline без `plugins`/`ignorers` (backward-compatible). Обидва файли копіюються idempotent — наявний `stryker.config.mjs` / `stryker-vue-macros-ignorer.mjs` не перетирається.
|
|
16
|
+
|
|
17
|
+
**Augment існуючого config.** Якщо у Vue JS-root `stryker.config.mjs` **уже лежить** (наприклад, після апгрейду з версії без Vue-підтримки), концерн `stryker_config` точково вставляє у наявний файл `plugins: [..., './stryker-vue-macros-ignorer.mjs']` і `ignorers: ['vue-macros']`, зберігши решту полів і коментарів. Редагування — string-splice за AST-аналізом (oxc-parser — лише для пошуку offsets, не для re-serialize), тож форматування й коментарі не переписуються; idempotent — повторний `fix test` не дублює entries. Якщо `export default` — **не** object-literal (factory/функція/змінна) або масиви динамічні (spread/computed), augment пропускається з вимогою додати плагін вручну згідно [`stryker.config.vue.baseline.mjs`](./data/stryker_config/stryker.config.vue.baseline.mjs).
|
|
18
|
+
|
|
19
|
+
### `.gitignore` тест-артефактів
|
|
20
|
+
|
|
21
|
+
Концерн `stryker_config` без дублювання додає у кореневий `.gitignore` тест-патерни:
|
|
22
|
+
|
|
23
|
+
- `**/reports/stryker/` — увесь каталог Stryker-output-у (backup'и `tempDirName`, `mutation.json`, HTML/dashboard-репорти якщо додасте інші reporter-и).
|
|
24
|
+
- `**/coverage/` — весь output vitest v8 coverage (`lcov.info` + HTML `lcov-report/`). Ефемерний: регенерується кожним прогоном, фінальні метрики живуть у `COVERAGE.md`.
|
|
25
|
+
|
|
26
|
+
Це запобігає випадковому коміту build-артефактів.
|