@mutineerjs/mutineer 0.1.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/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/admin/assets/index-B7nXq-e7.js +32 -0
- package/dist/admin/assets/index-B7nXq-e7.js.map +1 -0
- package/dist/admin/assets/index-BDQLkBUE.js +32 -0
- package/dist/admin/assets/index-BDQLkBUE.js.map +1 -0
- package/dist/admin/assets/index-DVkP-Tc7.css +1 -0
- package/dist/admin/index.html +13 -0
- package/dist/admin/server/admin.d.ts +6 -0
- package/dist/admin/server/admin.js +234 -0
- package/dist/bin/mutate-vitest.d.ts +2 -0
- package/dist/bin/mutate-vitest.js +90 -0
- package/dist/bin/mutineer.d.ts +2 -0
- package/dist/bin/mutineer.js +46 -0
- package/dist/core/__tests__/module.spec.d.ts +1 -0
- package/dist/core/__tests__/module.spec.js +6 -0
- package/dist/core/module.d.ts +11 -0
- package/dist/core/module.js +14 -0
- package/dist/core/sfc.d.ts +12 -0
- package/dist/core/sfc.js +54 -0
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.js +1 -0
- package/dist/core/variant-utils.d.ts +30 -0
- package/dist/core/variant-utils.js +54 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/mutators/__tests__/registry.spec.d.ts +1 -0
- package/dist/mutators/__tests__/registry.spec.js +43 -0
- package/dist/mutators/__tests__/utils.spec.d.ts +1 -0
- package/dist/mutators/__tests__/utils.spec.js +15 -0
- package/dist/mutators/registry.d.ts +37 -0
- package/dist/mutators/registry.js +101 -0
- package/dist/mutators/types.d.ts +39 -0
- package/dist/mutators/types.js +7 -0
- package/dist/mutators/utils.d.ts +37 -0
- package/dist/mutators/utils.js +151 -0
- package/dist/plugin/viteMutate.d.ts +15 -0
- package/dist/plugin/viteMutate.js +52 -0
- package/dist/plugin/vitest.setup.d.ts +47 -0
- package/dist/plugin/vitest.setup.js +118 -0
- package/dist/plugin/withVitest.d.ts +13 -0
- package/dist/plugin/withVitest.js +30 -0
- package/dist/runner/__tests__/discover.spec.d.ts +1 -0
- package/dist/runner/__tests__/discover.spec.js +59 -0
- package/dist/runner/__tests__/orchestrator.spec.d.ts +1 -0
- package/dist/runner/__tests__/orchestrator.spec.js +55 -0
- package/dist/runner/adapters/__tests__/jest.spec.d.ts +1 -0
- package/dist/runner/adapters/__tests__/jest.spec.js +88 -0
- package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.d.ts +1 -0
- package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.js +59 -0
- package/dist/runner/adapters/__tests__/vitest.spec.d.ts +1 -0
- package/dist/runner/adapters/__tests__/vitest.spec.js +118 -0
- package/dist/runner/adapters/index.d.ts +10 -0
- package/dist/runner/adapters/index.js +9 -0
- package/dist/runner/adapters/jest/__tests__/index.spec.d.ts +1 -0
- package/dist/runner/adapters/jest/__tests__/index.spec.js +88 -0
- package/dist/runner/adapters/jest/index.d.ts +24 -0
- package/dist/runner/adapters/jest/index.js +216 -0
- package/dist/runner/adapters/jest/worker-runtime.d.ts +37 -0
- package/dist/runner/adapters/jest/worker-runtime.js +171 -0
- package/dist/runner/adapters/jest-worker-runtime.d.ts +37 -0
- package/dist/runner/adapters/jest-worker-runtime.js +171 -0
- package/dist/runner/adapters/jest.d.ts +24 -0
- package/dist/runner/adapters/jest.js +216 -0
- package/dist/runner/adapters/types.d.ts +89 -0
- package/dist/runner/adapters/types.js +8 -0
- package/dist/runner/adapters/vitest/__tests__/index.spec.d.ts +1 -0
- package/dist/runner/adapters/vitest/__tests__/index.spec.js +118 -0
- package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.d.ts +1 -0
- package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.js +59 -0
- package/dist/runner/adapters/vitest/index.d.ts +33 -0
- package/dist/runner/adapters/vitest/index.js +267 -0
- package/dist/runner/adapters/vitest/worker-runtime.d.ts +25 -0
- package/dist/runner/adapters/vitest/worker-runtime.js +118 -0
- package/dist/runner/adapters/vitest-worker-runtime.d.ts +25 -0
- package/dist/runner/adapters/vitest-worker-runtime.js +118 -0
- package/dist/runner/adapters/vitest.d.ts +33 -0
- package/dist/runner/adapters/vitest.js +267 -0
- package/dist/runner/args.d.ts +50 -0
- package/dist/runner/args.js +123 -0
- package/dist/runner/cache.d.ts +38 -0
- package/dist/runner/cache.js +118 -0
- package/dist/runner/changed.d.ts +22 -0
- package/dist/runner/changed.js +210 -0
- package/dist/runner/cleanup.d.ts +4 -0
- package/dist/runner/cleanup.js +21 -0
- package/dist/runner/config.d.ts +13 -0
- package/dist/runner/config.js +94 -0
- package/dist/runner/discover.d.ts +7 -0
- package/dist/runner/discover.js +258 -0
- package/dist/runner/jest/__tests__/adapter.spec.d.ts +1 -0
- package/dist/runner/jest/__tests__/adapter.spec.js +110 -0
- package/dist/runner/jest/adapter.d.ts +24 -0
- package/dist/runner/jest/adapter.js +191 -0
- package/dist/runner/jest/index.d.ts +8 -0
- package/dist/runner/jest/index.js +7 -0
- package/dist/runner/jest/pool.d.ts +47 -0
- package/dist/runner/jest/pool.js +307 -0
- package/dist/runner/jest/resolver.cjs +61 -0
- package/dist/runner/jest/resolver.d.cts +11 -0
- package/dist/runner/jest/worker-runtime.d.ts +30 -0
- package/dist/runner/jest/worker-runtime.js +98 -0
- package/dist/runner/jest/worker.d.mts +1 -0
- package/dist/runner/jest/worker.mjs +55 -0
- package/dist/runner/orchestrator.d.ts +13 -0
- package/dist/runner/orchestrator.js +387 -0
- package/dist/runner/pool/__tests__/index.spec.d.ts +1 -0
- package/dist/runner/pool/__tests__/index.spec.js +83 -0
- package/dist/runner/pool/__tests__/pool-plugin.spec.d.ts +1 -0
- package/dist/runner/pool/__tests__/pool-plugin.spec.js +59 -0
- package/dist/runner/pool/__tests__/pool-redirect-loader.spec.d.ts +1 -0
- package/dist/runner/pool/__tests__/pool-redirect-loader.spec.js +78 -0
- package/dist/runner/pool/index.d.ts +8 -0
- package/dist/runner/pool/index.js +9 -0
- package/dist/runner/pool/jest/pool.d.ts +52 -0
- package/dist/runner/pool/jest/pool.js +309 -0
- package/dist/runner/pool/jest/worker.d.mts +1 -0
- package/dist/runner/pool/jest/worker.mjs +60 -0
- package/dist/runner/pool/jest-pool.d.ts +52 -0
- package/dist/runner/pool/jest-pool.js +309 -0
- package/dist/runner/pool/jest-worker.d.mts +1 -0
- package/dist/runner/pool/jest-worker.mjs +60 -0
- package/dist/runner/pool/plugin.d.ts +18 -0
- package/dist/runner/pool/plugin.js +60 -0
- package/dist/runner/pool/pool-plugin.d.ts +18 -0
- package/dist/runner/pool/pool-plugin.js +60 -0
- package/dist/runner/pool/pool-redirect-loader.d.ts +19 -0
- package/dist/runner/pool/pool-redirect-loader.js +116 -0
- package/dist/runner/pool/pool-redirect-loader.mjs +146 -0
- package/dist/runner/pool/redirect-loader.d.ts +19 -0
- package/dist/runner/pool/redirect-loader.js +116 -0
- package/dist/runner/pool/vitest/pool.d.ts +70 -0
- package/dist/runner/pool/vitest/pool.js +376 -0
- package/dist/runner/pool/vitest/worker.d.mts +15 -0
- package/dist/runner/pool/vitest/worker.mjs +96 -0
- package/dist/runner/pool/vitest-worker.d.mts +15 -0
- package/dist/runner/pool/vitest-worker.mjs +96 -0
- package/dist/runner/shared/index.d.ts +9 -0
- package/dist/runner/shared/index.js +8 -0
- package/dist/runner/shared/mutant-paths.d.ts +15 -0
- package/dist/runner/shared/mutant-paths.js +30 -0
- package/dist/runner/shared/redirect-state.d.ts +45 -0
- package/dist/runner/shared/redirect-state.js +50 -0
- package/dist/runner/shared-module-redirect.d.ts +56 -0
- package/dist/runner/shared-module-redirect.js +84 -0
- package/dist/runner/types.d.ts +88 -0
- package/dist/runner/types.js +8 -0
- package/dist/runner/variants.d.ts +21 -0
- package/dist/runner/variants.js +66 -0
- package/dist/runner/vitest/__tests__/adapter.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/adapter.spec.js +131 -0
- package/dist/runner/vitest/__tests__/plugin.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/plugin.spec.js +65 -0
- package/dist/runner/vitest/__tests__/pool.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/pool.spec.js +106 -0
- package/dist/runner/vitest/__tests__/redirect-loader.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/redirect-loader.spec.js +87 -0
- package/dist/runner/vitest/__tests__/worker-runtime.spec.d.ts +1 -0
- package/dist/runner/vitest/__tests__/worker-runtime.spec.js +75 -0
- package/dist/runner/vitest/adapter.d.ts +33 -0
- package/dist/runner/vitest/adapter.js +277 -0
- package/dist/runner/vitest/index.d.ts +11 -0
- package/dist/runner/vitest/index.js +10 -0
- package/dist/runner/vitest/plugin.d.ts +12 -0
- package/dist/runner/vitest/plugin.js +49 -0
- package/dist/runner/vitest/pool.d.ts +65 -0
- package/dist/runner/vitest/pool.js +376 -0
- package/dist/runner/vitest/redirect-loader.d.ts +30 -0
- package/dist/runner/vitest/redirect-loader.js +123 -0
- package/dist/runner/vitest/worker-runtime.d.ts +16 -0
- package/dist/runner/vitest/worker-runtime.js +105 -0
- package/dist/runner/vitest/worker.d.mts +15 -0
- package/dist/runner/vitest/worker.mjs +92 -0
- package/dist/types/api.d.ts +20 -0
- package/dist/types/api.js +1 -0
- package/dist/types/config.d.ts +48 -0
- package/dist/types/config.js +1 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.js +11 -0
- package/dist/types/mutant.d.ts +44 -0
- package/dist/types/mutant.js +7 -0
- package/dist/utils/PoolSpinner.d.ts +5 -0
- package/dist/utils/PoolSpinner.js +6 -0
- package/dist/utils/ProgressBar.d.ts +11 -0
- package/dist/utils/ProgressBar.js +9 -0
- package/dist/utils/__tests__/coverage.spec.d.ts +1 -0
- package/dist/utils/__tests__/coverage.spec.js +91 -0
- package/dist/utils/__tests__/progress.spec.d.ts +1 -0
- package/dist/utils/__tests__/progress.spec.js +50 -0
- package/dist/utils/__tests__/summary.spec.d.ts +1 -0
- package/dist/utils/__tests__/summary.spec.js +54 -0
- package/dist/utils/coverage.d.ts +57 -0
- package/dist/utils/coverage.js +204 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +18 -0
- package/dist/utils/progress.d.ts +25 -0
- package/dist/utils/progress.js +90 -0
- package/dist/utils/summary.d.ts +12 -0
- package/dist/utils/summary.js +107 -0
- package/package.json +59 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { normalizePath } from 'vite';
|
|
6
|
+
import { createLogger } from '../utils/logger.js';
|
|
7
|
+
const log = createLogger('changed');
|
|
8
|
+
// Constants
|
|
9
|
+
const NULL_SEP = '\0';
|
|
10
|
+
const DEFAULT_BASE_REF = 'main';
|
|
11
|
+
const SUPPORTED_EXTENSIONS = ['.ts', '.js', '.vue'];
|
|
12
|
+
const TEST_FILE_PATTERN = /\.(test|spec)\.(js|ts|vue|mjs|cjs)$/;
|
|
13
|
+
const IMPORT_PATTERN = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
14
|
+
const EXPORT_PATTERN = /export\s+(?:\{[^}]*\}|\*)\s+from\s+['"]([^'"]+)['"]/g;
|
|
15
|
+
const REQUIRE_PATTERN = /require\(['"]([^'"]+)['"]\)/g;
|
|
16
|
+
/**
|
|
17
|
+
* Recursively resolve local file dependencies starting from a source file.
|
|
18
|
+
* Parses import/export/require statements and follows local references.
|
|
19
|
+
*
|
|
20
|
+
* @param file - Path to the file to analyze
|
|
21
|
+
* @param cwd - Working directory for resolving relative paths
|
|
22
|
+
* @param seen - Set of already-visited files (to prevent infinite recursion)
|
|
23
|
+
* @param maxDepth - Maximum depth to recurse (default: 1, meaning direct imports only)
|
|
24
|
+
* @param currentDepth - Current recursion depth
|
|
25
|
+
* @returns Array of absolute paths to resolved dependencies
|
|
26
|
+
*/
|
|
27
|
+
function resolveLocalDependencies(file, cwd, seen = new Set(), maxDepth = 1, currentDepth = 0) {
|
|
28
|
+
// Stop if we've exceeded the max depth
|
|
29
|
+
if (currentDepth >= maxDepth)
|
|
30
|
+
return [];
|
|
31
|
+
// Ignore files that no longer exist (deleted/renamed, etc.)
|
|
32
|
+
if (!fs.existsSync(file))
|
|
33
|
+
return [];
|
|
34
|
+
if (seen.has(file))
|
|
35
|
+
return [];
|
|
36
|
+
seen.add(file);
|
|
37
|
+
let content;
|
|
38
|
+
try {
|
|
39
|
+
content = fs.readFileSync(file, 'utf8');
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// If the file went missing between existsSync and read, just skip it
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const deps = [];
|
|
46
|
+
const patterns = [IMPORT_PATTERN, EXPORT_PATTERN, REQUIRE_PATTERN];
|
|
47
|
+
for (const pattern of patterns) {
|
|
48
|
+
const matches = content.matchAll(pattern);
|
|
49
|
+
for (const match of matches) {
|
|
50
|
+
const dep = match[1];
|
|
51
|
+
if (!dep.startsWith('.'))
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
const require = createRequire(file);
|
|
55
|
+
let resolvedPath;
|
|
56
|
+
// Try direct resolution first
|
|
57
|
+
try {
|
|
58
|
+
resolvedPath = require.resolve(dep);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Try different extensions if direct resolution fails
|
|
62
|
+
for (const ext of SUPPORTED_EXTENSIONS) {
|
|
63
|
+
try {
|
|
64
|
+
// Try replacing existing extension
|
|
65
|
+
resolvedPath = require.resolve(dep.replace(/\.(js|ts|vue)$/, ext));
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
try {
|
|
70
|
+
// Try adding extension
|
|
71
|
+
resolvedPath = require.resolve(dep + ext);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!resolvedPath) {
|
|
81
|
+
log.warn(`Could not resolve path for dependency ${dep} in ${file}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Skip anything outside the repo/cwd, node_modules, tests, or missing
|
|
85
|
+
if (!resolvedPath.startsWith(cwd) ||
|
|
86
|
+
resolvedPath.includes('node_modules'))
|
|
87
|
+
continue;
|
|
88
|
+
if (TEST_FILE_PATTERN.test(resolvedPath))
|
|
89
|
+
continue;
|
|
90
|
+
if (!fs.existsSync(resolvedPath))
|
|
91
|
+
continue;
|
|
92
|
+
deps.push(normalizePath(resolvedPath));
|
|
93
|
+
deps.push(...resolveLocalDependencies(resolvedPath, cwd, seen, maxDepth, currentDepth + 1));
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
log.warn(`Failed to resolve dependency ${dep} in ${file}: ${err}`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return [...new Set(deps)];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse output from git commands using null-separator format.
|
|
105
|
+
*/
|
|
106
|
+
function splitZ(s) {
|
|
107
|
+
return s.split(NULL_SEP).filter(Boolean);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Execute a git command and return the output or null if command fails.
|
|
111
|
+
*/
|
|
112
|
+
function runGitCommand(cwd, args) {
|
|
113
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
114
|
+
return result.status === 0 ? result.stdout : null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Find the Git repository root from a given working directory.
|
|
118
|
+
* Tries multiple candidate directories (passed cwd, INIT_CWD, process.cwd(), PWD).
|
|
119
|
+
*/
|
|
120
|
+
function findRepoContext(passedCwd) {
|
|
121
|
+
const candidates = [
|
|
122
|
+
passedCwd,
|
|
123
|
+
process.env.INIT_CWD,
|
|
124
|
+
process.cwd(),
|
|
125
|
+
process.env.PWD,
|
|
126
|
+
].filter(Boolean);
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
for (const candidate of candidates) {
|
|
129
|
+
const resolved = path.resolve(candidate);
|
|
130
|
+
if (seen.has(resolved))
|
|
131
|
+
continue;
|
|
132
|
+
seen.add(resolved);
|
|
133
|
+
const output = runGitCommand(resolved, ['rev-parse', '--show-toplevel']);
|
|
134
|
+
if (output) {
|
|
135
|
+
return { repoRoot: output.trim(), workingDir: resolved };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* List all changed files in a Git repository.
|
|
142
|
+
*
|
|
143
|
+
* Returns files that are:
|
|
144
|
+
* - Committed but not yet in baseRef (compared to baseRef...HEAD)
|
|
145
|
+
* - Modified but not yet committed (compared to HEAD)
|
|
146
|
+
* - Untracked but not ignored
|
|
147
|
+
*
|
|
148
|
+
* When includeDeps is true, also includes local dependencies of changed files.
|
|
149
|
+
*
|
|
150
|
+
* @param cwd - Working directory (will search for repo root if needed)
|
|
151
|
+
* @param options - Configuration options
|
|
152
|
+
* @returns Array of absolute paths to changed files
|
|
153
|
+
*/
|
|
154
|
+
export function listChangedFiles(cwd, options = {}) {
|
|
155
|
+
const repo = findRepoContext(cwd);
|
|
156
|
+
if (!repo) {
|
|
157
|
+
const msg = `Mutineer could not locate a Git repository. Checked: ${cwd}, ${process.env.INIT_CWD}, ${process.cwd()}`;
|
|
158
|
+
if (!options.quiet)
|
|
159
|
+
log.warn(msg);
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const { repoRoot, workingDir } = repo;
|
|
163
|
+
const baseRef = options.baseRef || DEFAULT_BASE_REF;
|
|
164
|
+
// Fetch changed files from git
|
|
165
|
+
const diffCommittedOutput = runGitCommand(repoRoot, [
|
|
166
|
+
'diff',
|
|
167
|
+
'-z',
|
|
168
|
+
'--name-only',
|
|
169
|
+
'--diff-filter=ACMR',
|
|
170
|
+
`${baseRef}...HEAD`,
|
|
171
|
+
]);
|
|
172
|
+
const diffWorkingOutput = runGitCommand(repoRoot, [
|
|
173
|
+
'diff',
|
|
174
|
+
'-z',
|
|
175
|
+
'--name-only',
|
|
176
|
+
'--diff-filter=ACMR',
|
|
177
|
+
'HEAD',
|
|
178
|
+
]);
|
|
179
|
+
const untrackedOutput = runGitCommand(repoRoot, [
|
|
180
|
+
'ls-files',
|
|
181
|
+
'-z',
|
|
182
|
+
'--others',
|
|
183
|
+
'--exclude-standard',
|
|
184
|
+
]);
|
|
185
|
+
// If all git commands failed, return empty
|
|
186
|
+
if (!diffCommittedOutput && !diffWorkingOutput && !untrackedOutput) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
// Merge all changed files from different git sources
|
|
190
|
+
const rels = [
|
|
191
|
+
...(diffCommittedOutput ? splitZ(diffCommittedOutput) : []),
|
|
192
|
+
...(diffWorkingOutput ? splitZ(diffWorkingOutput) : []),
|
|
193
|
+
...(untrackedOutput ? splitZ(untrackedOutput) : []),
|
|
194
|
+
];
|
|
195
|
+
const out = new Set();
|
|
196
|
+
for (const p of rels) {
|
|
197
|
+
const rel = p.replace(/^\.?\//, '');
|
|
198
|
+
const abs = normalizePath(path.isAbsolute(rel) ? path.normalize(rel) : path.resolve(repoRoot, rel));
|
|
199
|
+
// Skip deleted / missing
|
|
200
|
+
if (!fs.existsSync(abs))
|
|
201
|
+
continue;
|
|
202
|
+
out.add(abs);
|
|
203
|
+
if (options.includeDeps && /\.(js|ts|vue|mjs|cjs)$/.test(abs)) {
|
|
204
|
+
const maxDepth = options.maxDepth ?? 1;
|
|
205
|
+
const deps = resolveLocalDependencies(abs, workingDir, new Set(), maxDepth);
|
|
206
|
+
deps.forEach((dep) => out.add(dep));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return [...out];
|
|
210
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
/**
|
|
3
|
+
* Clean up all __mutineer__ temp directories created during mutation testing.
|
|
4
|
+
*/
|
|
5
|
+
export async function cleanupMutineerDirs(cwd) {
|
|
6
|
+
const glob = await import('fast-glob');
|
|
7
|
+
const dirs = await glob.default('**/__mutineer__', {
|
|
8
|
+
cwd,
|
|
9
|
+
onlyDirectories: true,
|
|
10
|
+
absolute: true,
|
|
11
|
+
ignore: ['**/node_modules/**'],
|
|
12
|
+
});
|
|
13
|
+
for (const dir of dirs) {
|
|
14
|
+
try {
|
|
15
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Ignore cleanup errors
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MutineerConfig } from '../index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Loads the Mutineer configuration file.
|
|
4
|
+
*
|
|
5
|
+
* Searches for mutineer.config.ts/js/mjs in the project root, or uses a user-provided path.
|
|
6
|
+
* TypeScript configs are loaded via Vite's loader; JS/MJS configs are imported directly.
|
|
7
|
+
*
|
|
8
|
+
* @param cwd - Current working directory to search from
|
|
9
|
+
* @param configPath - Optional explicit path to the config file
|
|
10
|
+
* @returns Loaded MutineerConfig (may be partial; defaults applied by caller)
|
|
11
|
+
* @throws Error if no config file is found or if loading fails
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadMutineerConfig(cwd: string, configPath?: string): Promise<MutineerConfig>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { loadConfigFromFile } from 'vite';
|
|
5
|
+
import { createLogger } from '../utils/logger.js';
|
|
6
|
+
// Constants
|
|
7
|
+
const CONFIG_FILENAMES = [
|
|
8
|
+
'mutineer.config.ts',
|
|
9
|
+
'mutineer.config.js',
|
|
10
|
+
'mutineer.config.mjs',
|
|
11
|
+
];
|
|
12
|
+
const VITE_CONFIG_OPTIONS = { command: 'build', mode: 'development' };
|
|
13
|
+
const log = createLogger('config');
|
|
14
|
+
/**
|
|
15
|
+
* Attempt to load and parse a JavaScript/TypeScript module.
|
|
16
|
+
* @param filePath - Path to the module file
|
|
17
|
+
* @returns The default export or the module itself
|
|
18
|
+
* @throws Error if module cannot be loaded
|
|
19
|
+
*/
|
|
20
|
+
async function loadModule(filePath) {
|
|
21
|
+
const moduleUrl = pathToFileURL(filePath).href;
|
|
22
|
+
const mod = await import(moduleUrl);
|
|
23
|
+
return mod.default || mod;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validate that the loaded configuration has the expected shape.
|
|
27
|
+
* While we allow partial configs (defaults applied elsewhere), this ensures
|
|
28
|
+
* the structure is an object and not some other unexpected type.
|
|
29
|
+
*/
|
|
30
|
+
function validateConfig(config) {
|
|
31
|
+
if (typeof config !== 'object' && config === null) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// Config is valid even if empty; defaults are applied elsewhere.
|
|
35
|
+
// For stricter validation, property-level checks could be added here.
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Loads the Mutineer configuration file.
|
|
40
|
+
*
|
|
41
|
+
* Searches for mutineer.config.ts/js/mjs in the project root, or uses a user-provided path.
|
|
42
|
+
* TypeScript configs are loaded via Vite's loader; JS/MJS configs are imported directly.
|
|
43
|
+
*
|
|
44
|
+
* @param cwd - Current working directory to search from
|
|
45
|
+
* @param configPath - Optional explicit path to the config file
|
|
46
|
+
* @returns Loaded MutineerConfig (may be partial; defaults applied by caller)
|
|
47
|
+
* @throws Error if no config file is found or if loading fails
|
|
48
|
+
*/
|
|
49
|
+
export async function loadMutineerConfig(cwd, configPath) {
|
|
50
|
+
// Build list of candidate file paths to check
|
|
51
|
+
const candidates = configPath
|
|
52
|
+
? [path.resolve(cwd, configPath)]
|
|
53
|
+
: CONFIG_FILENAMES.map((filename) => path.join(cwd, filename));
|
|
54
|
+
// Find the first config file that exists
|
|
55
|
+
const configFile = candidates.find((f) => fs.existsSync(f));
|
|
56
|
+
if (!configFile) {
|
|
57
|
+
const suggestion = configPath
|
|
58
|
+
? `No config found at ${configPath}`
|
|
59
|
+
: `No config found in ${cwd}. Expected one of: ${CONFIG_FILENAMES.join(', ')}`;
|
|
60
|
+
throw new Error(suggestion);
|
|
61
|
+
}
|
|
62
|
+
log.debug(`Loading config from: ${configFile}`);
|
|
63
|
+
try {
|
|
64
|
+
// Load TypeScript config via Vite, or import JS/MJS directly
|
|
65
|
+
const loadedConfig = configFile.endsWith('.ts')
|
|
66
|
+
? await loadTypeScriptConfig(configFile)
|
|
67
|
+
: await loadModule(configFile);
|
|
68
|
+
if (!validateConfig(loadedConfig)) {
|
|
69
|
+
throw new Error(`Config file does not export a valid configuration object: ${configFile}`);
|
|
70
|
+
}
|
|
71
|
+
log.debug('Config loaded successfully:', loadedConfig);
|
|
72
|
+
return loadedConfig;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
76
|
+
throw new Error(`Failed to load config from ${configFile}: ${message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Load a TypeScript config file using Vite's loader.
|
|
81
|
+
* @param filePath - Path to the .ts config file
|
|
82
|
+
* @returns The loaded configuration object
|
|
83
|
+
* @throws Error if loading fails
|
|
84
|
+
*/
|
|
85
|
+
async function loadTypeScriptConfig(filePath) {
|
|
86
|
+
try {
|
|
87
|
+
const loaded = await loadConfigFromFile(VITE_CONFIG_OPTIONS, filePath);
|
|
88
|
+
return loaded?.config ?? {};
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
92
|
+
throw new Error(`Cannot load TypeScript config. Ensure 'vite' is installed or rename to .js/.mjs:\n${message}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MutateTarget, MutineerConfig } from '../types/config.js';
|
|
2
|
+
export type TestMap = Map<string, Set<string>>;
|
|
3
|
+
export interface DiscoveryResult {
|
|
4
|
+
readonly targets: MutateTarget[];
|
|
5
|
+
readonly testMap: TestMap;
|
|
6
|
+
}
|
|
7
|
+
export declare function autoDiscoverTargetsAndTests(root: string, cfg: MutineerConfig): Promise<DiscoveryResult>;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { createServer, normalizePath, } from 'vite';
|
|
5
|
+
import { createLogger } from '../utils/logger.js';
|
|
6
|
+
const TEST_PATTERNS_DEFAULT = [
|
|
7
|
+
'**/*.test.[jt]s?(x)',
|
|
8
|
+
'**/*.spec.[jt]s?(x)',
|
|
9
|
+
];
|
|
10
|
+
const EXT_DEFAULT = ['.vue', '.ts', '.js', '.tsx', '.jsx'];
|
|
11
|
+
// naive but fast: matches `import 'x'` and `import ... from 'x'`
|
|
12
|
+
const IMPORT_RE = /import\s+(?:[^'\"]*from\s+)?['\"]([^'\"]+)['\"]/g;
|
|
13
|
+
const log = createLogger('discover');
|
|
14
|
+
const MAX_CRAWL_DEPTH = 12;
|
|
15
|
+
function toArray(v) {
|
|
16
|
+
if (Array.isArray(v))
|
|
17
|
+
return [...v];
|
|
18
|
+
if (v === null || v === undefined)
|
|
19
|
+
return [];
|
|
20
|
+
return [v];
|
|
21
|
+
}
|
|
22
|
+
function safeRead(file) {
|
|
23
|
+
try {
|
|
24
|
+
return fs.readFileSync(file, 'utf8');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Type guard to check if a resolved module is a valid absolute path.
|
|
32
|
+
* Used to validate Vite plugin resolution results.
|
|
33
|
+
*/
|
|
34
|
+
function isValidResolvedPath(resolved) {
|
|
35
|
+
return typeof resolved === 'string' && resolved.length > 0;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve an import spec to a normalized id (query stripped).
|
|
39
|
+
* Falls back to the original spec when resolution fails.
|
|
40
|
+
*/
|
|
41
|
+
async function resolveId(server, specOrAbs, importer) {
|
|
42
|
+
if (path.isAbsolute(specOrAbs))
|
|
43
|
+
return normalizePath(path.resolve(specOrAbs));
|
|
44
|
+
try {
|
|
45
|
+
const resolved = await server.pluginContainer.resolveId(specOrAbs, importer);
|
|
46
|
+
// Extract id from ResolveId result (can be string or object with id property)
|
|
47
|
+
let candidateId;
|
|
48
|
+
if (isValidResolvedPath(resolved)) {
|
|
49
|
+
candidateId = resolved;
|
|
50
|
+
}
|
|
51
|
+
else if (resolved && typeof resolved === 'object' && 'id' in resolved) {
|
|
52
|
+
const { id } = resolved;
|
|
53
|
+
if (isValidResolvedPath(id))
|
|
54
|
+
candidateId = id;
|
|
55
|
+
}
|
|
56
|
+
// Strip query string and normalize path
|
|
57
|
+
if (candidateId) {
|
|
58
|
+
const q = candidateId.indexOf('?');
|
|
59
|
+
return normalizePath(q >= 0 ? candidateId.slice(0, q) : candidateId);
|
|
60
|
+
}
|
|
61
|
+
// Fallback to original spec if resolution failed
|
|
62
|
+
return normalizePath(specOrAbs);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Resolver may throw for virtual or unsupported ids; fall back to spec
|
|
66
|
+
return normalizePath(specOrAbs);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function isUnder(anyAbs, rootsAbs) {
|
|
70
|
+
const n = normalizePath(anyAbs);
|
|
71
|
+
return rootsAbs.some((r) => n.startsWith(normalizePath(r)));
|
|
72
|
+
}
|
|
73
|
+
function looksLikeVueScriptSetup(p) {
|
|
74
|
+
return p.endsWith('.vue');
|
|
75
|
+
}
|
|
76
|
+
/** Extract import specs from source code (fast, regex-based). */
|
|
77
|
+
function extractImportSpecs(code) {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const m of code.matchAll(IMPORT_RE)) {
|
|
80
|
+
if (m && m[1])
|
|
81
|
+
out.push(m[1]);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
async function loadPlugins(exts) {
|
|
86
|
+
if (!exts.has('.vue'))
|
|
87
|
+
return [];
|
|
88
|
+
try {
|
|
89
|
+
const mod = await import('@vitejs/plugin-vue');
|
|
90
|
+
const vue = mod.default ?? mod;
|
|
91
|
+
return typeof vue === 'function' ? [vue()] : [];
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
95
|
+
log.warn(`Unable to load @vitejs/plugin-vue; Vue SFC imports may fail to resolve (${detail})`);
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if a path matches any of the exclude patterns.
|
|
101
|
+
* Patterns are matched against the path relative to root.
|
|
102
|
+
*/
|
|
103
|
+
function isExcludedPath(absPath, rootAbs, excludePatterns) {
|
|
104
|
+
if (!excludePatterns.length)
|
|
105
|
+
return false;
|
|
106
|
+
const rel = path.relative(rootAbs, absPath);
|
|
107
|
+
return excludePatterns.some((pattern) => {
|
|
108
|
+
// Support simple prefix matching (e.g., 'admin' matches 'admin/foo.ts')
|
|
109
|
+
if (!pattern.includes('*')) {
|
|
110
|
+
return rel.startsWith(pattern) || rel.startsWith(pattern + path.sep);
|
|
111
|
+
}
|
|
112
|
+
// For glob patterns, use fast-glob's isDynamicPattern check and simple matching
|
|
113
|
+
return fg.isDynamicPattern(pattern)
|
|
114
|
+
? new RegExp('^' +
|
|
115
|
+
pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') +
|
|
116
|
+
'(/|$)').test(rel)
|
|
117
|
+
: rel.startsWith(pattern);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export async function autoDiscoverTargetsAndTests(root, cfg) {
|
|
121
|
+
const rootAbs = path.resolve(root);
|
|
122
|
+
const sourceRoots = toArray(cfg.source ?? 'src').map((s) => path.resolve(rootAbs, s));
|
|
123
|
+
const exts = new Set(toArray(cfg.extensions ?? EXT_DEFAULT));
|
|
124
|
+
const testGlobs = toArray(cfg.testPatterns ?? TEST_PATTERNS_DEFAULT);
|
|
125
|
+
const excludePatterns = toArray(cfg.excludePaths);
|
|
126
|
+
// Build ignore patterns for fast-glob
|
|
127
|
+
const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/.*/**'];
|
|
128
|
+
const userIgnore = excludePatterns.map((p) => p.endsWith('**') ? p : `${p}/**`);
|
|
129
|
+
const ignore = [...defaultIgnore, ...userIgnore];
|
|
130
|
+
// 1) locate tests on disk (absolute paths)
|
|
131
|
+
const tests = await fg(testGlobs, {
|
|
132
|
+
cwd: rootAbs,
|
|
133
|
+
absolute: true,
|
|
134
|
+
ignore,
|
|
135
|
+
});
|
|
136
|
+
if (!tests.length)
|
|
137
|
+
return { targets: [], testMap: new Map() };
|
|
138
|
+
const testSet = new Set(tests.map((t) => normalizePath(t)));
|
|
139
|
+
// 2) Vite server for alias/tsconfig path resolution (no execution)
|
|
140
|
+
const plugins = await loadPlugins(exts);
|
|
141
|
+
const quietLogger = {
|
|
142
|
+
hasWarned: false,
|
|
143
|
+
info() { },
|
|
144
|
+
warn() { },
|
|
145
|
+
warnOnce() { },
|
|
146
|
+
error(msg) {
|
|
147
|
+
// Vite logs a "WebSocket server error" when it cannot bind the HMR port;
|
|
148
|
+
// since we run in middleware mode and do not need HMR, silence that noise.
|
|
149
|
+
if (typeof msg === 'string' &&
|
|
150
|
+
msg.includes('WebSocket server error') &&
|
|
151
|
+
msg.includes('listen EPERM'))
|
|
152
|
+
return;
|
|
153
|
+
log.error(typeof msg === 'string' ? msg : String(msg));
|
|
154
|
+
},
|
|
155
|
+
clearScreen() { },
|
|
156
|
+
hasErrorLogged() {
|
|
157
|
+
return false;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
const server = await createServer({
|
|
161
|
+
root: rootAbs,
|
|
162
|
+
logLevel: 'error',
|
|
163
|
+
customLogger: quietLogger,
|
|
164
|
+
clearScreen: false,
|
|
165
|
+
server: { middlewareMode: true, hmr: false },
|
|
166
|
+
plugins,
|
|
167
|
+
});
|
|
168
|
+
const targets = new Map();
|
|
169
|
+
const testMap = new Map();
|
|
170
|
+
const contentCache = new Map();
|
|
171
|
+
const resolveCache = new Map(); // key: importer\0spec -> resolved id
|
|
172
|
+
async function crawl(absFile, depth, seen, currentTestAbs) {
|
|
173
|
+
if (depth > MAX_CRAWL_DEPTH)
|
|
174
|
+
return; // sane guard for huge graphs
|
|
175
|
+
const key = normalizePath(absFile);
|
|
176
|
+
if (seen.has(key))
|
|
177
|
+
return;
|
|
178
|
+
seen.add(key);
|
|
179
|
+
// @todo is listing node_modules
|
|
180
|
+
// if this file is within source and has supported extension, register as target mapped to current test
|
|
181
|
+
const ext = path.extname(absFile);
|
|
182
|
+
if (exts.has(ext) &&
|
|
183
|
+
isUnder(absFile, sourceRoots) &&
|
|
184
|
+
fs.existsSync(absFile) &&
|
|
185
|
+
!testSet.has(key) &&
|
|
186
|
+
!isExcludedPath(absFile, rootAbs, excludePatterns)) {
|
|
187
|
+
if (!targets.has(key)) {
|
|
188
|
+
targets.set(key, {
|
|
189
|
+
file: absFile,
|
|
190
|
+
kind: looksLikeVueScriptSetup(absFile)
|
|
191
|
+
? 'vue:script-setup'
|
|
192
|
+
: 'module',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (!testMap.has(key))
|
|
196
|
+
testMap.set(key, new Set());
|
|
197
|
+
testMap.get(key).add(currentTestAbs);
|
|
198
|
+
}
|
|
199
|
+
// read file content to find further imports (works for .vue too; imports are inside <script>)
|
|
200
|
+
let code = contentCache.get(absFile);
|
|
201
|
+
if (code === undefined) {
|
|
202
|
+
code = safeRead(absFile);
|
|
203
|
+
contentCache.set(absFile, code ?? null);
|
|
204
|
+
}
|
|
205
|
+
if (!code)
|
|
206
|
+
return;
|
|
207
|
+
// find import specs and resolve relative to absFile
|
|
208
|
+
for (const spec of extractImportSpecs(code)) {
|
|
209
|
+
if (!spec)
|
|
210
|
+
continue;
|
|
211
|
+
const cacheKey = `${absFile}\0${spec}`;
|
|
212
|
+
let resolved = resolveCache.get(cacheKey);
|
|
213
|
+
if (!resolved) {
|
|
214
|
+
resolved = await resolveId(server, spec, absFile);
|
|
215
|
+
resolveCache.set(cacheKey, resolved);
|
|
216
|
+
}
|
|
217
|
+
// vite ids could be URLs; ensure we turn into absolute disk path when possible
|
|
218
|
+
const next = path.isAbsolute(resolved)
|
|
219
|
+
? resolved
|
|
220
|
+
: normalizePath(path.resolve(rootAbs, resolved));
|
|
221
|
+
// skip node_modules and virtual ids
|
|
222
|
+
if (next.includes('/node_modules/'))
|
|
223
|
+
continue;
|
|
224
|
+
if (!path.isAbsolute(next))
|
|
225
|
+
continue;
|
|
226
|
+
await crawl(next, depth + 1, seen, currentTestAbs);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
for (const testAbs of tests) {
|
|
231
|
+
const seen = new Set();
|
|
232
|
+
// prime with the test's own direct imports
|
|
233
|
+
const code = safeRead(testAbs);
|
|
234
|
+
if (!code)
|
|
235
|
+
continue;
|
|
236
|
+
const firstHop = [];
|
|
237
|
+
for (const spec of extractImportSpecs(code)) {
|
|
238
|
+
if (!spec)
|
|
239
|
+
continue;
|
|
240
|
+
const resolved = await resolveId(server, spec, testAbs);
|
|
241
|
+
const next = path.isAbsolute(resolved)
|
|
242
|
+
? resolved
|
|
243
|
+
: normalizePath(path.resolve(rootAbs, resolved));
|
|
244
|
+
if (!path.isAbsolute(next))
|
|
245
|
+
continue;
|
|
246
|
+
firstHop.push(next);
|
|
247
|
+
}
|
|
248
|
+
log.debug(`test ${testAbs} first-hop imports ${firstHop.length}`);
|
|
249
|
+
for (const abs of firstHop) {
|
|
250
|
+
await crawl(abs, 0, seen, testAbs);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { targets: Array.from(targets.values()), testMap };
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
await server.close();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|