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

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-BPLSUYU7.js} +936 -179
  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 readFileSync30 } from "node:fs";
5410
+ import { tmpdir as tmpdir2 } from "node:os";
5411
+ import { isAbsolute as isAbsolute3, join as join38, resolve as resolve6 } from "node:path";
5397
5412
 
5398
5413
  // src/roles/test/presets/toolchain.json
5399
5414
  var toolchain_default2 = {
@@ -5470,17 +5485,18 @@ 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 readFileSync28, readdirSync as readdirSync8 } from "node:fs";
5489
+ import { createRequire as createRequire6 } from "node:module";
5490
+ import { dirname as dirname8, join as join36, 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 readFileSync25, statSync as statSync6 } from "node:fs";
5494
+ import { join as join31 } from "node:path";
5479
5495
  import { parseSync as parseSync8 } from "oxc-parser";
5480
5496
 
5481
5497
  // 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";
5498
+ import { existsSync as existsSync23, readFileSync as readFileSync21, readdirSync as readdirSync3 } from "node:fs";
5499
+ import { join as join27, relative as relative4 } from "node:path";
5484
5500
  var JEST_CONFIG_FILES = [
5485
5501
  "jest.config.ts",
5486
5502
  "jest.config.js",
@@ -5600,6 +5616,17 @@ function readTestAdoption(cwd) {
5600
5616
  };
5601
5617
  }
5602
5618
 
5619
+ // src/roles/test/support-file-name.ts
5620
+ var JEST_PREFIX = "jest.";
5621
+ var VITEST_PREFIX = "vitest.";
5622
+ function vitestSupportName(relative8) {
5623
+ const cut = relative8.lastIndexOf("/");
5624
+ const directory = cut === -1 ? "" : relative8.slice(0, cut + 1);
5625
+ const base = relative8.slice(cut + 1);
5626
+ if (!base.startsWith(JEST_PREFIX)) return relative8;
5627
+ return `${directory}${VITEST_PREFIX}${base.slice(JEST_PREFIX.length)}`;
5628
+ }
5629
+
5603
5630
  // src/roles/test/codemod/migrate-file.ts
5604
5631
  import { parseSync as parseSync7 } from "oxc-parser";
5605
5632
 
@@ -5715,7 +5742,7 @@ function rewriteAssertionReturnValue(source, program) {
5715
5742
  for (const reading of readings) {
5716
5743
  const from = reading.start;
5717
5744
  let to = reading.end;
5718
- while (to < source.length && /[\s;]/.test(source[to])) to += 1;
5745
+ while (to < source.length && /[ \t;]/.test(source[to])) to += 1;
5719
5746
  let head = from;
5720
5747
  while (head > 0 && /[ \t]/.test(source[head - 1])) head -= 1;
5721
5748
  if (head > 0 && source[head - 1] === "\n") head -= 1;
@@ -6450,9 +6477,9 @@ ${source}`;
6450
6477
  }
6451
6478
 
6452
6479
  // 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";
6480
+ import { existsSync as existsSync24, readFileSync as readFileSync22 } from "node:fs";
6481
+ import { createRequire as createRequire4 } from "node:module";
6482
+ import { dirname as dirname4, join as join28 } from "node:path";
6456
6483
  function isCommonJsPackage(fromFile, specifier) {
6457
6484
  if (specifier.startsWith(".") || specifier.startsWith("/")) return false;
6458
6485
  let resolved;
@@ -6581,12 +6608,47 @@ function applyNamedRules(source, program, fileName = "") {
6581
6608
  const applied = /* @__PURE__ */ new Set();
6582
6609
  const asyncify = /* @__PURE__ */ new Set();
6583
6610
  const mocked = mockedSpecifiers(program);
6584
- const wholeStatement = /* @__PURE__ */ new Set();
6611
+ const wholeStatement = /* @__PURE__ */ new Map();
6585
6612
  walk(program, (node) => {
6586
6613
  if (node.type !== "ExpressionStatement") return;
6587
6614
  const expression = node.expression;
6588
- if (expression !== void 0) wholeStatement.add(expression);
6615
+ if (expression !== void 0) wholeStatement.set(expression, node);
6616
+ });
6617
+ const standsAlone = /* @__PURE__ */ new Set();
6618
+ const alone = (node) => {
6619
+ if (node !== void 0) standsAlone.add(node);
6620
+ };
6621
+ walk(program, (node) => {
6622
+ switch (node.type) {
6623
+ case "VariableDeclarator":
6624
+ return alone(node.init);
6625
+ case "AssignmentExpression":
6626
+ return alone(node.right);
6627
+ case "ReturnStatement":
6628
+ case "ExportDefaultDeclaration":
6629
+ return alone(node.argument ?? node.declaration);
6630
+ case "ArrowFunctionExpression":
6631
+ return node.expression === true ? alone(node.body) : void 0;
6632
+ case "ConditionalExpression":
6633
+ alone(node.consequent);
6634
+ return alone(node.alternate);
6635
+ case "ArrayExpression":
6636
+ return (node.elements ?? []).forEach((e) => alone(e ?? void 0));
6637
+ case "Property":
6638
+ case "ObjectProperty":
6639
+ return node.shorthand === true ? void 0 : alone(node.value);
6640
+ case "CallExpression":
6641
+ case "NewExpression":
6642
+ return (node.arguments ?? []).forEach((a) => alone(a));
6643
+ default:
6644
+ return;
6645
+ }
6589
6646
  });
6647
+ const placed = (node, expression, trailing = "") => {
6648
+ if (trailing !== "") return `(${expression})${trailing}`;
6649
+ if (wholeStatement.has(node)) return expression;
6650
+ return standsAlone.has(node) ? expression : `(${expression})`;
6651
+ };
6590
6652
  const handedToTheRunnerAsAModule = /* @__PURE__ */ new Set();
6591
6653
  walk(program, (node) => {
6592
6654
  const returned = mockFactoryReturn(node);
@@ -6616,7 +6678,7 @@ function applyNamedRules(source, program, fileName = "") {
6616
6678
  edits.push({
6617
6679
  start: node.start,
6618
6680
  end: node.end,
6619
- text: `(await import(${argumentText(source, node)}))`
6681
+ text: placed(node, `await import(${argumentText(source, node)})`)
6620
6682
  });
6621
6683
  applied.add("jest.requireMock");
6622
6684
  if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
@@ -6628,7 +6690,7 @@ function applyNamedRules(source, program, fileName = "") {
6628
6690
  edits.push({
6629
6691
  start: node.start,
6630
6692
  end: node.end,
6631
- text: `(await vi.${asyncReplacement}(${argumentText(source, node)}))`
6693
+ text: placed(node, `await vi.${asyncReplacement}(${argumentText(source, node)})`)
6632
6694
  });
6633
6695
  applied.add(`jest.${member}`);
6634
6696
  if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
@@ -6641,7 +6703,7 @@ function applyNamedRules(source, program, fileName = "") {
6641
6703
  edits.push({
6642
6704
  start: node.start,
6643
6705
  end: node.end,
6644
- text: wholeStatement.has(node) ? `await import(${argumentText(source, node)});` : `(await import(${argumentText(source, node)}))${interop}`
6706
+ text: placed(node, `await import(${argumentText(source, node)})`, interop)
6645
6707
  });
6646
6708
  applied.add("require");
6647
6709
  if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
@@ -6932,6 +6994,78 @@ function rewriteMockFactoryDefault(source, program, defaultImported) {
6932
6994
  };
6933
6995
  }
6934
6996
 
6997
+ // src/roles/test/codemod/mock-promise-resolve.ts
6998
+ var MOCK_FACTORY = /* @__PURE__ */ new Set(["fn", "spyOn"]);
6999
+ function isMockFactory(source, node) {
7000
+ if (node.type !== "CallExpression") return false;
7001
+ const callee = node.callee;
7002
+ if (callee?.type !== "MemberExpression") return false;
7003
+ const object = callee.object;
7004
+ const property = callee.property;
7005
+ if (object?.type !== "Identifier" || property?.type !== "Identifier") return false;
7006
+ const runner = source.slice(object.start, object.end);
7007
+ const method = source.slice(property.start, property.end);
7008
+ return (runner === "jest" || runner === "vi") && MOCK_FACTORY.has(method);
7009
+ }
7010
+ function resolvedValue(source, executor) {
7011
+ if (executor.type !== "ArrowFunctionExpression" && executor.type !== "FunctionExpression") {
7012
+ return void 0;
7013
+ }
7014
+ const params = executor.params;
7015
+ if (params?.length !== 1) return void 0;
7016
+ const parameter = params[0];
7017
+ if (parameter.type !== "Identifier") return void 0;
7018
+ const resolveName = source.slice(parameter.start, parameter.end);
7019
+ const body = executor.body;
7020
+ if (body === void 0) return void 0;
7021
+ let call;
7022
+ if (body.type === "BlockStatement") {
7023
+ const statements = body.body;
7024
+ if (statements?.length !== 1) return void 0;
7025
+ const only = statements[0];
7026
+ if (only.type !== "ExpressionStatement") return void 0;
7027
+ call = only.expression;
7028
+ } else {
7029
+ call = body;
7030
+ }
7031
+ if (call?.type !== "CallExpression") return void 0;
7032
+ const callee = call.callee;
7033
+ if (callee?.type !== "Identifier") return void 0;
7034
+ if (source.slice(callee.start, callee.end) !== resolveName) return void 0;
7035
+ const args = call.arguments;
7036
+ if (args?.length !== 1) return void 0;
7037
+ return args[0];
7038
+ }
7039
+ function rewriteMockPromiseResolve(source, program) {
7040
+ const edits = [];
7041
+ walk(program, (node) => {
7042
+ if (!isMockFactory(source, node)) return;
7043
+ for (const argument of node.arguments ?? []) {
7044
+ walk(argument, (inner) => {
7045
+ if (inner.type !== "NewExpression") return;
7046
+ const callee = inner.callee;
7047
+ if (callee?.type !== "Identifier") return;
7048
+ if (source.slice(callee.start, callee.end) !== "Promise") return;
7049
+ if ((inner.typeArguments ?? inner.typeParameters) !== void 0) return;
7050
+ const executors = inner.arguments ?? [];
7051
+ if (executors.length !== 1) return;
7052
+ const value = resolvedValue(source, executors[0]);
7053
+ if (value === void 0) return;
7054
+ edits.push({
7055
+ start: inner.start,
7056
+ end: inner.end,
7057
+ text: `Promise.resolve(${source.slice(value.start, value.end)})`
7058
+ });
7059
+ });
7060
+ }
7061
+ });
7062
+ let result = source;
7063
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
7064
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
7065
+ }
7066
+ return { source: result, rewritten: edits.length };
7067
+ }
7068
+
6935
7069
  // src/roles/test/codemod/propagate-await.ts
6936
7070
  import { parseSync as parseSync5 } from "oxc-parser";
6937
7071
  var FUNCTIONS3 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
@@ -7059,9 +7193,9 @@ function propagateAwait(fileName, source, original, elsewhere = /* @__PURE__ */
7059
7193
  }
7060
7194
 
7061
7195
  // 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";
7196
+ import { readFileSync as readFileSync23, readdirSync as readdirSync4, statSync as statSync4 } from "node:fs";
7197
+ import { createRequire as createRequire5 } from "node:module";
7198
+ import { dirname as dirname5, join as join29 } from "node:path";
7065
7199
  var MEMBER_TYPES13 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
7066
7200
  var PACKAGE_SUBPATH = /^(@[^/]+\/[^/]+|[^@.][^/]*)\/(.+)$/;
7067
7201
  var PURE_REEXPORT = /^module\.exports\s*=\s*require\(\s*(['"])([^'"]+)\1\s*\)\s*;?$/;
@@ -7160,12 +7294,9 @@ ${text}` });
7160
7294
  // src/roles/test/codemod/repoint-msw.ts
