@hublo/sentinel 1.4.0-alpha.2 → 1.4.0-alpha.21

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 (40) hide show
  1. package/dist/bin/sentinel.d.ts +1 -0
  2. package/dist/bin/sentinel.js +10 -11
  3. package/dist/chunk-2XLX6PFR.js.map +1 -0
  4. package/dist/{chunk-3TDUIKVQ.js → chunk-3ZKTXODW.js} +6 -6
  5. package/dist/chunk-3ZKTXODW.js.map +1 -0
  6. package/dist/{chunk-4UIZJ3TR.js → chunk-7LE72AFG.js} +982 -206
  7. package/dist/{chunk-WLFE5RUU.js → chunk-NW6UHNYX.js} +6 -6
  8. package/dist/chunk-NW6UHNYX.js.map +1 -0
  9. package/dist/chunk-PWV3BMDA.js.map +1 -0
  10. package/dist/{chunk-CPCUPK4J.js → chunk-TKDVAYYV.js} +4 -4
  11. package/dist/chunk-TKDVAYYV.js.map +1 -0
  12. package/dist/chunk-ZV5KMOS7.js +427 -0
  13. package/dist/chunk-ZV5KMOS7.js.map +1 -0
  14. package/dist/index.d.ts +23 -0
  15. package/dist/index.js +2 -3
  16. package/dist/roles/build/nest/toolchain.js +7 -7
  17. package/dist/roles/test/nest/toolchain.d.ts +3 -23
  18. package/dist/roles/test/nest/toolchain.js +5 -269
  19. package/dist/roles/test/nest/toolchain.js.map +1 -1
  20. package/dist/roles/test/react/toolchain.d.ts +5 -1
  21. package/dist/roles/test/react/toolchain.js +11 -3
  22. package/dist/roles/test/react/toolchain.js.map +1 -1
  23. package/dist/roles/test/setup/{nest.js → jest-parity.js} +5 -5
  24. package/dist/roles/test/setup/jest-parity.js.map +1 -0
  25. package/dist/roles/test/setup/workspace-entry.js +2 -2
  26. package/dist/roles/test/setup/workspace-entry.js.map +1 -1
  27. package/dist/roles/test/shared-test-config.d.ts +32 -0
  28. package/dist/roles/test/shared-test-config.js +8 -0
  29. package/dist/roles/test/shared-test-config.js.map +1 -0
  30. package/dist/roles/test/tools/msw.d.ts +1 -0
  31. package/dist/roles/test/tools/msw.js +3 -0
  32. package/dist/roles/test/tools/msw.js.map +1 -0
  33. package/docs/test-adoption.md +54 -10
  34. package/docs/using-sentinel.md +25 -10
  35. package/package.json +14 -7
  36. package/types/jest-global.d.ts +41 -0
  37. package/types/mock-extended.d.ts +52 -0
  38. package/dist/chunk-NX4GHIHF.js +0 -25
  39. package/dist/roles/test/setup/nest.js.map +0 -1
  40. /package/dist/roles/test/setup/{nest.d.ts → jest-parity.d.ts} +0 -0
@@ -5,7 +5,7 @@ import {
5
5
  readNxProjectName,
6
6
  readOwnPackage,
7
7
  readProjectPackageJson
8
- } from "./chunk-WLFE5RUU.js";
8
+ } from "./chunk-NW6UHNYX.js";
9
9
 
10
10
  // src/core/registry.ts
11
11
  var adapters = [];
