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

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.
@@ -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"));
@@ -5394,6 +5402,9 @@ function registerLint() {
5394
5402
 
5395
5403
  // src/roles/test/adapters/vitest/vitest.adapter.ts
5396
5404
  import { spawnSync as spawnSync5 } from "child_process";
5405
+ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync30 } from "fs";
5406
+ import { tmpdir as tmpdir2 } from "os";
5407
+ import { join as join37 } from "path";
5397
5408
 
5398
5409
  // src/roles/test/presets/toolchain.json
5399
5410
  var toolchain_default2 = {
@@ -5470,7 +5481,7 @@ function testTarget(options) {
5470
5481
  }
5471
5482
 
5472
5483
  // src/roles/test/jest-migration.ts
5473
- import { existsSync as existsSync28, readFileSync as readFileSync28 } from "fs";
5484
+ import { existsSync as existsSync29, readFileSync as readFileSync28 } from "fs";
5474
5485
  import { join as join35 } from "path";
5475
5486
 
5476
5487
  // src/roles/test/codemod/migrate-module.ts
@@ -6581,12 +6592,47 @@ function applyNamedRules(source, program, fileName = "") {
6581
6592
  const applied = /* @__PURE__ */ new Set();
6582
6593
  const asyncify = /* @__PURE__ */ new Set();
6583
6594
  const mocked = mockedSpecifiers(program);
6584
- const wholeStatement = /* @__PURE__ */ new Set();
6595
+ const wholeStatement = /* @__PURE__ */ new Map();
6585
6596
  walk(program, (node) => {
6586
6597
  if (node.type !== "ExpressionStatement") return;
6587
6598
  const expression = node.expression;
6588
- if (expression !== void 0) wholeStatement.add(expression);
6599
+ if (expression !== void 0) wholeStatement.set(expression, node);
6600
+ });
6601
+ const standsAlone = /* @__PURE__ */ new Set();
6602
+ const alone = (node) => {
6603
+ if (node !== void 0) standsAlone.add(node);
6604
+ };
6605
+ walk(program, (node) => {
6606
+ switch (node.type) {
6607
+ case "VariableDeclarator":
6608
+ return alone(node.init);
6609
+ case "AssignmentExpression":
6610
+ return alone(node.right);
6611
+ case "ReturnStatement":
6612
+ case "ExportDefaultDeclaration":
6613
+ return alone(node.argument ?? node.declaration);
6614
+ case "ArrowFunctionExpression":
6615
+ return node.expression === true ? alone(node.body) : void 0;
6616
+ case "ConditionalExpression":
6617
+ alone(node.consequent);
6618
+ return alone(node.alternate);
6619
+ case "ArrayExpression":
6620
+ return (node.elements ?? []).forEach((e) => alone(e ?? void 0));
6621
+ case "Property":
6622
+ case "ObjectProperty":
6623
+ return node.shorthand === true ? void 0 : alone(node.value);
6624
+ case "CallExpression":
6625
+ case "NewExpression":
6626
+ return (node.arguments ?? []).forEach((a) => alone(a));
6627
+ default:
6628
+ return;
6629
+ }
6589
6630
  });
6631
+ const placed = (node, expression, trailing = "") => {
6632
+ if (trailing !== "") return `(${expression})${trailing}`;
6633
+ if (wholeStatement.has(node)) return expression;
6634
+ return standsAlone.has(node) ? expression : `(${expression})`;
6635
+ };
6590
6636
  const handedToTheRunnerAsAModule = /* @__PURE__ */ new Set();
6591
6637
  walk(program, (node) => {
6592
6638
  const returned = mockFactoryReturn(node);
@@ -6616,7 +6662,7 @@ function applyNamedRules(source, program, fileName = "") {
6616
6662
  edits.push({
6617
6663
  start: node.start,
6618
6664
  end: node.end,
6619
- text: `(await import(${argumentText(source, node)}))`
6665
+ text: placed(node, `await import(${argumentText(source, node)})`)
6620
6666
  });
6621
6667
  applied.add("jest.requireMock");
6622
6668
  if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
@@ -6628,7 +6674,7 @@ function applyNamedRules(source, program, fileName = "") {
6628
6674
  edits.push({
6629
6675
  start: node.start,
6630
6676
  end: node.end,
6631
- text: `(await vi.${asyncReplacement}(${argumentText(source, node)}))`
6677
+ text: placed(node, `await vi.${asyncReplacement}(${argumentText(source, node)})`)
6632
6678
  });
6633
6679
  applied.add(`jest.${member}`);
6634
6680
  if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
@@ -6641,7 +6687,7 @@ function applyNamedRules(source, program, fileName = "") {
6641
6687
  edits.push({
6642
6688
  start: node.start,
6643
6689
  end: node.end,
6644
- text: wholeStatement.has(node) ? `await import(${argumentText(source, node)});` : `(await import(${argumentText(source, node)}))${interop}`
6690
+ text: placed(node, `await import(${argumentText(source, node)})`, interop)
6645
6691
  });
6646
6692
  applied.add("require");
6647
6693
  if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
@@ -7161,11 +7207,6 @@ ${text}` });
7161
7207
  import { parseSync as parseSync6 } from "oxc-parser";
7162
7208
  var SENTINEL_MSW_SERVER = "@hublo/sentinel/test/msw";
7163
7209
  var MSW_SERVER_MODULE = /(^|\/)msw\/server$/;
7164
- var MSW_PACKAGE = /^msw$/;
7165
- function importsMswPackage(declaration) {
7166
- const source = declaration.source?.value;
7167
- return source !== void 0 && MSW_PACKAGE.test(source);
7168
- }
7169
7210
  function importsServer(declaration) {
7170
7211
  return (declaration.specifiers ?? []).some((specifier) => {
7171
7212
  const imported = specifier.imported;
@@ -7181,8 +7222,7 @@ function repointMswServer(fileName, source) {
7181
7222
  if (node.type !== "ImportDeclaration") return;
7182
7223
  const specifier = node.source;
7183
7224
  if (specifier?.value === void 0) return;
7184
- const isServerModule = MSW_SERVER_MODULE.test(specifier.value) && importsServer(node);
7185
- if (!isServerModule && !importsMswPackage(node)) return;
7225
+ if (!MSW_SERVER_MODULE.test(specifier.value) || !importsServer(node)) return;
7186
7226
  edits.push({ start: specifier.start, end: specifier.end });
7187
7227
  });
7188
7228
  if (edits.length === 0) return { source, repointed: false };
@@ -7570,13 +7610,16 @@ function migrateModuleTests(cwd, setupFiles = []) {
7570
7610
  }
7571
7611
 
7572
7612
  // src/roles/test/nest/generate-config.ts
7573
- import { readdirSync as readdirSync7, readFileSync as readFileSync26, statSync as statSync7 } from "fs";
7613
+ import { existsSync as existsSync26, readdirSync as readdirSync7, readFileSync as readFileSync26, statSync as statSync7 } from "fs";
7574
7614
  import { join as join32, relative as relative6 } from "path";
7575
7615
  var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
7576
- var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/nest";
7616
+ var REACT_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
7617
+ function flavourOf(reading) {
7618
+ return reading.testEnvironment === "jsdom" ? "react" : "nest";
7619
+ }
7620
+ var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/jest-parity";
7577
7621
  var SENTINEL_MSW_SETUP = "@hublo/sentinel/test/setup/msw-lifecycle";
7578
7622
  var SENTINEL_WORKSPACE_SETUP = "@hublo/sentinel/test/setup/workspace";
7579
- var NEST_TEST_CONFIG_FILE = "vitest.config.mts";
7580
7623
  function hopsToRoot(cwd, workspaceRoot) {
7581
7624
  const up = relative6(cwd, workspaceRoot).replaceAll("\\", "/");
7582
7625
  return up === "" ? "." : up;
@@ -7643,14 +7686,29 @@ function readModuleTraits(cwd) {
7643
7686
  return traits;
7644
7687
  }
7645
7688
  var withoutRootDir = (value) => value.replaceAll("<rootDir>/", "./").replaceAll("<rootDir>", ".");
7646
- function setupBlock(reading, msw) {
7689
+ function ownsMswLifecycle(cwd, reading) {
7690
+ const declared = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []].filter(
7691
+ (file) => !file.endsWith("jest.setup.after.env.js")
7692
+ );
7693
+ for (const raw of declared) {
7694
+ const file = join32(cwd, raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", "."));
7695
+ if (!existsSync26(file)) continue;
7696
+ try {
7697
+ if (/\.listen\s*\(/.test(readFileSync26(file, "utf8"))) return true;
7698
+ } catch {
7699
+ continue;
7700
+ }
7701
+ }
7702
+ return false;
7703
+ }
7704
+ function setupBlock(cwd, reading, msw) {
7647
7705
  const declared = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []];
7648
7706
  const namedTheRootSetup = declared.some((file) => file.endsWith("jest.setup.after.env.js"));
7649
7707
  const own = declared.filter((file) => !file.endsWith("jest.setup.after.env.js")).map((file) => `'${withoutRootDir(file)}'`);
7650
7708
  const sentinel = [
7651
7709
  `'${SENTINEL_TEST_SETUP}'`,
7652
7710
  ...namedTheRootSetup ? [`'${SENTINEL_WORKSPACE_SETUP}'`] : [],
7653
- ...msw ? [`'${SENTINEL_MSW_SETUP}'`] : []
7711
+ ...msw && !ownsMswLifecycle(cwd, reading) ? [`'${SENTINEL_MSW_SETUP}'`] : []
7654
7712
  ];
7655
7713
  return ` setupFiles: [${[...sentinel, ...own].join(", ")}],`;
7656
7714
  }
@@ -7697,7 +7755,8 @@ function aliasBlock(reading) {
7697
7755
  if (reading.aliases === void 0 || reading.aliases.length === 0) return "";
7698
7756
  const entries = reading.aliases.map((alias) => {
7699
7757
  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)}')`;
7758
+ const deadToken = /<rootdir>/i.test(alias.replacement) && !alias.replacement.includes("<rootDir>");
7759
+ const target = isPackage(alias.replacement) ? `'${alias.replacement}'` : deadToken ? `'${alias.replacement}'` : `path.resolve(import.meta.dirname, '${withoutRootDir(alias.replacement)}')`;
7701
7760
  return ` { find: /${pattern}/, replacement: ${target} },`;
7702
7761
  });
7703
7762
  return [" resolve: {", " alias: [", ...entries, " ],", " },"].join("\n");
@@ -7712,7 +7771,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
7712
7771
  const up = hopsToRoot(cwd, workspaceRoot);
7713
7772
  const test = [
7714
7773
  environmentBlock(reading),
7715
- setupBlock(reading, traits.msw),
7774
+ setupBlock(cwd, reading, traits.msw),
7716
7775
  envBlock(reading),
7717
7776
  timeoutBlock(reading),
7718
7777
  concurrencyBlock(cwd, reading, target),
@@ -7727,7 +7786,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
7727
7786
  `import path from 'node:path'`,
7728
7787
  "",
7729
7788
  ...needsDefaults ? [`import { configDefaults } from 'vitest/config'`, ""] : [],
7730
- `import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
7789
+ flavourOf(reading) === "react" ? `import { defineConfig, reactTestConfig } from '${REACT_TEST_PRESET_SPECIFIER}'` : `import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
7731
7790
  ].join("\n");
7732
7791
  const overrides = [aliasBlock(reading), " test: {", ...test, " },"].filter(
7733
7792
  (line) => line !== ""
@@ -7738,7 +7797,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
7738
7797
  source: `${imports}
7739
7798
 
7740
7799
  export default defineConfig(
7741
- nestTestConfig({
7800
+ ${flavourOf(reading) === "react" ? "reactTestConfig" : "nestTestConfig"}({
7742
7801
  root: import.meta.dirname,
7743
7802
  workspaceRoot: path.resolve(import.meta.dirname, '${up}'),${decoratorLine}
7744
7803
  overrides: {
@@ -7751,7 +7810,7 @@ ${overrides.join("\n")}
7751
7810
  }
7752
7811
 
7753
7812
  // src/roles/test/nest/read-jest-config.ts
7754
- import { existsSync as existsSync26, readFileSync as readFileSync27 } from "fs";
7813
+ import { existsSync as existsSync27, readFileSync as readFileSync27 } from "fs";
7755
7814
  import { join as join33 } from "path";
7756
7815
  var CARRIED = [
7757
7816
  "testEnvironment",
@@ -7942,7 +8001,7 @@ function extendedConfig(source, cwd) {
7942
8001
  (name) => name.replace(/^\.\//, "")
7943
8002
  );
7944
8003
  const file = candidates.find(
7945
- (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync26(join33(cwd, name))
8004
+ (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync27(join33(cwd, name))
7946
8005
  );
7947
8006
  if (file === void 0) continue;
7948
8007
  if (!new RegExp(String.raw`\{\s*\.\.\.${binding}\b`).test(source)) continue;
@@ -7969,8 +8028,8 @@ function sideEffects(source, extended) {
7969
8028
  return found;
7970
8029
  }
7971
8030
  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;
8031
+ const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync27(join33(cwd, name)));
8032
+ if (file === void 0 || !existsSync27(join33(cwd, file))) return void 0;
7974
8033
  let source;
7975
8034
  try {
7976
8035
  source = readFileSync27(join33(cwd, file), "utf8");
@@ -8054,7 +8113,7 @@ function readJestConfig(cwd, named) {
8054
8113
  }
8055
8114
 
8056
8115
  // src/roles/test/nest/run-modes.ts
8057
- import { existsSync as existsSync27 } from "fs";
8116
+ import { existsSync as existsSync28 } from "fs";
8058
8117
  import { basename as basename2, join as join34 } from "path";
8059
8118
  var JEST_EXECUTOR = "@nx/jest:jest";
8060
8119
  var PRIMARY_CONFIG_FILE = "vitest.config.mts";
@@ -8076,7 +8135,7 @@ function runModes(cwd, fallback) {
8076
8135
  const jestConfig = entry.options?.jestConfig;
8077
8136
  if (jestConfig === void 0) continue;
8078
8137
  const name = configName(jestConfig);
8079
- if (!existsSync27(join34(cwd, name))) continue;
8138
+ if (!existsSync28(join34(cwd, name))) continue;
8080
8139
  declared.push({
8081
8140
  target,
8082
8141
  jestConfig: name,
@@ -8105,7 +8164,7 @@ var SPEC_TSCONFIG = "tsconfig.spec.json";
8105
8164
  var VITEST_GLOBAL_TYPES = "vitest/globals";
8106
8165
  function specTypesOperation(cwd) {
8107
8166
  const path = join35(cwd, SPEC_TSCONFIG);
8108
- if (!existsSync28(path)) return void 0;
8167
+ if (!existsSync29(path)) return void 0;
8109
8168
  let config;
8110
8169
  try {
8111
8170
  config = parseJsonc(
@@ -8173,8 +8232,9 @@ function planJestMigration(context, adoption) {
8173
8232
  `${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
8233
  );
8175
8234
  }
8176
- const targets = projectTargets(context.cwd) ?? {};
8177
- const existingFor = (name) => targets[name];
8235
+ const fromProject = projectTargets(context.cwd) ?? {};
8236
+ const fromManifest = manifestTargets(context.cwd);
8237
+ const existingFor = (name) => fromProject[name] ?? fromManifest[name];
8178
8238
  const siblings = siblingScriptRewrites(context.cwd);
8179
8239
  const specTypes = specTypesOperation(context.cwd);
8180
8240
  const scripts = Object.fromEntries(
@@ -8208,9 +8268,10 @@ function planJestMigration(context, adoption) {
8208
8268
  (name) => ({ kind: "delete", path: name })
8209
8269
  )
8210
8270
  ];
8271
+ const escaping = (prepared[0]?.reading.aliases ?? []).map((alias) => alias.replacement).filter((replacement) => replacement.includes(".."));
8211
8272
  const notes = [
8212
8273
  `${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.`
8274
+ `${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
8275
  ];
8215
8276
  if (specTypes !== void 0) {
8216
8277
  notes.push(
@@ -8218,7 +8279,7 @@ function planJestMigration(context, adoption) {
8218
8279
  );
8219
8280
  }
8220
8281
  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.`
8282
+ `run this module's own formatter (or lint --fix) on what this wrote, after installing. Two things need it: the Vitest type imports are written 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 formatter would pick. Neither is a defect, and both fail a format check. Layout is a repository setting, so sentinel leaves it to the tool that owns it.`
8222
8283
  );
8223
8284
  notes.push(
8224
8285
  `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.`
@@ -8308,7 +8369,7 @@ function plan3(context) {
8308
8369
  `${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
8370
  );
8310
8371
  }
8311
- const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
8372
+ const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME] ?? manifestTargets(context.cwd)[TEST_SCRIPT_NAME];
8312
8373
  const outputs = existing?.outputs ?? [];
8313
8374
  const dependsOn = existing?.dependsOn ?? [];
8314
8375
  const configurations = existing?.configurations ?? {};
@@ -8363,6 +8424,329 @@ function plan3(context) {
8363
8424
  return { operations, notes };
8364
8425
  }
8365
8426
 
8427
+ // src/roles/test/reference.ts
8428
+ import { execFileSync } from "child_process";
8429
+ import { createHash } from "crypto";
8430
+ import { existsSync as existsSync30, mkdirSync, mkdtempSync, readFileSync as readFileSync29, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
8431
+ import { tmpdir } from "os";
8432
+ import { join as join36, resolve as resolve5 } from "path";
8433
+
8434
+ // src/roles/test/baseline.ts
8435
+ function readSnapshot(report) {
8436
+ const files = report?.testResults ?? [];
8437
+ const passing = [];
8438
+ const failing = [];
8439
+ const unloadable = [];
8440
+ for (const file of files) {
8441
+ const assertions = file?.assertionResults ?? [];
8442
+ if (file?.status === "failed" && assertions.length === 0) {
8443
+ unloadable.push(typeof file.name === "string" ? file.name : "an unnamed file");
8444
+ }
8445
+ for (const assertion of assertions) {
8446
+ const name = assertion?.fullName;
8447
+ if (typeof name !== "string" || name === "") continue;
8448
+ if (assertion.status === "passed") passing.push(name);
8449
+ else if (assertion.status === "failed") failing.push(name);
8450
+ }
8451
+ }
8452
+ const claimed = report?.numFailedTests;
8453
+ return {
8454
+ files: files.length,
8455
+ passing: passing.sort(),
8456
+ failing: failing.sort(),
8457
+ unloadable: unloadable.sort(),
8458
+ claimedFailing: typeof claimed === "number" ? claimed : failing.length
8459
+ };
8460
+ }
8461
+ function difference(left, right) {
8462
+ const remaining = /* @__PURE__ */ new Map();
8463
+ for (const name of right) remaining.set(name, (remaining.get(name) ?? 0) + 1);
8464
+ const missing = [];
8465
+ for (const name of left) {
8466
+ const left_ = remaining.get(name) ?? 0;
8467
+ if (left_ === 0) {
8468
+ missing.push(name);
8469
+ continue;
8470
+ }
8471
+ remaining.set(name, left_ - 1);
8472
+ }
8473
+ return missing.sort();
8474
+ }
8475
+ var withoutRenderingQuotes = (name) => name.replaceAll(String.raw`\'`, "'").replaceAll(String.raw`\"`, '"').replaceAll(/['"`]\s*\+\s*['"`]/g, "").replaceAll(/(^|[\s:,[{(])(''|"")($|[\s,)\]}])/g, "$1<empty-string>$3").replaceAll(/['"`]/g, "");
8476
+ function withoutRenderingNoise(name) {
8477
+ const spelled = withSortedKeys(
8478
+ withoutUndefinedEntries(
8479
+ withoutErrorRendering(
8480
+ withoutNaN(
8481
+ withoutTimestamps(withoutRenderingQuotes(withoutClassBodies(withoutStackFrames(name))))
8482
+ )
8483
+ )
8484
+ )
8485
+ );
8486
+ return withoutArrayBrackets(
8487
+ 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, ",")
8488
+ );
8489
+ }
8490
+ function withoutArrayBrackets(name) {
8491
+ return name.replaceAll(
8492
+ /\[([^[\]]*)\]/g,
8493
+ (match, inner) => (
8494
+ // `[Object]` and `[Array]` are the formatter saying it STOPPED, not an array literal.
8495
+ // Stripping their brackets would erase the marker `collapsedOf` pairs on.
8496
+ /^(?:Object|Array)$/.test(inner) ? match : inner.replaceAll(/,\s+/g, ",")
8497
+ )
8498
+ );
8499
+ }
8500
+ var withoutClassBodies = (name) => name.replaceAll(
8501
+ /class\s+([A-Za-z_$][\w$]*)\s*\{\s*\}/g,
8502
+ (_match, className) => `class ${className}`
8503
+ ).replaceAll(
8504
+ /class\s+([A-Za-z_$][\w$]*)[\s\S]*?\n\}/g,
8505
+ (_match, className) => `class ${className}`
8506
+ );
8507
+ var withoutTimestamps = (name) => name.replaceAll(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, "<instant>");
8508
+ var withoutNaN = (name) => name.replaceAll(/\bNaN\b/g, "null");
8509
+ function entriesOf(inner) {
8510
+ const parts = [];
8511
+ let depth = 0;
8512
+ let start = 0;
8513
+ for (let index = 0; index < inner.length; index += 1) {
8514
+ const character = inner[index];
8515
+ if (character === "[" || character === "(") depth += 1;
8516
+ else if (character === "]" || character === ")") depth -= 1;
8517
+ else if (character === "," && depth === 0) {
8518
+ parts.push(inner.slice(start, index));
8519
+ start = index + 1;
8520
+ }
8521
+ if (depth < 0) return void 0;
8522
+ }
8523
+ if (depth !== 0) return void 0;
8524
+ parts.push(inner.slice(start));
8525
+ return parts.map((part) => part.trim()).filter(Boolean);
8526
+ }
8527
+ var sortedGroup = (match, inner) => {
8528
+ if (!inner.includes(":")) return match;
8529
+ const parts = entriesOf(inner);
8530
+ if (parts === void 0) return match;
8531
+ if (parts.length < 2 || !parts.every((part) => part.includes(":"))) return match;
8532
+ return `{${[...parts].sort().join(",")}}`;
8533
+ };
8534
+ var HELD = "\u241F";
8535
+ var withSortedKeys = (name) => {
8536
+ const held = [];
8537
+ let current = name;
8538
+ for (let depth = 0; depth < 24; depth += 1) {
8539
+ const next = current.replaceAll(/\{([^{}]*)\}/g, (match, inner) => {
8540
+ held.push(sortedGroup(match, inner));
8541
+ return `${HELD}${held.length - 1}${HELD}`;
8542
+ });
8543
+ if (next === current) break;
8544
+ current = next;
8545
+ }
8546
+ for (let depth = 0; depth < 24 && current.includes(HELD); depth += 1) {
8547
+ current = current.replaceAll(
8548
+ new RegExp(`${HELD}(\\d+)${HELD}`, "g"),
8549
+ (_match, index) => held[Number(index)]
8550
+ );
8551
+ }
8552
+ return current;
8553
+ };
8554
+ var withoutStackFrames = (name) => {
8555
+ const frame = name.search(/\n\s+at\s/);
8556
+ return frame === -1 ? name : name.slice(0, frame);
8557
+ };
8558
+ var withoutErrorRendering = (name) => name.replaceAll(/\[[A-Za-z]*(?:Error|Exception):[^\][]*\]/g, "{}");
8559
+ 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, "{}");
8560
+ function collapsedOf(candidate, name) {
8561
+ if (!/\[(?:Object|Array)\]/.test(candidate)) return false;
8562
+ const pattern = candidate.split(/\[(?:Object|Array)\]/).map((part) => part.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
8563
+ return new RegExp(`^${pattern}$`, "s").test(name);
8564
+ }
8565
+ function truncationOf(candidate, name) {
8566
+ const cut = candidate.indexOf("\u2026");
8567
+ if (cut === -1) return false;
8568
+ const head = candidate.slice(0, cut);
8569
+ const tail = candidate.slice(cut + 1);
8570
+ return name.startsWith(head) && name.endsWith(tail);
8571
+ }
8572
+ function pairRenamed(missing, added) {
8573
+ const remaining = [...added];
8574
+ const renamed = [];
8575
+ const unmatched = [];
8576
+ for (const name of missing) {
8577
+ const normalised = withoutRenderingNoise(name);
8578
+ const at = remaining.findIndex((candidate) => withoutRenderingNoise(candidate) === normalised);
8579
+ if (at === -1) {
8580
+ unmatched.push(name);
8581
+ continue;
8582
+ }
8583
+ renamed.push({ before: name, after: remaining[at] });
8584
+ remaining.splice(at, 1);
8585
+ }
8586
+ const lost = [];
8587
+ for (const name of unmatched) {
8588
+ const normalised = withoutRenderingNoise(name);
8589
+ const at = remaining.findIndex(
8590
+ (candidate) => truncationOf(withoutRenderingNoise(candidate), normalised) || collapsedOf(normalised, withoutRenderingNoise(candidate))
8591
+ );
8592
+ if (at === -1) {
8593
+ lost.push(name);
8594
+ continue;
8595
+ }
8596
+ renamed.push({ before: name, after: remaining[at] });
8597
+ remaining.splice(at, 1);
8598
+ }
8599
+ return { renamed, missing: lost, added: remaining };
8600
+ }
8601
+ function compareSnapshots(before, after) {
8602
+ const beforeAll = [...before.passing, ...before.failing];
8603
+ const afterAll = [...after.passing, ...after.failing];
8604
+ const paired = pairRenamed(difference(beforeAll, afterAll), difference(afterAll, beforeAll));
8605
+ const renamedBefore = new Set(paired.renamed.map((pair) => pair.before));
8606
+ const regressed = difference(before.passing, after.passing).filter(
8607
+ (name) => !paired.missing.includes(name) && !renamedBefore.has(name)
8608
+ );
8609
+ const failedBefore = new Set(before.failing);
8610
+ const recovered = after.passing.filter((name) => failedBefore.has(name));
8611
+ return {
8612
+ identical: paired.missing.length === 0 && paired.added.length === 0 && regressed.length === 0,
8613
+ missing: paired.missing,
8614
+ added: paired.added,
8615
+ regressed,
8616
+ renamed: paired.renamed,
8617
+ recovered,
8618
+ alreadyFailing: before.failing.length
8619
+ };
8620
+ }
8621
+ function unusableAsBaseline(snapshot) {
8622
+ if (snapshot.unloadable.length > 0) {
8623
+ 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`;
8624
+ }
8625
+ if (snapshot.claimedFailing > snapshot.failing.length) {
8626
+ 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`;
8627
+ }
8628
+ if (snapshot.passing.length === 0) {
8629
+ return `the reference run reports no passing test at all, so a green run after would prove nothing: there is no claim to preserve`;
8630
+ }
8631
+ return void 0;
8632
+ }
8633
+ function renamedExample(before, after) {
8634
+ let shared = 0;
8635
+ while (shared < before.length && shared < after.length && before[shared] === after[shared]) {
8636
+ shared += 1;
8637
+ }
8638
+ const boundary = before.lastIndexOf(" ", shared);
8639
+ const from = boundary > 0 ? boundary + 1 : 0;
8640
+ const ellipsis = from > 0 ? "\u2026" : "";
8641
+ return `"${ellipsis}${before.slice(from)}" -> "${ellipsis}${after.slice(from)}"`;
8642
+ }
8643
+ function describeComparison(comparison) {
8644
+ const firstRename = comparison.renamed[0];
8645
+ 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)}).`;
8646
+ 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.` : "";
8647
+ const recoveredNote = comparison.recovered.length > 0 ? ` ${comparison.recovered.length} test(s) were failing before and pass now.` : "";
8648
+ if (comparison.identical) {
8649
+ return `every test that ran before runs and passes now.${recoveredNote}${renamedNote}${alreadyRedNote}`;
8650
+ }
8651
+ const parts = [];
8652
+ if (comparison.recovered.length > 0) {
8653
+ parts.push(`${comparison.recovered.length} test(s) were failing before and pass now`);
8654
+ }
8655
+ if (comparison.missing.length > 0) {
8656
+ parts.push(
8657
+ `${comparison.missing.length} test(s) no longer exist: ${comparison.missing.slice(0, 3).join(", ")}`
8658
+ );
8659
+ }
8660
+ if (comparison.added.length > 0) {
8661
+ parts.push(
8662
+ `${comparison.added.length} test(s) appeared, which a migration should not do: ${comparison.added.slice(0, 3).join(", ")}`
8663
+ );
8664
+ }
8665
+ if (comparison.regressed.length > 0) {
8666
+ parts.push(
8667
+ `${comparison.regressed.length} test(s) passed before and do not now: ${comparison.regressed.slice(0, 3).join(", ")}`
8668
+ );
8669
+ }
8670
+ return `${parts.join("; ")}.${renamedNote}${alreadyRedNote}`;
8671
+ }
8672
+
8673
+ // src/roles/test/reference.ts
8674
+ function referencePath(cwd) {
8675
+ const key = createHash("sha256").update(resolve5(cwd)).digest("hex").slice(0, 16);
8676
+ return join36(tmpdir(), "sentinel-test-reference", `${key}.json`);
8677
+ }
8678
+ function runJest(cwd, config) {
8679
+ const bin = resolveBin(cwd, "jest");
8680
+ if (bin === void 0) return void 0;
8681
+ const root = findWorkspaceRoot(cwd) ?? cwd;
8682
+ const out = join36(mkdtempSync(join36(tmpdir(), "sentinel-reference-")), "jest.json");
8683
+ try {
8684
+ execFileSync(bin, ["-c", join36(cwd, config), "--json", `--outputFile=${out}`, "--silent"], {
8685
+ cwd: root,
8686
+ stdio: ["ignore", "ignore", "ignore"],
8687
+ maxBuffer: 1 << 28
8688
+ });
8689
+ } catch {
8690
+ }
8691
+ if (!existsSync30(out)) return void 0;
8692
+ try {
8693
+ return JSON.parse(readFileSync29(out, "utf8"));
8694
+ } catch {
8695
+ return void 0;
8696
+ }
8697
+ }
8698
+ function captureReference(cwd, config) {
8699
+ const report = runJest(cwd, config);
8700
+ if (report === void 0) {
8701
+ return {
8702
+ captured: false,
8703
+ 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.`
8704
+ };
8705
+ }
8706
+ const snapshot = readSnapshot(report);
8707
+ const unusable = unusableAsBaseline(snapshot);
8708
+ if (unusable !== void 0) {
8709
+ return {
8710
+ captured: false,
8711
+ message: `no reference recorded: ${unusable} The migration is applied anyway, unproved.`
8712
+ };
8713
+ }
8714
+ const stored = {
8715
+ runner: "jest",
8716
+ config,
8717
+ takenAt: (/* @__PURE__ */ new Date()).toISOString(),
8718
+ snapshot
8719
+ };
8720
+ const path = referencePath(cwd);
8721
+ mkdirSync(join36(path, ".."), { recursive: true });
8722
+ writeFileSync3(path, `${JSON.stringify(stored, void 0, 2)}
8723
+ `);
8724
+ return {
8725
+ captured: true,
8726
+ message: `recorded ${snapshot.passing.length} passing test name(s) from ${config} as the reference. The next \`sentinel --run --test\` compares against it.`
8727
+ };
8728
+ }
8729
+ function hasReference(cwd) {
8730
+ return existsSync30(referencePath(cwd));
8731
+ }
8732
+ function compareAgainstReference(cwd, report) {
8733
+ const path = referencePath(cwd);
8734
+ let stored;
8735
+ try {
8736
+ stored = JSON.parse(readFileSync29(path, "utf8"));
8737
+ } catch {
8738
+ rmSync3(path, { force: true });
8739
+ return { ok: true, message: `the recorded reference could not be read, so nothing was proved.` };
8740
+ }
8741
+ const comparison = compareSnapshots(stored.snapshot, readSnapshot(report));
8742
+ if (comparison.identical) rmSync3(path, { force: true });
8743
+ 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.`;
8744
+ return {
8745
+ ok: comparison.identical,
8746
+ message: `${describeComparison(comparison)} (reference: ${stored.config}, ${stored.takenAt})${kept}`
8747
+ };
8748
+ }
8749
+
8366
8750
  // src/roles/test/adapters/vitest/vitest.adapter.ts
8367
8751
  function resolveVitest(cwd) {
8368
8752
  const fromSentinel = binFromOwnInstall("vitest", "vitest");
@@ -8376,6 +8760,22 @@ var VitestAdapter = class extends BaseAdapter {
8376
8760
  appliesTo(_preset) {
8377
8761
  return true;
8378
8762
  }
8763
+ /**
8764
+ * Record what this module's tests do TODAY, at the last moment it is still true.
8765
+ *
8766
+ * The plan about to be applied deletes the jest config, and with it any way of asking again. A
8767
+ * migration that cannot be compared to what came before is a migration nobody can check, which
8768
+ * is the whole reason this role has a baseline gate at all.
8769
+ */
8770
+ async beforeInit(ctx) {
8771
+ const adoption = readTestAdoption(ctx.cwd);
8772
+ if (adoption.state !== "jest") return Promise.resolve();
8773
+ const [config] = adoption.jestConfigs;
8774
+ if (config === void 0) return Promise.resolve();
8775
+ this.say(`recording the jest reference before migrating...`);
8776
+ this.say(captureReference(ctx.cwd, config).message);
8777
+ return Promise.resolve();
8778
+ }
8379
8779
  plan(context) {
8380
8780
  return plan3(context);
8381
8781
  }
@@ -8422,13 +8822,31 @@ var VitestAdapter = class extends BaseAdapter {
8422
8822
  }
8423
8823
  const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
8424
8824
  const watching = options.includes("--watch") || options.includes("-w");
8425
- const argv = watching ? [...options, ...paths] : ["run", ...options, ...paths];
8825
+ const proving = !watching && hasReference(ctx.cwd);
8826
+ const reportFile = proving ? join37(mkdtempSync2(join37(tmpdir2(), "sentinel-run-")), "vitest.json") : void 0;
8827
+ const reporters = reportFile === void 0 ? [] : ["--reporter=default", "--reporter=json", `--outputFile=${reportFile}`];
8828
+ const argv = watching ? [...options, ...paths] : ["run", ...reporters, ...options, ...paths];
8426
8829
  const result = spawnSync5(bin, argv, { cwd: ctx.cwd, stdio: "inherit" });
8427
8830
  if (result.error) {
8428
8831
  this.say(`could not run vitest (${result.error.message})`);
8429
8832
  return Promise.resolve({ ok: false, code: 1 });
8430
8833
  }
8431
8834
  const code = result.status ?? 1;
8835
+ if (reportFile !== void 0) {
8836
+ let report;
8837
+ try {
8838
+ report = JSON.parse(readFileSync30(reportFile, "utf8"));
8839
+ } catch {
8840
+ report = void 0;
8841
+ }
8842
+ if (report === void 0) {
8843
+ this.say(`vitest wrote no report, so this run proved nothing against the reference.`);
8844
+ } else {
8845
+ const outcome = compareAgainstReference(ctx.cwd, report);
8846
+ this.say(outcome.message);
8847
+ if (!outcome.ok) return Promise.resolve({ ok: false, code: code === 0 ? 1 : code });
8848
+ }
8849
+ }
8432
8850
  return Promise.resolve({ ok: code === 0, code });
8433
8851
  }
8434
8852
  say(message) {
@@ -8445,9 +8863,9 @@ function registerTest() {
8445
8863
 
8446
8864
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
8447
8865
  import { spawnSync as spawnSync6 } from "child_process";
8448
- import { existsSync as existsSync29, readFileSync as readFileSync30 } from "fs";
8866
+ import { existsSync as existsSync31, readFileSync as readFileSync32 } from "fs";
8449
8867
  import { createRequire as createRequire6 } from "module";
8450
- import { join as join37 } from "path";
8868
+ import { join as join39 } from "path";
8451
8869
 
8452
8870
  // src/roles/typescript/presets/nest.json
8453
8871
  var nest_default2 = {
@@ -8628,8 +9046,8 @@ function readTsconfigAdoption(cwd) {
8628
9046
  }
8629
9047
 
8630
9048
  // src/roles/typescript/adapters/tsc/plan.ts
8631
- import { readFileSync as readFileSync29 } from "fs";
8632
- import { join as join36 } from "path";
9049
+ import { readFileSync as readFileSync31 } from "fs";
9050
+ import { join as join38 } from "path";
8633
9051
 
8634
9052
  // src/roles/typescript/typecheck-script.ts
8635
9053
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -8722,7 +9140,7 @@ function planAdoption(context) {
8722
9140
  };
8723
9141
  }
8724
9142
  const existing = parseJsonc(
8725
- readFileSync29(join36(context.cwd, target.path), "utf8"),
9143
+ readFileSync31(join38(context.cwd, target.path), "utf8"),
8726
9144
  target.path
8727
9145
  );
8728
9146
  const extendsChain = composeExtends(existing.extends, preset);
@@ -8860,7 +9278,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8860
9278
  let chain;
8861
9279
  try {
8862
9280
  const parsed = parseJsonc(
8863
- readFileSync30(join37(cwd, target.path), "utf8"),
9281
+ readFileSync32(join39(cwd, target.path), "utf8"),
8864
9282
  target.path
8865
9283
  );
8866
9284
  chain = parsed.extends;
@@ -8873,7 +9291,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8873
9291
  );
8874
9292
  if (preset === void 0) return void 0;
8875
9293
  try {
8876
- createRequire6(join37(cwd, "noop.js")).resolve(preset);
9294
+ createRequire6(join39(cwd, "noop.js")).resolve(preset);
8877
9295
  return void 0;
8878
9296
  } catch {
8879
9297
  return preset;
@@ -8969,7 +9387,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8969
9387
  referencedProjects(cwd, config) {
8970
9388
  try {
8971
9389
  const parsed = parseJsonc(
8972
- readFileSync30(join37(cwd, config), "utf8"),
9390
+ readFileSync32(join39(cwd, config), "utf8"),
8973
9391
  config
8974
9392
  );
8975
9393
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -8985,7 +9403,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8985
9403
  * check.
8986
9404
  */
8987
9405
  typecheckTarget(cwd) {
8988
- if (existsSync29(join37(cwd, "tsconfig.json"))) return "tsconfig.json";
9406
+ if (existsSync31(join39(cwd, "tsconfig.json"))) return "tsconfig.json";
8989
9407
  const target = resolveTsconfigTarget(cwd);
8990
9408
  return target.reason === "none" ? null : target.path;
8991
9409
  }
@@ -9190,8 +9608,8 @@ function replaceLines(current, replacements) {
9190
9608
  }
9191
9609
 
9192
9610
  // 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";
9611
+ import { existsSync as existsSync32, readFileSync as readFileSync33, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
9612
+ import { resolve as resolve6, sep } from "path";
9195
9613
  import { applyEdits as applyEdits3, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
9196
9614
 
9197
9615
  // src/shared/deep-merge.ts
@@ -9201,15 +9619,15 @@ function isPlainObject(value) {
9201
9619
 
9202
9620
  // src/core/apply-plan.ts
9203
9621
  function resolveWithinRoot(cwd, relativePath) {
9204
- const root = resolve5(cwd);
9205
- const absolutePath = resolve5(root, relativePath);
9622
+ const root = resolve6(cwd);
9623
+ const absolutePath = resolve6(root, relativePath);
9206
9624
  if (absolutePath !== root && !absolutePath.startsWith(root + sep)) {
9207
9625
  throw new Error(`Refusing to write outside the module root: "${relativePath}".`);
9208
9626
  }
9209
9627
  return absolutePath;
9210
9628
  }
9211
9629
  function readIfExists(absolutePath) {
9212
- return existsSync30(absolutePath) ? readFileSync31(absolutePath, "utf8") : void 0;
9630
+ return existsSync32(absolutePath) ? readFileSync33(absolutePath, "utf8") : void 0;
9213
9631
  }
9214
9632
  function* leaves(value, prefix = []) {
9215
9633
  for (const [key, keyValue] of Object.entries(value)) {
@@ -9311,14 +9729,14 @@ function preparePlan(cwd, plan4) {
9311
9729
  }
9312
9730
  function writeFileAtomic(absolutePath, contents) {
9313
9731
  const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
9314
- writeFileSync3(tempPath, contents);
9732
+ writeFileSync4(tempPath, contents);
9315
9733
  renameSync(tempPath, absolutePath);
9316
9734
  }
9317
9735
  function applyPlan(cwd, plan4) {
9318
9736
  const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
9319
9737
  for (const file of changed) {
9320
9738
  if (file.deleted) {
9321
- rmSync3(file.absolutePath, { force: true });
9739
+ rmSync4(file.absolutePath, { force: true });
9322
9740
  continue;
9323
9741
  }
9324
9742
  writeFileAtomic(file.absolutePath, file.after);
@@ -9327,8 +9745,8 @@ function applyPlan(cwd, plan4) {
9327
9745
  }
9328
9746
 
9329
9747
  // 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";
9748
+ import { existsSync as existsSync33, readdirSync as readdirSync8, readFileSync as readFileSync34 } from "fs";
9749
+ import { join as join40 } from "path";
9332
9750
  var PATH_SIGNALS = [
9333
9751
  {
9334
9752
  preset: "nest",
@@ -9362,12 +9780,12 @@ function sourceImports(cwd, pattern) {
9362
9780
  }
9363
9781
  for (const entry of entries) {
9364
9782
  if (entry.isDirectory()) {
9365
- if (!NOT_SCANNED.has(entry.name)) stack.push(join38(dir, entry.name));
9783
+ if (!NOT_SCANNED.has(entry.name)) stack.push(join40(dir, entry.name));
9366
9784
  continue;
9367
9785
  }
9368
9786
  if (!SOURCE_FILE2.test(entry.name)) continue;
9369
9787
  try {
9370
- if (pattern.test(readFileSync32(join38(dir, entry.name), "utf8"))) return true;
9788
+ if (pattern.test(readFileSync34(join40(dir, entry.name), "utf8"))) return true;
9371
9789
  } catch {
9372
9790
  continue;
9373
9791
  }
@@ -9376,11 +9794,11 @@ function sourceImports(cwd, pattern) {
9376
9794
  return false;
9377
9795
  }
9378
9796
  function dependencyNames(cwd) {
9379
- const path = join38(cwd, "package.json");
9380
- if (!existsSync31(path)) return [];
9797
+ const path = join40(cwd, "package.json");
9798
+ if (!existsSync33(path)) return [];
9381
9799
  try {
9382
9800
  const manifest = parseJsonc(
9383
- readFileSync32(path, "utf8"),
9801
+ readFileSync34(path, "utf8"),
9384
9802
  path
9385
9803
  );
9386
9804
  return [
@@ -9403,7 +9821,7 @@ function declaresJsx(cwd) {
9403
9821
  for (const name of entries) {
9404
9822
  try {
9405
9823
  const config = parseJsonc(
9406
- readFileSync32(join38(cwd, name), "utf8"),
9824
+ readFileSync34(join40(cwd, name), "utf8"),
9407
9825
  name
9408
9826
  );
9409
9827
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -9548,6 +9966,15 @@ async function dispatch(opts) {
9548
9966
  if (opts.dryRun) {
9549
9967
  return previewPlan(opts, plan4);
9550
9968
  }
9969
+ if (adapter.beforeInit) {
9970
+ await adapter.beforeInit({
9971
+ module: basename3(opts.cwd),
9972
+ cwd: opts.cwd,
9973
+ preset,
9974
+ ci: false,
9975
+ fix: false
9976
+ });
9977
+ }
9551
9978
  for (const change of applyPlan(opts.cwd, plan4)) {
9552
9979
  process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
9553
9980
  `);
@@ -9591,4 +10018,4 @@ export {
9591
10018
  detectFramework,
9592
10019
  dispatch
9593
10020
  };
9594
- //# sourceMappingURL=chunk-4UIZJ3TR.js.map
10021
+ //# sourceMappingURL=chunk-ASBZQAXV.js.map