@mutineerjs/mutineer 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/bin/mutineer.d.ts +1 -1
  2. package/dist/bin/mutineer.js +3 -1
  3. package/dist/mutators/__tests__/operator.spec.js +97 -1
  4. package/dist/mutators/__tests__/registry.spec.js +8 -0
  5. package/dist/mutators/operator.d.ts +8 -0
  6. package/dist/mutators/operator.js +58 -1
  7. package/dist/mutators/registry.js +9 -1
  8. package/dist/mutators/utils.d.ts +2 -0
  9. package/dist/mutators/utils.js +58 -1
  10. package/dist/runner/__tests__/args.spec.js +57 -1
  11. package/dist/runner/__tests__/cache.spec.js +65 -8
  12. package/dist/runner/__tests__/cleanup.spec.js +30 -0
  13. package/dist/runner/__tests__/coverage-resolver.spec.js +2 -0
  14. package/dist/runner/__tests__/discover.spec.js +128 -0
  15. package/dist/runner/__tests__/orchestrator.spec.js +167 -2
  16. package/dist/runner/__tests__/pool-executor.spec.js +60 -1
  17. package/dist/runner/args.d.ts +13 -0
  18. package/dist/runner/args.js +27 -0
  19. package/dist/runner/cache.d.ts +19 -3
  20. package/dist/runner/cache.js +14 -7
  21. package/dist/runner/cleanup.d.ts +3 -1
  22. package/dist/runner/cleanup.js +18 -1
  23. package/dist/runner/coverage-resolver.js +1 -1
  24. package/dist/runner/discover.d.ts +1 -1
  25. package/dist/runner/discover.js +30 -20
  26. package/dist/runner/orchestrator.d.ts +1 -0
  27. package/dist/runner/orchestrator.js +22 -8
  28. package/dist/runner/pool-executor.d.ts +5 -0
  29. package/dist/runner/pool-executor.js +15 -4
  30. package/dist/runner/vitest/__tests__/adapter.spec.js +41 -0
  31. package/dist/runner/vitest/adapter.js +13 -9
  32. package/dist/types/config.d.ts +2 -0
  33. package/dist/utils/__tests__/summary.spec.js +43 -1
  34. package/dist/utils/summary.d.ts +18 -0
  35. package/dist/utils/summary.js +25 -0
  36. package/package.json +2 -1
@@ -200,7 +200,7 @@ async function createResolver(rootAbs, exts) {
200
200
  return createNodeResolver();
201
201
  }
202
202
  }
