@mutineerjs/mutineer 0.1.0 → 0.2.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/README.md +1 -13
- package/dist/runner/__tests__/discover.spec.js +1 -1
- package/dist/runner/changed.js +1 -1
- package/dist/runner/config.js +1 -1
- package/dist/runner/discover.js +119 -73
- package/dist/runner/orchestrator.js +1 -1
- package/dist/types/config.d.ts +0 -7
- package/dist/utils/normalizePath.d.ts +2 -0
- package/dist/utils/normalizePath.js +4 -0
- package/package.json +17 -9
package/README.md
CHANGED
|
@@ -48,20 +48,8 @@ Mutations are applied using Babel AST analysis, so operators inside strings and
|
|
|
48
48
|
|
|
49
49
|
## Installation
|
|
50
50
|
|
|
51
|
-
Mutineer is not yet published to npm. Install it locally by linking:
|
|
52
|
-
|
|
53
51
|
```bash
|
|
54
|
-
|
|
55
|
-
git clone <repo-url> mutineer
|
|
56
|
-
cd mutineer
|
|
57
|
-
npm install
|
|
58
|
-
npm run build
|
|
59
|
-
|
|
60
|
-
# Link globally
|
|
61
|
-
npm link
|
|
62
|
-
|
|
63
|
-
# In your project directory
|
|
64
|
-
npm link mutineer
|
|
52
|
+
npm i @mutineerjs/mutineer
|
|
65
53
|
```
|
|
66
54
|
|
|
67
55
|
## Usage
|
|
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import fssync from 'node:fs';
|
|
6
|
-
import { normalizePath } from '
|
|
6
|
+
import { normalizePath } from '../../utils/normalizePath.js';
|
|
7
7
|
import { autoDiscoverTargetsAndTests } from '../discover.js';
|
|
8
8
|
// Mock Vite server creation to avoid opening a real port during tests
|
|
9
9
|
vi.mock('vite', async () => {
|
package/dist/runner/changed.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
|
-
import { normalizePath } from '
|
|
5
|
+
import { normalizePath } from '../utils/normalizePath.js';
|
|
6
6
|
import { createLogger } from '../utils/logger.js';
|
|
7
7
|
const log = createLogger('changed');
|
|
8
8
|
// Constants
|
package/dist/runner/config.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import { loadConfigFromFile } from 'vite';
|
|
5
4
|
import { createLogger } from '../utils/logger.js';
|
|
6
5
|
// Constants
|
|
7
6
|
const CONFIG_FILENAMES = [
|
|
@@ -84,6 +83,7 @@ export async function loadMutineerConfig(cwd, configPath) {
|
|
|
84
83
|
*/
|
|
85
84
|
async function loadTypeScriptConfig(filePath) {
|
|
86
85
|
try {
|
|
86
|
+
const { loadConfigFromFile } = await import('vite');
|
|
87
87
|
const loaded = await loadConfigFromFile(VITE_CONFIG_OPTIONS, filePath);
|
|
88
88
|
return loaded?.config ?? {};
|
|
89
89
|
}
|
package/dist/runner/discover.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
3
4
|
import fg from 'fast-glob';
|
|
4
|
-
import {
|
|
5
|
+
import { normalizePath } from '../utils/normalizePath.js';
|
|
5
6
|
import { createLogger } from '../utils/logger.js';
|
|
6
7
|
const TEST_PATTERNS_DEFAULT = [
|
|
7
8
|
'**/*.test.[jt]s?(x)',
|
|
@@ -34,38 +35,6 @@ function safeRead(file) {
|
|
|
34
35
|
function isValidResolvedPath(resolved) {
|
|
35
36
|
return typeof resolved === 'string' && resolved.length > 0;
|
|
36
37
|
}
|
|
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
38
|
function isUnder(anyAbs, rootsAbs) {
|
|
70
39
|
const n = normalizePath(anyAbs);
|
|
71
40
|
return rootsAbs.some((r) => n.startsWith(normalizePath(r)));
|
|
@@ -82,20 +51,6 @@ function extractImportSpecs(code) {
|
|
|
82
51
|
}
|
|
83
52
|
return out;
|
|
84
53
|
}
|
|
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
54
|
/**
|
|
100
55
|
* Check if a path matches any of the exclude patterns.
|
|
101
56
|
* Patterns are matched against the path relative to root.
|
|
@@ -117,35 +72,34 @@ function isExcludedPath(absPath, rootAbs, excludePatterns) {
|
|
|
117
72
|
: rel.startsWith(pattern);
|
|
118
73
|
});
|
|
119
74
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Resolver strategies
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
/**
|
|
79
|
+
* Create a Vite-based resolver using a dev server for alias/tsconfig path resolution.
|
|
80
|
+
* Returns the resolver function and a cleanup function to close the server.
|
|
81
|
+
*/
|
|
82
|
+
async function createViteResolver(rootAbs, exts) {
|
|
83
|
+
const { createServer } = await import('vite');
|
|
84
|
+
// Load Vue plugin if needed
|
|
85
|
+
let plugins = [];
|
|
86
|
+
if (exts.has('.vue')) {
|
|
87
|
+
try {
|
|
88
|
+
const mod = await import('@vitejs/plugin-vue');
|
|
89
|
+
const vue = mod.default ?? mod;
|
|
90
|
+
plugins = typeof vue === 'function' ? [vue()] : [];
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
94
|
+
log.warn(`Unable to load @vitejs/plugin-vue; Vue SFC imports may fail to resolve (${detail})`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
141
97
|
const quietLogger = {
|
|
142
98
|
hasWarned: false,
|
|
143
99
|
info() { },
|
|
144
100
|
warn() { },
|
|
145
101
|
warnOnce() { },
|
|
146
102
|
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
103
|
if (typeof msg === 'string' &&
|
|
150
104
|
msg.includes('WebSocket server error') &&
|
|
151
105
|
msg.includes('listen EPERM'))
|
|
@@ -165,6 +119,98 @@ export async function autoDiscoverTargetsAndTests(root, cfg) {
|
|
|
165
119
|
server: { middlewareMode: true, hmr: false },
|
|
166
120
|
plugins,
|
|
167
121
|
});
|
|
122
|
+
const resolve = async (specOrAbs, importer) => {
|
|
123
|
+
if (path.isAbsolute(specOrAbs))
|
|
124
|
+
return normalizePath(path.resolve(specOrAbs));
|
|
125
|
+
try {
|
|
126
|
+
const resolved = await server.pluginContainer.resolveId(specOrAbs, importer);
|
|
127
|
+
let candidateId;
|
|
128
|
+
if (isValidResolvedPath(resolved)) {
|
|
129
|
+
candidateId = resolved;
|
|
130
|
+
}
|
|
131
|
+
else if (resolved && typeof resolved === 'object' && 'id' in resolved) {
|
|
132
|
+
const { id } = resolved;
|
|
133
|
+
if (isValidResolvedPath(id))
|
|
134
|
+
candidateId = id;
|
|
135
|
+
}
|
|
136
|
+
if (candidateId) {
|
|
137
|
+
const q = candidateId.indexOf('?');
|
|
138
|
+
return normalizePath(q >= 0 ? candidateId.slice(0, q) : candidateId);
|
|
139
|
+
}
|
|
140
|
+
return normalizePath(specOrAbs);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return normalizePath(specOrAbs);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
return { resolve, cleanup: () => server.close() };
|
|
147
|
+
}
|
|
148
|
+
const SUPPORTED_EXTENSIONS = ['.ts', '.js', '.tsx', '.jsx', '.vue'];
|
|
149
|
+
/**
|
|
150
|
+
* Create a Node-based resolver using createRequire for basic module resolution.
|
|
151
|
+
* Used as a fallback when vite is not installed.
|
|
152
|
+
*/
|
|
153
|
+
function createNodeResolver() {
|
|
154
|
+
const resolve = async (specOrAbs, importer) => {
|
|
155
|
+
if (path.isAbsolute(specOrAbs))
|
|
156
|
+
return normalizePath(path.resolve(specOrAbs));
|
|
157
|
+
// Skip bare specifiers (packages) — we only care about relative imports
|
|
158
|
+
if (!specOrAbs.startsWith('.'))
|
|
159
|
+
return normalizePath(specOrAbs);
|
|
160
|
+
if (!importer)
|
|
161
|
+
return normalizePath(specOrAbs);
|
|
162
|
+
const require = createRequire(importer);
|
|
163
|
+
try {
|
|
164
|
+
return normalizePath(require.resolve(specOrAbs));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Try with different extensions
|
|
168
|
+
for (const ext of SUPPORTED_EXTENSIONS) {
|
|
169
|
+
try {
|
|
170
|
+
return normalizePath(require.resolve(specOrAbs + ext));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return normalizePath(specOrAbs);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
return { resolve, cleanup: async () => { } };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Try to create a Vite resolver, falling back to Node resolver if vite is not available.
|
|
183
|
+
*/
|
|
184
|
+
async function createResolver(rootAbs, exts) {
|
|
185
|
+
try {
|
|
186
|
+
return await createViteResolver(rootAbs, exts);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
log.debug('Vite not available, using Node module resolution for discovery');
|
|
190
|
+
return createNodeResolver();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export async function autoDiscoverTargetsAndTests(root, cfg) {
|
|
194
|
+
const rootAbs = path.resolve(root);
|
|
195
|
+
const sourceRoots = toArray(cfg.source ?? 'src').map((s) => path.resolve(rootAbs, s));
|
|
196
|
+
const exts = new Set(toArray(cfg.extensions ?? EXT_DEFAULT));
|
|
197
|
+
const testGlobs = toArray(cfg.testPatterns ?? TEST_PATTERNS_DEFAULT);
|
|
198
|
+
const excludePatterns = toArray(cfg.excludePaths);
|
|
199
|
+
// Build ignore patterns for fast-glob
|
|
200
|
+
const defaultIgnore = ['**/node_modules/**', '**/dist/**', '**/.*/**'];
|
|
201
|
+
const userIgnore = excludePatterns.map((p) => p.endsWith('**') ? p : `${p}/**`);
|
|
202
|
+
const ignore = [...defaultIgnore, ...userIgnore];
|
|
203
|
+
// 1) locate tests on disk (absolute paths)
|
|
204
|
+
const tests = await fg(testGlobs, {
|
|
205
|
+
cwd: rootAbs,
|
|
206
|
+
absolute: true,
|
|
207
|
+
ignore,
|
|
208
|
+
});
|
|
209
|
+
if (!tests.length)
|
|
210
|
+
return { targets: [], testMap: new Map() };
|
|
211
|
+
const testSet = new Set(tests.map((t) => normalizePath(t)));
|
|
212
|
+
// 2) Create resolver (Vite if available, otherwise Node-based fallback)
|
|
213
|
+
const { resolve, cleanup } = await createResolver(rootAbs, exts);
|
|
168
214
|
const targets = new Map();
|
|
169
215
|
const testMap = new Map();
|
|
170
216
|
const contentCache = new Map();
|
|
@@ -211,7 +257,7 @@ export async function autoDiscoverTargetsAndTests(root, cfg) {
|
|
|
211
257
|
const cacheKey = `${absFile}\0${spec}`;
|
|
212
258
|
let resolved = resolveCache.get(cacheKey);
|
|
213
259
|
if (!resolved) {
|
|
214
|
-
resolved = await
|
|
260
|
+
resolved = await resolve(spec, absFile);
|
|
215
261
|
resolveCache.set(cacheKey, resolved);
|
|
216
262
|
}
|
|
217
263
|
// vite ids could be URLs; ensure we turn into absolute disk path when possible
|
|
@@ -237,7 +283,7 @@ export async function autoDiscoverTargetsAndTests(root, cfg) {
|
|
|
237
283
|
for (const spec of extractImportSpecs(code)) {
|
|
238
284
|
if (!spec)
|
|
239
285
|
continue;
|
|
240
|
-
const resolved = await
|
|
286
|
+
const resolved = await resolve(spec, testAbs);
|
|
241
287
|
const next = path.isAbsolute(resolved)
|
|
242
288
|
? resolved
|
|
243
289
|
: normalizePath(path.resolve(rootAbs, resolved));
|
|
@@ -253,6 +299,6 @@ export async function autoDiscoverTargetsAndTests(root, cfg) {
|
|
|
253
299
|
return { targets: Array.from(targets.values()), testMap };
|
|
254
300
|
}
|
|
255
301
|
finally {
|
|
256
|
-
await
|
|
302
|
+
await cleanup();
|
|
257
303
|
}
|
|
258
304
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import os from 'node:os';
|
|
14
|
-
import { normalizePath } from '
|
|
14
|
+
import { normalizePath } from '../utils/normalizePath.js';
|
|
15
15
|
import { render } from 'ink';
|
|
16
16
|
import { createElement } from 'react';
|
|
17
17
|
import { autoDiscoverTargetsAndTests } from './discover.js';
|
package/dist/types/config.d.ts
CHANGED
|
@@ -19,13 +19,6 @@ export interface MutineerConfig {
|
|
|
19
19
|
readonly testPatterns?: readonly string[];
|
|
20
20
|
readonly extensions?: readonly string[];
|
|
21
21
|
readonly autoDiscover?: boolean;
|
|
22
|
-
/**
|
|
23
|
-
* Control how Vitest output is handled for mutant runs:
|
|
24
|
-
* - 'mute' (default) suppresses all output
|
|
25
|
-
* - 'minimal' echoes only pass/fail summaries
|
|
26
|
-
* - 'inherit' streams full Vitest output to the CLI
|
|
27
|
-
*/
|
|
28
|
-
readonly mutantOutput?: 'mute' | 'minimal' | 'inherit';
|
|
29
22
|
readonly minKillPercent?: number;
|
|
30
23
|
/** Preferred test runner (defaults to vitest) */
|
|
31
24
|
readonly runner?: 'vitest' | 'jest';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutineerjs/mutineer",
|
|
3
|
-
"version": "v0.
|
|
3
|
+
"version": "v0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,11 @@
|
|
|
27
27
|
"mutate": "MUTINEER_DEBUG=0 tsx src/bin/mutineer.ts --config mutineer.config.ts"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@babel/parser": "^7.28.4",
|
|
31
|
+
"@babel/traverse": "^7.28.4",
|
|
32
|
+
"@babel/types": "^7.28.4",
|
|
30
33
|
"chalk": "^5.6.2",
|
|
34
|
+
"fast-glob": "^3.3.3",
|
|
31
35
|
"ink": "^5.2.1",
|
|
32
36
|
"ink-spinner": "^5.0.0",
|
|
33
37
|
"magic-string": "^0.30.9",
|
|
@@ -35,25 +39,29 @@
|
|
|
35
39
|
"tsx": "^4.20.6"
|
|
36
40
|
},
|
|
37
41
|
"peerDependencies": {
|
|
42
|
+
"@vitejs/plugin-vue": "^5.1.4",
|
|
43
|
+
"@vitest/coverage-v8": "^4.0.15",
|
|
38
44
|
"@vue/compiler-sfc": ">=3.4.0",
|
|
39
|
-
"esbuild": "^0.25.10"
|
|
45
|
+
"esbuild": "^0.25.10",
|
|
46
|
+
"vite": "^6.3.6",
|
|
47
|
+
"vitest": "^4.0.15"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@vitejs/plugin-vue": { "optional": true },
|
|
51
|
+
"@vitest/coverage-v8": { "optional": true },
|
|
52
|
+
"@vue/compiler-sfc": { "optional": true },
|
|
53
|
+
"vitest": { "optional": true },
|
|
54
|
+
"vite": { "optional": true }
|
|
40
55
|
},
|
|
41
56
|
"devDependencies": {
|
|
42
|
-
"@babel/parser": "^7.28.4",
|
|
43
|
-
"@babel/traverse": "^7.28.4",
|
|
44
|
-
"@babel/types": "^7.28.4",
|
|
45
57
|
"@types/babel__traverse": "^7.28.0",
|
|
46
58
|
"@types/node": "^24.7.0",
|
|
47
59
|
"@types/react": "^19.2.14",
|
|
48
60
|
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
|
49
61
|
"@typescript-eslint/parser": "^8.47.0",
|
|
50
|
-
"@vitejs/plugin-vue": "^5.1.4",
|
|
51
|
-
"@vitest/coverage-v8": "^4.0.15",
|
|
52
62
|
"eslint": "^9.39.1",
|
|
53
|
-
"fast-glob": "^3.3.3",
|
|
54
63
|
"jsdom": "^27.0.0",
|
|
55
64
|
"typescript": "^5.5.4",
|
|
56
|
-
"vite": "^6.3.6",
|
|
57
65
|
"vue": "^3.5.12"
|
|
58
66
|
}
|
|
59
67
|
}
|