@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.
Files changed (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +218 -0
  3. package/dist/admin/assets/index-B7nXq-e7.js +32 -0
  4. package/dist/admin/assets/index-B7nXq-e7.js.map +1 -0
  5. package/dist/admin/assets/index-BDQLkBUE.js +32 -0
  6. package/dist/admin/assets/index-BDQLkBUE.js.map +1 -0
  7. package/dist/admin/assets/index-DVkP-Tc7.css +1 -0
  8. package/dist/admin/index.html +13 -0
  9. package/dist/admin/server/admin.d.ts +6 -0
  10. package/dist/admin/server/admin.js +234 -0
  11. package/dist/bin/mutate-vitest.d.ts +2 -0
  12. package/dist/bin/mutate-vitest.js +90 -0
  13. package/dist/bin/mutineer.d.ts +2 -0
  14. package/dist/bin/mutineer.js +46 -0
  15. package/dist/core/__tests__/module.spec.d.ts +1 -0
  16. package/dist/core/__tests__/module.spec.js +6 -0
  17. package/dist/core/module.d.ts +11 -0
  18. package/dist/core/module.js +14 -0
  19. package/dist/core/sfc.d.ts +12 -0
  20. package/dist/core/sfc.js +54 -0
  21. package/dist/core/types.d.ts +6 -0
  22. package/dist/core/types.js +1 -0
  23. package/dist/core/variant-utils.d.ts +30 -0
  24. package/dist/core/variant-utils.js +54 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.js +3 -0
  27. package/dist/mutators/__tests__/registry.spec.d.ts +1 -0
  28. package/dist/mutators/__tests__/registry.spec.js +43 -0
  29. package/dist/mutators/__tests__/utils.spec.d.ts +1 -0
  30. package/dist/mutators/__tests__/utils.spec.js +15 -0
  31. package/dist/mutators/registry.d.ts +37 -0
  32. package/dist/mutators/registry.js +101 -0
  33. package/dist/mutators/types.d.ts +39 -0
  34. package/dist/mutators/types.js +7 -0
  35. package/dist/mutators/utils.d.ts +37 -0
  36. package/dist/mutators/utils.js +151 -0
  37. package/dist/plugin/viteMutate.d.ts +15 -0
  38. package/dist/plugin/viteMutate.js +52 -0
  39. package/dist/plugin/vitest.setup.d.ts +47 -0
  40. package/dist/plugin/vitest.setup.js +118 -0
  41. package/dist/plugin/withVitest.d.ts +13 -0
  42. package/dist/plugin/withVitest.js +30 -0
  43. package/dist/runner/__tests__/discover.spec.d.ts +1 -0
  44. package/dist/runner/__tests__/discover.spec.js +59 -0
  45. package/dist/runner/__tests__/orchestrator.spec.d.ts +1 -0
  46. package/dist/runner/__tests__/orchestrator.spec.js +55 -0
  47. package/dist/runner/adapters/__tests__/jest.spec.d.ts +1 -0
  48. package/dist/runner/adapters/__tests__/jest.spec.js +88 -0
  49. package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.d.ts +1 -0
  50. package/dist/runner/adapters/__tests__/vitest-worker-runtime.spec.js +59 -0
  51. package/dist/runner/adapters/__tests__/vitest.spec.d.ts +1 -0
  52. package/dist/runner/adapters/__tests__/vitest.spec.js +118 -0
  53. package/dist/runner/adapters/index.d.ts +10 -0
  54. package/dist/runner/adapters/index.js +9 -0
  55. package/dist/runner/adapters/jest/__tests__/index.spec.d.ts +1 -0
  56. package/dist/runner/adapters/jest/__tests__/index.spec.js +88 -0
  57. package/dist/runner/adapters/jest/index.d.ts +24 -0
  58. package/dist/runner/adapters/jest/index.js +216 -0
  59. package/dist/runner/adapters/jest/worker-runtime.d.ts +37 -0
  60. package/dist/runner/adapters/jest/worker-runtime.js +171 -0
  61. package/dist/runner/adapters/jest-worker-runtime.d.ts +37 -0
  62. package/dist/runner/adapters/jest-worker-runtime.js +171 -0
  63. package/dist/runner/adapters/jest.d.ts +24 -0
  64. package/dist/runner/adapters/jest.js +216 -0
  65. package/dist/runner/adapters/types.d.ts +89 -0
  66. package/dist/runner/adapters/types.js +8 -0
  67. package/dist/runner/adapters/vitest/__tests__/index.spec.d.ts +1 -0
  68. package/dist/runner/adapters/vitest/__tests__/index.spec.js +118 -0
  69. package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.d.ts +1 -0
  70. package/dist/runner/adapters/vitest/__tests__/worker-runtime.spec.js +59 -0
  71. package/dist/runner/adapters/vitest/index.d.ts +33 -0
  72. package/dist/runner/adapters/vitest/index.js +267 -0
  73. package/dist/runner/adapters/vitest/worker-runtime.d.ts +25 -0
  74. package/dist/runner/adapters/vitest/worker-runtime.js +118 -0
  75. package/dist/runner/adapters/vitest-worker-runtime.d.ts +25 -0
  76. package/dist/runner/adapters/vitest-worker-runtime.js +118 -0
  77. package/dist/runner/adapters/vitest.d.ts +33 -0
  78. package/dist/runner/adapters/vitest.js +267 -0
  79. package/dist/runner/args.d.ts +50 -0
  80. package/dist/runner/args.js +123 -0
  81. package/dist/runner/cache.d.ts +38 -0
  82. package/dist/runner/cache.js +118 -0
  83. package/dist/runner/changed.d.ts +22 -0
  84. package/dist/runner/changed.js +210 -0
  85. package/dist/runner/cleanup.d.ts +4 -0
  86. package/dist/runner/cleanup.js +21 -0
  87. package/dist/runner/config.d.ts +13 -0
  88. package/dist/runner/config.js +94 -0
  89. package/dist/runner/discover.d.ts +7 -0
  90. package/dist/runner/discover.js +258 -0
  91. package/dist/runner/jest/__tests__/adapter.spec.d.ts +1 -0
  92. package/dist/runner/jest/__tests__/adapter.spec.js +110 -0
  93. package/dist/runner/jest/adapter.d.ts +24 -0
  94. package/dist/runner/jest/adapter.js +191 -0
  95. package/dist/runner/jest/index.d.ts +8 -0
  96. package/dist/runner/jest/index.js +7 -0
  97. package/dist/runner/jest/pool.d.ts +47 -0
  98. package/dist/runner/jest/pool.js +307 -0
  99. package/dist/runner/jest/resolver.cjs +61 -0
  100. package/dist/runner/jest/resolver.d.cts +11 -0
  101. package/dist/runner/jest/worker-runtime.d.ts +30 -0
  102. package/dist/runner/jest/worker-runtime.js +98 -0
  103. package/dist/runner/jest/worker.d.mts +1 -0
  104. package/dist/runner/jest/worker.mjs +55 -0
  105. package/dist/runner/orchestrator.d.ts +13 -0
  106. package/dist/runner/orchestrator.js +387 -0
  107. package/dist/runner/pool/__tests__/index.spec.d.ts +1 -0
  108. package/dist/runner/pool/__tests__/index.spec.js +83 -0
  109. package/dist/runner/pool/__tests__/pool-plugin.spec.d.ts +1 -0
  110. package/dist/runner/pool/__tests__/pool-plugin.spec.js +59 -0
  111. package/dist/runner/pool/__tests__/pool-redirect-loader.spec.d.ts +1 -0
  112. package/dist/runner/pool/__tests__/pool-redirect-loader.spec.js +78 -0
  113. package/dist/runner/pool/index.d.ts +8 -0
  114. package/dist/runner/pool/index.js +9 -0
  115. package/dist/runner/pool/jest/pool.d.ts +52 -0
  116. package/dist/runner/pool/jest/pool.js +309 -0
  117. package/dist/runner/pool/jest/worker.d.mts +1 -0
  118. package/dist/runner/pool/jest/worker.mjs +60 -0
  119. package/dist/runner/pool/jest-pool.d.ts +52 -0
  120. package/dist/runner/pool/jest-pool.js +309 -0
  121. package/dist/runner/pool/jest-worker.d.mts +1 -0
  122. package/dist/runner/pool/jest-worker.mjs +60 -0
  123. package/dist/runner/pool/plugin.d.ts +18 -0
  124. package/dist/runner/pool/plugin.js +60 -0
  125. package/dist/runner/pool/pool-plugin.d.ts +18 -0
  126. package/dist/runner/pool/pool-plugin.js +60 -0
  127. package/dist/runner/pool/pool-redirect-loader.d.ts +19 -0
  128. package/dist/runner/pool/pool-redirect-loader.js +116 -0
  129. package/dist/runner/pool/pool-redirect-loader.mjs +146 -0
  130. package/dist/runner/pool/redirect-loader.d.ts +19 -0
  131. package/dist/runner/pool/redirect-loader.js +116 -0
  132. package/dist/runner/pool/vitest/pool.d.ts +70 -0
  133. package/dist/runner/pool/vitest/pool.js +376 -0
  134. package/dist/runner/pool/vitest/worker.d.mts +15 -0
  135. package/dist/runner/pool/vitest/worker.mjs +96 -0
  136. package/dist/runner/pool/vitest-worker.d.mts +15 -0
  137. package/dist/runner/pool/vitest-worker.mjs +96 -0
  138. package/dist/runner/shared/index.d.ts +9 -0
  139. package/dist/runner/shared/index.js +8 -0
  140. package/dist/runner/shared/mutant-paths.d.ts +15 -0
  141. package/dist/runner/shared/mutant-paths.js +30 -0
  142. package/dist/runner/shared/redirect-state.d.ts +45 -0
  143. package/dist/runner/shared/redirect-state.js +50 -0
  144. package/dist/runner/shared-module-redirect.d.ts +56 -0
  145. package/dist/runner/shared-module-redirect.js +84 -0
  146. package/dist/runner/types.d.ts +88 -0
  147. package/dist/runner/types.js +8 -0
  148. package/dist/runner/variants.d.ts +21 -0
  149. package/dist/runner/variants.js +66 -0
  150. package/dist/runner/vitest/__tests__/adapter.spec.d.ts +1 -0
  151. package/dist/runner/vitest/__tests__/adapter.spec.js +131 -0
  152. package/dist/runner/vitest/__tests__/plugin.spec.d.ts +1 -0
  153. package/dist/runner/vitest/__tests__/plugin.spec.js +65 -0
  154. package/dist/runner/vitest/__tests__/pool.spec.d.ts +1 -0
  155. package/dist/runner/vitest/__tests__/pool.spec.js +106 -0
  156. package/dist/runner/vitest/__tests__/redirect-loader.spec.d.ts +1 -0
  157. package/dist/runner/vitest/__tests__/redirect-loader.spec.js +87 -0
  158. package/dist/runner/vitest/__tests__/worker-runtime.spec.d.ts +1 -0
  159. package/dist/runner/vitest/__tests__/worker-runtime.spec.js +75 -0
  160. package/dist/runner/vitest/adapter.d.ts +33 -0
  161. package/dist/runner/vitest/adapter.js +277 -0
  162. package/dist/runner/vitest/index.d.ts +11 -0
  163. package/dist/runner/vitest/index.js +10 -0
  164. package/dist/runner/vitest/plugin.d.ts +12 -0
  165. package/dist/runner/vitest/plugin.js +49 -0
  166. package/dist/runner/vitest/pool.d.ts +65 -0
  167. package/dist/runner/vitest/pool.js +376 -0
  168. package/dist/runner/vitest/redirect-loader.d.ts +30 -0
  169. package/dist/runner/vitest/redirect-loader.js +123 -0
  170. package/dist/runner/vitest/worker-runtime.d.ts +16 -0
  171. package/dist/runner/vitest/worker-runtime.js +105 -0
  172. package/dist/runner/vitest/worker.d.mts +15 -0
  173. package/dist/runner/vitest/worker.mjs +92 -0
  174. package/dist/types/api.d.ts +20 -0
  175. package/dist/types/api.js +1 -0
  176. package/dist/types/config.d.ts +48 -0
  177. package/dist/types/config.js +1 -0
  178. package/dist/types/index.d.ts +13 -0
  179. package/dist/types/index.js +11 -0
  180. package/dist/types/mutant.d.ts +44 -0
  181. package/dist/types/mutant.js +7 -0
  182. package/dist/utils/PoolSpinner.d.ts +5 -0
  183. package/dist/utils/PoolSpinner.js +6 -0
  184. package/dist/utils/ProgressBar.d.ts +11 -0
  185. package/dist/utils/ProgressBar.js +9 -0
  186. package/dist/utils/__tests__/coverage.spec.d.ts +1 -0
  187. package/dist/utils/__tests__/coverage.spec.js +91 -0
  188. package/dist/utils/__tests__/progress.spec.d.ts +1 -0
  189. package/dist/utils/__tests__/progress.spec.js +50 -0
  190. package/dist/utils/__tests__/summary.spec.d.ts +1 -0
  191. package/dist/utils/__tests__/summary.spec.js +54 -0
  192. package/dist/utils/coverage.d.ts +57 -0
  193. package/dist/utils/coverage.js +204 -0
  194. package/dist/utils/logger.d.ts +8 -0
  195. package/dist/utils/logger.js +18 -0
  196. package/dist/utils/progress.d.ts +25 -0
  197. package/dist/utils/progress.js +90 -0
  198. package/dist/utils/summary.d.ts +12 -0
  199. package/dist/utils/summary.js +107 -0
  200. 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,4 @@
1
+ /**
2
+ * Clean up all __mutineer__ temp directories created during mutation testing.
3
+ */
4
+ export declare function cleanupMutineerDirs(cwd: string): Promise<void>;
@@ -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 {};