203
- export async function autoDiscoverTargetsAndTests(root, cfg) {
203
+ export async function autoDiscoverTargetsAndTests(root, cfg, onProgress) {
204
204
  const rootAbs = path.resolve(root);
205
205
  const sourceRoots = toArray(cfg.source ?? 'src').map((s) => path.resolve(rootAbs, s));
206
206
  const exts = new Set(toArray(cfg.extensions ?? EXT_DEFAULT));
@@ -219,6 +219,7 @@ export async function autoDiscoverTargetsAndTests(root, cfg) {
219
219
  if (!tests.length)
220
220
  return { targets: [], testMap: new Map(), directTestMap: new Map() };
221
221
  const testSet = new Set(tests.map((t) => normalizePath(t)));
222
+ onProgress?.(`Found ${tests.length} test file(s), resolving imports...`);
222
223
  // 2) Create resolver (Vite if available, otherwise Node-based fallback)
223
224
  const { resolve, cleanup } = await createResolver(rootAbs, exts);
224
225
  const targets = new Map();
@@ -226,6 +227,7 @@ export async function autoDiscoverTargetsAndTests(root, cfg) {
226
227
  const directTestMap = new Map();
227
228
  const contentCache = new Map();
228
229
  const resolveCache = new Map(); // key: importer\0spec -> resolved id
230
+ const childrenCache = new Map(); // key: normalized file -> resolved child abs paths
229
231
  async function crawl(absFile, depth, seen, currentTestAbs) {
230
232
  if (depth > MAX_CRAWL_DEPTH)
231
233
  return; // sane guard for huge graphs
@@ -266,27 +268,34 @@ export async function autoDiscoverTargetsAndTests(root, cfg) {
266
268
  }
267
269
  if (!code)
268
270
  return;
269
- // find import specs and resolve relative to absFile
270
- for (const spec of extractImportSpecs(code)) {
271
- if (!spec)
272
- continue;
273
- const cacheKey = `${absFile}\0${spec}`;
274
- let resolved = resolveCache.get(cacheKey);
275
- if (!resolved) {
276
- resolved = await resolve(spec, absFile);
277
- resolveCache.set(cacheKey, resolved);
271
+ // find import specs and resolve relative to absFile, memoized per file
272
+ let children = childrenCache.get(key);
273
+ if (children === undefined) {
274
+ const resolved = [];
275
+ for (const spec of extractImportSpecs(code)) {
276
+ if (!spec)
277
+ continue;
278
+ const cacheKey = `${absFile}\0${spec}`;
279
+ let resolvedId = resolveCache.get(cacheKey);
280
+ if (!resolvedId) {
281
+ resolvedId = await resolve(spec, absFile);
282
+ resolveCache.set(cacheKey, resolvedId);
283
+ }
284
+ // vite ids could be URLs; ensure we turn into absolute disk path when possible
285
+ const next = path.isAbsolute(resolvedId)
286
+ ? resolvedId
287
+ : normalizePath(path.resolve(rootAbs, resolvedId));
288
+ // skip node_modules and virtual ids
289
+ if (next.includes('/node_modules/'))
290
+ continue;
291
+ if (!path.isAbsolute(next))
292
+ continue;
293
+ resolved.push(next);
278
294
  }
279
- // vite ids could be URLs; ensure we turn into absolute disk path when possible
280
- const next = path.isAbsolute(resolved)
281
- ? resolved
282
- : normalizePath(path.resolve(rootAbs, resolved));
283
- // skip node_modules and virtual ids
284
- if (next.includes('/node_modules/'))
285
- continue;
286
- if (!path.isAbsolute(next))
287
- continue;
288
- await crawl(next, depth + 1, seen, currentTestAbs);
295
+ childrenCache.set(key, resolved);
296
+ children = resolved;
289
297
  }
298
+ await Promise.all(children.map((next) => crawl(next, depth + 1, seen, currentTestAbs)));
290
299
  }
291
300
  try {
292
301
  await Promise.all(tests.map(async (testAbs) => {
@@ -312,6 +321,7 @@ export async function autoDiscoverTargetsAndTests(root, cfg) {
312
321
  await crawl(abs, 0, seen, testAbs);
313
322
  }
314
323
  }));
324
+ onProgress?.(`Discovery complete: ${targets.size} source file(s), ${tests.length} test file(s)`);
315
325
  return { targets: Array.from(targets.values()), testMap, directTestMap };
316
326
  }
317
327
  finally {
@@ -9,5 +9,6 @@
9
9
  * 5. Execute mutants via worker pool
10
10
  * 6. Report results
11
11
  */
12
+ export declare function parseMutantTimeoutMs(raw: string | undefined): number;
12
13
  export { readMutantCache } from './cache.js';
13
14
  export declare function runOrchestrator(cliArgs: string[], cwd: string): Promise<void>;
@@ -26,11 +26,11 @@ import { prepareTasks } from './tasks.js';
26
26
  import { executePool } from './pool-executor.js';
27
27
  const log = createLogger('orchestrator');
28
28
  // Per-mutant test timeout (ms). Can be overridden with env MUTINEER_MUTANT_TIMEOUT_MS
29
- const MUTANT_TIMEOUT_MS = (() => {
30
- const raw = process.env.MUTINEER_MUTANT_TIMEOUT_MS;
29
+ export function parseMutantTimeoutMs(raw) {
31
30
  const n = raw ? Number(raw) : NaN;
32
31
  return Number.isFinite(n) && n > 0 ? n : 30_000;
33
- })();
32
+ }
33
+ const MUTANT_TIMEOUT_MS = parseMutantTimeoutMs(process.env.MUTINEER_MUTANT_TIMEOUT_MS);
34
34
  // Re-export readMutantCache for external use
35
35
  export { readMutantCache } from './cache.js';
36
36
  export async function runOrchestrator(cliArgs, cwd) {
@@ -38,7 +38,7 @@ export async function runOrchestrator(cliArgs, cwd) {
38
38
  const cfgPath = extractConfigPath(cliArgs);
39
39
  const cfg = await loadMutineerConfig(cwd, cfgPath);
40
40
  const opts = parseCliOptions(cliArgs, cfg);
41
- await clearCacheOnStart(cwd);
41
+ await clearCacheOnStart(cwd, opts.shard);
42
42
  // Create test runner adapter
43
43
  const adapter = (opts.runner === 'jest' ? createJestAdapter : createVitestAdapter)({
44
44
  cwd,
@@ -66,8 +66,9 @@ export async function runOrchestrator(cliArgs, cwd) {
66
66
  }))
67
67
  : null;
68
68
  // 4. Discover targets and tests
69
- const cache = await readMutantCache(cwd);
70
- const discovered = await autoDiscoverTargetsAndTests(cwd, cfg);
69
+ const cache = await readMutantCache(cwd, opts.shard);
70
+ log.info('Discovering tests...');
71
+ const discovered = await autoDiscoverTargetsAndTests(cwd, cfg, (msg) => log.info(msg));
71
72
  const { testMap, directTestMap } = discovered;
72
73
  const targets = cfg.targets?.length
73
74
  ? [...cfg.targets]
@@ -104,7 +105,8 @@ export async function runOrchestrator(cliArgs, cwd) {
104
105
  }
105
106
  }
106
107
  if (!baselineTests.length) {
107
- log.info('No tests found for targets. Exiting.');
108
+ log.error('No tests found for the selected targets. Ensure your source files are covered by at least one test file.');
109
+ process.exitCode = 1;
108
110
  return;
109
111
  }
110
112
  // 5. Run baseline tests (with coverage if needed for filtering)
@@ -137,7 +139,17 @@ export async function runOrchestrator(cliArgs, cwd) {
137
139
  return;
138
140
  }
139
141
  // 8. Prepare tasks and execute via worker pool
140
- const tasks = prepareTasks(variants, updatedCoverage.perTestCoverage, directTestMap);
142
+ let tasks = prepareTasks(variants, updatedCoverage.perTestCoverage, directTestMap);
143
+ // Apply shard filter if requested
144
+ if (opts.shard) {
145
+ const { index, total } = opts.shard;
146
+ tasks = tasks.filter((_, i) => i % total === index - 1);
147
+ log.info(`Shard ${index}/${total}: running ${tasks.length} mutant(s)`);
148
+ if (tasks.length === 0) {
149
+ log.info('No mutants assigned to this shard. Exiting.');
150
+ return;
151
+ }
152
+ }
141
153
  await executePool({
142
154
  tasks,
143
155
  adapter,
@@ -145,6 +157,8 @@ export async function runOrchestrator(cliArgs, cwd) {
145
157
  concurrency: opts.concurrency,
146
158
  progressMode: opts.progressMode,
147
159
  minKillPercent: opts.minKillPercent,
160
+ reportFormat: opts.reportFormat,
148
161
  cwd,
162
+ shard: opts.shard,
149
163
  });
150
164
  }
@@ -8,7 +8,12 @@ export interface PoolExecutionOptions {
8
8
  concurrency: number;
9
9
  progressMode: 'bar' | 'list' | 'quiet';
10
10
  minKillPercent?: number;
11
+ reportFormat?: 'text' | 'json';
11
12
  cwd: string;
13
+ shard?: {
14
+ index: number;
15
+ total: number;
16
+ };
12
17
  }
13
18
  /**
14
19
  * Execute all mutant tasks through the worker pool.
@@ -1,8 +1,9 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { render } from 'ink';
3
4
  import { createElement } from 'react';
4
5
  import { Progress } from '../utils/progress.js';
5
- import { computeSummary, printSummary } from '../utils/summary.js';
6
+ import { computeSummary, printSummary, buildJsonReport, } from '../utils/summary.js';
6
7
  import { saveCacheAtomic } from './cache.js';
7
8
  import { cleanupMutineerDirs } from './cleanup.js';
8
9
  import { PoolSpinner } from '../utils/PoolSpinner.js';
@@ -29,7 +30,18 @@ export async function executePool(opts) {
29
30
  const durationMs = Date.now() - mutationStartTime;
30
31
  progress.finish();
31
32
  const summary = computeSummary(cache);
32
- printSummary(summary, cache, durationMs);
33
+ if (opts.reportFormat === 'json') {
34
+ const report = buildJsonReport(summary, cache, durationMs);
35
+ const shardSuffix = opts.shard
36
+ ? `-shard-${opts.shard.index}-of-${opts.shard.total}`
37
+ : '';
38
+ const outPath = path.join(opts.cwd, `mutineer-report${shardSuffix}.json`);
39
+ fs.writeFileSync(outPath, JSON.stringify(report, null, 2));
40
+ log.info(`JSON report written to ${path.relative(process.cwd(), outPath)}`);
41
+ }
42
+ else {
43
+ printSummary(summary, cache, durationMs);
44
+ }
33
45
  if (opts.minKillPercent !== undefined) {
34
46
  const killRateString = summary.killRate.toFixed(2);
35
47
  const thresholdString = opts.minKillPercent.toFixed(2);
@@ -68,7 +80,6 @@ export async function executePool(opts) {
68
80
  let nextIdx = 0;
69
81
  async function processTask(task) {
70
82
  const { v, tests, key, directTests } = task;
71
- log.debug('Cache ' + JSON.stringify(cache));
72
83
  const cached = cache[key];
73
84
  if (cached) {
74
85
  progress.update(cached.status);
@@ -159,7 +170,7 @@ export async function executePool(opts) {
159
170
  for (let i = 0; i < workerCount; i++)
160
171
  workers.push(worker());
161
172
  await Promise.all(workers);
162
- await saveCacheAtomic(cwd, cache);
173
+ await saveCacheAtomic(cwd, cache, opts.shard);
163
174
  }
164
175
  finally {
165
176
  process.removeAllListeners('SIGINT');
@@ -122,6 +122,21 @@ describe('Vitest adapter', () => {
122
122
  expect(argStr).toContain('--coverage.thresholds.branches=0');
123
123
  expect(argStr).toContain('--coverage.thresholds.statements=0');
124
124
  });
125
+ it('strips --shard= flag from vitest args', async () => {
126
+ const adapter = makeAdapter({ cliArgs: ['--shard=1/4'] });
127
+ spawnMock.mockImplementationOnce(() => ({
128
+ on: (evt, cb) => {
129
+ if (evt === 'exit')
130
+ cb(0);
131
+ },
132
+ }));
133
+ await adapter.runBaseline(['test-a'], {
134
+ collectCoverage: false,
135
+ perTestCoverage: false,
136
+ });
137
+ const args = spawnMock.mock.calls[0][1];
138
+ expect(args.join(' ')).not.toContain('--shard');
139
+ });
125
140
  it('detects coverage config from vitest config file', async () => {
126
141
  const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-vitest-'));
127
142
  const cfgPath = path.join(tmp, 'vitest.config.ts');
@@ -140,6 +155,32 @@ describe('Vitest adapter', () => {
140
155
  }
141
156
  });
142
157
  });
158
+ describe('hasCoverageProvider', () => {
159
+ it('returns true when @vitest/coverage-v8 is resolvable', () => {
160
+ const adapter = makeAdapter({ cwd: process.cwd() });
161
+ // coverage-v8 is installed as a devDependency, so this must resolve
162
+ expect(adapter.hasCoverageProvider()).toBe(true);
163
+ });
164
+ it('returns false when neither provider is resolvable', () => {
165
+ const adapter = makeAdapter({ cwd: '/tmp' });
166
+ expect(adapter.hasCoverageProvider()).toBe(false);
167
+ });
168
+ it('returns true when @vitest/coverage-istanbul is resolvable', () => {
169
+ const adapter = makeAdapter({ cwd: process.cwd() });
170
+ const origResolve = require.resolve;
171
+ const resolveStub = vi
172
+ .spyOn(require, 'resolve')
173
+ .mockImplementation((id, opts) => {
174
+ if (String(id).includes('coverage-v8'))
175
+ throw new Error('not found');
176
+ if (String(id).includes('coverage-istanbul'))
177
+ return '/fake/path';
178
+ return origResolve(id, opts);
179
+ });
180
+ expect(adapter.hasCoverageProvider()).toBe(true);
181
+ resolveStub.mockRestore();
182
+ });
183
+ });
143
184
  describe('isCoverageRequestedInArgs', () => {
144
185
  it('detects enabled coverage flags', () => {
145
186
  expect(isCoverageRequestedInArgs(['--coverage'])).toBe(true);
@@ -37,6 +37,7 @@ function stripMutineerArgs(args) {
37
37
  '--config',
38
38
  '-c',
39
39
  '--coverage-file',
40
+ '--shard',
40
41
  ]);
41
42
  const dropExact = new Set([
42
43
  '-m',
@@ -59,6 +60,8 @@ function stripMutineerArgs(args) {
59
60
  continue;
60
61
  if (a.startsWith('--config=') || a.startsWith('-c='))
61
62
  continue;
63
+ if (a.startsWith('--shard='))
64
+ continue;
62
65
  out.push(a);
63
66
  }
64
67
  return out;
@@ -196,15 +199,16 @@ export class VitestAdapter {
196
199
  }
197
200
  }
198
201
  hasCoverageProvider() {
199
- try {
200
- require.resolve('@vitest/coverage-v8/package.json', {
201
- paths: [this.options.cwd],
202
- });
203
- return true;
204
- }
205
- catch {
206
- return false;
207
- }
202
+ const packages = ['@vitest/coverage-v8', '@vitest/coverage-istanbul'];
203
+ return packages.some((pkg) => {
204
+ try {
205
+ require.resolve(`${pkg}/package.json`, { paths: [this.options.cwd] });
206
+ return true;
207
+ }
208
+ catch {
209
+ return false;
210
+ }
211
+ });
208
212
  }
209
213
  async detectCoverageConfig() {
210
214
  const configPath = this.options.config.vitestConfig;
@@ -40,4 +40,6 @@ export interface MutineerConfig {
40
40
  readonly perTestCoverage?: boolean;
41
41
  /** Per-mutant test timeout in milliseconds (default: 30000) */
42
42
  readonly timeout?: number;
43
+ /** Output report format: 'text' (default) or 'json' (writes mutineer-report.json) */
44
+ readonly report?: 'text' | 'json';
43
45
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
- import { computeSummary, printSummary, summarise } from '../summary.js';
2
+ import { computeSummary, printSummary, summarise, buildJsonReport, } from '../summary.js';
3
3
  /** Strip ANSI escape codes for clean text assertions */
4
4
  const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*m/g, '');
5
5
  function makeEntry(overrides) {
@@ -97,6 +97,48 @@ describe('summary', () => {
97
97
  expect(lines.some((l) => l.includes('↳'))).toBe(false);
98
98
  logSpy.mockRestore();
99
99
  });
100
+ it('buildJsonReport includes schemaVersion, timestamp, summary, and mutants', () => {
101
+ const cache = {
102
+ a: makeEntry({ status: 'killed', file: '/tmp/a.ts', mutator: 'flip' }),
103
+ b: makeEntry({ status: 'escaped', file: '/tmp/b.ts', mutator: 'wrap' }),
104
+ };
105
+ const summary = computeSummary(cache);
106
+ const report = buildJsonReport(summary, cache, 1000);
107
+ expect(report.schemaVersion).toBe(1);
108
+ expect(typeof report.timestamp).toBe('string');
109
+ expect(report.durationMs).toBe(1000);
110
+ expect(report.summary).toEqual(summary);
111
+ expect(report.mutants).toHaveLength(2);
112
+ });
113
+ it('buildJsonReport mutant entries have required fields', () => {
114
+ const cache = {
115
+ a: makeEntry({
116
+ status: 'escaped',
117
+ file: '/tmp/a.ts',
118
+ mutator: 'flip',
119
+ originalSnippet: 'a === b',
120
+ mutatedSnippet: 'a !== b',
121
+ coveringTests: ['/tmp/a.spec.ts'],
122
+ }),
123
+ };
124
+ const summary = computeSummary(cache);
125
+ const report = buildJsonReport(summary, cache);
126
+ const mutant = report.mutants[0];
127
+ expect(mutant.file).toBe('/tmp/a.ts');
128
+ expect(mutant.status).toBe('escaped');
129
+ expect(mutant.mutator).toBe('flip');
130
+ expect(mutant.originalSnippet).toBe('a === b');
131
+ expect(mutant.mutatedSnippet).toBe('a !== b');
132
+ expect(mutant.coveringTests).toEqual(['/tmp/a.spec.ts']);
133
+ });
134
+ it('buildJsonReport omits optional fields when absent', () => {
135
+ const cache = { a: makeEntry({ status: 'killed' }) };
136
+ const summary = computeSummary(cache);
137
+ const report = buildJsonReport(summary, cache);
138
+ expect('durationMs' in report).toBe(false);
139
+ expect('originalSnippet' in report.mutants[0]).toBe(false);
140
+ expect('coveringTests' in report.mutants[0]).toBe(false);
141
+ });
100
142
  it('summarise returns summary and prints', () => {
101
143
  const cache = { a: makeEntry({ status: 'killed' }) };
102
144
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
@@ -9,4 +9,22 @@ export interface Summary {
9
9
  }
10
10
  export declare function computeSummary(cache: Readonly<Record<string, MutantCacheEntry>>): Summary;
11
11
  export declare function printSummary(summary: Summary, cache?: Readonly<Record<string, MutantCacheEntry>>, durationMs?: number): void;
12
+ export interface JsonMutant {
13
+ readonly file: string;
14
+ readonly line: number;
15
+ readonly col: number;
16
+ readonly mutator: string;
17
+ readonly status: string;
18
+ readonly originalSnippet?: string;
19
+ readonly mutatedSnippet?: string;
20
+ readonly coveringTests?: readonly string[];
21
+ }
22
+ export interface JsonReport {
23
+ readonly schemaVersion: 1;
24
+ readonly timestamp: string;
25
+ readonly durationMs?: number;
26
+ readonly summary: Summary;
27
+ readonly mutants: JsonMutant[];
28
+ }
29
+ export declare function buildJsonReport(summary: Summary, cache: Readonly<Record<string, MutantCacheEntry>>, durationMs?: number): JsonReport;
12
30
  export declare function summarise(cache: Readonly<Record<string, MutantCacheEntry>>): Summary;
@@ -115,6 +115,31 @@ export function printSummary(summary, cache, durationMs) {
115
115
  }
116
116
  console.log(chalk.dim(SEPARATOR) + '\n');
117
117
  }
118
+ export function buildJsonReport(summary, cache, durationMs) {
119
+ const mutants = Object.values(cache).map((entry) => ({
120
+ file: entry.file,
121
+ line: entry.line,
122
+ col: entry.col,
123
+ mutator: entry.mutator,
124
+ status: entry.status,
125
+ ...(entry.originalSnippet !== undefined && {
126
+ originalSnippet: entry.originalSnippet,
127
+ }),
128
+ ...(entry.mutatedSnippet !== undefined && {
129
+ mutatedSnippet: entry.mutatedSnippet,
130
+ }),
131
+ ...(entry.coveringTests !== undefined && {
132
+ coveringTests: entry.coveringTests,
133
+ }),
134
+ }));
135
+ return {
136
+ schemaVersion: 1,
137
+ timestamp: new Date().toISOString(),
138
+ ...(durationMs !== undefined && { durationMs }),
139
+ summary,
140
+ mutants,
141
+ };
142
+ }
118
143
  export function summarise(cache) {
119
144
  const s = computeSummary(cache);
120
145
  printSummary(s, cache);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutineerjs/mutineer",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "A fast, targeted mutation testing framework for JavaScript and TypeScript",
5
5
  "type": "module",
6
6
  "private": false,
@@ -98,6 +98,7 @@
98
98
  }
99
99
  },
100
100
  "devDependencies": {
101
+ "@vitest/coverage-v8": "^4.0.15",
101
102
  "@commitlint/cli": "^20.4.3",
102
103
  "@commitlint/config-conventional": "^20.4.3",
103
104
  "@types/babel__traverse": "^7.28.0",