@@ -109,7 +109,7 @@ var SWEEPABLE_TARGETS = TARGETS.filter(
109
109
  var PRESET_NAMES = ["react", "nest", "svelte", "node", "tools"];
110
110
 
111
111
  // src/shared/color.ts
112
- import { styleText } from "util";
112
+ import { styleText } from "node:util";
113
113
  function palette(stream) {
114
114
  const paint = (format, text) => styleText(format, text, { stream });
115
115
  return {
@@ -123,8 +123,8 @@ function palette(stream) {
123
123
  }
124
124
 
125
125
  // src/roles/build/nest/nx-target.ts
126
- import { existsSync, readFileSync } from "fs";
127
- import { join, relative } from "path";
126
+ import { existsSync, readFileSync } from "node:fs";
127
+ import { join, relative } from "node:path";
128
128
  var DEPRECATED_NX_BUILDER = "@nx/webpack:webpack";
129
129
  var TRANSLATABLE_WEBPACK_CONFIG = "tools/webpack-configs/update.webpack.config.js";
130
130
  var readProjectJson = (cwd) => {
@@ -195,8 +195,8 @@ function workspaceRootHops(cwd, target) {
195
195
  }
196
196
 
197
197
  // src/roles/build/read-adoption.ts
198
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
199
- import { join as join4 } from "path";
198
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
199
+ import { join as join4 } from "node:path";
200
200
 
201
201
  // src/core/config/module-type.ts
202
202
  function esmReadiness(fileName, packageType) {
@@ -210,8 +210,8 @@ function esmReadiness(fileName, packageType) {
210
210
  }
211
211
 
212
212
  // src/core/config/manifest.ts
213
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
214
- import { basename, join as join2 } from "path";
213
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
214
+ import { basename, join as join2 } from "node:path";
215
215
  function moduleScripts(cwd) {
216
216
  try {
217
217
  const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
@@ -220,6 +220,14 @@ function moduleScripts(cwd) {
220
220
  return {};
221
221
  }
222
222
  }
223
+ function manifestTargets(cwd) {
224
+ try {
225
+ const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
226
+ return pkg.nx?.targets ?? {};
227
+ } catch {
228
+ return {};
229
+ }
230
+ }
223
231
  function moduleName2(cwd) {
224
232
  try {
225
233
  const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
@@ -256,8 +264,8 @@ function manifestOperation(cwd, scripts) {
256
264
  }
257
265
 
258
266
  // src/core/config/project-json.ts
259
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
260
- import { join as join3 } from "path";
267
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
268
+ import { join as join3 } from "node:path";
261
269
  function projectTargets(cwd) {
262
270
  const path = join3(cwd, "project.json");
263
271
  if (!existsSync3(path)) return void 0;
@@ -514,11 +522,11 @@ function readBuildAdoption(cwd) {
514
522
  }
515
523
 
516
524
  // src/roles/build/adapters/vite/vite-role.adapter.ts
517
- import { spawnSync } from "child_process";
525
+ import { spawnSync } from "node:child_process";
518
526
 
519
527
  // src/core/config/tool-args.ts
520
- import { existsSync as existsSync5 } from "fs";
521
- import { isAbsolute, resolve as resolve2 } from "path";
528
+ import { existsSync as existsSync5 } from "node:fs";
529
+ import { isAbsolute, resolve as resolve2 } from "node:path";
522
530
  function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
523
531
  const takesValue = new Set(valueFlags);
524
532
  const options = [];
@@ -535,9 +543,9 @@ function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
535
543
  }
536
544
 
537
545
  // src/shared/resolve-bin.ts
538
- import { existsSync as existsSync6 } from "fs";
539
- import { createRequire } from "module";
540
- import { delimiter, dirname, join as join5 } from "path";
546
+ import { existsSync as existsSync6 } from "node:fs";
547
+ import { createRequire } from "node:module";
548
+ import { delimiter, dirname, join as join5 } from "node:path";
541
549
  var require2 = createRequire(import.meta.url);
542
550
  function resolveBin(fromDir, name) {
543
551
  let dir = fromDir;
@@ -553,9 +561,9 @@ function binFromOwnInstall(packageName, binName) {
553
561
  try {
554
562
  const manifest = require2.resolve(`${packageName}/package.json`);
555
563
  const bin = require2(manifest).bin;
556
- const relative7 = typeof bin === "string" ? bin : bin?.[binName];
557
- if (!relative7) return void 0;
558
- const executable = join5(dirname(manifest), relative7);
564
+ const relative8 = typeof bin === "string" ? bin : bin?.[binName];
565
+ if (!relative8) return void 0;
566
+ const executable = join5(dirname(manifest), relative8);
559
567
  return existsSync6(executable) ? executable : void 0;
560
568
  } catch {
561
569
  return void 0;
@@ -568,12 +576,12 @@ function binSearchPath(cwd) {
568
576
  }
569
577
 
570
578
  // src/roles/build/plan.ts
571
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
572
- import { join as join10 } from "path";
579
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
580
+ import { join as join10 } from "node:path";
573
581
 
574
582
  // src/core/config/existing-command.ts
575
- import { readFileSync as readFileSync5 } from "fs";
576
- import { join as join6 } from "path";
583
+ import { readFileSync as readFileSync5 } from "node:fs";
584
+ import { join as join6 } from "node:path";
577
585
 
578
586
  // src/shared/jsonc.ts
579
587
  import { parse, printParseErrorCode } from "jsonc-parser";
@@ -623,8 +631,8 @@ function existingCommand(cwd, target) {
623
631
  }
624
632
 
625
633
  // src/core/config/nx-target.ts
626
- import { existsSync as existsSync7 } from "fs";
627
- import { join as join7 } from "path";
634
+ import { existsSync as existsSync7 } from "node:fs";
635
+ import { join as join7 } from "node:path";
628
636
  function nxTargetMetadataOperations(options) {
629
637
  const { targets } = options;
630
638
  if (Object.keys(targets).length === 0) return [];
@@ -689,8 +697,8 @@ function keepsOtherCommands(script, sentinelCommand) {
689
697
  }
690
698
 
691
699
  // src/roles/format/read-adoption.ts
692
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
693
- import { join as join8 } from "path";
700
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
701
+ import { join as join8 } from "node:path";
694
702
 
695
703
  // src/roles/format/config-policy.ts
696
704
  var FORMAT_CONFIG_FILE = ".oxfmtrc.json";
@@ -901,8 +909,8 @@ function devTargetOperations(cwd, devScript) {
901
909
  }
902
910
 
903
911
  // src/roles/build/nest/adopt.ts
904
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
905
- import { join as join9 } from "path";
912
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
913
+ import { join as join9 } from "node:path";
906
914
  var NEST_CONFIG_FILE = "vite.config.mts";
907
915
  function asSource(value, depth) {
908
916
  const pad = " ".repeat(depth);
@@ -1445,8 +1453,8 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
1445
1453
  };
1446
1454
 
1447
1455
  // src/roles/build/declared-outputs.ts
1448
- import { existsSync as existsSync11, rmSync, statSync, writeFileSync } from "fs";
1449
- import { join as join11 } from "path";
1456
+ import { existsSync as existsSync11, rmSync, statSync, writeFileSync } from "node:fs";
1457
+ import { join as join11 } from "node:path";
1450
1458
  function declaredBuildTarget(cwd) {
1451
1459
  return readProjectPackageJson(cwd).nx?.targets?.[BUILD_SCRIPT_NAME];
1452
1460
  }
@@ -1492,7 +1500,7 @@ function outputsProblem(cwd, startedAt) {
1492
1500
  }
1493
1501
 
1494
1502
  // src/roles/build/prerequisite.ts
1495
- import { spawnSync as spawnSync2 } from "child_process";
1503
+ import { spawnSync as spawnSync2 } from "node:child_process";
1496
1504
  var PREBUILD_SCRIPT_NAME = `pre${BUILD_SCRIPT_NAME}`;
1497
1505
  function insideBuildScript(env = process.env) {
1498
1506
  return env.npm_lifecycle_event === BUILD_SCRIPT_NAME;
@@ -1618,13 +1626,13 @@ function registerBuild() {
1618
1626
  }
1619
1627
 
1620
1628
  // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
1621
- import { spawnSync as spawnSync3 } from "child_process";
1622
- import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
1623
- import { join as join16 } from "path";
1629
+ import { spawnSync as spawnSync3 } from "node:child_process";
1630
+ import { existsSync as existsSync14, readFileSync as readFileSync12 } from "node:fs";
1631
+ import { join as join16 } from "node:path";
1624
1632
 
1625
1633
  // src/core/config/has-source.ts
1626
- import { readdirSync } from "fs";
1627
- import { extname, join as join12, relative as relative2 } from "path";
1634
+ import { readdirSync } from "node:fs";
1635
+ import { extname, join as join12, relative as relative2 } from "node:path";
1628
1636
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
1629
1637
  "node_modules",
1630
1638
  "dist",
@@ -1742,8 +1750,8 @@ function writesWhenRewritten(name, command) {
1742
1750
  var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
1743
1751
 
1744
1752
  // src/roles/format/inherited-ignores.ts
1745
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
1746
- import { join as join13 } from "path";
1753
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
1754
+ import { join as join13 } from "node:path";
1747
1755
  var ROOT_IGNORE_FILE = ".prettierignore";
1748
1756
  function isPattern(line) {
1749
1757
  const trimmed = line.trim();
@@ -1808,13 +1816,13 @@ function needsSvelteCompiler(output) {
1808
1816
  }
1809
1817
 
1810
1818
  // src/roles/format/prettier-config.ts
1811
- import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
1812
- import { join as join15 } from "path";
1819
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "node:fs";
1820
+ import { join as join15 } from "node:path";
1813
1821
 
1814
1822
  // src/roles/format/resolve-oxfmt.ts
1815
- import { readFileSync as readFileSync10 } from "fs";
1816
- import { createRequire as createRequire2 } from "module";
1817
- import { dirname as dirname2, join as join14 } from "path";
1823
+ import { readFileSync as readFileSync10 } from "node:fs";
1824
+ import { createRequire as createRequire2 } from "node:module";
1825
+ import { dirname as dirname2, join as join14 } from "node:path";
1818
1826
  function resolveOxfmt(cwd) {
1819
1827
  return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
1820
1828
  }
@@ -2314,9 +2322,9 @@ function registerFormat() {
2314
2322
  }
2315
2323
 
2316
2324
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
2317
- import { spawnSync as spawnSync4 } from "child_process";
2318
- import { existsSync as existsSync22, readFileSync as readFileSync20, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
2319
- import { join as join26 } from "path";
2325
+ import { spawnSync as spawnSync4 } from "node:child_process";
2326
+ import { existsSync as existsSync22, readFileSync as readFileSync20, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
2327
+ import { join as join26 } from "node:path";
2320
2328
 
2321
2329
  // src/core/config/deferred-rules.ts
2322
2330
  function deferredRuleNames(rules) {
@@ -2324,8 +2332,8 @@ function deferredRuleNames(rules) {
2324
2332
  }
2325
2333
 
2326
2334
  // src/core/fix-report.ts
2327
- import { statSync as statSync2 } from "fs";
2328
- import { join as join17 } from "path";
2335
+ import { statSync as statSync2 } from "node:fs";
2336
+ import { join as join17 } from "node:path";
2329
2337
  var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
2330
2338
  function readFailure(output) {
2331
2339
  const lines = output.split("\n").map((line2) => line2.trim());
@@ -2360,8 +2368,12 @@ function changedSince(cwd, before, extensions) {
2360
2368
  return changed.sort();
2361
2369
  }
2362
2370
  var NAMED_IN_REPORT = 5;
2371
+ var NOT_INSTALLED_YET = /invalid config file[\s\S]*node_modules/i;
2363
2372
  function describeFixOutcome(outcome, toolLabel) {
2364
2373
  if (outcome.failure !== void 0) {
2374
+ if (NOT_INSTALLED_YET.test(outcome.failure)) {
2375
+ return `the ${toolLabel} preset is not installed in this module yet, which is expected: this command just added it and the install comes next. Nothing was fixed. Run \`pnpm install\`, then \`sentinel --run --${toolLabel} --fix\` to clear what a machine can clear.`;
2376
+ }
2365
2377
  return `the ${toolLabel} autofix could not run, so nothing was fixed and this module has not been checked: ${outcome.failure}`;
2366
2378
  }
2367
2379
  if (outcome.changed.length === 0) return `the ${toolLabel} autofix changed nothing`;
@@ -2372,8 +2384,8 @@ function describeFixOutcome(outcome, toolLabel) {
2372
2384
  }
2373
2385
 
2374
2386
  // src/roles/lint/module-baseline.ts
2375
- import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
2376
- import { join as join18 } from "path";
2387
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
2388
+ import { join as join18 } from "node:path";
2377
2389
  var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
2378
2390
  var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
2379
2391
  var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
@@ -4247,8 +4259,8 @@ function downgradedRulesFor(preset) {
4247
4259
  }
4248
4260
 
4249
4261
  // src/roles/lint/extra-layers.ts
4250
- import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
4251
- import { isAbsolute as isAbsolute2, join as join19, resolve as resolve3 } from "path";
4262
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "node:fs";
4263
+ import { isAbsolute as isAbsolute2, join as join19, resolve as resolve3 } from "node:path";
4252
4264
  function ruleCount(cwd, specifier) {
4253
4265
  const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
4254
4266
  if (!existsSync16(path)) return void 0;
@@ -4333,8 +4345,8 @@ function errorCountsByConfigRule(stdout) {
4333
4345
  }
4334
4346
 
4335
4347
  // src/core/config/read-adoption.ts
4336
- import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
4337
- import { join as join21 } from "path";
4348
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "node:fs";
4349
+ import { join as join21 } from "node:path";
4338
4350
 
4339
4351
  // src/core/config/owned-keys.ts
4340
4352
  function presetOwnedKeys(config, permitted, presetSets) {
@@ -4349,8 +4361,8 @@ function localOnlyKeys(config, permitted, presetSets) {
4349
4361
  }
4350
4362
 
4351
4363
  // src/core/config/resolve-config-target.ts
4352
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
4353
- import { join as join20 } from "path";
4364
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "node:fs";
4365
+ import { join as join20 } from "node:path";
4354
4366
  function readExtends(absolutePath) {
4355
4367
  let parsed;
4356
4368
  try {
@@ -4441,10 +4453,10 @@ function readLintAdoption(cwd) {
4441
4453
  }
4442
4454
 
4443
4455
  // src/roles/lint/resolve-oxlint.ts
4444
- import { existsSync as existsSync19 } from "fs";
4445
- import { createRequire as createRequire3 } from "module";
4446
- import { delimiter as delimiter2, dirname as dirname3, join as join22 } from "path";
4447
- import { fileURLToPath } from "url";
4456
+ import { existsSync as existsSync19 } from "node:fs";
4457
+ import { createRequire as createRequire3 } from "node:module";
4458
+ import { delimiter as delimiter2, dirname as dirname3, join as join22 } from "node:path";
4459
+ import { fileURLToPath } from "node:url";
4448
4460
  var PACKAGE_OF = {
4449
4461
  oxlint: "oxlint",
4450
4462
  tsgolint: "oxlint-tsgolint"
@@ -4486,12 +4498,12 @@ function oxlintSearchPath(cwd) {
4486
4498
  }
4487
4499
 
4488
4500
  // src/roles/lint/adapters/oxlint/plan.ts
4489
- import { existsSync as existsSync21, readFileSync as readFileSync19 } from "fs";
4490
- import { join as join25 } from "path";
4501
+ import { existsSync as existsSync21, readFileSync as readFileSync19 } from "node:fs";
4502
+ import { join as join25 } from "node:path";
4491
4503
 
4492
4504
  // src/roles/lint/eslint-ignores.ts
4493
- import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
4494
- import { join as join23 } from "path";
4505
+ import { existsSync as existsSync20, readFileSync as readFileSync17 } from "node:fs";
4506
+ import { join as join23 } from "node:path";
4495
4507
  import { parseSync as parseSync2 } from "oxc-parser";
4496
4508
 
4497
4509
  // src/shared/ast.ts
@@ -4615,8 +4627,8 @@ function lintPresetFor(preset) {
4615
4627
  }
4616
4628
 
4617
4629
  // src/roles/lint/rename-suppressions.ts
4618
- import { readdirSync as readdirSync2, readFileSync as readFileSync18, statSync as statSync3 } from "fs";
4619
- import { join as join24, relative as relative3 } from "path";
4630
+ import { readdirSync as readdirSync2, readFileSync as readFileSync18, statSync as statSync3 } from "node:fs";
4631
+ import { join as join24, relative as relative3 } from "node:path";
4620
4632
  var SOURCE_EXTENSIONS = [
4621
4633
  ".ts",
4622
4634
  ".tsx",
@@ -5393,7 +5405,10 @@ function registerLint() {
5393
5405
  }
5394
5406
 
5395
5407
  // src/roles/test/adapters/vitest/vitest.adapter.ts
5396
- import { spawnSync as spawnSync5 } from "child_process";
5408
+ import { spawnSync as spawnSync5 } from "node:child_process";
5409
+ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync31 } from "node:fs";
5410
+ import { tmpdir as tmpdir2 } from "node:os";
5411
+ import { isAbsolute as isAbsolute3, join as join39, resolve as resolve6 } from "node:path";
5397
5412
 
5398
5413
  // src/roles/test/presets/toolchain.json
5399
5414
  var toolchain_default2 = {
@@ -5470,17 +5485,36 @@ function testTarget(options) {
5470
5485
  }
5471
5486
 
5472
5487
  // src/roles/test/jest-migration.ts
5473
- import { existsSync as existsSync28, readFileSync as readFileSync28 } from "fs";
5474
- import { join as join35 } from "path";
5488
+ import { existsSync as existsSync30, readFileSync as readFileSync29, readdirSync as readdirSync8 } from "node:fs";
5489
+ import { createRequire as createRequire6 } from "node:module";
5490
+ import { dirname as dirname8, join as join37, relative as relative7 } from "node:path";
5475
5491
 
5476
5492
  // src/roles/test/codemod/migrate-module.ts
5477
- import { readdirSync as readdirSync6, readFileSync as readFileSync25, statSync as statSync6 } from "fs";
5478
- import { join as join31 } from "path";
5493
+ import { readdirSync as readdirSync6, readFileSync as readFileSync26, statSync as statSync6 } from "node:fs";
5494
+ import { join as join32 } from "node:path";
5479
5495
  import { parseSync as parseSync8 } from "oxc-parser";
5480
5496
 
5497
+ // src/roles/test/msw-lifecycle.ts
5498
+ import { existsSync as existsSync23, readFileSync as readFileSync21 } from "node:fs";
5499
+ import { join as join27 } from "node:path";
5500
+ var ROOT_SETUP = "jest.setup.after.env.js";
5501
+ function ownsMswLifecycle(cwd, setupFiles) {
5502
+ for (const raw of setupFiles) {
5503
+ if (raw.endsWith(ROOT_SETUP)) continue;
5504
+ const file = join27(cwd, raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", "."));
5505
+ if (!existsSync23(file)) continue;
5506
+ try {
5507
+ if (/\.listen\s*\(/.test(readFileSync21(file, "utf8"))) return true;
5508
+ } catch {
5509
+ continue;
5510
+ }
5511
+ }
5512
+ return false;
5513
+ }
5514
+
5481
5515
  // src/roles/test/read-adoption.ts
5482
- import { existsSync as existsSync23, readFileSync as readFileSync21, readdirSync as readdirSync3 } from "fs";
5483
- import { join as join27, relative as relative4 } from "path";
5516
+ import { existsSync as existsSync24, readFileSync as readFileSync22, readdirSync as readdirSync3 } from "node:fs";
5517
+ import { join as join28, relative as relative4 } from "node:path";
5484
5518
  var JEST_CONFIG_FILES = [
5485
5519
  "jest.config.ts",
5486
5520
  "jest.config.js",
@@ -5489,15 +5523,15 @@ var JEST_CONFIG_FILES = [
5489
5523
  "jest.integration-config.ts"
5490
5524
  ];
5491
5525
  function testConfigFile(cwd) {
5492
- return TEST_CONFIG_FILES.find((name) => existsSync23(join27(cwd, name)));
5526
+ return TEST_CONFIG_FILES.find((name) => existsSync24(join28(cwd, name)));
5493
5527
  }
5494
5528
  function jestConfigFiles(cwd) {
5495
- return JEST_CONFIG_FILES.filter((name) => existsSync23(join27(cwd, name)));
5529
+ return JEST_CONFIG_FILES.filter((name) => existsSync24(join28(cwd, name)));
5496
5530
  }
5497
5531
  function rootJestConfigFiles(cwd) {
5498
5532
  const root = findWorkspaceRoot(cwd);
5499
5533
  if (root === void 0 || root === cwd) return [];
5500
- return JEST_CONFIG_FILES.filter((name) => existsSync23(join27(root, name)));
5534
+ return JEST_CONFIG_FILES.filter((name) => existsSync24(join28(root, name)));
5501
5535
  }
5502
5536
  var TEST_FILE = /[.-](spec|test)\.[cm]?[jt]sx?$/;
5503
5537
  var NOT_WALKED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo"]);
@@ -5520,9 +5554,9 @@ function walkTestFiles(cwd, stopAtFirst) {
5520
5554
  }
5521
5555
  for (const entry of entries) {
5522
5556
  if (entry.isDirectory()) {
5523
- if (!NOT_WALKED.has(entry.name)) stack.push(join27(dir, entry.name));
5557
+ if (!NOT_WALKED.has(entry.name)) stack.push(join28(dir, entry.name));
5524
5558
  } else if (TEST_FILE.test(entry.name)) {
5525
- found.push(relative4(cwd, join27(dir, entry.name)).replaceAll("\\", "/"));
5559
+ found.push(relative4(cwd, join28(dir, entry.name)).replaceAll("\\", "/"));
5526
5560
  if (stopAtFirst) return found;
5527
5561
  }
5528
5562
  }
@@ -5531,7 +5565,7 @@ function walkTestFiles(cwd, stopAtFirst) {
5531
5565
  }
5532
5566
  function readTestConfigSource(cwd, fileName) {
5533
5567
  try {
5534
- return readFileSync21(join27(cwd, fileName), "utf8");
5568
+ return readFileSync22(join28(cwd, fileName), "utf8");
5535
5569
  } catch {
5536
5570
  return "";
5537
5571
  }
@@ -5600,6 +5634,17 @@ function readTestAdoption(cwd) {
5600
5634
  };
5601
5635
  }
5602
5636
 
5637
+ // src/roles/test/support-file-name.ts
5638
+ var JEST_PREFIX = "jest.";
5639
+ var VITEST_PREFIX = "vitest.";
5640
+ function vitestSupportName(relative8) {
5641
+ const cut = relative8.lastIndexOf("/");
5642
+ const directory = cut === -1 ? "" : relative8.slice(0, cut + 1);
5643
+ const base = relative8.slice(cut + 1);
5644
+ if (!base.startsWith(JEST_PREFIX)) return relative8;
5645
+ return `${directory}${VITEST_PREFIX}${base.slice(JEST_PREFIX.length)}`;
5646
+ }
5647
+
5603
5648
  // src/roles/test/codemod/migrate-file.ts
5604
5649
  import { parseSync as parseSync7 } from "oxc-parser";
5605
5650
 
@@ -5715,7 +5760,7 @@ function rewriteAssertionReturnValue(source, program) {
5715
5760
  for (const reading of readings) {
5716
5761
  const from = reading.start;
5717
5762
  let to = reading.end;
5718
- while (to < source.length && /[\s;]/.test(source[to])) to += 1;
5763
+ while (to < source.length && /[ \t;]/.test(source[to])) to += 1;
5719
5764
  let head = from;
5720
5765
  while (head > 0 && /[ \t]/.test(source[head - 1])) head -= 1;
5721
5766
  if (head > 0 && source[head - 1] === "\n") head -= 1;
@@ -6450,9 +6495,9 @@ ${source}`;
6450
6495
  }
6451
6496
 
6452
6497
  // src/roles/test/codemod/named-rules.ts
6453
- import { existsSync as existsSync24, readFileSync as readFileSync22 } from "fs";
6454
- import { createRequire as createRequire4 } from "module";
6455
- import { dirname as dirname4, join as join28 } from "path";
6498
+ import { existsSync as existsSync25, readFileSync as readFileSync23 } from "node:fs";
6499
+ import { createRequire as createRequire4 } from "node:module";
6500
+ import { dirname as dirname4, join as join29 } from "node:path";
6456
6501
  function isCommonJsPackage(fromFile, specifier) {
6457
6502
  if (specifier.startsWith(".") || specifier.startsWith("/")) return false;
6458
6503
  let resolved;
@@ -6466,10 +6511,10 @@ function isCommonJsPackage(fromFile, specifier) {
6466
6511
  if (resolved.endsWith(".cjs")) return true;
6467
6512
  let dir = dirname4(resolved);
6468
6513
  for (let depth = 0; depth < 12; depth += 1) {
6469
- const manifest = join28(dir, "package.json");
6470
- if (existsSync24(manifest)) {
6514
+ const manifest = join29(dir, "package.json");
6515
+ if (existsSync25(manifest)) {
6471
6516
  try {
6472
- return JSON.parse(readFileSync22(manifest, "utf8")).type !== "module";
6517
+ return JSON.parse(readFileSync23(manifest, "utf8")).type !== "module";
6473
6518
  } catch {
6474
6519
  return false;
6475
6520
  }
@@ -6581,12 +6626,47 @@ function applyNamedRules(source, program, fileName = "") {
6581
6626
  const applied = /* @__PURE__ */ new Set();
6582
6627
  const asyncify = /* @__PURE__ */ new Set();
6583
6628
  const mocked = mockedSpecifiers(program);
6584
- const wholeStatement = /* @__PURE__ */ new Set();
6629
+ const wholeStatement = /* @__PURE__ */ new Map();
6585
6630
  walk(program, (node) => {
6586
6631
  if (node.type !== "ExpressionStatement") return;
6587
6632
  const expression = node.expression;
6588
- if (expression !== void 0) wholeStatement.add(expression);
6633
+ if (expression !== void 0) wholeStatement.set(expression, node);
6634
+ });
6635
+ const standsAlone = /* @__PURE__ */ new Set();
6636
+ const alone = (node) => {
6637
+ if (node !== void 0) standsAlone.add(node);
6638
+ };
6639
+ walk(program, (node) => {
6640
+ switch (node.type) {
6641
+ case "VariableDeclarator":
6642
+ return alone(node.init);
6643
+ case "AssignmentExpression":
6644
+ return alone(node.right);
6645
+ case "ReturnStatement":
6646
+ case "ExportDefaultDeclaration":
6647
+ return alone(node.argument ?? node.declaration);
6648
+ case "ArrowFunctionExpression":
6649
+ return node.expression === true ? alone(node.body) : void 0;
6650
+ case "ConditionalExpression":
6651
+ alone(node.consequent);
6652
+ return alone(node.alternate);
6653
+ case "ArrayExpression":
6654
+ return (node.elements ?? []).forEach((e) => alone(e ?? void 0));
6655
+ case "Property":
6656
+ case "ObjectProperty":
6657
+ return node.shorthand === true ? void 0 : alone(node.value);
6658
+ case "CallExpression":
6659
+ case "NewExpression":
6660
+ return (node.arguments ?? []).forEach((a) => alone(a));
6661
+ default:
6662
+ return;
6663
+ }
6589
6664
  });
6665
+ const placed = (node, expression, trailing = "") => {
6666
+ if (trailing !== "") return `(${expression})${trailing}`;
6667
+ if (wholeStatement.has(node)) return expression;
6668
+ return standsAlone.has(node) ? expression : `(${expression})`;
6669
+ };
6590
6670
  const handedToTheRunnerAsAModule = /* @__PURE__ */ new Set();
6591
6671
  walk(program, (node) => {
6592
6672
  const returned = mockFactoryReturn(node);
@@ -6616,7 +6696,7 @@ function applyNamedRules(source, program, fileName = "") {
6616
6696
  edits.push({
6617
6697
  start: node.start,
6618
6698
  end: node.end,
6619
- text: `(await import(${argumentText(source, node)}))`
6699
+ text: placed(node, `await import(${argumentText(source, node)})`)
6620
6700
  });
6621
6701
  applied.add("jest.requireMock");
6622
6702
  if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
@@ -6628,7 +6708,7 @@ function applyNamedRules(source, program, fileName = "") {
6628
6708
  edits.push({
6629
6709
  start: node.start,
6630
6710
  end: node.end,
6631
- text: `(await vi.${asyncReplacement}(${argumentText(source, node)}))`
6711
+ text: placed(node, `await vi.${asyncReplacement}(${argumentText(source, node)})`)
6632
6712
  });
6633
6713
  applied.add(`jest.${member}`);
6634
6714
  if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
@@ -6641,7 +6721,7 @@ function applyNamedRules(source, program, fileName = "") {
6641
6721
  edits.push({
6642
6722
  start: node.start,
6643
6723
  end: node.end,
6644
- text: wholeStatement.has(node) ? `await import(${argumentText(source, node)});` : `(await import(${argumentText(source, node)}))${interop}`
6724
+ text: placed(node, `await import(${argumentText(source, node)})`, interop)
6645
6725
  });
6646
6726
  applied.add("require");
6647
6727
  if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
@@ -6932,6 +7012,78 @@ function rewriteMockFactoryDefault(source, program, defaultImported) {
6932
7012
  };
6933
7013
  }
6934
7014
 
7015
+ // src/roles/test/codemod/mock-promise-resolve.ts
7016
+ var MOCK_FACTORY = /* @__PURE__ */ new Set(["fn", "spyOn"]);
7017
+ function isMockFactory(source, node) {
7018
+ if (node.type !== "CallExpression") return false;
7019
+ const callee = node.callee;
7020
+ if (callee?.type !== "MemberExpression") return false;
7021
+ const object = callee.object;
7022
+ const property = callee.property;
7023
+ if (object?.type !== "Identifier" || property?.type !== "Identifier") return false;
7024
+ const runner = source.slice(object.start, object.end);
7025
+ const method = source.slice(property.start, property.end);
7026
+ return (runner === "jest" || runner === "vi") && MOCK_FACTORY.has(method);
7027
+ }
7028
+ function resolvedValue(source, executor) {
7029
+ if (executor.type !== "ArrowFunctionExpression" && executor.type !== "FunctionExpression") {
7030
+ return void 0;
7031
+ }
7032
+ const params = executor.params;
7033
+ if (params?.length !== 1) return void 0;
7034
+ const parameter = params[0];
7035
+ if (parameter.type !== "Identifier") return void 0;
7036
+ const resolveName = source.slice(parameter.start, parameter.end);
7037
+ const body = executor.body;
7038
+ if (body === void 0) return void 0;
7039
+ let call;
7040
+ if (body.type === "BlockStatement") {
7041
+ const statements = body.body;
7042
+ if (statements?.length !== 1) return void 0;
7043
+ const only = statements[0];
7044
+ if (only.type !== "ExpressionStatement") return void 0;
7045
+ call = only.expression;
7046
+ } else {
7047
+ call = body;
7048
+ }
7049
+ if (call?.type !== "CallExpression") return void 0;
7050
+ const callee = call.callee;
7051
+ if (callee?.type !== "Identifier") return void 0;
7052
+ if (source.slice(callee.start, callee.end) !== resolveName) return void 0;
7053
+ const args = call.arguments;
7054
+ if (args?.length !== 1) return void 0;
7055
+ return args[0];
7056
+ }
7057
+ function rewriteMockPromiseResolve(source, program) {
7058
+ const edits = [];
7059
+ walk(program, (node) => {
7060
+ if (!isMockFactory(source, node)) return;
7061
+ for (const argument of node.arguments ?? []) {
7062
+ walk(argument, (inner) => {
7063
+ if (inner.type !== "NewExpression") return;
7064
+ const callee = inner.callee;
7065
+ if (callee?.type !== "Identifier") return;
7066
+ if (source.slice(callee.start, callee.end) !== "Promise") return;
7067
+ if ((inner.typeArguments ?? inner.typeParameters) !== void 0) return;
7068
+ const executors = inner.arguments ?? [];
7069
+ if (executors.length !== 1) return;
7070
+ const value = resolvedValue(source, executors[0]);
7071
+ if (value === void 0) return;
7072
+ edits.push({
7073
+ start: inner.start,
7074
+ end: inner.end,
7075
+ text: `Promise.resolve(${source.slice(value.start, value.end)})`
7076
+ });
7077
+ });
7078
+ }
7079
+ });
7080
+ let result = source;
7081
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
7082
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
7083
+ }
7084
+ return { source: result, rewritten: edits.length };
7085
+ }
7086
+
6935
7087
  // src/roles/test/codemod/propagate-await.ts
6936
7088
  import { parseSync as parseSync5 } from "oxc-parser";
6937
7089
  var FUNCTIONS3 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
@@ -7059,9 +7211,9 @@ function propagateAwait(fileName, source, original, elsewhere = /* @__PURE__ */
7059
7211
  }
7060
7212
 
7061
7213
  // src/roles/test/codemod/reexport-barrel-mock.ts
7062
- import { readFileSync as readFileSync23, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
7063
- import { createRequire as createRequire5 } from "module";
7064
- import { dirname as dirname5, join as join29 } from "path";
7214
+ import { readFileSync as readFileSync24, readdirSync as readdirSync4, statSync as statSync4 } from "node:fs";
7215
+ import { createRequire as createRequire5 } from "node:module";
7216
+ import { dirname as dirname5, join as join30 } from "node:path";
7065
7217
  var MEMBER_TYPES13 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
7066
7218
  var PACKAGE_SUBPATH = /^(@[^/]+\/[^/]+|[^@.][^/]*)\/(.+)$/;
7067
7219
  var PURE_REEXPORT = /^module\.exports\s*=\s*require\(\s*(['"])([^'"]+)\1\s*\)\s*;?$/;
@@ -7114,11 +7266,11 @@ function barrelsFor(fromFile, name, target) {
7114
7266
  const aliases = [];
7115
7267
  for (const entry of entries) {
7116
7268
  if (!entry.endsWith(".js")) continue;
7117
- const path = join29(root, entry);
7269
+ const path = join30(root, entry);
7118
7270
  let body;
7119
7271
  try {
7120
7272
  if (!statSync4(path).isFile()) continue;
7121
- body = readFileSync23(path, "utf8");
7273
+ body = readFileSync24(path, "utf8");
7122
7274
  } catch {
7123
7275
  continue;
7124
7276
  }
@@ -7160,12 +7312,9 @@ ${text}` });
7160
7312
  // src/roles/test/codemod/repoint-msw.ts
7161
7313
  import { parseSync as parseSync6 } from "oxc-parser";
7162
7314
  var SENTINEL_MSW_SERVER = "@hublo/sentinel/test/msw";
7163
- var MSW_SERVER_MODULE = /(^|\/)msw\/server$/;
7315
+ var SENTINEL_MSW_API = "@hublo/sentinel/test/tools/msw";
7164
7316
  var MSW_PACKAGE = /^msw$/;
7165
- function importsMswPackage(declaration) {
7166
- const source = declaration.source?.value;
7167
- return source !== void 0 && MSW_PACKAGE.test(source);
7168
- }
7317
+ var MSW_SERVER_MODULE = /(^|\/)msw\/server$/;
7169
7318
  function importsServer(declaration) {
7170
7319
  return (declaration.specifiers ?? []).some((specifier) => {
7171
7320
  const imported = specifier.imported;
@@ -7181,21 +7330,28 @@ function repointMswServer(fileName, source) {
7181
7330
  if (node.type !== "ImportDeclaration") return;
7182
7331
  const specifier = node.source;
7183
7332
  if (specifier?.value === void 0) return;
7184
- const isServerModule = MSW_SERVER_MODULE.test(specifier.value) && importsServer(node);
7185
- if (!isServerModule && !importsMswPackage(node)) return;
7186
- edits.push({ start: specifier.start, end: specifier.end });
7333
+ const isServer = MSW_SERVER_MODULE.test(specifier.value) && importsServer(node);
7334
+ const isPackage2 = MSW_PACKAGE.test(specifier.value);
7335
+ if (!isServer && !isPackage2) return;
7336
+ edits.push({
7337
+ start: specifier.start,
7338
+ end: specifier.end,
7339
+ to: isServer ? SENTINEL_MSW_SERVER : SENTINEL_MSW_API
7340
+ });
7187
7341
  });
7188
7342
  if (edits.length === 0) return { source, repointed: false };
7189
7343
  let rewritten = source;
7190
7344
  for (const edit of edits.sort((left, right) => right.start - left.start)) {
7191
7345
  const quote = source[edit.start] === '"' ? '"' : "'";
7192
- rewritten = rewritten.slice(0, edit.start) + `${quote}${SENTINEL_MSW_SERVER}${quote}` + rewritten.slice(edit.end);
7346
+ rewritten = rewritten.slice(0, edit.start) + `${quote}${edit.to}${quote}` + rewritten.slice(edit.end);
7193
7347
  }
7194
7348
  return { source: rewritten, repointed: true };
7195
7349
  }
7196
7350
 
7197
7351
  // src/roles/test/codemod/spy-instance-type.ts
7198
7352
  var SPY_TYPES = /* @__PURE__ */ new Set(["SpyInstance"]);
7353
+ var QUALIFIED_ONLY = /* @__PURE__ */ new Set(["Mock"]);
7354
+ var VITEST_NAME = { SpyInstance: "MockInstance", Mock: "Mock" };
7199
7355
  function typeNameOf(source, node) {
7200
7356
  const name = node.typeName;
7201
7357
  if (name === void 0) return void 0;
@@ -7207,17 +7363,20 @@ function rewriteSpyInstanceType(source, program) {
7207
7363
  if (node.type !== "TSTypeReference") return;
7208
7364
  const written = typeNameOf(source, node);
7209
7365
  if (written === void 0) return;
7210
- const bare = written.startsWith("jest.") ? written.slice("jest.".length) : written;
7211
- if (!SPY_TYPES.has(bare)) return;
7366
+ const qualified = written.startsWith("jest.");
7367
+ const bare = qualified ? written.slice("jest.".length) : written;
7368
+ if (!SPY_TYPES.has(bare) && !(qualified && QUALIFIED_ONLY.has(bare))) return;
7212
7369
  const args = (node.typeArguments ?? node.typeParameters)?.params;
7213
7370
  if (args === void 0 || args.length === 0) return;
7214
7371
  const text = (argument) => source.slice(argument.start, argument.end);
7215
7372
  const returns = text(args[0]);
7216
- const parameters = args.length > 1 ? `...args: ${text(args[1])}` : "...args: any[]";
7373
+ const parameters = args.length > 1 ? `...args: ${text(args[1])}` : "...args: never[]";
7374
+ const target = VITEST_NAME[bare];
7217
7375
  edits.push({
7218
7376
  start: node.start,
7219
7377
  end: node.end,
7220
- text: `MockInstance<(${parameters}) => ${returns}>`
7378
+ text: `${target}<(${parameters}) => ${returns}>`,
7379
+ imports: target
7221
7380
  });
7222
7381
  });
7223
7382
  let result = source;
@@ -7227,7 +7386,8 @@ function rewriteSpyInstanceType(source, program) {
7227
7386
  return {
7228
7387
  source: result,
7229
7388
  rewritten: edits.length,
7230
- typeImports: edits.length > 0 ? ["MockInstance"] : []
7389
+ // Only the names actually written, so a file does not import a type it never uses.
7390
+ typeImports: [...new Set(edits.map((edit) => edit.imports))]
7231
7391
  };
7232
7392
  }
7233
7393
 
@@ -7252,7 +7412,7 @@ function parse2(fileName, source) {
7252
7412
  const { program, errors } = parseSync7(fileName, source, { sourceType: "module" });
7253
7413
  return errors.length > 0 ? void 0 : program;
7254
7414
  }
7255
- function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new Set(), asyncHelpers = /* @__PURE__ */ new Set()) {
7415
+ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new Set(), asyncHelpers = /* @__PURE__ */ new Set(), modulesOwnMsw = false) {
7256
7416
  const untouched = {
7257
7417
  source,
7258
7418
  changed: false,
@@ -7315,7 +7475,10 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
7315
7475
  const afterEmpty = parse2(fileName, emptyImplementations.source);
7316
7476
  if (afterEmpty === void 0) return unparsable("empty mock implementation");
7317
7477
  const hooks = rewriteHookReturnValue(emptyImplementations.source, afterEmpty);
7318
- const awaited = propagateAwait(fileName, hooks.source, original, asyncHelpers);
7478
+ const afterHooks = parse2(fileName, hooks.source);
7479
+ if (afterHooks === void 0) return unparsable("hook return value");
7480
+ const promises = rewriteMockPromiseResolve(hooks.source, afterHooks);
7481
+ const awaited = propagateAwait(fileName, promises.source, original, asyncHelpers);
7319
7482
  const program = parse2(fileName, awaited.source);
7320
7483
  if (program === void 0) return unparsable("hook body");
7321
7484
  const hoist = hoistCapturedBindings(
@@ -7330,7 +7493,7 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
7330
7493
  ...spyTypes.typeImports
7331
7494
  ]);
7332
7495
  const deep = normaliseDeepImports(withImport);
7333
- const repointed = repointMswServer(fileName, deep.source);
7496
+ const repointed = modulesOwnMsw ? { source: deep.source, repointed: false } : repointMswServer(fileName, deep.source);
7334
7497
  const finalProgram = parse2(fileName, repointed.source);
7335
7498
  if (finalProgram === void 0) return unparsable("msw server import");
7336
7499
  const barrels = rewriteReexportBarrelMocks(fileName, repointed.source, finalProgram);
@@ -7366,8 +7529,8 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
7366
7529
  }
7367
7530
 
7368
7531
  // src/roles/test/codemod/test-support-files.ts
7369
- import { existsSync as existsSync25, readFileSync as readFileSync24, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
7370
- import { dirname as dirname6, join as join30, relative as relative5, resolve as resolve4 } from "path";
7532
+ import { existsSync as existsSync26, readFileSync as readFileSync25, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
7533
+ import { dirname as dirname6, join as join31, relative as relative5, resolve as resolve4 } from "node:path";
7371
7534
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
7372
7535
  var TEST_FILE2 = /\.(spec|test)\.[cm]?[jt]sx?$/;
7373
7536
  var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
@@ -7387,13 +7550,13 @@ function relativeSpecifiers(source) {
7387
7550
  }
7388
7551
  function resolveSpecifier(fromFile, specifier) {
7389
7552
  const base = resolve4(dirname6(fromFile), specifier);
7390
- if (existsSync25(base) && statSync5(base).isFile()) return base;
7553
+ if (existsSync26(base) && statSync5(base).isFile()) return base;
7391
7554
  for (const extension of EXTENSIONS) {
7392
- if (existsSync25(`${base}${extension}`)) return `${base}${extension}`;
7555
+ if (existsSync26(`${base}${extension}`)) return `${base}${extension}`;
7393
7556
  }
7394
7557
  for (const extension of EXTENSIONS) {
7395
- const index = join30(base, `index${extension}`);
7396
- if (existsSync25(index)) return index;
7558
+ const index = join31(base, `index${extension}`);
7559
+ if (existsSync26(index)) return index;
7397
7560
  }
7398
7561
  return void 0;
7399
7562
  }
@@ -7410,9 +7573,9 @@ function moduleSources(cwd) {
7410
7573
  }
7411
7574
  for (const entry of entries) {
7412
7575
  if (entry.isDirectory()) {
7413
- if (!SKIPPED.has(entry.name)) stack.push(join30(dir, entry.name));
7576
+ if (!SKIPPED.has(entry.name)) stack.push(join31(dir, entry.name));
7414
7577
  } else if (SOURCE_FILE.test(entry.name)) {
7415
- found.push(join30(dir, entry.name));
7578
+ found.push(join31(dir, entry.name));
7416
7579
  }
7417
7580
  }
7418
7581
  }
@@ -7420,7 +7583,7 @@ function moduleSources(cwd) {
7420
7583
  }
7421
7584
  function read(path) {
7422
7585
  try {
7423
- return readFileSync24(path, "utf8");
7586
+ return readFileSync25(path, "utf8");
7424
7587
  } catch {
7425
7588
  return void 0;
7426
7589
  }
@@ -7432,7 +7595,7 @@ function setupFilesInside(cwd, named) {
7432
7595
  const full = resolve4(cwd, withoutToken);
7433
7596
  const inside = relative5(cwd, full);
7434
7597
  if (inside.startsWith("..") || inside === "") continue;
7435
- if (!existsSync25(full)) continue;
7598
+ if (!existsSync26(full)) continue;
7436
7599
  kept.push(inside.replaceAll("\\", "/"));
7437
7600
  }
7438
7601
  return kept;
@@ -7478,7 +7641,7 @@ function parse3(source) {
7478
7641
  }
7479
7642
  function read2(path) {
7480
7643
  try {
7481
- return readFileSync25(path, "utf8");
7644
+ return readFileSync26(path, "utf8");
7482
7645
  } catch {
7483
7646
  return void 0;
7484
7647
  }
@@ -7496,7 +7659,7 @@ function defaultImportedSpecifiers(cwd) {
7496
7659
  }
7497
7660
  for (const entry of entries) {
7498
7661
  if (SKIPPED2.has(entry)) continue;
7499
- const path = join31(dir, entry);
7662
+ const path = join32(dir, entry);
7500
7663
  let directory;
7501
7664
  try {
7502
7665
  directory = statSync6(path).isDirectory();
@@ -7517,6 +7680,7 @@ function defaultImportedSpecifiers(cwd) {
7517
7680
  return found;
7518
7681
  }
7519
7682
  function migrateModuleTests(cwd, setupFiles = []) {
7683
+ const modulesOwnMsw = ownsMswLifecycle(cwd, setupFiles);
7520
7684
  const operations = [];
7521
7685
  const blockers = [];
7522
7686
  let files = 0;
@@ -7524,14 +7688,22 @@ function migrateModuleTests(cwd, setupFiles = []) {
7524
7688
  let hoisted = 0;
7525
7689
  const defaultImported = defaultImportedSpecifiers(cwd);
7526
7690
  const asyncHelpers = /* @__PURE__ */ new Set();
7527
- for (const relative7 of listTestSupportFiles(cwd, setupFiles)) {
7528
- const source = read2(join31(cwd, relative7));
7691
+ for (const relative8 of listTestSupportFiles(cwd, setupFiles)) {
7692
+ const source = read2(join32(cwd, relative8));
7529
7693
  if (source === void 0) {
7530
- blockers.push({ file: relative7, reason: "this file could not be read" });
7694
+ blockers.push({ file: relative8, reason: "this file could not be read" });
7531
7695
  continue;
7532
7696
  }
7533
- const migrated = migrateTestFile(join31(cwd, relative7), source, defaultImported);
7534
- if (!migrated.changed) continue;
7697
+ const migrated = migrateTestFile(
7698
+ join32(cwd, relative8),
7699
+ source,
7700
+ defaultImported,
7701
+ /* @__PURE__ */ new Set(),
7702
+ modulesOwnMsw
7703
+ );
7704
+ const renamedTo = vitestSupportName(relative8);
7705
+ if (!migrated.changed && renamedTo === relative8) continue;
7706
+ if (renamedTo !== relative8) operations.push({ kind: "delete", path: relative8 });
7535
7707
  const before = parse3(source);
7536
7708
  const after = parse3(migrated.source);
7537
7709
  if (before !== void 0 && after !== void 0) {
@@ -7542,41 +7714,50 @@ function migrateModuleTests(cwd, setupFiles = []) {
7542
7714
  }
7543
7715
  files++;
7544
7716
  renamed += migrated.renamed;
7545
- operations.push({ kind: "write", path: relative7, contents: migrated.source });
7717
+ operations.push({ kind: "write", path: renamedTo, contents: migrated.source });
7546
7718
  }
7547
- for (const relative7 of listTestFiles(cwd)) {
7548
- const source = read2(join31(cwd, relative7));
7719
+ for (const relative8 of listTestFiles(cwd)) {
7720
+ const source = read2(join32(cwd, relative8));
7549
7721
  if (source === void 0) {
7550
- blockers.push({ file: relative7, reason: "this file could not be read" });
7722
+ blockers.push({ file: relative8, reason: "this file could not be read" });
7551
7723
  continue;
7552
7724
  }
7553
- const migrated = migrateTestFile(join31(cwd, relative7), source, defaultImported, asyncHelpers);
7725
+ const migrated = migrateTestFile(
7726
+ join32(cwd, relative8),
7727
+ source,
7728
+ defaultImported,
7729
+ asyncHelpers,
7730
+ modulesOwnMsw
7731
+ );
7554
7732
  for (const member of migrated.unhandled) {
7555
7733
  blockers.push({
7556
- file: `${relative7}:${member.line}`,
7734
+ file: `${relative8}:${member.line}`,
7557
7735
  reason: member.rule === void 0 ? `${member.member} has no rule, and guessing one is how a suite goes green for the wrong reason` : `${member.member} needs ${member.rule}`
7558
7736
  });
7559
7737
  }
7560
7738
  for (const refusal of migrated.refusals) {
7561
- blockers.push({ file: relative7, reason: refusal.reason });
7739
+ blockers.push({ file: relative8, reason: refusal.reason });
7562
7740
  }
7563
7741
  if (!migrated.changed) continue;
7564
7742
  files++;
7565
7743
  renamed += migrated.renamed;
7566
7744
  if (migrated.hoisted.length > 0) hoisted++;
7567
- operations.push({ kind: "write", path: relative7, contents: migrated.source });
7745
+ operations.push({ kind: "write", path: relative8, contents: migrated.source });
7568
7746
  }
7569
7747
  return blockers.length > 0 ? { operations: [], files, renamed, hoisted, blockers } : { operations, files, renamed, hoisted, blockers: [] };
7570
7748
  }
7571
7749
 
7572
7750
  // src/roles/test/nest/generate-config.ts
7573
- import { readdirSync as readdirSync7, readFileSync as readFileSync26, statSync as statSync7 } from "fs";
7574
- import { join as join32, relative as relative6 } from "path";
7751
+ import { readdirSync as readdirSync7, readFileSync as readFileSync27, statSync as statSync7 } from "node:fs";
7752
+ import { join as join33, relative as relative6 } from "node:path";
7575
7753
  var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
7576
- var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/nest";
7754
+ var REACT_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
7755
+ function flavourOf(reading) {
7756
+ return reading.testEnvironment === "jsdom" ? "react" : "nest";
7757
+ }
7758
+ var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/jest-parity";
7577
7759
  var SENTINEL_MSW_SETUP = "@hublo/sentinel/test/setup/msw-lifecycle";
7578
7760
  var SENTINEL_WORKSPACE_SETUP = "@hublo/sentinel/test/setup/workspace";
7579
- var NEST_TEST_CONFIG_FILE = "vitest.config.mts";
7580
7761
  function hopsToRoot(cwd, workspaceRoot) {
7581
7762
  const up = relative6(cwd, workspaceRoot).replaceAll("\\", "/");
7582
7763
  return up === "" ? "." : up;
@@ -7612,7 +7793,7 @@ function readModuleTraits(cwd) {
7612
7793
  }
7613
7794
  for (const entry of entries) {
7614
7795
  if (SKIPPED3.has(entry)) continue;
7615
- const path = join32(dir, entry);
7796
+ const path = join33(dir, entry);
7616
7797
  let directory;
7617
7798
  try {
7618
7799
  directory = statSync7(path).isDirectory();
@@ -7626,7 +7807,7 @@ function readModuleTraits(cwd) {
7626
7807
  if (!/\.tsx?$/.test(entry)) continue;
7627
7808
  let source;
7628
7809
  try {
7629
- source = readFileSync26(path, "utf8");
7810
+ source = readFileSync27(path, "utf8");
7630
7811
  } catch {
7631
7812
  continue;
7632
7813
  }
@@ -7643,14 +7824,17 @@ function readModuleTraits(cwd) {
7643
7824
  return traits;
7644
7825
  }
7645
7826
  var withoutRootDir = (value) => value.replaceAll("<rootDir>/", "./").replaceAll("<rootDir>", ".");
7646
- function setupBlock(reading, msw) {
7827
+ function ownsMswLifecycle2(cwd, reading) {
7828
+ return ownsMswLifecycle(cwd, [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []]);
7829
+ }
7830
+ function setupBlock(cwd, reading, msw) {
7647
7831
  const declared = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []];
7648
7832
  const namedTheRootSetup = declared.some((file) => file.endsWith("jest.setup.after.env.js"));
7649
- const own = declared.filter((file) => !file.endsWith("jest.setup.after.env.js")).map((file) => `'${withoutRootDir(file)}'`);
7833
+ const own = declared.filter((file) => !file.endsWith("jest.setup.after.env.js")).map((file) => `'${vitestSupportName(withoutRootDir(file))}'`);
7650
7834
  const sentinel = [
7651
7835
  `'${SENTINEL_TEST_SETUP}'`,
7652
7836
  ...namedTheRootSetup ? [`'${SENTINEL_WORKSPACE_SETUP}'`] : [],
7653
- ...msw ? [`'${SENTINEL_MSW_SETUP}'`] : []
7837
+ ...msw && namedTheRootSetup && !ownsMswLifecycle2(cwd, reading) ? [`'${SENTINEL_MSW_SETUP}'`] : []
7654
7838
  ];
7655
7839
  return ` setupFiles: [${[...sentinel, ...own].join(", ")}],`;
7656
7840
  }
@@ -7677,7 +7861,7 @@ function timeoutBlock(reading) {
7677
7861
  }
7678
7862
  function globalSetupBlock(reading) {
7679
7863
  if (reading.globalSetup === void 0) return "";
7680
- const files = reading.globalSetup.map((file) => `'${withoutRootDir(file)}'`).join(", ");
7864
+ const files = reading.globalSetup.map((file) => `'${vitestSupportName(withoutRootDir(file))}'`).join(", ");
7681
7865
  return ` globalSetup: [${files}],`;
7682
7866
  }
7683
7867
  function includeBlock(reading) {
@@ -7697,7 +7881,8 @@ function aliasBlock(reading) {
7697
7881
  if (reading.aliases === void 0 || reading.aliases.length === 0) return "";
7698
7882
  const entries = reading.aliases.map((alias) => {
7699
7883
  const pattern = alias.find.replaceAll(String.raw`\\`, "\\").replaceAll("/", String.raw`\/`);
7700
- const target = isPackage(alias.replacement) ? `'${alias.replacement}'` : `path.resolve(import.meta.dirname, '${withoutRootDir(alias.replacement)}')`;
7884
+ const deadToken = /<rootdir>/i.test(alias.replacement) && !alias.replacement.includes("<rootDir>");
7885
+ const target = isPackage(alias.replacement) ? `'${alias.replacement}'` : deadToken ? `'${alias.replacement}'` : `path.resolve(import.meta.dirname, '${withoutRootDir(alias.replacement)}')`;
7701
7886
  return ` { find: /${pattern}/, replacement: ${target} },`;
7702
7887
  });
7703
7888
  return [" resolve: {", " alias: [", ...entries, " ],", " },"].join("\n");
@@ -7712,7 +7897,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
7712
7897
  const up = hopsToRoot(cwd, workspaceRoot);
7713
7898
  const test = [
7714
7899
  environmentBlock(reading),
7715
- setupBlock(reading, traits.msw),
7900
+ setupBlock(cwd, reading, traits.msw),
7716
7901
  envBlock(reading),
7717
7902
  timeoutBlock(reading),
7718
7903
  concurrencyBlock(cwd, reading, target),
@@ -7727,7 +7912,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
7727
7912
  `import path from 'node:path'`,
7728
7913
  "",
7729
7914
  ...needsDefaults ? [`import { configDefaults } from 'vitest/config'`, ""] : [],
7730
- `import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
7915
+ flavourOf(reading) === "react" ? `import { defineConfig, reactTestConfig } from '${REACT_TEST_PRESET_SPECIFIER}'` : `import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
7731
7916
  ].join("\n");
7732
7917
  const overrides = [aliasBlock(reading), " test: {", ...test, " },"].filter(
7733
7918
  (line) => line !== ""
@@ -7738,7 +7923,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
7738
7923
  source: `${imports}
7739
7924
 
7740
7925
  export default defineConfig(
7741
- nestTestConfig({
7926
+ ${flavourOf(reading) === "react" ? "reactTestConfig" : "nestTestConfig"}({
7742
7927
  root: import.meta.dirname,
7743
7928
  workspaceRoot: path.resolve(import.meta.dirname, '${up}'),${decoratorLine}
7744
7929
  overrides: {
@@ -7751,8 +7936,8 @@ ${overrides.join("\n")}
7751
7936
  }
7752
7937
 
7753
7938
  // src/roles/test/nest/read-jest-config.ts
7754
- import { existsSync as existsSync26, readFileSync as readFileSync27 } from "fs";
7755
- import { join as join33 } from "path";
7939
+ import { existsSync as existsSync27, readFileSync as readFileSync28 } from "node:fs";
7940
+ import { join as join34 } from "node:path";
7756
7941
  var CARRIED = [
7757
7942
  "testEnvironment",
7758
7943
  "coverageDirectory",
@@ -7942,7 +8127,7 @@ function extendedConfig(source, cwd) {
7942
8127
  (name) => name.replace(/^\.\//, "")
7943
8128
  );
7944
8129
  const file = candidates.find(
7945
- (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync26(join33(cwd, name))
8130
+ (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync27(join34(cwd, name))
7946
8131
  );
7947
8132
  if (file === void 0) continue;
7948
8133
  if (!new RegExp(String.raw`\{\s*\.\.\.${binding}\b`).test(source)) continue;
@@ -7969,11 +8154,11 @@ function sideEffects(source, extended) {
7969
8154
  return found;
7970
8155
  }
7971
8156
  function readJestConfig(cwd, named) {
7972
- const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync26(join33(cwd, name)));
7973
- if (file === void 0 || !existsSync26(join33(cwd, file))) return void 0;
8157
+ const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync27(join34(cwd, name)));
8158
+ if (file === void 0 || !existsSync27(join34(cwd, file))) return void 0;
7974
8159
  let source;
7975
8160
  try {
7976
- source = readFileSync27(join33(cwd, file), "utf8");
8161
+ source = readFileSync28(join34(cwd, file), "utf8");
7977
8162
  } catch {
7978
8163
  return { file, refusals: [`${file} could not be read`], notes: [] };
7979
8164
  }
@@ -8054,8 +8239,8 @@ function readJestConfig(cwd, named) {
8054
8239
  }
8055
8240
 
8056
8241
  // src/roles/test/nest/run-modes.ts
8057
- import { existsSync as existsSync27 } from "fs";
8058
- import { basename as basename2, join as join34 } from "path";
8242
+ import { existsSync as existsSync28 } from "node:fs";
8243
+ import { basename as basename2, join as join35 } from "node:path";
8059
8244
  var JEST_EXECUTOR = "@nx/jest:jest";
8060
8245
  var PRIMARY_CONFIG_FILE = "vitest.config.mts";
8061
8246
  var PRIMARY_TARGET = "test";
@@ -8076,7 +8261,7 @@ function runModes(cwd, fallback) {
8076
8261
  const jestConfig = entry.options?.jestConfig;
8077
8262
  if (jestConfig === void 0) continue;
8078
8263
  const name = configName(jestConfig);
8079
- if (!existsSync27(join34(cwd, name))) continue;
8264
+ if (!existsSync28(join35(cwd, name))) continue;
8080
8265
  declared.push({
8081
8266
  target,
8082
8267
  jestConfig: name,
@@ -8100,16 +8285,63 @@ function runModes(cwd, fallback) {
8100
8285
  );
8101
8286
  }
8102
8287
 
8288
+ // src/roles/test/written-file-formatter.ts
8289
+ import { existsSync as existsSync29 } from "node:fs";
8290
+ import { dirname as dirname7, join as join36 } from "node:path";
8291
+ var PRETTIER_CONFIGS = [
8292
+ ".prettierrc",
8293
+ ".prettierrc.json",
8294
+ ".prettierrc.json5",
8295
+ ".prettierrc.yaml",
8296
+ ".prettierrc.yml",
8297
+ ".prettierrc.js",
8298
+ ".prettierrc.cjs",
8299
+ ".prettierrc.mjs",
8300
+ ".prettierrc.toml",
8301
+ "prettier.config.js",
8302
+ "prettier.config.cjs",
8303
+ "prettier.config.mjs"
8304
+ ];
8305
+ function prettierConfigFrom(cwd) {
8306
+ let dir = cwd;
8307
+ for (; ; ) {
8308
+ for (const name of PRETTIER_CONFIGS) {
8309
+ if (existsSync29(join36(dir, name))) return join36(dir, name);
8310
+ }
8311
+ const parent = dirname7(dir);
8312
+ if (parent === dir) return void 0;
8313
+ dir = parent;
8314
+ }
8315
+ }
8316
+ function writtenFileFormatter(cwd) {
8317
+ if (readFormatAdoption(cwd).configFile !== null) {
8318
+ const oxfmt = resolveOxfmt(cwd);
8319
+ if (oxfmt !== void 0) {
8320
+ return { bin: oxfmt, args: ["--write"], origin: "this module's own oxfmt config" };
8321
+ }
8322
+ }
8323
+ const config = prettierConfigFrom(cwd);
8324
+ const prettier = resolveBin(cwd, "prettier");
8325
+ if (config !== void 0 && prettier !== void 0) {
8326
+ return {
8327
+ bin: prettier,
8328
+ args: ["--write", "--log-level", "warn"],
8329
+ origin: `the workspace's Prettier`
8330
+ };
8331
+ }
8332
+ return void 0;
8333
+ }
8334
+
8103
8335
  // src/roles/test/jest-migration.ts
8104
8336
  var SPEC_TSCONFIG = "tsconfig.spec.json";
8105
8337
  var VITEST_GLOBAL_TYPES = "vitest/globals";
8106
8338
  function specTypesOperation(cwd) {
8107
- const path = join35(cwd, SPEC_TSCONFIG);
8108
- if (!existsSync28(path)) return void 0;
8339
+ const path = join37(cwd, SPEC_TSCONFIG);
8340
+ if (!existsSync30(path)) return void 0;
8109
8341
  let config;
8110
8342
  try {
8111
8343
  config = parseJsonc(
8112
- readFileSync28(path, "utf8"),
8344
+ readFileSync29(path, "utf8"),
8113
8345
  SPEC_TSCONFIG
8114
8346
  );
8115
8347
  } catch {
@@ -8127,6 +8359,70 @@ function specTypesOperation(cwd) {
8127
8359
  }
8128
8360
  };
8129
8361
  }
8362
+ function shippedTypesPath(cwd, declaration) {
8363
+ try {
8364
+ const manifest = createRequire6(join37(cwd, "noop.cjs")).resolve("@hublo/sentinel/package.json");
8365
+ const file = join37(dirname8(manifest), declaration);
8366
+ if (existsSync30(file)) {
8367
+ const path = relative7(cwd, file);
8368
+ return path.startsWith(".") ? path : `./${path}`;
8369
+ }
8370
+ } catch {
8371
+ }
8372
+ return `./node_modules/@hublo/sentinel/${declaration}`;
8373
+ }
8374
+ function tsconfigFileListOperations(cwd) {
8375
+ const operations = [];
8376
+ let names;
8377
+ try {
8378
+ names = readdirSync8(cwd).filter((name) => TSCONFIG_FILE.test(name));
8379
+ } catch {
8380
+ return operations;
8381
+ }
8382
+ for (const name of names) {
8383
+ let config;
8384
+ try {
8385
+ config = parseJsonc(
8386
+ readFileSync29(join37(cwd, name), "utf8"),
8387
+ name
8388
+ );
8389
+ } catch {
8390
+ continue;
8391
+ }
8392
+ const include = Array.isArray(config?.include) ? rewriteFileList(config.include) : void 0;
8393
+ const exclude = Array.isArray(config?.exclude) ? rewriteFileList(config.exclude) : void 0;
8394
+ const files = Array.isArray(config?.files) ? rewriteFileList(config.files) : void 0;
8395
+ const withTypes = name === SPEC_TSCONFIG ? withShippedTypes(cwd, files ?? config?.files) : void 0;
8396
+ if (include === void 0 && exclude === void 0 && files === void 0 && withTypes === void 0)
8397
+ continue;
8398
+ operations.push({
8399
+ kind: "merge-json",
8400
+ path: name,
8401
+ value: {
8402
+ ...include === void 0 ? {} : { include },
8403
+ ...exclude === void 0 ? {} : { exclude },
8404
+ ...withTypes === void 0 ? files === void 0 ? {} : { files } : { files: withTypes }
8405
+ }
8406
+ });
8407
+ }
8408
+ return operations;
8409
+ }
8410
+ function rewriteFileList(entries) {
8411
+ const rewritten = entries.filter((entry) => typeof entry !== "string" || !JEST_CONFIG_ENTRY.test(entry)).map((entry) => typeof entry === "string" ? vitestSupportName(entry) : entry);
8412
+ const unchanged = rewritten.length === entries.length && rewritten.every((entry, index) => entry === entries[index]);
8413
+ return unchanged ? void 0 : rewritten;
8414
+ }
8415
+ function withShippedTypes(cwd, files) {
8416
+ const entries = Array.isArray(files) ? files : [];
8417
+ const missing = SHIPPED_TYPES.filter(
8418
+ (declaration) => !entries.some((entry) => entry.endsWith(declaration))
8419
+ );
8420
+ if (missing.length === 0) return void 0;
8421
+ return [...entries, ...missing.map((declaration) => shippedTypesPath(cwd, declaration))];
8422
+ }
8423
+ var SHIPPED_TYPES = ["types/mock-extended.d.ts", "types/jest-global.d.ts"];
8424
+ var JEST_CONFIG_ENTRY = /(^|\/)jest\.[\w.-]*config\.[cm]?[jt]s$/;
8425
+ var TSCONFIG_FILE = /^tsconfig[\w.-]*\.json$/;
8130
8426
  function siblingScriptRewrites(cwd) {
8131
8427
  const rewrites = {};
8132
8428
  for (const [name, command] of Object.entries(moduleScripts(cwd))) {
@@ -8173,8 +8469,9 @@ function planJestMigration(context, adoption) {
8173
8469
  `${migration.blockers.length} thing(s) in this module's test files need a human before it can move. Nothing was written, because a config without its files is a suite calling jest.* under Vitest.${listed}`
8174
8470
  );
8175
8471
  }
8176
- const targets = projectTargets(context.cwd) ?? {};
8177
- const existingFor = (name) => targets[name];
8472
+ const fromProject = projectTargets(context.cwd) ?? {};
8473
+ const fromManifest = manifestTargets(context.cwd);
8474
+ const existingFor = (name) => fromProject[name] ?? fromManifest[name];
8178
8475
  const siblings = siblingScriptRewrites(context.cwd);
8179
8476
  const specTypes = specTypesOperation(context.cwd);
8180
8477
  const scripts = Object.fromEntries(
@@ -8191,6 +8488,7 @@ function planJestMigration(context, adoption) {
8191
8488
  })),
8192
8489
  ...migration.operations,
8193
8490
  ...specTypes === void 0 ? [] : [specTypes],
8491
+ ...tsconfigFileListOperations(context.cwd),
8194
8492
  manifestOperation(context.cwd, { ...scripts, ...siblings }),
8195
8493
  ...prepared.flatMap(
8196
8494
  ({ mode }) => nxTargetOperations({
@@ -8208,18 +8506,26 @@ function planJestMigration(context, adoption) {
8208
8506
  (name) => ({ kind: "delete", path: name })
8209
8507
  )
8210
8508
  ];
8509
+ const escaping = (prepared[0]?.reading.aliases ?? []).map((alias) => alias.replacement).filter((replacement) => replacement.includes(".."));
8211
8510
  const notes = [
8212
8511
  `${migration.files} test file(s) migrated from jest to Vitest, ${migration.renamed} site(s) rewritten${migration.hoisted > 0 ? `, ${migration.hoisted} of them moving a mock factory's captures into vi.hoisted` : ""}.`,
8213
- `${NEST_TEST_CONFIG_FILE} written from what ${adoption.jestConfigs.join(", ")} declared, and that config deleted. Every specifier in the new one is a package name: nothing in it reaches outside this module.`
8512
+ `${prepared.map(({ mode }) => mode.vitestConfig).join(", ")} written from what ${prepared.map(({ mode }) => mode.jestConfig).join(", ")} declared, and that config deleted.` + (escaping.length === 0 ? ` Every specifier in the new one is a package name: nothing in it reaches outside this module.` : ` \u26A0\uFE0F ${escaping.length} alias(es) carried from that config reach OUTSIDE this module: ${escaping.slice(0, 3).join(", ")}. They came from its \`moduleNameMapper\` and are kept as they were, but a module that reaches out cannot be moved on its own.`)
8214
8513
  ];
8215
8514
  if (specTypes !== void 0) {
8216
8515
  notes.push(
8217
8516
  `${SPEC_TSCONFIG} now declares \`${VITEST_GLOBAL_TYPES}\` instead of \`jest\`. Without it the module keeps depending on @types/jest and \`jest.Mock\` still resolves, so the migration would look finished while the dependency it cuts is still declared.`
8218
8517
  );
8219
8518
  }
8220
- notes.push(
8221
- `run this module's own lint --fix (or its formatter) after installing: the Vitest type imports are written beside the existing ones, and ordering belongs to the tool that enforces it.`
8222
- );
8519
+ const formatter = writtenFileFormatter(context.cwd);
8520
+ if (formatter !== void 0) {
8521
+ notes.push(
8522
+ `the files this rewrote were run through ${formatter.origin} afterwards. Two things needed it: the Vitest type imports land beside the existing ones, and a rewritten call is often SHORTER than the one it replaced, so the line breaks around it are no longer the ones your config would pick. Neither is a defect, and both would have failed your format check. What no formatter does is ORDER imports, so if your lint enforces \`import/order\`, run its fixer too.`
8523
+ );
8524
+ } else {
8525
+ notes.push(
8526
+ `no style is declared here, by this module or its workspace, so what was written keeps the shape it was written in, and the Vitest type imports sit beside the existing ones rather than where your \`import/order\` wants them. That rule is classified by a prefix list this package cannot read, so it is left to the tool that owns it: run your module's own lint fixer (\`nx run <module>:lint --fix\` here) and its formatter before committing.`
8527
+ );
8528
+ }
8223
8529
  notes.push(
8224
8530
  `then run this module's TYPECHECK, not only its tests. The migration rewrites types as well as calls, and a wrong type is invisible to a green suite: measured on a real module, \`--run --test\` passed while \`--run --typescript\` reported ten errors it had not had before.`
8225
8531
  );
@@ -8308,7 +8614,7 @@ function plan3(context) {
8308
8614
  `${configFile} now imports ${rewrite.moved.join(", ")} from sentinel. Only the import lines changed: every alias, setup file, timeout and coverage path is untouched, so this suite runs what it ran before.`
8309
8615
  );
8310
8616
  }
8311
- const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
8617
+ const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME] ?? manifestTargets(context.cwd)[TEST_SCRIPT_NAME];
8312
8618
  const outputs = existing?.outputs ?? [];
8313
8619
  const dependsOn = existing?.dependsOn ?? [];
8314
8620
  const configurations = existing?.configurations ?? {};
@@ -8363,6 +8669,329 @@ function plan3(context) {
8363
8669
  return { operations, notes };
8364
8670
  }
8365
8671
 
8672
+ // src/roles/test/reference.ts
8673
+ import { execFileSync } from "node:child_process";
8674
+ import { createHash } from "node:crypto";
8675
+ import { existsSync as existsSync31, mkdirSync, mkdtempSync, readFileSync as readFileSync30, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
8676
+ import { tmpdir } from "node:os";
8677
+ import { join as join38, resolve as resolve5 } from "node:path";
8678
+
8679
+ // src/roles/test/baseline.ts
8680
+ function readSnapshot(report) {
8681
+ const files = report?.testResults ?? [];
8682
+ const passing = [];
8683
+ const failing = [];
8684
+ const unloadable = [];
8685
+ for (const file of files) {
8686
+ const assertions = file?.assertionResults ?? [];
8687
+ if (file?.status === "failed" && assertions.length === 0) {
8688
+ unloadable.push(typeof file.name === "string" ? file.name : "an unnamed file");
8689
+ }
8690
+ for (const assertion of assertions) {
8691
+ const name = assertion?.fullName;
8692
+ if (typeof name !== "string" || name === "") continue;
8693
+ if (assertion.status === "passed") passing.push(name);
8694
+ else if (assertion.status === "failed") failing.push(name);
8695
+ }
8696
+ }
8697
+ const claimed = report?.numFailedTests;
8698
+ return {
8699
+ files: files.length,
8700
+ passing: passing.sort(),
8701
+ failing: failing.sort(),
8702
+ unloadable: unloadable.sort(),
8703
+ claimedFailing: typeof claimed === "number" ? claimed : failing.length
8704
+ };
8705
+ }
8706
+ function difference(left, right) {
8707
+ const remaining = /* @__PURE__ */ new Map();
8708
+ for (const name of right) remaining.set(name, (remaining.get(name) ?? 0) + 1);
8709
+ const missing = [];
8710
+ for (const name of left) {
8711
+ const left_ = remaining.get(name) ?? 0;
8712
+ if (left_ === 0) {
8713
+ missing.push(name);
8714
+ continue;
8715
+ }
8716
+ remaining.set(name, left_ - 1);
8717
+ }
8718
+ return missing.sort();
8719
+ }
8720
+ var withoutRenderingQuotes = (name) => name.replaceAll(String.raw`\'`, "'").replaceAll(String.raw`\"`, '"').replaceAll(/['"`]\s*\+\s*['"`]/g, "").replaceAll(/(^|[\s:,[{(])(''|"")($|[\s,)\]}])/g, "$1<empty-string>$3").replaceAll(/['"`]/g, "");
8721
+ function withoutRenderingNoise(name) {
8722
+ const spelled = withSortedKeys(
8723
+ withoutUndefinedEntries(
8724
+ withoutErrorRendering(
8725
+ withoutNaN(
8726
+ withoutTimestamps(withoutRenderingQuotes(withoutClassBodies(withoutStackFrames(name))))
8727
+ )
8728
+ )
8729
+ )
8730
+ );
8731
+ return withoutArrayBrackets(
8732
+ spelled.replaceAll(/\[\s+/g, "[").replaceAll(/\s+\]/g, "]").replaceAll(/\{\s+/g, "{").replaceAll(/\s+\}/g, "}").replaceAll(/(?<=[[{,\s])\+0\b/g, "0").replaceAll(/:\s+/g, ":").replaceAll(/,\s+/g, ",")
8733
+ );
8734
+ }
8735
+ function withoutArrayBrackets(name) {
8736
+ return name.replaceAll(
8737
+ /\[([^[\]]*)\]/g,
8738
+ (match, inner) => (
8739
+ // `[Object]` and `[Array]` are the formatter saying it STOPPED, not an array literal.
8740
+ // Stripping their brackets would erase the marker `collapsedOf` pairs on.
8741
+ /^(?:Object|Array)$/.test(inner) ? match : inner.replaceAll(/,\s+/g, ",")
8742
+ )
8743
+ );
8744
+ }
8745
+ var withoutClassBodies = (name) => name.replaceAll(
8746
+ /class\s+([A-Za-z_$][\w$]*)\s*\{\s*\}/g,
8747
+ (_match, className) => `class ${className}`
8748
+ ).replaceAll(
8749
+ /class\s+([A-Za-z_$][\w$]*)[\s\S]*?\n\}/g,
8750
+ (_match, className) => `class ${className}`
8751
+ );
8752
+ var withoutTimestamps = (name) => name.replaceAll(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, "<instant>");
8753
+ var withoutNaN = (name) => name.replaceAll(/\bNaN\b/g, "null");
8754
+ function entriesOf(inner) {
8755
+ const parts = [];
8756
+ let depth = 0;
8757
+ let start = 0;
8758
+ for (let index = 0; index < inner.length; index += 1) {
8759
+ const character = inner[index];
8760
+ if (character === "[" || character === "(") depth += 1;
8761
+ else if (character === "]" || character === ")") depth -= 1;
8762
+ else if (character === "," && depth === 0) {
8763
+ parts.push(inner.slice(start, index));
8764
+ start = index + 1;
8765
+ }
8766
+ if (depth < 0) return void 0;
8767
+ }
8768
+ if (depth !== 0) return void 0;
8769
+ parts.push(inner.slice(start));
8770
+ return parts.map((part) => part.trim()).filter(Boolean);
8771
+ }
8772
+ var sortedGroup = (match, inner) => {
8773
+ if (!inner.includes(":")) return match;
8774
+ const parts = entriesOf(inner);
8775
+ if (parts === void 0) return match;
8776
+ if (parts.length < 2 || !parts.every((part) => part.includes(":"))) return match;
8777
+ return `{${[...parts].sort().join(",")}}`;
8778
+ };
8779
+ var HELD = "\u241F";
8780
+ var withSortedKeys = (name) => {
8781
+ const held = [];
8782
+ let current = name;
8783
+ for (let depth = 0; depth < 24; depth += 1) {
8784
+ const next = current.replaceAll(/\{([^{}]*)\}/g, (match, inner) => {
8785
+ held.push(sortedGroup(match, inner));
8786
+ return `${HELD}${held.length - 1}${HELD}`;
8787
+ });
8788
+ if (next === current) break;
8789
+ current = next;
8790
+ }
8791
+ for (let depth = 0; depth < 24 && current.includes(HELD); depth += 1) {
8792
+ current = current.replaceAll(
8793
+ new RegExp(`${HELD}(\\d+)${HELD}`, "g"),
8794
+ (_match, index) => held[Number(index)]
8795
+ );
8796
+ }
8797
+ return current;
8798
+ };
8799
+ var withoutStackFrames = (name) => {
8800
+ const frame = name.search(/\n\s+at\s/);
8801
+ return frame === -1 ? name : name.slice(0, frame);
8802
+ };
8803
+ var withoutErrorRendering = (name) => name.replaceAll(/\[[A-Za-z]*(?:Error|Exception):[^\][]*\]/g, "{}");
8804
+ var withoutUndefinedEntries = (name) => name.replaceAll(/([{,])\s*[A-Za-z_$][\w$]*\s*:\s*undefined\s*(?=[,}])/g, "$1").replaceAll(/\{\s*,/g, "{").replaceAll(/,\s*\}/g, "}").replaceAll(/\{\s*\}/g, "{}");
8805
+ function collapsedOf(candidate, name) {
8806
+ if (!/\[(?:Object|Array)\]/.test(candidate)) return false;
8807
+ const pattern = candidate.split(/\[(?:Object|Array)\]/).map((part) => part.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
8808
+ return new RegExp(`^${pattern}$`, "s").test(name);
8809
+ }
8810
+ function truncationOf(candidate, name) {
8811
+ const cut = candidate.indexOf("\u2026");
8812
+ if (cut === -1) return false;
8813
+ const head = candidate.slice(0, cut);
8814
+ const tail = candidate.slice(cut + 1);
8815
+ return name.startsWith(head) && name.endsWith(tail);
8816
+ }
8817
+ function pairRenamed(missing, added) {
8818
+ const remaining = [...added];
8819
+ const renamed = [];
8820
+ const unmatched = [];
8821
+ for (const name of missing) {
8822
+ const normalised = withoutRenderingNoise(name);
8823
+ const at = remaining.findIndex((candidate) => withoutRenderingNoise(candidate) === normalised);
8824
+ if (at === -1) {
8825
+ unmatched.push(name);
8826
+ continue;
8827
+ }
8828
+ renamed.push({ before: name, after: remaining[at] });
8829
+ remaining.splice(at, 1);
8830
+ }
8831
+ const lost = [];
8832
+ for (const name of unmatched) {
8833
+ const normalised = withoutRenderingNoise(name);
8834
+ const at = remaining.findIndex(
8835
+ (candidate) => truncationOf(withoutRenderingNoise(candidate), normalised) || collapsedOf(normalised, withoutRenderingNoise(candidate))
8836
+ );
8837
+ if (at === -1) {
8838
+ lost.push(name);
8839
+ continue;
8840
+ }
8841
+ renamed.push({ before: name, after: remaining[at] });
8842
+ remaining.splice(at, 1);
8843
+ }
8844
+ return { renamed, missing: lost, added: remaining };
8845
+ }
8846
+ function compareSnapshots(before, after) {
8847
+ const beforeAll = [...before.passing, ...before.failing];
8848
+ const afterAll = [...after.passing, ...after.failing];
8849
+ const paired = pairRenamed(difference(beforeAll, afterAll), difference(afterAll, beforeAll));
8850
+ const renamedBefore = new Set(paired.renamed.map((pair) => pair.before));
8851
+ const regressed = difference(before.passing, after.passing).filter(
8852
+ (name) => !paired.missing.includes(name) && !renamedBefore.has(name)
8853
+ );
8854
+ const failedBefore = new Set(before.failing);
8855
+ const recovered = after.passing.filter((name) => failedBefore.has(name));
8856
+ return {
8857
+ identical: paired.missing.length === 0 && paired.added.length === 0 && regressed.length === 0,
8858
+ missing: paired.missing,
8859
+ added: paired.added,
8860
+ regressed,
8861
+ renamed: paired.renamed,
8862
+ recovered,
8863
+ alreadyFailing: before.failing.length
8864
+ };
8865
+ }
8866
+ function unusableAsBaseline(snapshot) {
8867
+ if (snapshot.unloadable.length > 0) {
8868
+ return `${snapshot.unloadable.length} file(s) in the reference run did not load, so they reported no test at all and the run is short by however many they hold: ${snapshot.unloadable.slice(0, 3).join(", ")}. A reference that quietly lost tests cannot prove anything about the run after it`;
8869
+ }
8870
+ if (snapshot.claimedFailing > snapshot.failing.length) {
8871
+ return `the reference run says ${snapshot.claimedFailing} test(s) failed and names only ${snapshot.failing.length} of them, so the comparison has nothing to pair the rest against and would report them as invented by the migration`;
8872
+ }
8873
+ if (snapshot.passing.length === 0) {
8874
+ return `the reference run reports no passing test at all, so a green run after would prove nothing: there is no claim to preserve`;
8875
+ }
8876
+ return void 0;
8877
+ }
8878
+ function renamedExample(before, after) {
8879
+ let shared = 0;
8880
+ while (shared < before.length && shared < after.length && before[shared] === after[shared]) {
8881
+ shared += 1;
8882
+ }
8883
+ const boundary = before.lastIndexOf(" ", shared);
8884
+ const from = boundary > 0 ? boundary + 1 : 0;
8885
+ const ellipsis = from > 0 ? "\u2026" : "";
8886
+ return `"${ellipsis}${before.slice(from)}" -> "${ellipsis}${after.slice(from)}"`;
8887
+ }
8888
+ function describeComparison(comparison) {
8889
+ const firstRename = comparison.renamed[0];
8890
+ const renamedNote = firstRename === void 0 ? "" : ` ${comparison.renamed.length} test(s) kept their identity and changed name, because the runners render an each title's interpolated value differently (e.g. ${renamedExample(firstRename.before, firstRename.after)}).`;
8891
+ const alreadyRedNote = comparison.alreadyFailing > 0 ? ` ${comparison.alreadyFailing} test(s) were already failing before the migration and are outside this comparison, which covers only what passed.` : "";
8892
+ const recoveredNote = comparison.recovered.length > 0 ? ` ${comparison.recovered.length} test(s) were failing before and pass now.` : "";
8893
+ if (comparison.identical) {
8894
+ return `every test that ran before runs and passes now.${recoveredNote}${renamedNote}${alreadyRedNote}`;
8895
+ }
8896
+ const parts = [];
8897
+ if (comparison.recovered.length > 0) {
8898
+ parts.push(`${comparison.recovered.length} test(s) were failing before and pass now`);
8899
+ }
8900
+ if (comparison.missing.length > 0) {
8901
+ parts.push(
8902
+ `${comparison.missing.length} test(s) no longer exist: ${comparison.missing.slice(0, 3).join(", ")}`
8903
+ );
8904
+ }
8905
+ if (comparison.added.length > 0) {
8906
+ parts.push(
8907
+ `${comparison.added.length} test(s) appeared, which a migration should not do: ${comparison.added.slice(0, 3).join(", ")}`
8908
+ );
8909
+ }
8910
+ if (comparison.regressed.length > 0) {
8911
+ parts.push(
8912
+ `${comparison.regressed.length} test(s) passed before and do not now: ${comparison.regressed.slice(0, 3).join(", ")}`
8913
+ );
8914
+ }
8915
+ return `${parts.join("; ")}.${renamedNote}${alreadyRedNote}`;
8916
+ }
8917
+
8918
+ // src/roles/test/reference.ts
8919
+ function referencePath(cwd) {
8920
+ const key = createHash("sha256").update(resolve5(cwd)).digest("hex").slice(0, 16);
8921
+ return join38(tmpdir(), "sentinel-test-reference", `${key}.json`);
8922
+ }
8923
+ function runJest(cwd, config) {
8924
+ const bin = resolveBin(cwd, "jest");
8925
+ if (bin === void 0) return void 0;
8926
+ const root = findWorkspaceRoot(cwd) ?? cwd;
8927
+ const out = join38(mkdtempSync(join38(tmpdir(), "sentinel-reference-")), "jest.json");
8928
+ try {
8929
+ execFileSync(bin, ["-c", join38(cwd, config), "--json", `--outputFile=${out}`, "--silent"], {
8930
+ cwd: root,
8931
+ stdio: ["ignore", "ignore", "ignore"],
8932
+ maxBuffer: 1 << 28
8933
+ });
8934
+ } catch {
8935
+ }
8936
+ if (!existsSync31(out)) return void 0;
8937
+ try {
8938
+ return JSON.parse(readFileSync30(out, "utf8"));
8939
+ } catch {
8940
+ return void 0;
8941
+ }
8942
+ }
8943
+ function captureReference(cwd, config) {
8944
+ const report = runJest(cwd, config);
8945
+ if (report === void 0) {
8946
+ return {
8947
+ captured: false,
8948
+ message: `could not run jest to record a reference, so this migration cannot be PROVED. It is still applied: a module whose jest run is broken can move, it just cannot be compared.`
8949
+ };
8950
+ }
8951
+ const snapshot = readSnapshot(report);
8952
+ const unusable = unusableAsBaseline(snapshot);
8953
+ if (unusable !== void 0) {
8954
+ return {
8955
+ captured: false,
8956
+ message: `no reference recorded: ${unusable} The migration is applied anyway, unproved.`
8957
+ };
8958
+ }
8959
+ const stored = {
8960
+ runner: "jest",
8961
+ config,
8962
+ takenAt: (/* @__PURE__ */ new Date()).toISOString(),
8963
+ snapshot
8964
+ };
8965
+ const path = referencePath(cwd);
8966
+ mkdirSync(join38(path, ".."), { recursive: true });
8967
+ writeFileSync3(path, `${JSON.stringify(stored, void 0, 2)}
8968
+ `);
8969
+ return {
8970
+ captured: true,
8971
+ message: `recorded ${snapshot.passing.length} passing test name(s) from ${config} as the reference. The next \`sentinel --run --test\` compares against it.`
8972
+ };
8973
+ }
8974
+ function hasReference(cwd) {
8975
+ return existsSync31(referencePath(cwd));
8976
+ }
8977
+ function compareAgainstReference(cwd, report) {
8978
+ const path = referencePath(cwd);
8979
+ let stored;
8980
+ try {
8981
+ stored = JSON.parse(readFileSync30(path, "utf8"));
8982
+ } catch {
8983
+ rmSync3(path, { force: true });
8984
+ return { ok: true, message: `the recorded reference could not be read, so nothing was proved.` };
8985
+ }
8986
+ const comparison = compareSnapshots(stored.snapshot, readSnapshot(report));
8987
+ if (comparison.identical) rmSync3(path, { force: true });
8988
+ const kept = comparison.identical ? "" : ` The reference is KEPT until a run passes it, so this does not go green by running again. Fix the suite and re-run, or delete ${path} to abandon the proof on purpose.`;
8989
+ return {
8990
+ ok: comparison.identical,
8991
+ message: `${describeComparison(comparison)} (reference: ${stored.config}, ${stored.takenAt})${kept}`
8992
+ };
8993
+ }
8994
+
8366
8995
  // src/roles/test/adapters/vitest/vitest.adapter.ts
8367
8996
  function resolveVitest(cwd) {
8368
8997
  const fromSentinel = binFromOwnInstall("vitest", "vitest");
@@ -8370,12 +8999,102 @@ function resolveVitest(cwd) {
8370
8999
  const fromModule = resolveBin(cwd, "vitest");
8371
9000
  return fromModule !== void 0 ? { bin: fromModule, origin: "module" } : { bin: void 0, origin: "module" };
8372
9001
  }
9002
+ var FORMATTABLE = /\.(m|c)?(j|t)sx?$|\.json$/;
9003
+ var FORMAT_BATCH = 25;
8373
9004
  var VitestAdapter = class extends BaseAdapter {
8374
9005
  target = "test";
8375
9006
  runner = "vitest";
8376
9007
  appliesTo(_preset) {
8377
9008
  return true;
8378
9009
  }
9010
+ /**
9011
+ * Record what this module's tests do TODAY, at the last moment it is still true.
9012
+ *
9013
+ * The plan about to be applied deletes the jest config, and with it any way of asking again. A
9014
+ * migration that cannot be compared to what came before is a migration nobody can check, which
9015
+ * is the whole reason this role has a baseline gate at all.
9016
+ */
9017
+ async beforeInit(ctx) {
9018
+ const adoption = readTestAdoption(ctx.cwd);
9019
+ if (adoption.state !== "jest") return Promise.resolve();
9020
+ const [config] = adoption.jestConfigs;
9021
+ if (config === void 0) return Promise.resolve();
9022
+ this.say(`recording the jest reference before migrating...`);
9023
+ this.say(captureReference(ctx.cwd, config).message);
9024
+ return Promise.resolve();
9025
+ }
9026
+ /**
9027
+ * Hand what was just written to the module's OWN formatter.
9028
+ *
9029
+ * ⚠️ Not a style sentinel imposes: the module adopted `.oxfmtrc.json`, and this runs that
9030
+ * config over the files this role created. Without it, `--init --test` leaves a module whose
9031
+ * own `--run --format` fails on a file the module never wrote.
9032
+ *
9033
+ * Measured on `apps/nest/microservices/agency`, in this repository's CI:
9034
+ *
9035
+ * agency:format 12 files, including vitest.config.mts and
9036
+ * vitest.integration.config.mts, both generated here
9037
+ *
9038
+ * The generator cannot avoid it by wrapping at a fixed width, because the width is the
9039
+ * MODULE's decision: `printWidth` lives in its own config, and 12 modules already set their
9040
+ * own. Guessing it is how a tool starts owning a setting it does not own.
9041
+ *
9042
+ * The same delegation the build role asks a human to do by hand, done.
9043
+ *
9044
+ * ## ⚠️ Two boundaries, both learned by crossing them
9045
+ *
9046
+ * ### Only a module that has a formatter CONFIG
9047
+ *
9048
+ * The question is not "can a formatter be found". It always can: sentinel depends on oxfmt and
9049
+ * ships the binary, so `resolveOxfmt` falls back to its own install and never returns undefined
9050
+ * once sentinel is installed. Asking it that way meant a module with no oxfmt config was
9051
+ * formatted with oxfmt's DEFAULTS, which is a style nobody here chose.
9052
+ *
9053
+ * So the CONFIG is read. A module that wrote its own `.oxfmtrc.json` without adopting the role
9054
+ * has still stated a style, and running it is delegation; running oxfmt with no config is us
9055
+ * choosing one.
9056
+ *
9057
+ * Measured on `apps/nest/microservices/institution`, which has not adopted format and whose lint
9058
+ * forbids semicolons: `institution:lint` went from green to failing, thousands of `semi`, on a
9059
+ * module whose 2958 tests pass. So the adoption is read, not the binary.
9060
+ *
9061
+ * ### Only the files this just WROTE
9062
+ *
9063
+ * `--write .` is a whole-directory pass, and that is not a tidy-up. Same measurement: 1813 files
9064
+ * changed, 47333 insertions, 55506 deletions, in a module whose migration touched 268. A
9065
+ * migration diff is read by a person.
9066
+ *
9067
+ * A module that has not adopted format keeps what was written exactly as written, and is told so.
9068
+ * The codemod rewrites tokens in place, so those files keep the shape they already had.
9069
+ */
9070
+ async afterInit(ctx) {
9071
+ const written = (ctx.written ?? []).filter((file) => FORMATTABLE.test(file));
9072
+ if (written.length === 0) return Promise.resolve();
9073
+ const formatter = writtenFileFormatter(ctx.cwd);
9074
+ if (formatter === void 0) {
9075
+ this.say(
9076
+ `no style is declared here, by this module or its workspace, so the ${written.length} file(s) this wrote keep the shape they were written in. Run your own formatter over them before committing.`
9077
+ );
9078
+ return Promise.resolve();
9079
+ }
9080
+ let failed = 0;
9081
+ for (let at = 0; at < written.length; at += FORMAT_BATCH) {
9082
+ const batch = written.slice(at, at + FORMAT_BATCH);
9083
+ const pass = spawnSync5(formatter.bin, [...formatter.args, ...batch], {
9084
+ cwd: ctx.cwd,
9085
+ encoding: "utf8"
9086
+ });
9087
+ if (pass.status !== 0) failed += batch.length;
9088
+ }
9089
+ this.say(
9090
+ failed === 0 ? `formatted the ${written.length} file(s) this wrote with ${formatter.origin}, so a format check passes on files it did not write itself.` : (
9091
+ // Adoption SUCCEEDED; only the tidy-up did not. Failing here would revert a migration
9092
+ // over a formatting pass, which is the wrong trade.
9093
+ `${formatter.origin} could not run over ${failed} of the ${written.length} file(s) this wrote. Adoption itself is applied; run your formatter before committing.`
9094
+ )
9095
+ );
9096
+ return Promise.resolve();
9097
+ }
8379
9098
  plan(context) {
8380
9099
  return plan3(context);
8381
9100
  }
@@ -8420,15 +9139,36 @@ var VitestAdapter = class extends BaseAdapter {
8420
9139
  `using the MODULE's vitest, not sentinel's. An adopted config gets its plugins from sentinel, and two copies fail in ways that never mention a version. Remove vitest from this module's package.json.`
8421
9140
  );
8422
9141
  }
8423
- const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
9142
+ const { options: typedOptions, paths: typedPaths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
9143
+ valueFlags: ["--config", "-c"]
9144
+ });
9145
+ const { cwd: runFrom, options, paths } = fromWorkspaceRoot(ctx.cwd, typedOptions, typedPaths);
8424
9146
  const watching = options.includes("--watch") || options.includes("-w");
8425
- const argv = watching ? [...options, ...paths] : ["run", ...options, ...paths];
8426
- const result = spawnSync5(bin, argv, { cwd: ctx.cwd, stdio: "inherit" });
9147
+ const proving = !watching && hasReference(ctx.cwd);
9148
+ const reportFile = proving ? join39(mkdtempSync2(join39(tmpdir2(), "sentinel-run-")), "vitest.json") : void 0;
9149
+ const reporters = reportFile === void 0 ? [] : ["--reporter=default", "--reporter=json", `--outputFile=${reportFile}`];
9150
+ const argv = watching ? [...options, ...paths] : ["run", ...reporters, ...options, ...paths];
9151
+ const result = spawnSync5(bin, argv, { cwd: runFrom, stdio: "inherit" });
8427
9152
  if (result.error) {
8428
9153
  this.say(`could not run vitest (${result.error.message})`);
8429
9154
  return Promise.resolve({ ok: false, code: 1 });
8430
9155
  }
8431
9156
  const code = result.status ?? 1;
9157
+ if (reportFile !== void 0) {
9158
+ let report;
9159
+ try {
9160
+ report = JSON.parse(readFileSync31(reportFile, "utf8"));
9161
+ } catch {
9162
+ report = void 0;
9163
+ }
9164
+ if (report === void 0) {
9165
+ this.say(`vitest wrote no report, so this run proved nothing against the reference.`);
9166
+ } else {
9167
+ const outcome = compareAgainstReference(ctx.cwd, report);
9168
+ this.say(outcome.message);
9169
+ if (!outcome.ok) return Promise.resolve({ ok: false, code: code === 0 ? 1 : code });
9170
+ }
9171
+ }
8432
9172
  return Promise.resolve({ ok: code === 0, code });
8433
9173
  }
8434
9174
  say(message) {
@@ -8436,6 +9176,31 @@ var VitestAdapter = class extends BaseAdapter {
8436
9176
  `);
8437
9177
  }
8438
9178
  };
9179
+ function fromWorkspaceRoot(moduleDir, options, paths) {
9180
+ const root = findWorkspaceRoot(moduleDir);
9181
+ if (root === void 0 || root === moduleDir) {
9182
+ return { cwd: moduleDir, options: [...options], paths: [...paths] };
9183
+ }
9184
+ const absolute = (value) => isAbsolute3(value) ? value : resolve6(moduleDir, value);
9185
+ const moved = [];
9186
+ let previousWasConfig = false;
9187
+ for (const option of options) {
9188
+ if (previousWasConfig) {
9189
+ moved.push(absolute(option));
9190
+ previousWasConfig = false;
9191
+ continue;
9192
+ }
9193
+ if (option.startsWith("--config=")) moved.push(`--config=${absolute(option.slice(9))}`);
9194
+ else moved.push(option);
9195
+ previousWasConfig = option === "--config" || option === "-c";
9196
+ }
9197
+ return {
9198
+ cwd: root,
9199
+ // `--root` LAST, so a caller cannot be silently overridden by our own flag.
9200
+ options: [...moved, "--root", moduleDir],
9201
+ paths: paths.map((path) => absolute(path))
9202
+ };
9203
+ }
8439
9204
 
8440
9205
  // src/roles/test/register.ts
8441
9206
  function registerTest() {
@@ -8444,10 +9209,10 @@ function registerTest() {
8444
9209
  }
8445
9210
 
8446
9211
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
8447
- import { spawnSync as spawnSync6 } from "child_process";
8448
- import { existsSync as existsSync29, readFileSync as readFileSync30 } from "fs";
8449
- import { createRequire as createRequire6 } from "module";
8450
- import { join as join37 } from "path";
9212
+ import { spawnSync as spawnSync6 } from "node:child_process";
9213
+ import { existsSync as existsSync32, readFileSync as readFileSync33 } from "node:fs";
9214
+ import { createRequire as createRequire7 } from "node:module";
9215
+ import { join as join41 } from "node:path";
8451
9216
 
8452
9217
  // src/roles/typescript/presets/nest.json
8453
9218
  var nest_default2 = {
@@ -8628,8 +9393,8 @@ function readTsconfigAdoption(cwd) {
8628
9393
  }
8629
9394
 
8630
9395
  // src/roles/typescript/adapters/tsc/plan.ts
8631
- import { readFileSync as readFileSync29 } from "fs";
8632
- import { join as join36 } from "path";
9396
+ import { readFileSync as readFileSync32 } from "node:fs";
9397
+ import { join as join40 } from "node:path";
8633
9398
 
8634
9399
  // src/roles/typescript/typecheck-script.ts
8635
9400
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -8722,7 +9487,7 @@ function planAdoption(context) {
8722
9487
  };
8723
9488
  }
8724
9489
  const existing = parseJsonc(
8725
- readFileSync29(join36(context.cwd, target.path), "utf8"),
9490
+ readFileSync32(join40(context.cwd, target.path), "utf8"),
8726
9491
  target.path
8727
9492
  );
8728
9493
  const extendsChain = composeExtends(existing.extends, preset);
@@ -8860,7 +9625,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8860
9625
  let chain;
8861
9626
  try {
8862
9627
  const parsed = parseJsonc(
8863
- readFileSync30(join37(cwd, target.path), "utf8"),
9628
+ readFileSync33(join41(cwd, target.path), "utf8"),
8864
9629
  target.path
8865
9630
  );
8866
9631
  chain = parsed.extends;
@@ -8873,7 +9638,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8873
9638
  );
8874
9639
  if (preset === void 0) return void 0;
8875
9640
  try {
8876
- createRequire6(join37(cwd, "noop.js")).resolve(preset);
9641
+ createRequire7(join41(cwd, "noop.js")).resolve(preset);
8877
9642
  return void 0;
8878
9643
  } catch {
8879
9644
  return preset;
@@ -8969,7 +9734,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8969
9734
  referencedProjects(cwd, config) {
8970
9735
  try {
8971
9736
  const parsed = parseJsonc(
8972
- readFileSync30(join37(cwd, config), "utf8"),
9737
+ readFileSync33(join41(cwd, config), "utf8"),
8973
9738
  config
8974
9739
  );
8975
9740
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -8985,7 +9750,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8985
9750
  * check.
8986
9751
  */
8987
9752
  typecheckTarget(cwd) {
8988
- if (existsSync29(join37(cwd, "tsconfig.json"))) return "tsconfig.json";
9753
+ if (existsSync32(join41(cwd, "tsconfig.json"))) return "tsconfig.json";
8989
9754
  const target = resolveTsconfigTarget(cwd);
8990
9755
  return target.reason === "none" ? null : target.path;
8991
9756
  }
@@ -9134,7 +9899,7 @@ function detectFramework(packageJson) {
9134
9899
  }
9135
9900
 
9136
9901
  // src/core/dispatch.ts
9137
- import { basename as basename3 } from "path";
9902
+ import { basename as basename3 } from "node:path";
9138
9903
 
9139
9904
  // src/shared/text.ts
9140
9905
  function ensureLines(current, lines) {
@@ -9190,8 +9955,8 @@ function replaceLines(current, replacements) {
9190
9955
  }
9191
9956
 
9192
9957
  // src/core/apply-plan.ts
9193
- import { existsSync as existsSync30, readFileSync as readFileSync31, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
9194
- import { resolve as resolve5, sep } from "path";
9958
+ import { existsSync as existsSync33, readFileSync as readFileSync34, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
9959
+ import { resolve as resolve7, sep } from "node:path";
9195
9960
  import { applyEdits as applyEdits3, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
9196
9961
 
9197
9962
  // src/shared/deep-merge.ts
@@ -9201,15 +9966,15 @@ function isPlainObject(value) {
9201
9966
 
9202
9967
  // src/core/apply-plan.ts
9203
9968
  function resolveWithinRoot(cwd, relativePath) {
9204
- const root = resolve5(cwd);
9205
- const absolutePath = resolve5(root, relativePath);
9969
+ const root = resolve7(cwd);
9970
+ const absolutePath = resolve7(root, relativePath);
9206
9971
  if (absolutePath !== root && !absolutePath.startsWith(root + sep)) {
9207
9972
  throw new Error(`Refusing to write outside the module root: "${relativePath}".`);
9208
9973
  }
9209
9974
  return absolutePath;
9210
9975
  }
9211
9976
  function readIfExists(absolutePath) {
9212
- return existsSync30(absolutePath) ? readFileSync31(absolutePath, "utf8") : void 0;
9977
+ return existsSync33(absolutePath) ? readFileSync34(absolutePath, "utf8") : void 0;
9213
9978
  }
9214
9979
  function* leaves(value, prefix = []) {
9215
9980
  for (const [key, keyValue] of Object.entries(value)) {
@@ -9311,14 +10076,14 @@ function preparePlan(cwd, plan4) {
9311
10076
  }
9312
10077
  function writeFileAtomic(absolutePath, contents) {
9313
10078
  const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
9314
- writeFileSync3(tempPath, contents);
10079
+ writeFileSync4(tempPath, contents);
9315
10080
  renameSync(tempPath, absolutePath);
9316
10081
  }
9317
10082
  function applyPlan(cwd, plan4) {
9318
10083
  const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
9319
10084
  for (const file of changed) {
9320
10085
  if (file.deleted) {
9321
- rmSync3(file.absolutePath, { force: true });
10086
+ rmSync4(file.absolutePath, { force: true });
9322
10087
  continue;
9323
10088
  }
9324
10089
  writeFileAtomic(file.absolutePath, file.after);
@@ -9327,8 +10092,8 @@ function applyPlan(cwd, plan4) {
9327
10092
  }
9328
10093
 
9329
10094
  // src/core/config/preset-evidence.ts
9330
- import { existsSync as existsSync31, readdirSync as readdirSync8, readFileSync as readFileSync32 } from "fs";
9331
- import { join as join38 } from "path";
10095
+ import { existsSync as existsSync34, readdirSync as readdirSync9, readFileSync as readFileSync35 } from "node:fs";
10096
+ import { join as join42 } from "node:path";
9332
10097
  var PATH_SIGNALS = [
9333
10098
  {
9334
10099
  preset: "nest",
@@ -9356,18 +10121,18 @@ function sourceImports(cwd, pattern) {
9356
10121
  const dir = stack.pop();
9357
10122
  let entries;
9358
10123
  try {
9359
- entries = readdirSync8(dir, { withFileTypes: true });
10124
+ entries = readdirSync9(dir, { withFileTypes: true });
9360
10125
  } catch {
9361
10126
  continue;
9362
10127
  }
9363
10128
  for (const entry of entries) {
9364
10129
  if (entry.isDirectory()) {
9365
- if (!NOT_SCANNED.has(entry.name)) stack.push(join38(dir, entry.name));
10130
+ if (!NOT_SCANNED.has(entry.name)) stack.push(join42(dir, entry.name));
9366
10131
  continue;
9367
10132
  }
9368
10133
  if (!SOURCE_FILE2.test(entry.name)) continue;
9369
10134
  try {
9370
- if (pattern.test(readFileSync32(join38(dir, entry.name), "utf8"))) return true;
10135
+ if (pattern.test(readFileSync35(join42(dir, entry.name), "utf8"))) return true;
9371
10136
  } catch {
9372
10137
  continue;
9373
10138
  }
@@ -9376,11 +10141,11 @@ function sourceImports(cwd, pattern) {
9376
10141
  return false;
9377
10142
  }
9378
10143
  function dependencyNames(cwd) {
9379
- const path = join38(cwd, "package.json");
9380
- if (!existsSync31(path)) return [];
10144
+ const path = join42(cwd, "package.json");
10145
+ if (!existsSync34(path)) return [];
9381
10146
  try {
9382
10147
  const manifest = parseJsonc(
9383
- readFileSync32(path, "utf8"),
10148
+ readFileSync35(path, "utf8"),
9384
10149
  path
9385
10150
  );
9386
10151
  return [
@@ -9394,7 +10159,7 @@ function dependencyNames(cwd) {
9394
10159
  function declaresJsx(cwd) {
9395
10160
  let entries;
9396
10161
  try {
9397
- entries = readdirSync8(cwd).filter(
10162
+ entries = readdirSync9(cwd).filter(
9398
10163
  (name) => name.startsWith("tsconfig") && name.endsWith(".json")
9399
10164
  );
9400
10165
  } catch {
@@ -9403,7 +10168,7 @@ function declaresJsx(cwd) {
9403
10168
  for (const name of entries) {
9404
10169
  try {
9405
10170
  const config = parseJsonc(
9406
- readFileSync32(join38(cwd, name), "utf8"),
10171
+ readFileSync35(join42(cwd, name), "utf8"),
9407
10172
  name
9408
10173
  );
9409
10174
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -9548,9 +10313,20 @@ async function dispatch(opts) {
9548
10313
  if (opts.dryRun) {
9549
10314
  return previewPlan(opts, plan4);
9550
10315
  }
10316
+ if (adapter.beforeInit) {
10317
+ await adapter.beforeInit({
10318
+ module: basename3(opts.cwd),
10319
+ cwd: opts.cwd,
10320
+ preset,
10321
+ ci: false,
10322
+ fix: false
10323
+ });
10324
+ }
10325
+ const written = [];
9551
10326
  for (const change of applyPlan(opts.cwd, plan4)) {
9552
10327
  process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
9553
10328
  `);
10329
+ if (!change.deleted) written.push(change.path);
9554
10330
  }
9555
10331
  for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
9556
10332
  `);
@@ -9562,7 +10338,8 @@ async function dispatch(opts) {
9562
10338
  cwd: opts.cwd,
9563
10339
  preset,
9564
10340
  ci: false,
9565
- fix: true
10341
+ fix: true,
10342
+ written
9566
10343
  });
9567
10344
  }
9568
10345
  return 0;
@@ -9591,4 +10368,3 @@ export {
9591
10368
  detectFramework,
9592
10369
  dispatch
9593
10370
  };
9594
- //# sourceMappingURL=chunk-4UIZJ3TR.js.map