7161
7295
  import { parseSync as parseSync6 } from "oxc-parser";
7162
7296
  var SENTINEL_MSW_SERVER = "@hublo/sentinel/test/msw";
7163
- var MSW_SERVER_MODULE = /(^|\/)msw\/server$/;
7297
+ var SENTINEL_MSW_API = "@hublo/sentinel/test/tools/msw";
7164
7298
  var MSW_PACKAGE = /^msw$/;
7165
- function importsMswPackage(declaration) {
7166
- const source = declaration.source?.value;
7167
- return source !== void 0 && MSW_PACKAGE.test(source);
7168
- }
7299
+ var MSW_SERVER_MODULE = /(^|\/)msw\/server$/;
7169
7300
  function importsServer(declaration) {
7170
7301
  return (declaration.specifiers ?? []).some((specifier) => {
7171
7302
  const imported = specifier.imported;
@@ -7181,21 +7312,28 @@ function repointMswServer(fileName, source) {
7181
7312
  if (node.type !== "ImportDeclaration") return;
7182
7313
  const specifier = node.source;
7183
7314
  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 });
7315
+ const isServer = MSW_SERVER_MODULE.test(specifier.value) && importsServer(node);
7316
+ const isPackage2 = MSW_PACKAGE.test(specifier.value);
7317
+ if (!isServer && !isPackage2) return;
7318
+ edits.push({
7319
+ start: specifier.start,
7320
+ end: specifier.end,
7321
+ to: isServer ? SENTINEL_MSW_SERVER : SENTINEL_MSW_API
7322
+ });
7187
7323
  });
7188
7324
  if (edits.length === 0) return { source, repointed: false };
7189
7325
  let rewritten = source;
7190
7326
  for (const edit of edits.sort((left, right) => right.start - left.start)) {
7191
7327
  const quote = source[edit.start] === '"' ? '"' : "'";
7192
- rewritten = rewritten.slice(0, edit.start) + `${quote}${SENTINEL_MSW_SERVER}${quote}` + rewritten.slice(edit.end);
7328
+ rewritten = rewritten.slice(0, edit.start) + `${quote}${edit.to}${quote}` + rewritten.slice(edit.end);
7193
7329
  }
7194
7330
  return { source: rewritten, repointed: true };
7195
7331
  }
7196
7332
 
7197
7333
  // src/roles/test/codemod/spy-instance-type.ts
7198
7334
  var SPY_TYPES = /* @__PURE__ */ new Set(["SpyInstance"]);
7335
+ var QUALIFIED_ONLY = /* @__PURE__ */ new Set(["Mock"]);
7336
+ var VITEST_NAME = { SpyInstance: "MockInstance", Mock: "Mock" };
7199
7337
  function typeNameOf(source, node) {
7200
7338
  const name = node.typeName;
7201
7339
  if (name === void 0) return void 0;
@@ -7207,17 +7345,20 @@ function rewriteSpyInstanceType(source, program) {
7207
7345
  if (node.type !== "TSTypeReference") return;
7208
7346
  const written = typeNameOf(source, node);
7209
7347
  if (written === void 0) return;
7210
- const bare = written.startsWith("jest.") ? written.slice("jest.".length) : written;
7211
- if (!SPY_TYPES.has(bare)) return;
7348
+ const qualified = written.startsWith("jest.");
7349
+ const bare = qualified ? written.slice("jest.".length) : written;
7350
+ if (!SPY_TYPES.has(bare) && !(qualified && QUALIFIED_ONLY.has(bare))) return;
7212
7351
  const args = (node.typeArguments ?? node.typeParameters)?.params;
7213
7352
  if (args === void 0 || args.length === 0) return;
7214
7353
  const text = (argument) => source.slice(argument.start, argument.end);
7215
7354
  const returns = text(args[0]);
7216
- const parameters = args.length > 1 ? `...args: ${text(args[1])}` : "...args: any[]";
7355
+ const parameters = args.length > 1 ? `...args: ${text(args[1])}` : "...args: never[]";
7356
+ const target = VITEST_NAME[bare];
7217
7357
  edits.push({
7218
7358
  start: node.start,
7219
7359
  end: node.end,
7220
- text: `MockInstance<(${parameters}) => ${returns}>`
7360
+ text: `${target}<(${parameters}) => ${returns}>`,
7361
+ imports: target
7221
7362
  });
7222
7363
  });
7223
7364
  let result = source;
@@ -7227,7 +7368,8 @@ function rewriteSpyInstanceType(source, program) {
7227
7368
  return {
7228
7369
  source: result,
7229
7370
  rewritten: edits.length,
7230
- typeImports: edits.length > 0 ? ["MockInstance"] : []
7371
+ // Only the names actually written, so a file does not import a type it never uses.
7372
+ typeImports: [...new Set(edits.map((edit) => edit.imports))]
7231
7373
  };
7232
7374
  }
7233
7375
 
@@ -7315,7 +7457,10 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
7315
7457
  const afterEmpty = parse2(fileName, emptyImplementations.source);
7316
7458
  if (afterEmpty === void 0) return unparsable("empty mock implementation");
7317
7459
  const hooks = rewriteHookReturnValue(emptyImplementations.source, afterEmpty);
7318
- const awaited = propagateAwait(fileName, hooks.source, original, asyncHelpers);
7460
+ const afterHooks = parse2(fileName, hooks.source);
7461
+ if (afterHooks === void 0) return unparsable("hook return value");
7462
+ const promises = rewriteMockPromiseResolve(hooks.source, afterHooks);
7463
+ const awaited = propagateAwait(fileName, promises.source, original, asyncHelpers);
7319
7464
  const program = parse2(fileName, awaited.source);
7320
7465
  if (program === void 0) return unparsable("hook body");
7321
7466
  const hoist = hoistCapturedBindings(
@@ -7366,8 +7511,8 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
7366
7511
  }
7367
7512
 
7368
7513
  // 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";
7514
+ import { existsSync as existsSync25, readFileSync as readFileSync24, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
7515
+ import { dirname as dirname6, join as join30, relative as relative5, resolve as resolve4 } from "node:path";
7371
7516
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
7372
7517
  var TEST_FILE2 = /\.(spec|test)\.[cm]?[jt]sx?$/;
7373
7518
  var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
@@ -7524,14 +7669,16 @@ function migrateModuleTests(cwd, setupFiles = []) {
7524
7669
  let hoisted = 0;
7525
7670
  const defaultImported = defaultImportedSpecifiers(cwd);
7526
7671
  const asyncHelpers = /* @__PURE__ */ new Set();
7527
- for (const relative7 of listTestSupportFiles(cwd, setupFiles)) {
7528
- const source = read2(join31(cwd, relative7));
7672
+ for (const relative8 of listTestSupportFiles(cwd, setupFiles)) {
7673
+ const source = read2(join31(cwd, relative8));
7529
7674
  if (source === void 0) {
7530
- blockers.push({ file: relative7, reason: "this file could not be read" });
7675
+ blockers.push({ file: relative8, reason: "this file could not be read" });
7531
7676
  continue;
7532
7677
  }
7533
- const migrated = migrateTestFile(join31(cwd, relative7), source, defaultImported);
7534
- if (!migrated.changed) continue;
7678
+ const migrated = migrateTestFile(join31(cwd, relative8), source, defaultImported);
7679
+ const renamedTo = vitestSupportName(relative8);
7680
+ if (!migrated.changed && renamedTo === relative8) continue;
7681
+ if (renamedTo !== relative8) operations.push({ kind: "delete", path: relative8 });
7535
7682
  const before = parse3(source);
7536
7683
  const after = parse3(migrated.source);
7537
7684
  if (before !== void 0 && after !== void 0) {
@@ -7542,41 +7689,44 @@ function migrateModuleTests(cwd, setupFiles = []) {
7542
7689
  }
7543
7690
  files++;
7544
7691
  renamed += migrated.renamed;
7545
- operations.push({ kind: "write", path: relative7, contents: migrated.source });
7692
+ operations.push({ kind: "write", path: renamedTo, contents: migrated.source });
7546
7693
  }
7547
- for (const relative7 of listTestFiles(cwd)) {
7548
- const source = read2(join31(cwd, relative7));
7694
+ for (const relative8 of listTestFiles(cwd)) {
7695
+ const source = read2(join31(cwd, relative8));
7549
7696
  if (source === void 0) {
7550
- blockers.push({ file: relative7, reason: "this file could not be read" });
7697
+ blockers.push({ file: relative8, reason: "this file could not be read" });
7551
7698
  continue;
7552
7699
  }
7553
- const migrated = migrateTestFile(join31(cwd, relative7), source, defaultImported, asyncHelpers);
7700
+ const migrated = migrateTestFile(join31(cwd, relative8), source, defaultImported, asyncHelpers);
7554
7701
  for (const member of migrated.unhandled) {
7555
7702
  blockers.push({
7556
- file: `${relative7}:${member.line}`,
7703
+ file: `${relative8}:${member.line}`,
7557
7704
  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
7705
  });
7559
7706
  }
7560
7707
  for (const refusal of migrated.refusals) {
7561
- blockers.push({ file: relative7, reason: refusal.reason });
7708
+ blockers.push({ file: relative8, reason: refusal.reason });
7562
7709
  }
7563
7710
  if (!migrated.changed) continue;
7564
7711
  files++;
7565
7712
  renamed += migrated.renamed;
7566
7713
  if (migrated.hoisted.length > 0) hoisted++;
7567
- operations.push({ kind: "write", path: relative7, contents: migrated.source });
7714
+ operations.push({ kind: "write", path: relative8, contents: migrated.source });
7568
7715
  }
7569
7716
  return blockers.length > 0 ? { operations: [], files, renamed, hoisted, blockers } : { operations, files, renamed, hoisted, blockers: [] };
7570
7717
  }
7571
7718
 
7572
7719
  // 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";
7720
+ import { existsSync as existsSync26, readdirSync as readdirSync7, readFileSync as readFileSync26, statSync as statSync7 } from "node:fs";
7721
+ import { join as join32, relative as relative6 } from "node:path";
7575
7722
  var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
7576
- var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/nest";
7723
+ var REACT_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
7724
+ function flavourOf(reading) {
7725
+ return reading.testEnvironment === "jsdom" ? "react" : "nest";
7726
+ }
7727
+ var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/jest-parity";
7577
7728
  var SENTINEL_MSW_SETUP = "@hublo/sentinel/test/setup/msw-lifecycle";
7578
7729
  var SENTINEL_WORKSPACE_SETUP = "@hublo/sentinel/test/setup/workspace";
7579
- var NEST_TEST_CONFIG_FILE = "vitest.config.mts";
7580
7730
  function hopsToRoot(cwd, workspaceRoot) {
7581
7731
  const up = relative6(cwd, workspaceRoot).replaceAll("\\", "/");
7582
7732
  return up === "" ? "." : up;
@@ -7643,14 +7793,29 @@ function readModuleTraits(cwd) {
7643
7793
  return traits;
7644
7794
  }
7645
7795
  var withoutRootDir = (value) => value.replaceAll("<rootDir>/", "./").replaceAll("<rootDir>", ".");
7646
- function setupBlock(reading, msw) {
7796
+ function ownsMswLifecycle(cwd, reading) {
7797
+ const declared = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []].filter(
7798
+ (file) => !file.endsWith("jest.setup.after.env.js")
7799
+ );
7800
+ for (const raw of declared) {
7801
+ const file = join32(cwd, raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", "."));
7802
+ if (!existsSync26(file)) continue;
7803
+ try {
7804
+ if (/\.listen\s*\(/.test(readFileSync26(file, "utf8"))) return true;
7805
+ } catch {
7806
+ continue;
7807
+ }
7808
+ }
7809
+ return false;
7810
+ }
7811
+ function setupBlock(cwd, reading, msw) {
7647
7812
  const declared = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []];
7648
7813
  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)}'`);
7814
+ const own = declared.filter((file) => !file.endsWith("jest.setup.after.env.js")).map((file) => `'${vitestSupportName(withoutRootDir(file))}'`);
7650
7815
  const sentinel = [
7651
7816
  `'${SENTINEL_TEST_SETUP}'`,
7652
7817
  ...namedTheRootSetup ? [`'${SENTINEL_WORKSPACE_SETUP}'`] : [],
7653
- ...msw ? [`'${SENTINEL_MSW_SETUP}'`] : []
7818
+ ...msw && namedTheRootSetup && !ownsMswLifecycle(cwd, reading) ? [`'${SENTINEL_MSW_SETUP}'`] : []
7654
7819
  ];
7655
7820
  return ` setupFiles: [${[...sentinel, ...own].join(", ")}],`;
7656
7821
  }
@@ -7677,7 +7842,7 @@ function timeoutBlock(reading) {
7677
7842
  }
7678
7843
  function globalSetupBlock(reading) {
7679
7844
  if (reading.globalSetup === void 0) return "";
7680
- const files = reading.globalSetup.map((file) => `'${withoutRootDir(file)}'`).join(", ");
7845
+ const files = reading.globalSetup.map((file) => `'${vitestSupportName(withoutRootDir(file))}'`).join(", ");
7681
7846
  return ` globalSetup: [${files}],`;
7682
7847
  }
7683
7848
  function includeBlock(reading) {
@@ -7697,7 +7862,8 @@ function aliasBlock(reading) {
7697
7862
  if (reading.aliases === void 0 || reading.aliases.length === 0) return "";
7698
7863
  const entries = reading.aliases.map((alias) => {
7699
7864
  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)}')`;
7865
+ const deadToken = /<rootdir>/i.test(alias.replacement) && !alias.replacement.includes("<rootDir>");
7866
+ const target = isPackage(alias.replacement) ? `'${alias.replacement}'` : deadToken ? `'${alias.replacement}'` : `path.resolve(import.meta.dirname, '${withoutRootDir(alias.replacement)}')`;
7701
7867
  return ` { find: /${pattern}/, replacement: ${target} },`;
7702
7868
  });
7703
7869
  return [" resolve: {", " alias: [", ...entries, " ],", " },"].join("\n");
@@ -7712,7 +7878,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
7712
7878
  const up = hopsToRoot(cwd, workspaceRoot);
7713
7879
  const test = [
7714
7880
  environmentBlock(reading),
7715
- setupBlock(reading, traits.msw),
7881
+ setupBlock(cwd, reading, traits.msw),
7716
7882
  envBlock(reading),
7717
7883
  timeoutBlock(reading),
7718
7884
  concurrencyBlock(cwd, reading, target),
@@ -7727,7 +7893,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
7727
7893
  `import path from 'node:path'`,
7728
7894
  "",
7729
7895
  ...needsDefaults ? [`import { configDefaults } from 'vitest/config'`, ""] : [],
7730
- `import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
7896
+ flavourOf(reading) === "react" ? `import { defineConfig, reactTestConfig } from '${REACT_TEST_PRESET_SPECIFIER}'` : `import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
7731
7897
  ].join("\n");
7732
7898
  const overrides = [aliasBlock(reading), " test: {", ...test, " },"].filter(
7733
7899
  (line) => line !== ""
@@ -7738,7 +7904,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
7738
7904
  source: `${imports}
7739
7905
 
7740
7906
  export default defineConfig(
7741
- nestTestConfig({
7907
+ ${flavourOf(reading) === "react" ? "reactTestConfig" : "nestTestConfig"}({
7742
7908
  root: import.meta.dirname,
7743
7909
  workspaceRoot: path.resolve(import.meta.dirname, '${up}'),${decoratorLine}
7744
7910
  overrides: {
@@ -7751,8 +7917,8 @@ ${overrides.join("\n")}
7751
7917
  }
7752
7918
 
7753
7919
  // 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";
7920
+ import { existsSync as existsSync27, readFileSync as readFileSync27 } from "node:fs";
7921
+ import { join as join33 } from "node:path";
7756
7922
  var CARRIED = [
7757
7923
  "testEnvironment",
7758
7924
  "coverageDirectory",
@@ -7942,7 +8108,7 @@ function extendedConfig(source, cwd) {
7942
8108
  (name) => name.replace(/^\.\//, "")
7943
8109
  );
7944
8110
  const file = candidates.find(
7945
- (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync26(join33(cwd, name))
8111
+ (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync27(join33(cwd, name))
7946
8112
  );
7947
8113
  if (file === void 0) continue;
7948
8114
  if (!new RegExp(String.raw`\{\s*\.\.\.${binding}\b`).test(source)) continue;
@@ -7969,8 +8135,8 @@ function sideEffects(source, extended) {
7969
8135
  return found;
7970
8136
  }
7971
8137
  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;
8138
+ const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync27(join33(cwd, name)));
8139
+ if (file === void 0 || !existsSync27(join33(cwd, file))) return void 0;
7974
8140
  let source;
7975
8141
  try {
7976
8142
  source = readFileSync27(join33(cwd, file), "utf8");
@@ -8054,8 +8220,8 @@ function readJestConfig(cwd, named) {
8054
8220
  }
8055
8221
 
8056
8222
  // src/roles/test/nest/run-modes.ts
8057
- import { existsSync as existsSync27 } from "fs";
8058
- import { basename as basename2, join as join34 } from "path";
8223
+ import { existsSync as existsSync28 } from "node:fs";
8224
+ import { basename as basename2, join as join34 } from "node:path";
8059
8225
  var JEST_EXECUTOR = "@nx/jest:jest";
8060
8226
  var PRIMARY_CONFIG_FILE = "vitest.config.mts";
8061
8227
  var PRIMARY_TARGET = "test";
@@ -8076,7 +8242,7 @@ function runModes(cwd, fallback) {
8076
8242
  const jestConfig = entry.options?.jestConfig;
8077
8243
  if (jestConfig === void 0) continue;
8078
8244
  const name = configName(jestConfig);
8079
- if (!existsSync27(join34(cwd, name))) continue;
8245
+ if (!existsSync28(join34(cwd, name))) continue;
8080
8246
  declared.push({
8081
8247
  target,
8082
8248
  jestConfig: name,
@@ -8100,12 +8266,59 @@ function runModes(cwd, fallback) {
8100
8266
  );
8101
8267
  }
8102
8268
 
8269
+ // src/roles/test/written-file-formatter.ts
8270
+ import { existsSync as existsSync29 } from "node:fs";
8271
+ import { dirname as dirname7, join as join35 } from "node:path";
8272
+ var PRETTIER_CONFIGS = [
8273
+ ".prettierrc",
8274
+ ".prettierrc.json",
8275
+ ".prettierrc.json5",
8276
+ ".prettierrc.yaml",
8277
+ ".prettierrc.yml",
8278
+ ".prettierrc.js",
8279
+ ".prettierrc.cjs",
8280
+ ".prettierrc.mjs",
8281
+ ".prettierrc.toml",
8282
+ "prettier.config.js",
8283
+ "prettier.config.cjs",
8284
+ "prettier.config.mjs"
8285
+ ];
8286
+ function prettierConfigFrom(cwd) {
8287
+ let dir = cwd;
8288
+ for (; ; ) {
8289
+ for (const name of PRETTIER_CONFIGS) {
8290
+ if (existsSync29(join35(dir, name))) return join35(dir, name);
8291
+ }
8292
+ const parent = dirname7(dir);
8293
+ if (parent === dir) return void 0;
8294
+ dir = parent;
8295
+ }
8296
+ }
8297
+ function writtenFileFormatter(cwd) {
8298
+ if (readFormatAdoption(cwd).configFile !== null) {
8299
+ const oxfmt = resolveOxfmt(cwd);
8300
+ if (oxfmt !== void 0) {
8301
+ return { bin: oxfmt, args: ["--write"], origin: "this module's own oxfmt config" };
8302
+ }
8303
+ }
8304
+ const config = prettierConfigFrom(cwd);
8305
+ const prettier = resolveBin(cwd, "prettier");
8306
+ if (config !== void 0 && prettier !== void 0) {
8307
+ return {
8308
+ bin: prettier,
8309
+ args: ["--write", "--log-level", "warn"],
8310
+ origin: `the workspace's Prettier`
8311
+ };
8312
+ }
8313
+ return void 0;
8314
+ }
8315
+
8103
8316
  // src/roles/test/jest-migration.ts
8104
8317
  var SPEC_TSCONFIG = "tsconfig.spec.json";
8105
8318
  var VITEST_GLOBAL_TYPES = "vitest/globals";
8106
8319
  function specTypesOperation(cwd) {
8107
- const path = join35(cwd, SPEC_TSCONFIG);
8108
- if (!existsSync28(path)) return void 0;
8320
+ const path = join36(cwd, SPEC_TSCONFIG);
8321
+ if (!existsSync30(path)) return void 0;
8109
8322
  let config;
8110
8323
  try {
8111
8324
  config = parseJsonc(
@@ -8127,6 +8340,70 @@ function specTypesOperation(cwd) {
8127
8340
  }
8128
8341
  };
8129
8342
  }
8343
+ function shippedTypesPath(cwd, declaration) {
8344
+ try {
8345
+ const manifest = createRequire6(join36(cwd, "noop.cjs")).resolve("@hublo/sentinel/package.json");
8346
+ const file = join36(dirname8(manifest), declaration);
8347
+ if (existsSync30(file)) {
8348
+ const path = relative7(cwd, file);
8349
+ return path.startsWith(".") ? path : `./${path}`;
8350
+ }
8351
+ } catch {
8352
+ }
8353
+ return `./node_modules/@hublo/sentinel/${declaration}`;
8354
+ }
8355
+ function tsconfigFileListOperations(cwd) {
8356
+ const operations = [];
8357
+ let names;
8358
+ try {
8359
+ names = readdirSync8(cwd).filter((name) => TSCONFIG_FILE.test(name));
8360
+ } catch {
8361
+ return operations;
8362
+ }
8363
+ for (const name of names) {
8364
+ let config;
8365
+ try {
8366
+ config = parseJsonc(
8367
+ readFileSync28(join36(cwd, name), "utf8"),
8368
+ name
8369
+ );
8370
+ } catch {
8371
+ continue;
8372
+ }
8373
+ const include = Array.isArray(config?.include) ? rewriteFileList(config.include) : void 0;
8374
+ const exclude = Array.isArray(config?.exclude) ? rewriteFileList(config.exclude) : void 0;
8375
+ const files = Array.isArray(config?.files) ? rewriteFileList(config.files) : void 0;
8376
+ const withTypes = name === SPEC_TSCONFIG ? withShippedTypes(cwd, files ?? config?.files) : void 0;
8377
+ if (include === void 0 && exclude === void 0 && files === void 0 && withTypes === void 0)
8378
+ continue;
8379
+ operations.push({
8380
+ kind: "merge-json",
8381
+ path: name,
8382
+ value: {
8383
+ ...include === void 0 ? {} : { include },
8384
+ ...exclude === void 0 ? {} : { exclude },
8385
+ ...withTypes === void 0 ? files === void 0 ? {} : { files } : { files: withTypes }
8386
+ }
8387
+ });
8388
+ }
8389
+ return operations;
8390
+ }
8391
+ function rewriteFileList(entries) {
8392
+ const rewritten = entries.filter((entry) => typeof entry !== "string" || !JEST_CONFIG_ENTRY.test(entry)).map((entry) => typeof entry === "string" ? vitestSupportName(entry) : entry);
8393
+ const unchanged = rewritten.length === entries.length && rewritten.every((entry, index) => entry === entries[index]);
8394
+ return unchanged ? void 0 : rewritten;
8395
+ }
8396
+ function withShippedTypes(cwd, files) {
8397
+ const entries = Array.isArray(files) ? files : [];
8398
+ const missing = SHIPPED_TYPES.filter(
8399
+ (declaration) => !entries.some((entry) => entry.endsWith(declaration))
8400
+ );
8401
+ if (missing.length === 0) return void 0;
8402
+ return [...entries, ...missing.map((declaration) => shippedTypesPath(cwd, declaration))];
8403
+ }
8404
+ var SHIPPED_TYPES = ["types/mock-extended.d.ts", "types/jest-global.d.ts"];
8405
+ var JEST_CONFIG_ENTRY = /(^|\/)jest\.[\w.-]*config\.[cm]?[jt]s$/;
8406
+ var TSCONFIG_FILE = /^tsconfig[\w.-]*\.json$/;
8130
8407
  function siblingScriptRewrites(cwd) {
8131
8408
  const rewrites = {};
8132
8409
  for (const [name, command] of Object.entries(moduleScripts(cwd))) {
@@ -8173,8 +8450,9 @@ function planJestMigration(context, adoption) {
8173
8450
  `${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
8451
  );
8175
8452
  }
8176
- const targets = projectTargets(context.cwd) ?? {};
8177
- const existingFor = (name) => targets[name];
8453
+ const fromProject = projectTargets(context.cwd) ?? {};
8454
+ const fromManifest = manifestTargets(context.cwd);
8455
+ const existingFor = (name) => fromProject[name] ?? fromManifest[name];
8178
8456
  const siblings = siblingScriptRewrites(context.cwd);
8179
8457
  const specTypes = specTypesOperation(context.cwd);
8180
8458
  const scripts = Object.fromEntries(
@@ -8191,6 +8469,7 @@ function planJestMigration(context, adoption) {
8191
8469
  })),
8192
8470
  ...migration.operations,
8193
8471
  ...specTypes === void 0 ? [] : [specTypes],
8472
+ ...tsconfigFileListOperations(context.cwd),
8194
8473
  manifestOperation(context.cwd, { ...scripts, ...siblings }),
8195
8474
  ...prepared.flatMap(
8196
8475
  ({ mode }) => nxTargetOperations({
@@ -8208,18 +8487,26 @@ function planJestMigration(context, adoption) {
8208
8487
  (name) => ({ kind: "delete", path: name })
8209
8488
  )
8210
8489
  ];
8490
+ const escaping = (prepared[0]?.reading.aliases ?? []).map((alias) => alias.replacement).filter((replacement) => replacement.includes(".."));
8211
8491
  const notes = [
8212
8492
  `${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.`
8493
+ `${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
8494
  ];
8215
8495
  if (specTypes !== void 0) {
8216
8496
  notes.push(
8217
8497
  `${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
8498
  );
8219
8499
  }
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
- );
8500
+ const formatter = writtenFileFormatter(context.cwd);
8501
+ if (formatter !== void 0) {
8502
+ notes.push(
8503
+ `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.`
8504
+ );
8505
+ } else {
8506
+ notes.push(
8507
+ `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.`
8508
+ );
8509
+ }
8223
8510
  notes.push(
8224
8511
  `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
8512
  );
@@ -8308,7 +8595,7 @@ function plan3(context) {
8308
8595
  `${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
8596
  );
8310
8597
  }
8311
- const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
8598
+ const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME] ?? manifestTargets(context.cwd)[TEST_SCRIPT_NAME];
8312
8599
  const outputs = existing?.outputs ?? [];
8313
8600
  const dependsOn = existing?.dependsOn ?? [];
8314
8601
  const configurations = existing?.configurations ?? {};
@@ -8363,6 +8650,329 @@ function plan3(context) {
8363
8650
  return { operations, notes };
8364
8651
  }
8365
8652
 
8653
+ // src/roles/test/reference.ts
8654
+ import { execFileSync } from "node:child_process";
8655
+ import { createHash } from "node:crypto";
8656
+ import { existsSync as existsSync31, mkdirSync, mkdtempSync, readFileSync as readFileSync29, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
8657
+ import { tmpdir } from "node:os";
8658
+ import { join as join37, resolve as resolve5 } from "node:path";
8659
+
8660
+ // src/roles/test/baseline.ts
8661
+ function readSnapshot(report) {
8662
+ const files = report?.testResults ?? [];
8663
+ const passing = [];
8664
+ const failing = [];
8665
+ const unloadable = [];
8666
+ for (const file of files) {
8667
+ const assertions = file?.assertionResults ?? [];
8668
+ if (file?.status === "failed" && assertions.length === 0) {
8669
+ unloadable.push(typeof file.name === "string" ? file.name : "an unnamed file");
8670
+ }
8671
+ for (const assertion of assertions) {
8672
+ const name = assertion?.fullName;
8673
+ if (typeof name !== "string" || name === "") continue;
8674
+ if (assertion.status === "passed") passing.push(name);
8675
+ else if (assertion.status === "failed") failing.push(name);
8676
+ }
8677
+ }
8678
+ const claimed = report?.numFailedTests;
8679
+ return {
8680
+ files: files.length,
8681
+ passing: passing.sort(),
8682
+ failing: failing.sort(),
8683
+ unloadable: unloadable.sort(),
8684
+ claimedFailing: typeof claimed === "number" ? claimed : failing.length
8685
+ };
8686
+ }
8687
+ function difference(left, right) {
8688
+ const remaining = /* @__PURE__ */ new Map();
8689
+ for (const name of right) remaining.set(name, (remaining.get(name) ?? 0) + 1);
8690
+ const missing = [];
8691
+ for (const name of left) {
8692
+ const left_ = remaining.get(name) ?? 0;
8693
+ if (left_ === 0) {
8694
+ missing.push(name);
8695
+ continue;
8696
+ }
8697
+ remaining.set(name, left_ - 1);
8698
+ }
8699
+ return missing.sort();
8700
+ }
8701
+ var withoutRenderingQuotes = (name) => name.replaceAll(String.raw`\'`, "'").replaceAll(String.raw`\"`, '"').replaceAll(/['"`]\s*\+\s*['"`]/g, "").replaceAll(/(^|[\s:,[{(])(''|"")($|[\s,)\]}])/g, "$1<empty-string>$3").replaceAll(/['"`]/g, "");
8702
+ function withoutRenderingNoise(name) {
8703
+ const spelled = withSortedKeys(
8704
+ withoutUndefinedEntries(
8705
+ withoutErrorRendering(
8706
+ withoutNaN(
8707
+ withoutTimestamps(withoutRenderingQuotes(withoutClassBodies(withoutStackFrames(name))))
8708
+ )
8709
+ )
8710
+ )
8711
+ );
8712
+ return withoutArrayBrackets(
8713
+ 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, ",")
8714
+ );
8715
+ }
8716
+ function withoutArrayBrackets(name) {
8717
+ return name.replaceAll(
8718
+ /\[([^[\]]*)\]/g,
8719
+ (match, inner) => (
8720
+ // `[Object]` and `[Array]` are the formatter saying it STOPPED, not an array literal.
8721
+ // Stripping their brackets would erase the marker `collapsedOf` pairs on.
8722
+ /^(?:Object|Array)$/.test(inner) ? match : inner.replaceAll(/,\s+/g, ",")
8723
+ )
8724
+ );
8725
+ }
8726
+ var withoutClassBodies = (name) => name.replaceAll(
8727
+ /class\s+([A-Za-z_$][\w$]*)\s*\{\s*\}/g,
8728
+ (_match, className) => `class ${className}`
8729
+ ).replaceAll(
8730
+ /class\s+([A-Za-z_$][\w$]*)[\s\S]*?\n\}/g,
8731
+ (_match, className) => `class ${className}`
8732
+ );
8733
+ var withoutTimestamps = (name) => name.replaceAll(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, "<instant>");
8734
+ var withoutNaN = (name) => name.replaceAll(/\bNaN\b/g, "null");
8735
+ function entriesOf(inner) {
8736
+ const parts = [];
8737
+ let depth = 0;
8738
+ let start = 0;
8739
+ for (let index = 0; index < inner.length; index += 1) {
8740
+ const character = inner[index];
8741
+ if (character === "[" || character === "(") depth += 1;
8742
+ else if (character === "]" || character === ")") depth -= 1;
8743
+ else if (character === "," && depth === 0) {
8744
+ parts.push(inner.slice(start, index));
8745
+ start = index + 1;
8746
+ }
8747
+ if (depth < 0) return void 0;
8748
+ }
8749
+ if (depth !== 0) return void 0;
8750
+ parts.push(inner.slice(start));
8751
+ return parts.map((part) => part.trim()).filter(Boolean);
8752
+ }
8753
+ var sortedGroup = (match, inner) => {
8754
+ if (!inner.includes(":")) return match;
8755
+ const parts = entriesOf(inner);
8756
+ if (parts === void 0) return match;
8757
+ if (parts.length < 2 || !parts.every((part) => part.includes(":"))) return match;
8758
+ return `{${[...parts].sort().join(",")}}`;
8759
+ };
8760
+ var HELD = "\u241F";
8761
+ var withSortedKeys = (name) => {
8762
+ const held = [];
8763
+ let current = name;
8764
+ for (let depth = 0; depth < 24; depth += 1) {
8765
+ const next = current.replaceAll(/\{([^{}]*)\}/g, (match, inner) => {
8766
+ held.push(sortedGroup(match, inner));
8767
+ return `${HELD}${held.length - 1}${HELD}`;
8768
+ });
8769
+ if (next === current) break;
8770
+ current = next;
8771
+ }
8772
+ for (let depth = 0; depth < 24 && current.includes(HELD); depth += 1) {
8773
+ current = current.replaceAll(
8774
+ new RegExp(`${HELD}(\\d+)${HELD}`, "g"),
8775
+ (_match, index) => held[Number(index)]
8776
+ );
8777
+ }
8778
+ return current;
8779
+ };
8780
+ var withoutStackFrames = (name) => {
8781
+ const frame = name.search(/\n\s+at\s/);
8782
+ return frame === -1 ? name : name.slice(0, frame);
8783
+ };
8784
+ var withoutErrorRendering = (name) => name.replaceAll(/\[[A-Za-z]*(?:Error|Exception):[^\][]*\]/g, "{}");
8785
+ 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, "{}");
8786
+ function collapsedOf(candidate, name) {
8787
+ if (!/\[(?:Object|Array)\]/.test(candidate)) return false;
8788
+ const pattern = candidate.split(/\[(?:Object|Array)\]/).map((part) => part.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
8789
+ return new RegExp(`^${pattern}$`, "s").test(name);
8790
+ }
8791
+ function truncationOf(candidate, name) {
8792
+ const cut = candidate.indexOf("\u2026");
8793
+ if (cut === -1) return false;
8794
+ const head = candidate.slice(0, cut);
8795
+ const tail = candidate.slice(cut + 1);
8796
+ return name.startsWith(head) && name.endsWith(tail);
8797
+ }
8798
+ function pairRenamed(missing, added) {
8799
+ const remaining = [...added];
8800
+ const renamed = [];
8801
+ const unmatched = [];
8802
+ for (const name of missing) {
8803
+ const normalised = withoutRenderingNoise(name);
8804
+ const at = remaining.findIndex((candidate) => withoutRenderingNoise(candidate) === normalised);
8805
+ if (at === -1) {
8806
+ unmatched.push(name);
8807
+ continue;
8808
+ }
8809
+ renamed.push({ before: name, after: remaining[at] });
8810
+ remaining.splice(at, 1);
8811
+ }
8812
+ const lost = [];
8813
+ for (const name of unmatched) {
8814
+ const normalised = withoutRenderingNoise(name);
8815
+ const at = remaining.findIndex(
8816
+ (candidate) => truncationOf(withoutRenderingNoise(candidate), normalised) || collapsedOf(normalised, withoutRenderingNoise(candidate))
8817
+ );
8818
+ if (at === -1) {
8819
+ lost.push(name);
8820
+ continue;
8821
+ }
8822
+ renamed.push({ before: name, after: remaining[at] });
8823
+ remaining.splice(at, 1);
8824
+ }
8825
+ return { renamed, missing: lost, added: remaining };
8826
+ }
8827
+ function compareSnapshots(before, after) {
8828
+ const beforeAll = [...before.passing, ...before.failing];
8829
+ const afterAll = [...after.passing, ...after.failing];
8830
+ const paired = pairRenamed(difference(beforeAll, afterAll), difference(afterAll, beforeAll));
8831
+ const renamedBefore = new Set(paired.renamed.map((pair) => pair.before));
8832
+ const regressed = difference(before.passing, after.passing).filter(
8833
+ (name) => !paired.missing.includes(name) && !renamedBefore.has(name)
8834
+ );
8835
+ const failedBefore = new Set(before.failing);
8836
+ const recovered = after.passing.filter((name) => failedBefore.has(name));
8837
+ return {
8838
+ identical: paired.missing.length === 0 && paired.added.length === 0 && regressed.length === 0,
8839
+ missing: paired.missing,
8840
+ added: paired.added,
8841
+ regressed,
8842
+ renamed: paired.renamed,
8843
+ recovered,
8844
+ alreadyFailing: before.failing.length
8845
+ };
8846
+ }
8847
+ function unusableAsBaseline(snapshot) {
8848
+ if (snapshot.unloadable.length > 0) {
8849
+ 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`;
8850
+ }
8851
+ if (snapshot.claimedFailing > snapshot.failing.length) {
8852
+ 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`;
8853
+ }
8854
+ if (snapshot.passing.length === 0) {
8855
+ return `the reference run reports no passing test at all, so a green run after would prove nothing: there is no claim to preserve`;
8856
+ }
8857
+ return void 0;
8858
+ }
8859
+ function renamedExample(before, after) {
8860
+ let shared = 0;
8861
+ while (shared < before.length && shared < after.length && before[shared] === after[shared]) {
8862
+ shared += 1;
8863
+ }
8864
+ const boundary = before.lastIndexOf(" ", shared);
8865
+ const from = boundary > 0 ? boundary + 1 : 0;
8866
+ const ellipsis = from > 0 ? "\u2026" : "";
8867
+ return `"${ellipsis}${before.slice(from)}" -> "${ellipsis}${after.slice(from)}"`;
8868
+ }
8869
+ function describeComparison(comparison) {
8870
+ const firstRename = comparison.renamed[0];
8871
+ 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)}).`;
8872
+ 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.` : "";
8873
+ const recoveredNote = comparison.recovered.length > 0 ? ` ${comparison.recovered.length} test(s) were failing before and pass now.` : "";
8874
+ if (comparison.identical) {
8875
+ return `every test that ran before runs and passes now.${recoveredNote}${renamedNote}${alreadyRedNote}`;
8876
+ }
8877
+ const parts = [];
8878
+ if (comparison.recovered.length > 0) {
8879
+ parts.push(`${comparison.recovered.length} test(s) were failing before and pass now`);
8880
+ }
8881
+ if (comparison.missing.length > 0) {
8882
+ parts.push(
8883
+ `${comparison.missing.length} test(s) no longer exist: ${comparison.missing.slice(0, 3).join(", ")}`
8884
+ );
8885
+ }
8886
+ if (comparison.added.length > 0) {
8887
+ parts.push(
8888
+ `${comparison.added.length} test(s) appeared, which a migration should not do: ${comparison.added.slice(0, 3).join(", ")}`
8889
+ );
8890
+ }
8891
+ if (comparison.regressed.length > 0) {
8892
+ parts.push(
8893
+ `${comparison.regressed.length} test(s) passed before and do not now: ${comparison.regressed.slice(0, 3).join(", ")}`
8894
+ );
8895
+ }
8896
+ return `${parts.join("; ")}.${renamedNote}${alreadyRedNote}`;
8897
+ }
8898
+
8899
+ // src/roles/test/reference.ts
8900
+ function referencePath(cwd) {
8901
+ const key = createHash("sha256").update(resolve5(cwd)).digest("hex").slice(0, 16);
8902
+ return join37(tmpdir(), "sentinel-test-reference", `${key}.json`);
8903
+ }
8904
+ function runJest(cwd, config) {
8905
+ const bin = resolveBin(cwd, "jest");
8906
+ if (bin === void 0) return void 0;
8907
+ const root = findWorkspaceRoot(cwd) ?? cwd;
8908
+ const out = join37(mkdtempSync(join37(tmpdir(), "sentinel-reference-")), "jest.json");
8909
+ try {
8910
+ execFileSync(bin, ["-c", join37(cwd, config), "--json", `--outputFile=${out}`, "--silent"], {
8911
+ cwd: root,
8912
+ stdio: ["ignore", "ignore", "ignore"],
8913
+ maxBuffer: 1 << 28
8914
+ });
8915
+ } catch {
8916
+ }
8917
+ if (!existsSync31(out)) return void 0;
8918
+ try {
8919
+ return JSON.parse(readFileSync29(out, "utf8"));
8920
+ } catch {
8921
+ return void 0;
8922
+ }
8923
+ }
8924
+ function captureReference(cwd, config) {
8925
+ const report = runJest(cwd, config);
8926
+ if (report === void 0) {
8927
+ return {
8928
+ captured: false,
8929
+ 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.`
8930
+ };
8931
+ }
8932
+ const snapshot = readSnapshot(report);
8933
+ const unusable = unusableAsBaseline(snapshot);
8934
+ if (unusable !== void 0) {
8935
+ return {
8936
+ captured: false,
8937
+ message: `no reference recorded: ${unusable} The migration is applied anyway, unproved.`
8938
+ };
8939
+ }
8940
+ const stored = {
8941
+ runner: "jest",
8942
+ config,
8943
+ takenAt: (/* @__PURE__ */ new Date()).toISOString(),
8944
+ snapshot
8945
+ };
8946
+ const path = referencePath(cwd);
8947
+ mkdirSync(join37(path, ".."), { recursive: true });
8948
+ writeFileSync3(path, `${JSON.stringify(stored, void 0, 2)}
8949
+ `);
8950
+ return {
8951
+ captured: true,
8952
+ message: `recorded ${snapshot.passing.length} passing test name(s) from ${config} as the reference. The next \`sentinel --run --test\` compares against it.`
8953
+ };
8954
+ }
8955
+ function hasReference(cwd) {
8956
+ return existsSync31(referencePath(cwd));
8957
+ }
8958
+ function compareAgainstReference(cwd, report) {
8959
+ const path = referencePath(cwd);
8960
+ let stored;
8961
+ try {
8962
+ stored = JSON.parse(readFileSync29(path, "utf8"));
8963
+ } catch {
8964
+ rmSync3(path, { force: true });
8965
+ return { ok: true, message: `the recorded reference could not be read, so nothing was proved.` };
8966
+ }
8967
+ const comparison = compareSnapshots(stored.snapshot, readSnapshot(report));
8968
+ if (comparison.identical) rmSync3(path, { force: true });
8969
+ 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.`;
8970
+ return {
8971
+ ok: comparison.identical,
8972
+ message: `${describeComparison(comparison)} (reference: ${stored.config}, ${stored.takenAt})${kept}`
8973
+ };
8974
+ }
8975
+
8366
8976
  // src/roles/test/adapters/vitest/vitest.adapter.ts
8367
8977
  function resolveVitest(cwd) {
8368
8978
  const fromSentinel = binFromOwnInstall("vitest", "vitest");
@@ -8370,12 +8980,102 @@ function resolveVitest(cwd) {
8370
8980
  const fromModule = resolveBin(cwd, "vitest");
8371
8981
  return fromModule !== void 0 ? { bin: fromModule, origin: "module" } : { bin: void 0, origin: "module" };
8372
8982
  }
8983
+ var FORMATTABLE = /\.(m|c)?(j|t)sx?$|\.json$/;
8984
+ var FORMAT_BATCH = 25;
8373
8985
  var VitestAdapter = class extends BaseAdapter {
8374
8986
  target = "test";
8375
8987
  runner = "vitest";
8376
8988
  appliesTo(_preset) {
8377
8989
  return true;
8378
8990
  }
8991
+ /**
8992
+ * Record what this module's tests do TODAY, at the last moment it is still true.
8993
+ *
8994
+ * The plan about to be applied deletes the jest config, and with it any way of asking again. A
8995
+ * migration that cannot be compared to what came before is a migration nobody can check, which
8996
+ * is the whole reason this role has a baseline gate at all.
8997
+ */
8998
+ async beforeInit(ctx) {
8999
+ const adoption = readTestAdoption(ctx.cwd);
9000
+ if (adoption.state !== "jest") return Promise.resolve();
9001
+ const [config] = adoption.jestConfigs;
9002
+ if (config === void 0) return Promise.resolve();
9003
+ this.say(`recording the jest reference before migrating...`);
9004
+ this.say(captureReference(ctx.cwd, config).message);
9005
+ return Promise.resolve();
9006
+ }
9007
+ /**
9008
+ * Hand what was just written to the module's OWN formatter.
9009
+ *
9010
+ * ⚠️ Not a style sentinel imposes: the module adopted `.oxfmtrc.json`, and this runs that
9011
+ * config over the files this role created. Without it, `--init --test` leaves a module whose
9012
+ * own `--run --format` fails on a file the module never wrote.
9013
+ *
9014
+ * Measured on `apps/nest/microservices/agency`, in this repository's CI:
9015
+ *
9016
+ * agency:format 12 files, including vitest.config.mts and
9017
+ * vitest.integration.config.mts, both generated here
9018
+ *
9019
+ * The generator cannot avoid it by wrapping at a fixed width, because the width is the
9020
+ * MODULE's decision: `printWidth` lives in its own config, and 12 modules already set their
9021
+ * own. Guessing it is how a tool starts owning a setting it does not own.
9022
+ *
9023
+ * The same delegation the build role asks a human to do by hand, done.
9024
+ *
9025
+ * ## ⚠️ Two boundaries, both learned by crossing them
9026
+ *
9027
+ * ### Only a module that has a formatter CONFIG
9028
+ *
9029
+ * The question is not "can a formatter be found". It always can: sentinel depends on oxfmt and
9030
+ * ships the binary, so `resolveOxfmt` falls back to its own install and never returns undefined
9031
+ * once sentinel is installed. Asking it that way meant a module with no oxfmt config was
9032
+ * formatted with oxfmt's DEFAULTS, which is a style nobody here chose.
9033
+ *
9034
+ * So the CONFIG is read. A module that wrote its own `.oxfmtrc.json` without adopting the role
9035
+ * has still stated a style, and running it is delegation; running oxfmt with no config is us
9036
+ * choosing one.
9037
+ *
9038
+ * Measured on `apps/nest/microservices/institution`, which has not adopted format and whose lint
9039
+ * forbids semicolons: `institution:lint` went from green to failing, thousands of `semi`, on a
9040
+ * module whose 2958 tests pass. So the adoption is read, not the binary.
9041
+ *
9042
+ * ### Only the files this just WROTE
9043
+ *
9044
+ * `--write .` is a whole-directory pass, and that is not a tidy-up. Same measurement: 1813 files
9045
+ * changed, 47333 insertions, 55506 deletions, in a module whose migration touched 268. A
9046
+ * migration diff is read by a person.
9047
+ *
9048
+ * A module that has not adopted format keeps what was written exactly as written, and is told so.
9049
+ * The codemod rewrites tokens in place, so those files keep the shape they already had.
9050
+ */
9051
+ async afterInit(ctx) {
9052
+ const written = (ctx.written ?? []).filter((file) => FORMATTABLE.test(file));
9053
+ if (written.length === 0) return Promise.resolve();
9054
+ const formatter = writtenFileFormatter(ctx.cwd);
9055
+ if (formatter === void 0) {
9056
+ this.say(
9057
+ `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.`
9058
+ );
9059
+ return Promise.resolve();
9060
+ }
9061
+ let failed = 0;
9062
+ for (let at = 0; at < written.length; at += FORMAT_BATCH) {
9063
+ const batch = written.slice(at, at + FORMAT_BATCH);
9064
+ const pass = spawnSync5(formatter.bin, [...formatter.args, ...batch], {
9065
+ cwd: ctx.cwd,
9066
+ encoding: "utf8"
9067
+ });
9068
+ if (pass.status !== 0) failed += batch.length;
9069
+ }
9070
+ this.say(
9071
+ 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.` : (
9072
+ // Adoption SUCCEEDED; only the tidy-up did not. Failing here would revert a migration
9073
+ // over a formatting pass, which is the wrong trade.
9074
+ `${formatter.origin} could not run over ${failed} of the ${written.length} file(s) this wrote. Adoption itself is applied; run your formatter before committing.`
9075
+ )
9076
+ );
9077
+ return Promise.resolve();
9078
+ }
8379
9079
  plan(context) {
8380
9080
  return plan3(context);
8381
9081
  }
@@ -8420,15 +9120,36 @@ var VitestAdapter = class extends BaseAdapter {
8420
9120
  `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
9121
  );
8422
9122
  }
8423
- const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
9123
+ const { options: typedOptions, paths: typedPaths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
9124
+ valueFlags: ["--config", "-c"]
9125
+ });
9126
+ const { cwd: runFrom, options, paths } = fromWorkspaceRoot(ctx.cwd, typedOptions, typedPaths);
8424
9127
  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" });
9128
+ const proving = !watching && hasReference(ctx.cwd);
9129
+ const reportFile = proving ? join38(mkdtempSync2(join38(tmpdir2(), "sentinel-run-")), "vitest.json") : void 0;
9130
+ const reporters = reportFile === void 0 ? [] : ["--reporter=default", "--reporter=json", `--outputFile=${reportFile}`];
9131
+ const argv = watching ? [...options, ...paths] : ["run", ...reporters, ...options, ...paths];
9132
+ const result = spawnSync5(bin, argv, { cwd: runFrom, stdio: "inherit" });
8427
9133
  if (result.error) {
8428
9134
  this.say(`could not run vitest (${result.error.message})`);
8429
9135
  return Promise.resolve({ ok: false, code: 1 });
8430
9136
  }
8431
9137
  const code = result.status ?? 1;
9138
+ if (reportFile !== void 0) {
9139
+ let report;
9140
+ try {
9141
+ report = JSON.parse(readFileSync30(reportFile, "utf8"));
9142
+ } catch {
9143
+ report = void 0;
9144
+ }
9145
+ if (report === void 0) {
9146
+ this.say(`vitest wrote no report, so this run proved nothing against the reference.`);
9147
+ } else {
9148
+ const outcome = compareAgainstReference(ctx.cwd, report);
9149
+ this.say(outcome.message);
9150
+ if (!outcome.ok) return Promise.resolve({ ok: false, code: code === 0 ? 1 : code });
9151
+ }
9152
+ }
8432
9153
  return Promise.resolve({ ok: code === 0, code });
8433
9154
  }
8434
9155
  say(message) {
@@ -8436,6 +9157,31 @@ var VitestAdapter = class extends BaseAdapter {
8436
9157
  `);
8437
9158
  }
8438
9159
  };
9160
+ function fromWorkspaceRoot(moduleDir, options, paths) {
9161
+ const root = findWorkspaceRoot(moduleDir);
9162
+ if (root === void 0 || root === moduleDir) {
9163
+ return { cwd: moduleDir, options: [...options], paths: [...paths] };
9164
+ }
9165
+ const absolute = (value) => isAbsolute3(value) ? value : resolve6(moduleDir, value);
9166
+ const moved = [];
9167
+ let previousWasConfig = false;
9168
+ for (const option of options) {
9169
+ if (previousWasConfig) {
9170
+ moved.push(absolute(option));
9171
+ previousWasConfig = false;
9172
+ continue;
9173
+ }
9174
+ if (option.startsWith("--config=")) moved.push(`--config=${absolute(option.slice(9))}`);
9175
+ else moved.push(option);
9176
+ previousWasConfig = option === "--config" || option === "-c";
9177
+ }
9178
+ return {
9179
+ cwd: root,
9180
+ // `--root` LAST, so a caller cannot be silently overridden by our own flag.
9181
+ options: [...moved, "--root", moduleDir],
9182
+ paths: paths.map((path) => absolute(path))
9183
+ };
9184
+ }
8439
9185
 
8440
9186
  // src/roles/test/register.ts
8441
9187
  function registerTest() {
@@ -8444,10 +9190,10 @@ function registerTest() {
8444
9190
  }
8445
9191
 
8446
9192
  // 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";
9193
+ import { spawnSync as spawnSync6 } from "node:child_process";
9194
+ import { existsSync as existsSync32, readFileSync as readFileSync32 } from "node:fs";
9195
+ import { createRequire as createRequire7 } from "node:module";
9196
+ import { join as join40 } from "node:path";
8451
9197
 
8452
9198
  // src/roles/typescript/presets/nest.json
8453
9199
  var nest_default2 = {
@@ -8628,8 +9374,8 @@ function readTsconfigAdoption(cwd) {
8628
9374
  }
8629
9375
 
8630
9376
  // src/roles/typescript/adapters/tsc/plan.ts
8631
- import { readFileSync as readFileSync29 } from "fs";
8632
- import { join as join36 } from "path";
9377
+ import { readFileSync as readFileSync31 } from "node:fs";
9378
+ import { join as join39 } from "node:path";
8633
9379
 
8634
9380
  // src/roles/typescript/typecheck-script.ts
8635
9381
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -8722,7 +9468,7 @@ function planAdoption(context) {
8722
9468
  };
8723
9469
  }
8724
9470
  const existing = parseJsonc(
8725
- readFileSync29(join36(context.cwd, target.path), "utf8"),
9471
+ readFileSync31(join39(context.cwd, target.path), "utf8"),
8726
9472
  target.path
8727
9473
  );
8728
9474
  const extendsChain = composeExtends(existing.extends, preset);
@@ -8860,7 +9606,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8860
9606
  let chain;
8861
9607
  try {
8862
9608
  const parsed = parseJsonc(
8863
- readFileSync30(join37(cwd, target.path), "utf8"),
9609
+ readFileSync32(join40(cwd, target.path), "utf8"),
8864
9610
  target.path
8865
9611
  );
8866
9612
  chain = parsed.extends;
@@ -8873,7 +9619,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8873
9619
  );
8874
9620
  if (preset === void 0) return void 0;
8875
9621
  try {
8876
- createRequire6(join37(cwd, "noop.js")).resolve(preset);
9622
+ createRequire7(join40(cwd, "noop.js")).resolve(preset);
8877
9623
  return void 0;
8878
9624
  } catch {
8879
9625
  return preset;
@@ -8969,7 +9715,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8969
9715
  referencedProjects(cwd, config) {
8970
9716
  try {
8971
9717
  const parsed = parseJsonc(
8972
- readFileSync30(join37(cwd, config), "utf8"),
9718
+ readFileSync32(join40(cwd, config), "utf8"),
8973
9719
  config
8974
9720
  );
8975
9721
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -8985,7 +9731,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8985
9731
  * check.
8986
9732
  */
8987
9733
  typecheckTarget(cwd) {
8988
- if (existsSync29(join37(cwd, "tsconfig.json"))) return "tsconfig.json";
9734
+ if (existsSync32(join40(cwd, "tsconfig.json"))) return "tsconfig.json";
8989
9735
  const target = resolveTsconfigTarget(cwd);
8990
9736
  return target.reason === "none" ? null : target.path;
8991
9737
  }
@@ -9134,7 +9880,7 @@ function detectFramework(packageJson) {
9134
9880
  }
9135
9881
 
9136
9882
  // src/core/dispatch.ts
9137
- import { basename as basename3 } from "path";
9883
+ import { basename as basename3 } from "node:path";
9138
9884
 
9139
9885
  // src/shared/text.ts
9140
9886
  function ensureLines(current, lines) {
@@ -9190,8 +9936,8 @@ function replaceLines(current, replacements) {
9190
9936
  }
9191
9937
 
9192
9938
  // 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";
9939
+ import { existsSync as existsSync33, readFileSync as readFileSync33, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
9940
+ import { resolve as resolve7, sep } from "node:path";
9195
9941
  import { applyEdits as applyEdits3, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
9196
9942
 
9197
9943
  // src/shared/deep-merge.ts
@@ -9201,15 +9947,15 @@ function isPlainObject(value) {
9201
9947
 
9202
9948
  // src/core/apply-plan.ts
9203
9949
  function resolveWithinRoot(cwd, relativePath) {
9204
- const root = resolve5(cwd);
9205
- const absolutePath = resolve5(root, relativePath);
9950
+ const root = resolve7(cwd);
9951
+ const absolutePath = resolve7(root, relativePath);
9206
9952
  if (absolutePath !== root && !absolutePath.startsWith(root + sep)) {
9207
9953
  throw new Error(`Refusing to write outside the module root: "${relativePath}".`);
9208
9954
  }
9209
9955
  return absolutePath;
9210
9956
  }
9211
9957
  function readIfExists(absolutePath) {
9212
- return existsSync30(absolutePath) ? readFileSync31(absolutePath, "utf8") : void 0;
9958
+ return existsSync33(absolutePath) ? readFileSync33(absolutePath, "utf8") : void 0;
9213
9959
  }
9214
9960
  function* leaves(value, prefix = []) {
9215
9961
  for (const [key, keyValue] of Object.entries(value)) {
@@ -9311,14 +10057,14 @@ function preparePlan(cwd, plan4) {
9311
10057
  }
9312
10058
  function writeFileAtomic(absolutePath, contents) {
9313
10059
  const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
9314
- writeFileSync3(tempPath, contents);
10060
+ writeFileSync4(tempPath, contents);
9315
10061
  renameSync(tempPath, absolutePath);
9316
10062
  }
9317
10063
  function applyPlan(cwd, plan4) {
9318
10064
  const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
9319
10065
  for (const file of changed) {
9320
10066
  if (file.deleted) {
9321
- rmSync3(file.absolutePath, { force: true });
10067
+ rmSync4(file.absolutePath, { force: true });
9322
10068
  continue;
9323
10069
  }
9324
10070
  writeFileAtomic(file.absolutePath, file.after);
@@ -9327,8 +10073,8 @@ function applyPlan(cwd, plan4) {
9327
10073
  }
9328
10074
 
9329
10075
  // 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";
10076
+ import { existsSync as existsSync34, readdirSync as readdirSync9, readFileSync as readFileSync34 } from "node:fs";
10077
+ import { join as join41 } from "node:path";
9332
10078
  var PATH_SIGNALS = [
9333
10079
  {
9334
10080
  preset: "nest",
@@ -9356,18 +10102,18 @@ function sourceImports(cwd, pattern) {
9356
10102
  const dir = stack.pop();
9357
10103
  let entries;
9358
10104
  try {
9359
- entries = readdirSync8(dir, { withFileTypes: true });
10105
+ entries = readdirSync9(dir, { withFileTypes: true });
9360
10106
  } catch {
9361
10107
  continue;
9362
10108
  }
9363
10109
  for (const entry of entries) {
9364
10110
  if (entry.isDirectory()) {
9365
- if (!NOT_SCANNED.has(entry.name)) stack.push(join38(dir, entry.name));
10111
+ if (!NOT_SCANNED.has(entry.name)) stack.push(join41(dir, entry.name));
9366
10112
  continue;
9367
10113
  }
9368
10114
  if (!SOURCE_FILE2.test(entry.name)) continue;
9369
10115
  try {
9370
- if (pattern.test(readFileSync32(join38(dir, entry.name), "utf8"))) return true;
10116
+ if (pattern.test(readFileSync34(join41(dir, entry.name), "utf8"))) return true;
9371
10117
  } catch {
9372
10118
  continue;
9373
10119
  }
@@ -9376,11 +10122,11 @@ function sourceImports(cwd, pattern) {
9376
10122
  return false;
9377
10123
  }
9378
10124
  function dependencyNames(cwd) {
9379
- const path = join38(cwd, "package.json");
9380
- if (!existsSync31(path)) return [];
10125
+ const path = join41(cwd, "package.json");
10126
+ if (!existsSync34(path)) return [];
9381
10127
  try {
9382
10128
  const manifest = parseJsonc(
9383
- readFileSync32(path, "utf8"),
10129
+ readFileSync34(path, "utf8"),
9384
10130
  path
9385
10131
  );
9386
10132
  return [
@@ -9394,7 +10140,7 @@ function dependencyNames(cwd) {
9394
10140
  function declaresJsx(cwd) {
9395
10141
  let entries;
9396
10142
  try {
9397
- entries = readdirSync8(cwd).filter(
10143
+ entries = readdirSync9(cwd).filter(
9398
10144
  (name) => name.startsWith("tsconfig") && name.endsWith(".json")
9399
10145
  );
9400
10146
  } catch {
@@ -9403,7 +10149,7 @@ function declaresJsx(cwd) {
9403
10149
  for (const name of entries) {
9404
10150
  try {
9405
10151
  const config = parseJsonc(
9406
- readFileSync32(join38(cwd, name), "utf8"),
10152
+ readFileSync34(join41(cwd, name), "utf8"),
9407
10153
  name
9408
10154
  );
9409
10155
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -9548,9 +10294,20 @@ async function dispatch(opts) {
9548
10294
  if (opts.dryRun) {
9549
10295
  return previewPlan(opts, plan4);
9550
10296
  }
10297
+ if (adapter.beforeInit) {
10298
+ await adapter.beforeInit({
10299
+ module: basename3(opts.cwd),
10300
+ cwd: opts.cwd,
10301
+ preset,
10302
+ ci: false,
10303
+ fix: false
10304
+ });
10305
+ }
10306
+ const written = [];
9551
10307
  for (const change of applyPlan(opts.cwd, plan4)) {
9552
10308
  process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
9553
10309
  `);
10310
+ if (!change.deleted) written.push(change.path);
9554
10311
  }
9555
10312
  for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
9556
10313
  `);
@@ -9562,7 +10319,8 @@ async function dispatch(opts) {
9562
10319
  cwd: opts.cwd,
9563
10320
  preset,
9564
10321
  ci: false,
9565
- fix: true
10322
+ fix: true,
10323
+ written
9566
10324
  });
9567
10325
  }
9568
10326
  return 0;
@@ -9591,4 +10349,3 @@ export {
9591
10349
  detectFramework,
9592
10350
  dispatch
9593
10351
  };
9594
- //# sourceMappingURL=chunk-4UIZJ3TR.js.map