@mutineerjs/mutineer 0.1.1 → 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.
@@ -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 'vite';
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 () => {
@@ -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 'vite';
5
+ import { normalizePath } from '../utils/normalizePath.js';
6
6
  import { createLogger } from '../utils/logger.js';
7
7
  const log = createLogger('changed');
8
8
  // Constants
@@ -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
  }
@@ -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 { createServer, normalizePath, } from 'vite';
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
- 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);
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 resolveId(server, spec, absFile);
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 resolveId(server, spec, testAbs);
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 server.close();
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 'vite';
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';
@@ -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';
@@ -0,0 +1,2 @@
1
+ /** Normalize a file path to use forward slashes (same as vite's normalizePath). */
2
+ export declare function normalizePath(p: string): string;
@@ -0,0 +1,4 @@
1
+ /** Normalize a file path to use forward slashes (same as vite's normalizePath). */
2
+ export function normalizePath(p) {
3
+ return p.replace(/\\/g, '/');
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutineerjs/mutineer",
3
- "version": "v0.1.1",
3
+ "version": "v0.2.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {