@hublo/sentinel 1.4.0-alpha.1 → 1.4.0-alpha.3

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.
@@ -5394,6 +5394,9 @@ function registerLint() {
5394
5394
 
5395
5395
  // src/roles/test/adapters/vitest/vitest.adapter.ts
5396
5396
  import { spawnSync as spawnSync5 } from "child_process";
5397
+ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync30 } from "fs";
5398
+ import { tmpdir as tmpdir2 } from "os";
5399
+ import { join as join37 } from "path";
5397
5400
 
5398
5401
  // src/roles/test/presets/toolchain.json
5399
5402
  var toolchain_default2 = {
@@ -5456,12 +5459,12 @@ function rewriteVitestScript(command) {
5456
5459
  });
5457
5460
  }
5458
5461
  function testTarget(options) {
5459
- const { configFiles, outputs, dependsOn, configurations } = options;
5462
+ const { name, configFiles, outputs, dependsOn, configurations } = options;
5460
5463
  const cached = outputs !== void 0 && outputs.length > 0;
5461
5464
  return {
5462
- [TEST_SCRIPT_NAME]: {
5465
+ [name ?? TEST_SCRIPT_NAME]: {
5463
5466
  cache: cached,
5464
- inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)],
5467
+ inputs: ["default", "^default", ...configFiles.map((name2) => `{projectRoot}/${name2}`)],
5465
5468
  ...cached ? { outputs: [...outputs] } : {},
5466
5469
  ...dependsOn !== void 0 && dependsOn.length > 0 ? { dependsOn: [...dependsOn] } : {},
5467
5470
  ...configurations !== void 0 && Object.keys(configurations).length > 0 ? { configurations: { ...configurations } } : {}
@@ -5470,12 +5473,12 @@ function testTarget(options) {
5470
5473
  }
5471
5474
 
5472
5475
  // src/roles/test/jest-migration.ts
5473
- import { existsSync as existsSync26, readFileSync as readFileSync26 } from "fs";
5474
- import { join as join32 } from "path";
5476
+ import { existsSync as existsSync29, readFileSync as readFileSync28 } from "fs";
5477
+ import { join as join35 } from "path";
5475
5478
 
5476
5479
  // src/roles/test/codemod/migrate-module.ts
5477
- import { readdirSync as readdirSync5, readFileSync as readFileSync23, statSync as statSync5 } from "fs";
5478
- import { join as join29 } from "path";
5480
+ import { readdirSync as readdirSync6, readFileSync as readFileSync25, statSync as statSync6 } from "fs";
5481
+ import { join as join31 } from "path";
5479
5482
  import { parseSync as parseSync8 } from "oxc-parser";
5480
5483
 
5481
5484
  // src/roles/test/read-adoption.ts
@@ -5933,6 +5936,104 @@ function rewriteEmptyMockImplementation(source, program) {
5933
5936
  return { source: result, rewritten: insertAt.length };
5934
5937
  }
5935
5938
 
5939
+ // src/roles/test/codemod/fake-timer-options.ts
5940
+ var MEMBER_TYPES6 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
5941
+ var TIMER_CALLS = /* @__PURE__ */ new Set(["useFakeTimers"]);
5942
+ function timerOptions(node) {
5943
+ if (node.type !== "CallExpression") return void 0;
5944
+ const callee = node.callee;
5945
+ if (callee === void 0 || !MEMBER_TYPES6.has(callee.type)) return void 0;
5946
+ const object = callee.object;
5947
+ const property = callee.property;
5948
+ if (object?.type !== "Identifier") return void 0;
5949
+ if (object.name !== "jest" && object.name !== "vi") return void 0;
5950
+ if (property?.type !== "Identifier" || !TIMER_CALLS.has(property.name)) return void 0;
5951
+ const [options] = node.arguments ?? [];
5952
+ return options?.type === "ObjectExpression" ? options : void 0;
5953
+ }
5954
+ var FAKEABLE = [
5955
+ "setTimeout",
5956
+ "clearTimeout",
5957
+ "setImmediate",
5958
+ "clearImmediate",
5959
+ "setInterval",
5960
+ "clearInterval",
5961
+ "Date",
5962
+ "hrtime",
5963
+ "requestAnimationFrame",
5964
+ "cancelAnimationFrame",
5965
+ "requestIdleCallback",
5966
+ "cancelIdleCallback",
5967
+ "performance"
5968
+ ];
5969
+ function stringLiterals2(node) {
5970
+ if (node.type !== "ArrayExpression") return void 0;
5971
+ const values = [];
5972
+ for (const element of node.elements ?? []) {
5973
+ if (element?.type !== "Literal" || typeof element.value !== "string") return void 0;
5974
+ values.push(element.value);
5975
+ }
5976
+ return values;
5977
+ }
5978
+ function rewriteFakeTimerOptions(source, program) {
5979
+ const edits = [];
5980
+ const translated = [];
5981
+ const refusals = [];
5982
+ walk(program, (node) => {
5983
+ const options = timerOptions(node);
5984
+ if (options === void 0) return;
5985
+ for (const property of options.properties ?? []) {
5986
+ const key = property.key;
5987
+ if (key?.type !== "Identifier") continue;
5988
+ const value = property.value;
5989
+ if (value === void 0) continue;
5990
+ const span = { start: property.start, end: property.end };
5991
+ if (key.name === "advanceTimers") {
5992
+ if (value.type === "Literal" && value.value === true) {
5993
+ edits.push({ ...span, text: "shouldAdvanceTime: true" });
5994
+ translated.push("advanceTimers -> shouldAdvanceTime");
5995
+ continue;
5996
+ }
5997
+ if (value.type === "Literal" && typeof value.value === "number") {
5998
+ edits.push({
5999
+ ...span,
6000
+ text: `shouldAdvanceTime: true, advanceTimeDelta: ${value.value}`
6001
+ });
6002
+ translated.push("advanceTimers -> shouldAdvanceTime + advanceTimeDelta");
6003
+ continue;
6004
+ }
6005
+ refusals.push({
6006
+ binding: "advanceTimers",
6007
+ reason: "`useFakeTimers({ advanceTimers })` translates to sinon's `shouldAdvanceTime`, but only when the value can be read here: this one is neither `true` nor a number, and inventing a delta would be inventing behaviour."
6008
+ });
6009
+ continue;
6010
+ }
6011
+ if (key.name === "doNotFake") {
6012
+ const excluded = stringLiterals2(value);
6013
+ if (excluded === void 0) {
6014
+ refusals.push({
6015
+ binding: "doNotFake",
6016
+ reason: "`useFakeTimers({ doNotFake })` becomes sinon's `toFake`, which is its COMPLEMENT, so the list has to be readable here. This one is not a list of string literals."
6017
+ });
6018
+ continue;
6019
+ }
6020
+ const kept = FAKEABLE.filter((name) => !excluded.includes(name));
6021
+ edits.push({
6022
+ ...span,
6023
+ text: `toFake: [${kept.map((name) => `'${name}'`).join(", ")}]`
6024
+ });
6025
+ translated.push("doNotFake -> toFake");
6026
+ }
6027
+ }
6028
+ });
6029
+ if (edits.length === 0) return { source, translated, refusals };
6030
+ let result = source;
6031
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
6032
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
6033
+ }
6034
+ return { source: result, translated, refusals };
6035
+ }
6036
+
5936
6037
  // src/roles/test/codemod/scope.ts
5937
6038
  function readIdentifiers(node, runsLater) {
5938
6039
  const names = [];
@@ -6098,7 +6199,7 @@ function hoistCapturedBindings(source, program, factories, bindings, imported) {
6098
6199
  if (hoistable(name, bindings, imported)) continue;
6099
6200
  refusals.push({
6100
6201
  binding: name,
6101
- reason: bindings.get(name) === void 0 ? `it is declared without an initializer, so there is nothing for vi.hoisted to carry` : `its initializer reads something imported, so it cannot live above the import graph`
6202
+ reason: bindings.get(name) === void 0 ? `\`${name}\` cannot move into vi.hoisted: it is declared without an initializer, so there is nothing for vi.hoisted to carry` : `\`${name}\` cannot move into vi.hoisted: its initializer reads something imported, so it cannot live above the import graph`
6102
6203
  });
6103
6204
  }
6104
6205
  const declarations = simpleDeclarations(program);
@@ -6156,7 +6257,7 @@ function hoistCapturedBindings(source, program, factories, bindings, imported) {
6156
6257
 
6157
6258
  // src/roles/test/codemod/hook-return-value.ts
6158
6259
  var HOOKS = /* @__PURE__ */ new Set(["beforeEach", "afterEach", "beforeAll", "afterAll"]);
6159
- var MEMBER_TYPES6 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
6260
+ var MEMBER_TYPES7 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
6160
6261
  function hookCallback(node) {
6161
6262
  if (node.type !== "CallExpression") return void 0;
6162
6263
  const callee = node.callee;
@@ -6167,7 +6268,7 @@ function hookCallback(node) {
6167
6268
  function configuresAMock(body) {
6168
6269
  if (body.type !== "CallExpression") return false;
6169
6270
  const callee = body.callee;
6170
- if (callee === void 0 || !MEMBER_TYPES6.has(callee.type)) return false;
6271
+ if (callee === void 0 || !MEMBER_TYPES7.has(callee.type)) return false;
6171
6272
  const property = callee.property;
6172
6273
  return property?.type === "Identifier" && /^mock[A-Z]/.test(property.name);
6173
6274
  }
@@ -6352,7 +6453,37 @@ ${source}`;
6352
6453
  }
6353
6454
 
6354
6455
  // src/roles/test/codemod/named-rules.ts
6355
- var MEMBER_TYPES7 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
6456
+ import { existsSync as existsSync24, readFileSync as readFileSync22 } from "fs";
6457
+ import { createRequire as createRequire4 } from "module";
6458
+ import { dirname as dirname4, join as join28 } from "path";
6459
+ function isCommonJsPackage(fromFile, specifier) {
6460
+ if (specifier.startsWith(".") || specifier.startsWith("/")) return false;
6461
+ let resolved;
6462
+ try {
6463
+ resolved = createRequire4(fromFile).resolve(specifier);
6464
+ } catch {
6465
+ return false;
6466
+ }
6467
+ if (!resolved.includes(`${"/"}node_modules${"/"}`)) return false;
6468
+ if (resolved.endsWith(".mjs")) return false;
6469
+ if (resolved.endsWith(".cjs")) return true;
6470
+ let dir = dirname4(resolved);
6471
+ for (let depth = 0; depth < 12; depth += 1) {
6472
+ const manifest = join28(dir, "package.json");
6473
+ if (existsSync24(manifest)) {
6474
+ try {
6475
+ return JSON.parse(readFileSync22(manifest, "utf8")).type !== "module";
6476
+ } catch {
6477
+ return false;
6478
+ }
6479
+ }
6480
+ const parent = dirname4(dir);
6481
+ if (parent === dir) break;
6482
+ dir = parent;
6483
+ }
6484
+ return false;
6485
+ }
6486
+ var MEMBER_TYPES8 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
6356
6487
  var ASYNC_REPLACEMENTS = {
6357
6488
  requireActual: "importActual",
6358
6489
  requireMock: "importMock"
@@ -6362,7 +6493,7 @@ function mockedSpecifiers(program) {
6362
6493
  walk(program, (node) => {
6363
6494
  if (node.type !== "CallExpression") return;
6364
6495
  const callee = node.callee;
6365
- if (callee === void 0 || !MEMBER_TYPES7.has(callee.type)) return;
6496
+ if (callee === void 0 || !MEMBER_TYPES8.has(callee.type)) return;
6366
6497
  const object = callee.object;
6367
6498
  const property = callee.property;
6368
6499
  if (object?.type !== "Identifier") return;
@@ -6396,6 +6527,33 @@ function bareRequire(node) {
6396
6527
  const args = node.arguments ?? [];
6397
6528
  return args.length === 1 && args[0] !== void 0;
6398
6529
  }
6530
+ function mockFactoryReturn(node) {
6531
+ if (node.type !== "CallExpression") return void 0;
6532
+ const callee = node.callee;
6533
+ if (callee === void 0 || !MEMBER_TYPES8.has(callee.type)) return void 0;
6534
+ const object = callee.object;
6535
+ const property = callee.property;
6536
+ if (object?.type !== "Identifier") return void 0;
6537
+ if (!["jest", "vi", "vitest"].includes(object.name)) return void 0;
6538
+ if (property?.name !== "mock") return void 0;
6539
+ const factory = (node.arguments ?? [])[1];
6540
+ if (factory?.type !== "ArrowFunctionExpression" && factory?.type !== "FunctionExpression") {
6541
+ return void 0;
6542
+ }
6543
+ let body = factory.body;
6544
+ while (body?.type === "ParenthesizedExpression") body = body.expression;
6545
+ if (body === void 0) return void 0;
6546
+ if (body.type !== "BlockStatement") return body;
6547
+ const statements = body.body ?? [];
6548
+ if (statements.length !== 1) return void 0;
6549
+ const only = statements[0];
6550
+ if (only?.type !== "ReturnStatement") return void 0;
6551
+ let argument = only.argument;
6552
+ while (argument?.type === "ParenthesizedExpression") {
6553
+ argument = argument.expression;
6554
+ }
6555
+ return argument;
6556
+ }
6399
6557
  function ownedCall(node) {
6400
6558
  if (node.type !== "CallExpression") return void 0;
6401
6559
  const callee = node.callee;
@@ -6421,11 +6579,36 @@ function applyEdits(source, edits) {
6421
6579
  }
6422
6580
  return result;
6423
6581
  }
6424
- function applyNamedRules(source, program) {
6582
+ function applyNamedRules(source, program, fileName = "") {
6425
6583
  const edits = [];
6426
6584
  const applied = /* @__PURE__ */ new Set();
6427
6585
  const asyncify = /* @__PURE__ */ new Set();
6428
6586
  const mocked = mockedSpecifiers(program);
6587
+ const wholeStatement = /* @__PURE__ */ new Set();
6588
+ walk(program, (node) => {
6589
+ if (node.type !== "ExpressionStatement") return;
6590
+ const expression = node.expression;
6591
+ if (expression !== void 0) wholeStatement.add(expression);
6592
+ });
6593
+ const handedToTheRunnerAsAModule = /* @__PURE__ */ new Set();
6594
+ walk(program, (node) => {
6595
+ const returned = mockFactoryReturn(node);
6596
+ if (returned !== void 0) handedToTheRunnerAsAModule.add(returned);
6597
+ });
6598
+ const readsANameOffIt = /* @__PURE__ */ new Set();
6599
+ walk(program, (node) => {
6600
+ if (node.type === "VariableDeclarator") {
6601
+ const id = node.id;
6602
+ const init = node.init ?? void 0;
6603
+ if (id !== void 0 && id.type !== "Identifier" && init !== void 0) {
6604
+ readsANameOffIt.add(init);
6605
+ }
6606
+ return;
6607
+ }
6608
+ if (!MEMBER_TYPES8.has(node.type)) return;
6609
+ const object = node.object;
6610
+ if (object !== void 0) readsANameOffIt.add(object);
6611
+ });
6429
6612
  walkWithEnclosingFunction(program, void 0, (node, enclosing) => {
6430
6613
  const member = ownedCall(node);
6431
6614
  if (member === void 0 && !bareRequire(node)) return;
@@ -6455,10 +6638,13 @@ function applyNamedRules(source, program) {
6455
6638
  return;
6456
6639
  }
6457
6640
  if (bareRequire(node)) {
6641
+ const [specifier] = node.arguments ?? [];
6642
+ const literal = specifier?.type === "StringLiteral" || specifier?.type === "Literal" ? String(specifier.value) : void 0;
6643
+ const interop = literal !== void 0 && !readsANameOffIt.has(node) && !handedToTheRunnerAsAModule.has(node) && !wholeStatement.has(node) && isCommonJsPackage(fileName, literal) ? ".default" : "";
6458
6644
  edits.push({
6459
6645
  start: node.start,
6460
6646
  end: node.end,
6461
- text: `(await import(${argumentText(source, node)}))`
6647
+ text: wholeStatement.has(node) ? `await import(${argumentText(source, node)});` : `(await import(${argumentText(source, node)}))${interop}`
6462
6648
  });
6463
6649
  applied.add("require");
6464
6650
  if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
@@ -6490,13 +6676,13 @@ function applyNamedRules(source, program) {
6490
6676
  }
6491
6677
 
6492
6678
  // src/roles/test/codemod/isolate-modules.ts
6493
- var MEMBER_TYPES8 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
6679
+ var MEMBER_TYPES9 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
6494
6680
  var ISOLATORS = /* @__PURE__ */ new Set(["isolateModules", "isolateModulesAsync"]);
6495
6681
  var FUNCTIONS2 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression"]);
6496
6682
  function isolatingCall(node) {
6497
6683
  if (node.type !== "CallExpression") return void 0;
6498
6684
  const callee = node.callee;
6499
- if (callee === void 0 || !MEMBER_TYPES8.has(callee.type)) return void 0;
6685
+ if (callee === void 0 || !MEMBER_TYPES9.has(callee.type)) return void 0;
6500
6686
  const object = callee.object;
6501
6687
  const property = callee.property;
6502
6688
  if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
@@ -6507,7 +6693,7 @@ function isolatingCall(node) {
6507
6693
  function unusableIsolatingCall(node) {
6508
6694
  if (node.type !== "CallExpression") return void 0;
6509
6695
  const callee = node.callee;
6510
- if (callee === void 0 || !MEMBER_TYPES8.has(callee.type)) return void 0;
6696
+ if (callee === void 0 || !MEMBER_TYPES9.has(callee.type)) return void 0;
6511
6697
  const object = callee.object;
6512
6698
  const property = callee.property;
6513
6699
  if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
@@ -6558,11 +6744,11 @@ ${indent}await (${callback})()`
6558
6744
  }
6559
6745
 
6560
6746
  // src/roles/test/codemod/mock-as-value.ts
6561
- var MEMBER_TYPES9 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
6747
+ var MEMBER_TYPES10 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
6562
6748
  function isViMockCall(node) {
6563
6749
  if (node?.type !== "CallExpression") return false;
6564
6750
  const callee = node.callee;
6565
- if (callee === void 0 || !MEMBER_TYPES9.has(callee.type)) return false;
6751
+ if (callee === void 0 || !MEMBER_TYPES10.has(callee.type)) return false;
6566
6752
  const object = callee.object;
6567
6753
  const property = callee.property;
6568
6754
  return object?.type === "Identifier" && object.name === "vi" && property?.type === "Identifier" && (property.name === "mock" || property.name === "doMock");
@@ -6587,7 +6773,7 @@ function rewriteMockAsValue(source, program) {
6587
6773
  const memberUses = /* @__PURE__ */ new Map();
6588
6774
  const otherUses = /* @__PURE__ */ new Map();
6589
6775
  walk(program, (node) => {
6590
- if (MEMBER_TYPES9.has(node.type)) {
6776
+ if (MEMBER_TYPES10.has(node.type)) {
6591
6777
  const object = node.object;
6592
6778
  if (object?.type === "Identifier" && bindings.has(object.name)) {
6593
6779
  const list = memberUses.get(object.name) ?? [];
@@ -6633,12 +6819,12 @@ function rewriteMockAsValue(source, program) {
6633
6819
  }
6634
6820
 
6635
6821
  // src/roles/test/codemod/mock-factory-default.ts
6636
- var MEMBER_TYPES10 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
6822
+ var MEMBER_TYPES11 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
6637
6823
  var BINDING = "sentinelMockedModule";
6638
6824
  function mockCall(node) {
6639
6825
  if (node.type !== "CallExpression") return void 0;
6640
6826
  const callee = node.callee;
6641
- if (callee === void 0 || !MEMBER_TYPES10.has(callee.type)) return void 0;
6827
+ if (callee === void 0 || !MEMBER_TYPES11.has(callee.type)) return void 0;
6642
6828
  const property = callee.property;
6643
6829
  if (property?.type !== "Identifier" || property.name !== "mock") return void 0;
6644
6830
  const object = callee.object;
@@ -6657,7 +6843,7 @@ function returnedExpression(factory) {
6657
6843
  let body = factory.body;
6658
6844
  while (body?.type === "ParenthesizedExpression") body = body.expression;
6659
6845
  if (body === void 0) return void 0;
6660
- if (body.type !== "BlockStatement") return { node: body, isObject: false };
6846
+ if (body.type !== "BlockStatement") return { node: body, isObject: false, concise: true };
6661
6847
  const statements = body.body ?? [];
6662
6848
  if (statements.length !== 1) return void 0;
6663
6849
  const only = statements[0];
@@ -6681,6 +6867,20 @@ function returnedObject(factory) {
6681
6867
  }
6682
6868
  return void 0;
6683
6869
  }
6870
+ function isModuleNamespace(node) {
6871
+ let current = node;
6872
+ while (current?.type === "ParenthesizedExpression") {
6873
+ current = current.expression;
6874
+ }
6875
+ if (current?.type === "AwaitExpression") {
6876
+ let argument = current.argument;
6877
+ while (argument?.type === "ParenthesizedExpression") {
6878
+ argument = argument.expression;
6879
+ }
6880
+ current = argument;
6881
+ }
6882
+ return current?.type === "ImportExpression";
6883
+ }
6684
6884
  function namesDefault(object) {
6685
6885
  return (object.properties ?? []).some((property) => {
6686
6886
  const key = property.key;
@@ -6698,12 +6898,14 @@ function rewriteMockFactoryDefault(source, program, defaultImported) {
6698
6898
  if (returned === void 0) return;
6699
6899
  if (returned.isObject && namesDefault(returned.node)) return;
6700
6900
  if (returned.isObject && !defaultImported.has(call.specifier)) return;
6901
+ if (!returned.isObject && isModuleNamespace(returned.node)) return;
6701
6902
  if (!returned.isObject) {
6702
6903
  const expression = source.slice(returned.node.start, returned.node.end);
6703
6904
  edits.push({
6704
6905
  start: returned.node.start,
6705
6906
  end: returned.node.end,
6706
- literal: `{ default: ${expression} }`,
6907
+ // Parenthesised when it is the arrow's own body, or it is read as a block, not an object.
6908
+ literal: returned.concise === true ? `({ default: ${expression} })` : `{ default: ${expression} }`,
6707
6909
  specifier: call.specifier,
6708
6910
  wrap: false
6709
6911
  });
@@ -6736,7 +6938,7 @@ function rewriteMockFactoryDefault(source, program, defaultImported) {
6736
6938
  // src/roles/test/codemod/propagate-await.ts
6737
6939
  import { parseSync as parseSync5 } from "oxc-parser";
6738
6940
  var FUNCTIONS3 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
6739
- var MEMBER_TYPES11 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
6941
+ var MEMBER_TYPES12 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
6740
6942
  var ALREADY_A_PROMISE = /* @__PURE__ */ new Set(["then", "catch", "finally"]);
6741
6943
  function asyncFunctionNames(program) {
6742
6944
  const names = /* @__PURE__ */ new Set();
@@ -6810,7 +7012,7 @@ function alreadyHandled(call, parent) {
6810
7012
  if (parent === void 0) return false;
6811
7013
  if (parent.type === "AwaitExpression") return true;
6812
7014
  if (parent.type === "ReturnStatement" || parent.type === "ArrowFunctionExpression") return true;
6813
- if (MEMBER_TYPES11.has(parent.type) && parent.object === call) {
7015
+ if (MEMBER_TYPES12.has(parent.type) && parent.object === call) {
6814
7016
  const property = parent.property;
6815
7017
  return property?.type === "Identifier" && ALREADY_A_PROMISE.has(property.name);
6816
7018
  }
@@ -6859,6 +7061,105 @@ function propagateAwait(fileName, source, original, elsewhere = /* @__PURE__ */
6859
7061
  return { source: current, rewritten };
6860
7062
  }
6861
7063
 
7064
+ // src/roles/test/codemod/reexport-barrel-mock.ts
7065
+ import { readFileSync as readFileSync23, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
7066
+ import { createRequire as createRequire5 } from "module";
7067
+ import { dirname as dirname5, join as join29 } from "path";
7068
+ var MEMBER_TYPES13 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
7069
+ var PACKAGE_SUBPATH = /^(@[^/]+\/[^/]+|[^@.][^/]*)\/(.+)$/;
7070
+ var PURE_REEXPORT = /^module\.exports\s*=\s*require\(\s*(['"])([^'"]+)\1\s*\)\s*;?$/;
7071
+ function mockCalls(program) {
7072
+ const found = [];
7073
+ walk(program, (node) => {
7074
+ if (node.type !== "CallExpression") return;
7075
+ const callee = node.callee;
7076
+ if (callee === void 0 || !MEMBER_TYPES13.has(callee.type)) return;
7077
+ const object = callee.object;
7078
+ const property = callee.property;
7079
+ if (object?.type !== "Identifier") return;
7080
+ if (object.name !== "vi" && object.name !== "jest") return;
7081
+ if (property?.name !== "mock") return;
7082
+ const first = (node.arguments ?? [])[0];
7083
+ if (first?.type !== "Literal" || typeof first.value !== "string") return;
7084
+ found.push({
7085
+ specifier: first.value,
7086
+ start: node.start,
7087
+ end: node.end,
7088
+ literalStart: first.start,
7089
+ literalEnd: first.end
7090
+ });
7091
+ });
7092
+ return found;
7093
+ }
7094
+ function packageRoot(fromFile, name) {
7095
+ try {
7096
+ return dirname5(createRequire5(fromFile).resolve(`${name}/package.json`));
7097
+ } catch {
7098
+ return void 0;
7099
+ }
7100
+ }
7101
+ function resolveFrom(fromFile, specifier) {
7102
+ try {
7103
+ return createRequire5(fromFile).resolve(specifier);
7104
+ } catch {
7105
+ return void 0;
7106
+ }
7107
+ }
7108
+ function barrelsFor(fromFile, name, target) {
7109
+ const root = packageRoot(fromFile, name);
7110
+ if (root === void 0) return [];
7111
+ let entries;
7112
+ try {
7113
+ entries = readdirSync4(root);
7114
+ } catch {
7115
+ return [];
7116
+ }
7117
+ const aliases = [];
7118
+ for (const entry of entries) {
7119
+ if (!entry.endsWith(".js")) continue;
7120
+ const path = join29(root, entry);
7121
+ let body;
7122
+ try {
7123
+ if (!statSync4(path).isFile()) continue;
7124
+ body = readFileSync23(path, "utf8");
7125
+ } catch {
7126
+ continue;
7127
+ }
7128
+ const match = PURE_REEXPORT.exec(body.trim());
7129
+ if (match === null) continue;
7130
+ if (resolveFrom(path, match[2]) !== target) continue;
7131
+ aliases.push(`${name}/${entry.slice(0, -".js".length)}`);
7132
+ }
7133
+ return aliases.sort();
7134
+ }
7135
+ function rewriteReexportBarrelMocks(fromFile, source, program) {
7136
+ const calls = mockCalls(program);
7137
+ if (calls.length === 0) return { source, added: [] };
7138
+ const alreadyMocked = new Set(calls.map((call) => call.specifier));
7139
+ const added = [];
7140
+ const insertions = [];
7141
+ for (const call of calls) {
7142
+ const parts = PACKAGE_SUBPATH.exec(call.specifier);
7143
+ if (parts === null) continue;
7144
+ const target = resolveFrom(fromFile, call.specifier);
7145
+ if (target === void 0) continue;
7146
+ for (const alias of barrelsFor(fromFile, parts[1], target)) {
7147
+ if (alreadyMocked.has(alias)) continue;
7148
+ alreadyMocked.add(alias);
7149
+ added.push(alias);
7150
+ const text = source.slice(call.start, call.literalStart) + `'${alias}'` + source.slice(call.literalEnd, call.end);
7151
+ insertions.push({ at: call.end, text: `
7152
+ ${text}` });
7153
+ }
7154
+ }
7155
+ if (insertions.length === 0) return { source, added: [] };
7156
+ let rewritten = source;
7157
+ for (const insertion of insertions.sort((a, b) => b.at - a.at)) {
7158
+ rewritten = rewritten.slice(0, insertion.at) + insertion.text + rewritten.slice(insertion.at);
7159
+ }
7160
+ return { source: rewritten, added };
7161
+ }
7162
+
6862
7163
  // src/roles/test/codemod/repoint-msw.ts
6863
7164
  import { parseSync as parseSync6 } from "oxc-parser";
6864
7165
  var SENTINEL_MSW_SERVER = "@hublo/sentinel/test/msw";
@@ -6896,6 +7197,43 @@ function repointMswServer(fileName, source) {
6896
7197
  return { source: rewritten, repointed: true };
6897
7198
  }
6898
7199
 
7200
+ // src/roles/test/codemod/spy-instance-type.ts
7201
+ var SPY_TYPES = /* @__PURE__ */ new Set(["SpyInstance"]);
7202
+ function typeNameOf(source, node) {
7203
+ const name = node.typeName;
7204
+ if (name === void 0) return void 0;
7205
+ return source.slice(name.start, name.end);
7206
+ }
7207
+ function rewriteSpyInstanceType(source, program) {
7208
+ const edits = [];
7209
+ walk(program, (node) => {
7210
+ if (node.type !== "TSTypeReference") return;
7211
+ const written = typeNameOf(source, node);
7212
+ if (written === void 0) return;
7213
+ const bare = written.startsWith("jest.") ? written.slice("jest.".length) : written;
7214
+ if (!SPY_TYPES.has(bare)) return;
7215
+ const args = (node.typeArguments ?? node.typeParameters)?.params;
7216
+ if (args === void 0 || args.length === 0) return;
7217
+ const text = (argument) => source.slice(argument.start, argument.end);
7218
+ const returns = text(args[0]);
7219
+ const parameters = args.length > 1 ? `...args: ${text(args[1])}` : "...args: any[]";
7220
+ edits.push({
7221
+ start: node.start,
7222
+ end: node.end,
7223
+ text: `MockInstance<(${parameters}) => ${returns}>`
7224
+ });
7225
+ });
7226
+ let result = source;
7227
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
7228
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
7229
+ }
7230
+ return {
7231
+ source: result,
7232
+ rewritten: edits.length,
7233
+ typeImports: edits.length > 0 ? ["MockInstance"] : []
7234
+ };
7235
+ }
7236
+
6899
7237
  // src/roles/test/codemod/migrate-file.ts
6900
7238
  function mockFactories(program) {
6901
7239
  const factories = [];
@@ -6928,46 +7266,61 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
6928
7266
  rulesApplied: [],
6929
7267
  mswRepointed: false
6930
7268
  };
7269
+ const unparsable = (step) => ({
7270
+ ...untouched,
7271
+ refusals: [
7272
+ {
7273
+ binding: step,
7274
+ reason: `this file no longer parses after the \`${step}\` step, so nothing was written for it. That is a defect in sentinel, not in the file: please report it with the file name.`
7275
+ }
7276
+ ]
7277
+ });
6931
7278
  const original = parse2(fileName, source);
6932
- if (original === void 0) return untouched;
7279
+ if (original === void 0) return unparsable("reading the file");
6933
7280
  const titles = rewriteEachTitles(source, original);
6934
7281
  const tables = rewriteEachTableRows(titles.source, original);
6935
7282
  const afterTables = parse2(fileName, tables.source);
6936
- if (afterTables === void 0) return untouched;
6937
- const ruled = applyNamedRules(tables.source, afterTables);
7283
+ if (afterTables === void 0) return unparsable("each table rows");
7284
+ const spyTypes = rewriteSpyInstanceType(tables.source, afterTables);
7285
+ const afterSpyTypes = parse2(fileName, spyTypes.source);
7286
+ if (afterSpyTypes === void 0) return unparsable("SpyInstance types");
7287
+ const ruled = applyNamedRules(spyTypes.source, afterSpyTypes, fileName);
6938
7288
  const renamed = renameJestToVitest(fileName, ruled.source);
6939
7289
  const afterRename = parse2(fileName, renamed.source);
6940
- if (afterRename === void 0) return untouched;
6941
- const mocks = rewriteMockAsValue(renamed.source, afterRename);
7290
+ if (afterRename === void 0) return unparsable("jest to vi rename");
7291
+ const fakeTimers = rewriteFakeTimerOptions(renamed.source, afterRename);
7292
+ const afterFakeTimers = parse2(fileName, fakeTimers.source);
7293
+ if (afterFakeTimers === void 0) return unparsable("fake timer options");
7294
+ const mocks = rewriteMockAsValue(fakeTimers.source, afterFakeTimers);
6942
7295
  const afterMocks = parse2(fileName, mocks.source);
6943
- if (afterMocks === void 0) return untouched;
7296
+ if (afterMocks === void 0) return unparsable("mock as a value");
6944
7297
  const constructible = rewriteConstructibleMocks(
6945
7298
  mocks.source,
6946
7299
  afterMocks,
6947
7300
  mockFactories(afterMocks)
6948
7301
  );
6949
7302
  const afterConstructible = parse2(fileName, constructible.source);
6950
- if (afterConstructible === void 0) return untouched;
7303
+ if (afterConstructible === void 0) return unparsable("constructible mocks");
6951
7304
  const factoryDefault = rewriteMockFactoryDefault(
6952
7305
  constructible.source,
6953
7306
  afterConstructible,
6954
7307
  defaultImported
6955
7308
  );
6956
7309
  const beforeReturnValue = parse2(fileName, factoryDefault.source);
6957
- if (beforeReturnValue === void 0) return untouched;
7310
+ if (beforeReturnValue === void 0) return unparsable("mock factory default");
6958
7311
  const returnValue = rewriteAssertionReturnValue(factoryDefault.source, beforeReturnValue);
6959
7312
  const afterReturnValue = parse2(fileName, returnValue.source);
6960
- if (afterReturnValue === void 0) return untouched;
7313
+ if (afterReturnValue === void 0) return unparsable("assertion return value");
6961
7314
  const isolated = rewriteIsolateModules(returnValue.source, afterReturnValue);
6962
7315
  const afterIsolate = parse2(fileName, isolated.source);
6963
- if (afterIsolate === void 0) return untouched;
7316
+ if (afterIsolate === void 0) return unparsable("isolate modules");
6964
7317
  const emptyImplementations = rewriteEmptyMockImplementation(isolated.source, afterIsolate);
6965
7318
  const afterEmpty = parse2(fileName, emptyImplementations.source);
6966
- if (afterEmpty === void 0) return untouched;
7319
+ if (afterEmpty === void 0) return unparsable("empty mock implementation");
6967
7320
  const hooks = rewriteHookReturnValue(emptyImplementations.source, afterEmpty);
6968
7321
  const awaited = propagateAwait(fileName, hooks.source, original, asyncHelpers);
6969
7322
  const program = parse2(fileName, awaited.source);
6970
- if (program === void 0) return untouched;
7323
+ if (program === void 0) return unparsable("hook body");
6971
7324
  const hoist = hoistCapturedBindings(
6972
7325
  awaited.source,
6973
7326
  program,
@@ -6975,12 +7328,18 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
6975
7328
  moduleBindings(program),
6976
7329
  importedNames(program)
6977
7330
  );
6978
- const withImport = injectTypeImport(fileName, hoist.source, renamed.typeImports);
7331
+ const withImport = injectTypeImport(fileName, hoist.source, [
7332
+ ...renamed.typeImports,
7333
+ ...spyTypes.typeImports
7334
+ ]);
6979
7335
  const deep = normaliseDeepImports(withImport);
6980
7336
  const repointed = repointMswServer(fileName, deep.source);
7337
+ const finalProgram = parse2(fileName, repointed.source);
7338
+ if (finalProgram === void 0) return unparsable("msw server import");
7339
+ const barrels = rewriteReexportBarrelMocks(fileName, repointed.source, finalProgram);
6981
7340
  return {
6982
- source: repointed.source,
6983
- changed: repointed.source !== source,
7341
+ source: barrels.source,
7342
+ changed: barrels.source !== source,
6984
7343
  renamed: renamed.rewritten,
6985
7344
  hoisted: hoist.hoisted,
6986
7345
  unhandled: renamed.unhandled,
@@ -6993,9 +7352,13 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
6993
7352
  ...isolated.rewritten > 0 ? ["jest.isolateModules"] : [],
6994
7353
  ...emptyImplementations.rewritten > 0 ? ["empty mockImplementation"] : [],
6995
7354
  ...hooks.rewritten > 0 ? ["hook body, so it does not return its mock"] : [],
6996
- ...awaited.rewritten > 0 ? ["await propagated to a helper that became async"] : []
7355
+ ...spyTypes.rewritten > 0 ? ["SpyInstance<T> as a signature, not a return type"] : [],
7356
+ ...awaited.rewritten > 0 ? ["await propagated to a helper that became async"] : [],
7357
+ ...barrels.added.map((alias) => `mock carried to ${alias}, which re-exports the mocked file`),
7358
+ ...fakeTimers.translated
6997
7359
  ].sort(),
6998
7360
  refusals: [
7361
+ ...fakeTimers.refusals,
6999
7362
  ...hoist.refusals,
7000
7363
  ...mocks.refusals.map((refusal) => ({ binding: refusal.binding, reason: refusal.reason })),
7001
7364
  ...constructible.refusals,
@@ -7006,8 +7369,8 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
7006
7369
  }
7007
7370
 
7008
7371
  // src/roles/test/codemod/test-support-files.ts
7009
- import { existsSync as existsSync24, readFileSync as readFileSync22, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
7010
- import { dirname as dirname4, join as join28, relative as relative5, resolve as resolve4 } from "path";
7372
+ import { existsSync as existsSync25, readFileSync as readFileSync24, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
7373
+ import { dirname as dirname6, join as join30, relative as relative5, resolve as resolve4 } from "path";
7011
7374
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
7012
7375
  var TEST_FILE2 = /\.(spec|test)\.[cm]?[jt]sx?$/;
7013
7376
  var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
@@ -7026,14 +7389,14 @@ function relativeSpecifiers(source) {
7026
7389
  return found;
7027
7390
  }
7028
7391
  function resolveSpecifier(fromFile, specifier) {
7029
- const base = resolve4(dirname4(fromFile), specifier);
7030
- if (existsSync24(base) && statSync4(base).isFile()) return base;
7392
+ const base = resolve4(dirname6(fromFile), specifier);
7393
+ if (existsSync25(base) && statSync5(base).isFile()) return base;
7031
7394
  for (const extension of EXTENSIONS) {
7032
- if (existsSync24(`${base}${extension}`)) return `${base}${extension}`;
7395
+ if (existsSync25(`${base}${extension}`)) return `${base}${extension}`;
7033
7396
  }
7034
7397
  for (const extension of EXTENSIONS) {
7035
- const index = join28(base, `index${extension}`);
7036
- if (existsSync24(index)) return index;
7398
+ const index = join30(base, `index${extension}`);
7399
+ if (existsSync25(index)) return index;
7037
7400
  }
7038
7401
  return void 0;
7039
7402
  }
@@ -7044,15 +7407,15 @@ function moduleSources(cwd) {
7044
7407
  const dir = stack.pop();
7045
7408
  let entries;
7046
7409
  try {
7047
- entries = readdirSync4(dir, { withFileTypes: true });
7410
+ entries = readdirSync5(dir, { withFileTypes: true });
7048
7411
  } catch {
7049
7412
  continue;
7050
7413
  }
7051
7414
  for (const entry of entries) {
7052
7415
  if (entry.isDirectory()) {
7053
- if (!SKIPPED.has(entry.name)) stack.push(join28(dir, entry.name));
7416
+ if (!SKIPPED.has(entry.name)) stack.push(join30(dir, entry.name));
7054
7417
  } else if (SOURCE_FILE.test(entry.name)) {
7055
- found.push(join28(dir, entry.name));
7418
+ found.push(join30(dir, entry.name));
7056
7419
  }
7057
7420
  }
7058
7421
  }
@@ -7060,12 +7423,24 @@ function moduleSources(cwd) {
7060
7423
  }
7061
7424
  function read(path) {
7062
7425
  try {
7063
- return readFileSync22(path, "utf8");
7426
+ return readFileSync24(path, "utf8");
7064
7427
  } catch {
7065
7428
  return void 0;
7066
7429
  }
7067
7430
  }
7068
- function listTestSupportFiles(cwd) {
7431
+ function setupFilesInside(cwd, named) {
7432
+ const kept = [];
7433
+ for (const raw of named) {
7434
+ const withoutToken = raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", ".");
7435
+ const full = resolve4(cwd, withoutToken);
7436
+ const inside = relative5(cwd, full);
7437
+ if (inside.startsWith("..") || inside === "") continue;
7438
+ if (!existsSync25(full)) continue;
7439
+ kept.push(inside.replaceAll("\\", "/"));
7440
+ }
7441
+ return kept;
7442
+ }
7443
+ function listTestSupportFiles(cwd, setupFiles = []) {
7069
7444
  const sources = moduleSources(cwd);
7070
7445
  const contents = /* @__PURE__ */ new Map();
7071
7446
  for (const file of sources) {
@@ -7095,7 +7470,8 @@ function listTestSupportFiles(cwd) {
7095
7470
  queue.push(target);
7096
7471
  }
7097
7472
  }
7098
- return [...testOnly].filter((file) => /\brequire\s*\(/.test(contents.get(file) ?? "")).map((file) => relative5(cwd, file).replaceAll("\\", "/")).sort();
7473
+ const reachable = [...testOnly].filter((file) => /\brequire\s*\(/.test(contents.get(file) ?? "")).map((file) => relative5(cwd, file).replaceAll("\\", "/"));
7474
+ return [.../* @__PURE__ */ new Set([...reachable, ...setupFilesInside(cwd, setupFiles)])].sort();
7099
7475
  }
7100
7476
 
7101
7477
  // src/roles/test/codemod/migrate-module.ts
@@ -7105,7 +7481,7 @@ function parse3(source) {
7105
7481
  }
7106
7482
  function read2(path) {
7107
7483
  try {
7108
- return readFileSync23(path, "utf8");
7484
+ return readFileSync25(path, "utf8");
7109
7485
  } catch {
7110
7486
  return void 0;
7111
7487
  }
@@ -7117,16 +7493,16 @@ function defaultImportedSpecifiers(cwd) {
7117
7493
  const walk2 = (dir) => {
7118
7494
  let entries;
7119
7495
  try {
7120
- entries = readdirSync5(dir);
7496
+ entries = readdirSync6(dir);
7121
7497
  } catch {
7122
7498
  return;
7123
7499
  }
7124
7500
  for (const entry of entries) {
7125
7501
  if (SKIPPED2.has(entry)) continue;
7126
- const path = join29(dir, entry);
7502
+ const path = join31(dir, entry);
7127
7503
  let directory;
7128
7504
  try {
7129
- directory = statSync5(path).isDirectory();
7505
+ directory = statSync6(path).isDirectory();
7130
7506
  } catch {
7131
7507
  continue;
7132
7508
  }
@@ -7143,7 +7519,7 @@ function defaultImportedSpecifiers(cwd) {
7143
7519
  walk2(cwd);
7144
7520
  return found;
7145
7521
  }
7146
- function migrateModuleTests(cwd) {
7522
+ function migrateModuleTests(cwd, setupFiles = []) {
7147
7523
  const operations = [];
7148
7524
  const blockers = [];
7149
7525
  let files = 0;
@@ -7151,13 +7527,13 @@ function migrateModuleTests(cwd) {
7151
7527
  let hoisted = 0;
7152
7528
  const defaultImported = defaultImportedSpecifiers(cwd);
7153
7529
  const asyncHelpers = /* @__PURE__ */ new Set();
7154
- for (const relative7 of listTestSupportFiles(cwd)) {
7155
- const source = read2(join29(cwd, relative7));
7530
+ for (const relative7 of listTestSupportFiles(cwd, setupFiles)) {
7531
+ const source = read2(join31(cwd, relative7));
7156
7532
  if (source === void 0) {
7157
7533
  blockers.push({ file: relative7, reason: "this file could not be read" });
7158
7534
  continue;
7159
7535
  }
7160
- const migrated = migrateTestFile(join29(cwd, relative7), source, defaultImported);
7536
+ const migrated = migrateTestFile(join31(cwd, relative7), source, defaultImported);
7161
7537
  if (!migrated.changed) continue;
7162
7538
  const before = parse3(source);
7163
7539
  const after = parse3(migrated.source);
@@ -7172,12 +7548,12 @@ function migrateModuleTests(cwd) {
7172
7548
  operations.push({ kind: "write", path: relative7, contents: migrated.source });
7173
7549
  }
7174
7550
  for (const relative7 of listTestFiles(cwd)) {
7175
- const source = read2(join29(cwd, relative7));
7551
+ const source = read2(join31(cwd, relative7));
7176
7552
  if (source === void 0) {
7177
7553
  blockers.push({ file: relative7, reason: "this file could not be read" });
7178
7554
  continue;
7179
7555
  }
7180
- const migrated = migrateTestFile(join29(cwd, relative7), source, defaultImported, asyncHelpers);
7556
+ const migrated = migrateTestFile(join31(cwd, relative7), source, defaultImported, asyncHelpers);
7181
7557
  for (const member of migrated.unhandled) {
7182
7558
  blockers.push({
7183
7559
  file: `${relative7}:${member.line}`,
@@ -7185,10 +7561,7 @@ function migrateModuleTests(cwd) {
7185
7561
  });
7186
7562
  }
7187
7563
  for (const refusal of migrated.refusals) {
7188
- blockers.push({
7189
- file: relative7,
7190
- reason: `\`${refusal.binding}\` cannot move into vi.hoisted: ${refusal.reason}`
7191
- });
7564
+ blockers.push({ file: relative7, reason: refusal.reason });
7192
7565
  }
7193
7566
  if (!migrated.changed) continue;
7194
7567
  files++;
@@ -7200,11 +7573,16 @@ function migrateModuleTests(cwd) {
7200
7573
  }
7201
7574
 
7202
7575
  // src/roles/test/nest/generate-config.ts
7203
- import { readdirSync as readdirSync6, readFileSync as readFileSync24, statSync as statSync6 } from "fs";
7204
- import { join as join30, relative as relative6 } from "path";
7576
+ import { existsSync as existsSync26, readdirSync as readdirSync7, readFileSync as readFileSync26, statSync as statSync7 } from "fs";
7577
+ import { join as join32, relative as relative6 } from "path";
7205
7578
  var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
7579
+ var REACT_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
7580
+ function flavourOf(reading) {
7581
+ return reading.testEnvironment === "jsdom" ? "react" : "nest";
7582
+ }
7206
7583
  var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/nest";
7207
7584
  var SENTINEL_MSW_SETUP = "@hublo/sentinel/test/setup/msw-lifecycle";
7585
+ var SENTINEL_WORKSPACE_SETUP = "@hublo/sentinel/test/setup/workspace";
7208
7586
  var NEST_TEST_CONFIG_FILE = "vitest.config.mts";
7209
7587
  function hopsToRoot(cwd, workspaceRoot) {
7210
7588
  const up = relative6(cwd, workspaceRoot).replaceAll("\\", "/");
@@ -7235,16 +7613,16 @@ function readModuleTraits(cwd) {
7235
7613
  const walk2 = (dir) => {
7236
7614
  let entries;
7237
7615
  try {
7238
- entries = readdirSync6(dir);
7616
+ entries = readdirSync7(dir);
7239
7617
  } catch {
7240
7618
  return;
7241
7619
  }
7242
7620
  for (const entry of entries) {
7243
7621
  if (SKIPPED3.has(entry)) continue;
7244
- const path = join30(dir, entry);
7622
+ const path = join32(dir, entry);
7245
7623
  let directory;
7246
7624
  try {
7247
- directory = statSync6(path).isDirectory();
7625
+ directory = statSync7(path).isDirectory();
7248
7626
  } catch {
7249
7627
  continue;
7250
7628
  }
@@ -7255,7 +7633,7 @@ function readModuleTraits(cwd) {
7255
7633
  if (!/\.tsx?$/.test(entry)) continue;
7256
7634
  let source;
7257
7635
  try {
7258
- source = readFileSync24(path, "utf8");
7636
+ source = readFileSync26(path, "utf8");
7259
7637
  } catch {
7260
7638
  continue;
7261
7639
  }
@@ -7272,9 +7650,30 @@ function readModuleTraits(cwd) {
7272
7650
  return traits;
7273
7651
  }
7274
7652
  var withoutRootDir = (value) => value.replaceAll("<rootDir>/", "./").replaceAll("<rootDir>", ".");
7275
- function setupBlock(reading, msw) {
7276
- const own = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []].filter((file) => !file.endsWith("jest.setup.after.env.js")).map((file) => `'${withoutRootDir(file)}'`);
7277
- const sentinel = [`'${SENTINEL_TEST_SETUP}'`, ...msw ? [`'${SENTINEL_MSW_SETUP}'`] : []];
7653
+ function ownsMswLifecycle(cwd, reading) {
7654
+ const declared = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []].filter(
7655
+ (file) => !file.endsWith("jest.setup.after.env.js")
7656
+ );
7657
+ for (const raw of declared) {
7658
+ const file = join32(cwd, raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", "."));
7659
+ if (!existsSync26(file)) continue;
7660
+ try {
7661
+ if (/\.listen\s*\(/.test(readFileSync26(file, "utf8"))) return true;
7662
+ } catch {
7663
+ continue;
7664
+ }
7665
+ }
7666
+ return false;
7667
+ }
7668
+ function setupBlock(cwd, reading, msw) {
7669
+ const declared = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []];
7670
+ const namedTheRootSetup = declared.some((file) => file.endsWith("jest.setup.after.env.js"));
7671
+ const own = declared.filter((file) => !file.endsWith("jest.setup.after.env.js")).map((file) => `'${withoutRootDir(file)}'`);
7672
+ const sentinel = [
7673
+ `'${SENTINEL_TEST_SETUP}'`,
7674
+ ...namedTheRootSetup ? [`'${SENTINEL_WORKSPACE_SETUP}'`] : [],
7675
+ ...msw && !ownsMswLifecycle(cwd, reading) ? [`'${SENTINEL_MSW_SETUP}'`] : []
7676
+ ];
7278
7677
  return ` setupFiles: [${[...sentinel, ...own].join(", ")}],`;
7279
7678
  }
7280
7679
  function envBlock(reading) {
@@ -7282,6 +7681,19 @@ function envBlock(reading) {
7282
7681
  const entries = reading.env.map((entry) => ` ${entry.name}: ${entry.value},`);
7283
7682
  return [" env: {", ...entries, " },"].join("\n");
7284
7683
  }
7684
+ function concurrencyBlock(cwd, reading, target) {
7685
+ const declared = reading.maxWorkers;
7686
+ if (declared !== void 0) {
7687
+ return declared === "1" ? ` fileParallelism: false,` : ` maxWorkers: ${declared},`;
7688
+ }
7689
+ const ci = projectTargets(cwd)?.[target]?.configurations?.ci;
7690
+ if (ci === void 0) return "";
7691
+ if (ci.runInBand === true) return ` fileParallelism: false,`;
7692
+ if (typeof ci.maxWorkers === "number") {
7693
+ return ci.maxWorkers === 1 ? ` fileParallelism: false,` : ` maxWorkers: ${ci.maxWorkers},`;
7694
+ }
7695
+ return "";
7696
+ }
7285
7697
  function timeoutBlock(reading) {
7286
7698
  return reading.testTimeout === void 0 ? "" : ` testTimeout: ${reading.testTimeout},`;
7287
7699
  }
@@ -7300,14 +7712,19 @@ function excludeBlock(reading) {
7300
7712
  const globs = reading.excludeGlobs.map((glob) => `'${glob}'`).join(", ");
7301
7713
  return ` exclude: [...configDefaults.exclude, ${globs}],`;
7302
7714
  }
7715
+ function isPackage(replacement) {
7716
+ return !replacement.startsWith(".") && !replacement.startsWith("/") && !replacement.toLowerCase().includes("<rootdir>");
7717
+ }
7303
7718
  function aliasBlock(reading) {
7304
7719
  if (reading.aliases === void 0 || reading.aliases.length === 0) return "";
7305
- const entries = reading.aliases.map(
7306
- (alias) => ` { find: /${alias.find.replaceAll("/", "\\\\/")}/, replacement: path.resolve(import.meta.dirname, '${withoutRootDir(alias.replacement)}') },`
7307
- );
7720
+ const entries = reading.aliases.map((alias) => {
7721
+ const pattern = alias.find.replaceAll(String.raw`\\`, "\\").replaceAll("/", String.raw`\/`);
7722
+ const target = isPackage(alias.replacement) ? `'${alias.replacement}'` : `path.resolve(import.meta.dirname, '${withoutRootDir(alias.replacement)}')`;
7723
+ return ` { find: /${pattern}/, replacement: ${target} },`;
7724
+ });
7308
7725
  return [" resolve: {", " alias: [", ...entries, " ],", " },"].join("\n");
7309
7726
  }
7310
- function generateNestTestConfig(cwd, reading) {
7727
+ function generateNestTestConfig(cwd, reading, target = "test") {
7311
7728
  if (reading.refusals.length > 0) return { refusals: reading.refusals };
7312
7729
  const workspaceRoot = findWorkspaceRoot(cwd);
7313
7730
  if (workspaceRoot === void 0) {
@@ -7317,9 +7734,10 @@ function generateNestTestConfig(cwd, reading) {
7317
7734
  const up = hopsToRoot(cwd, workspaceRoot);
7318
7735
  const test = [
7319
7736
  environmentBlock(reading),
7320
- setupBlock(reading, traits.msw),
7737
+ setupBlock(cwd, reading, traits.msw),
7321
7738
  envBlock(reading),
7322
7739
  timeoutBlock(reading),
7740
+ concurrencyBlock(cwd, reading, target),
7323
7741
  globalSetupBlock(reading),
7324
7742
  includeBlock(reading),
7325
7743
  excludeBlock(reading),
@@ -7331,7 +7749,7 @@ function generateNestTestConfig(cwd, reading) {
7331
7749
  `import path from 'node:path'`,
7332
7750
  "",
7333
7751
  ...needsDefaults ? [`import { configDefaults } from 'vitest/config'`, ""] : [],
7334
- `import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
7752
+ flavourOf(reading) === "react" ? `import { defineConfig, reactTestConfig } from '${REACT_TEST_PRESET_SPECIFIER}'` : `import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
7335
7753
  ].join("\n");
7336
7754
  const overrides = [aliasBlock(reading), " test: {", ...test, " },"].filter(
7337
7755
  (line) => line !== ""
@@ -7342,7 +7760,7 @@ function generateNestTestConfig(cwd, reading) {
7342
7760
  source: `${imports}
7343
7761
 
7344
7762
  export default defineConfig(
7345
- nestTestConfig({
7763
+ ${flavourOf(reading) === "react" ? "reactTestConfig" : "nestTestConfig"}({
7346
7764
  root: import.meta.dirname,
7347
7765
  workspaceRoot: path.resolve(import.meta.dirname, '${up}'),${decoratorLine}
7348
7766
  overrides: {
@@ -7355,14 +7773,15 @@ ${overrides.join("\n")}
7355
7773
  }
7356
7774
 
7357
7775
  // src/roles/test/nest/read-jest-config.ts
7358
- import { existsSync as existsSync25, readFileSync as readFileSync25 } from "fs";
7359
- import { join as join31 } from "path";
7776
+ import { existsSync as existsSync27, readFileSync as readFileSync27 } from "fs";
7777
+ import { join as join33 } from "path";
7360
7778
  var CARRIED = [
7361
7779
  "testEnvironment",
7362
7780
  "coverageDirectory",
7363
7781
  "setupFilesAfterEnv",
7364
7782
  "setupFiles",
7365
7783
  "testTimeout",
7784
+ "maxWorkers",
7366
7785
  "globalSetup",
7367
7786
  "testMatch",
7368
7787
  "collectCoverageFrom",
@@ -7376,9 +7795,10 @@ var DISCARDED = [
7376
7795
  "transform",
7377
7796
  "extensionsToTreatAsEsm"
7378
7797
  ];
7379
- var STRING = String.raw`['"]([^'"]*)['"]`;
7798
+ var STRING = String.raw`['"\`]([^'"\`]*)['"\`]`;
7380
7799
  function readString(body, key) {
7381
- return new RegExp(String.raw`\b${key}\s*:\s*${STRING}`).exec(body)?.[1];
7800
+ const value = new RegExp(String.raw`\b${key}\s*:\s*${STRING}`).exec(body)?.[1];
7801
+ return value?.includes("${") === true ? void 0 : value;
7382
7802
  }
7383
7803
  function readNumber(body, key) {
7384
7804
  return new RegExp(String.raw`\b${key}\s*:\s*(\d+)`).exec(body)?.[1];
@@ -7486,19 +7906,73 @@ function configBody(source) {
7486
7906
  function declaredKeys(body) {
7487
7907
  const keys = [];
7488
7908
  let depth = 0;
7489
- for (const line of body.split("\n")) {
7490
- if (depth === 0) {
7491
- const key = /^\s*([A-Za-z_$][\w$]*)\s*:/.exec(line);
7492
- if (key !== null) keys.push(key[1]);
7909
+ let quote;
7910
+ let token = "";
7911
+ for (let i = 0; i < body.length; i += 1) {
7912
+ const char = body[i];
7913
+ if (quote !== void 0) {
7914
+ if (char === quote && body[i - 1] !== "\\") quote = void 0;
7915
+ continue;
7916
+ }
7917
+ if (char === "'" || char === '"' || char === "`") {
7918
+ quote = char;
7919
+ continue;
7920
+ }
7921
+ if (char === "/" && body[i + 1] === "/") {
7922
+ const end = body.indexOf("\n", i);
7923
+ i = end === -1 ? body.length : end;
7924
+ token = "";
7925
+ continue;
7926
+ }
7927
+ if (char === "/" && body[i + 1] === "*") {
7928
+ const end = body.indexOf("*/", i + 2);
7929
+ i = end === -1 ? body.length : end + 1;
7930
+ token = "";
7931
+ continue;
7932
+ }
7933
+ if (char === "{" || char === "[" || char === "(") {
7934
+ depth += 1;
7935
+ token = "";
7936
+ continue;
7937
+ }
7938
+ if (char === "}" || char === "]" || char === ")") {
7939
+ depth -= 1;
7940
+ token = "";
7941
+ continue;
7493
7942
  }
7494
- for (const char of line) {
7495
- if (char === "{" || char === "[") depth += 1;
7496
- else if (char === "}" || char === "]") depth -= 1;
7943
+ if (depth === 0 && char === ":") {
7944
+ const key = /([A-Za-z_$][\w$]*)\s*$/.exec(token)?.[1];
7945
+ if (key !== void 0) keys.push(key);
7946
+ token = "";
7947
+ continue;
7948
+ }
7949
+ if (depth === 0 && char === ",") {
7950
+ token = "";
7951
+ continue;
7497
7952
  }
7953
+ token += char;
7498
7954
  }
7499
7955
  return keys;
7500
7956
  }
7501
- function sideEffects(source) {
7957
+ function extendedConfig(source, cwd) {
7958
+ if (cwd === void 0) return void 0;
7959
+ for (const line of source.split("\n")) {
7960
+ const match = /^\s*import\s+(?!type\b)(\w+)\s+from\s+['"](\.[^'"]*)['"]/.exec(line);
7961
+ if (match === null) continue;
7962
+ const [, binding, specifier] = match;
7963
+ const candidates = [specifier, `${specifier}.ts`, `${specifier}.js`].map(
7964
+ (name) => name.replace(/^\.\//, "")
7965
+ );
7966
+ const file = candidates.find(
7967
+ (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync27(join33(cwd, name))
7968
+ );
7969
+ if (file === void 0) continue;
7970
+ if (!new RegExp(String.raw`\{\s*\.\.\.${binding}\b`).test(source)) continue;
7971
+ return { binding, file };
7972
+ }
7973
+ return void 0;
7974
+ }
7975
+ function sideEffects(source, extended) {
7502
7976
  const found = [];
7503
7977
  for (const line of source.split("\n")) {
7504
7978
  const valueImport = /^\s*import\s+(?!type\b)(.*)$/.exec(line);
@@ -7507,6 +7981,7 @@ function sideEffects(source) {
7507
7981
  ...(/\{([^}]*)\}/.exec(valueImport[1])?.[1] ?? "").split(","),
7508
7982
  /^\s*(\w+)\s*(?:,|from)/.exec(valueImport[1])?.[1] ?? ""
7509
7983
  ].map((name) => name.split(" as ").pop()?.trim() ?? "").filter((name) => name !== "");
7984
+ if (extended !== void 0 && bindings.includes(extended.binding)) continue;
7510
7985
  const used = bindings.some((name) => {
7511
7986
  const uses = source.match(new RegExp(String.raw`\b${name}\b`, "g")) ?? [];
7512
7987
  return uses.length > 1;
@@ -7515,16 +7990,17 @@ function sideEffects(source) {
7515
7990
  }
7516
7991
  return found;
7517
7992
  }
7518
- function readJestConfig(cwd) {
7519
- const file = JEST_CONFIG_FILES.find((name) => existsSync25(join31(cwd, name)));
7520
- if (file === void 0) return void 0;
7993
+ function readJestConfig(cwd, named) {
7994
+ const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync27(join33(cwd, name)));
7995
+ if (file === void 0 || !existsSync27(join33(cwd, file))) return void 0;
7521
7996
  let source;
7522
7997
  try {
7523
- source = readFileSync25(join31(cwd, file), "utf8");
7998
+ source = readFileSync27(join33(cwd, file), "utf8");
7524
7999
  } catch {
7525
8000
  return { file, refusals: [`${file} could not be read`], notes: [] };
7526
8001
  }
7527
- const refusals = sideEffects(source);
8002
+ const extended = extendedConfig(source, cwd);
8003
+ const refusals = sideEffects(source, extended);
7528
8004
  const body = configBody(source);
7529
8005
  if (body === void 0) {
7530
8006
  return { file, refusals: [...refusals, "its config object could not be located"], notes: [] };
@@ -7554,39 +8030,108 @@ function readJestConfig(cwd) {
7554
8030
  `\`moduleNameMapper\` entry \`${find}\` pointed at another build of the package it matches, which is jest saying it could not load the package as it ships. Vitest can, so the mapping is dropped rather than carried`
7555
8031
  );
7556
8032
  }
8033
+ for (const alias of mapper.aliases) {
8034
+ if (/<rootdir>/i.test(alias.replacement) && !alias.replacement.includes("<rootDir>")) {
8035
+ notes.push(
8036
+ `\`moduleNameMapper\` entry \`${alias.find}\` writes \`${/<[Rr]oot[Dd]ir>/.exec(alias.replacement)?.[0] ?? "<RootDir>"}\`, and jest only substitutes \`<rootDir>\`. That mapping resolved to a literal directory under jest too, so it has never done anything; it is carried unchanged rather than quietly repaired`
8037
+ );
8038
+ }
8039
+ }
7557
8040
  const coverageFrom = readCoverageFrom(body);
7558
8041
  const env = readEnvironment(source);
7559
8042
  const globalSetup = readString(body, "globalSetup");
7560
8043
  const testTimeout = readNumber(body, "testTimeout");
8044
+ const maxWorkers = readNumber(body, "maxWorkers");
7561
8045
  const testMatch = readStringArray(body, "testMatch");
8046
+ const base = extended === void 0 ? void 0 : readJestConfig(cwd, extended.file);
8047
+ const inherited = base === void 0 ? {} : { ...base, refusals: [], notes: [] };
8048
+ for (const [jestKey, field] of [
8049
+ ["testPathIgnorePatterns", "excludeGlobs"],
8050
+ ["setupFilesAfterEnv", "setupFilesAfterEnv"],
8051
+ ["setupFiles", "setupFiles"],
8052
+ ["testMatch", "testMatch"],
8053
+ ["collectCoverageFrom", "coverageFrom"],
8054
+ ["moduleNameMapper", "aliases"]
8055
+ ]) {
8056
+ if (declaredKeys(body).includes(jestKey)) delete inherited[field];
8057
+ }
7562
8058
  return {
8059
+ ...inherited,
7563
8060
  file,
7564
8061
  ...readString(body, "testEnvironment") !== void 0 ? { testEnvironment: readString(body, "testEnvironment") } : {},
7565
8062
  ...readString(body, "coverageDirectory") !== void 0 ? { coverageDirectory: readString(body, "coverageDirectory") } : {},
7566
8063
  ...afterEnv !== void 0 ? { setupFilesAfterEnv: afterEnv } : {},
7567
8064
  ...before !== void 0 ? { setupFiles: before } : {},
7568
8065
  ...testTimeout !== void 0 ? { testTimeout } : {},
8066
+ ...maxWorkers !== void 0 ? { maxWorkers } : {},
7569
8067
  ...globalSetup !== void 0 ? { globalSetup: [globalSetup] } : {},
7570
8068
  ...testMatch !== void 0 ? { testMatch } : {},
7571
8069
  ...coverageFrom !== void 0 ? { coverageFrom } : {},
7572
8070
  ...excludeGlobs.length > 0 ? { excludeGlobs } : {},
7573
8071
  ...mapper.aliases.length > 0 ? { aliases: mapper.aliases } : {},
7574
8072
  ...env.length > 0 ? { env } : {},
7575
- refusals,
7576
- notes
8073
+ refusals: [...base?.refusals ?? [], ...refusals],
8074
+ notes: [...base?.notes ?? [], ...notes]
7577
8075
  };
7578
8076
  }
7579
8077
 
8078
+ // src/roles/test/nest/run-modes.ts
8079
+ import { existsSync as existsSync28 } from "fs";
8080
+ import { basename as basename2, join as join34 } from "path";
8081
+ var JEST_EXECUTOR = "@nx/jest:jest";
8082
+ var PRIMARY_CONFIG_FILE = "vitest.config.mts";
8083
+ var PRIMARY_TARGET = "test";
8084
+ function configName(declared) {
8085
+ return basename2(declared.replaceAll("\\", "/"));
8086
+ }
8087
+ function configFileFor(target) {
8088
+ if (target === PRIMARY_TARGET) return PRIMARY_CONFIG_FILE;
8089
+ const suffix = target.startsWith("test-") ? target.slice("test-".length) : target;
8090
+ return `vitest.${suffix}.config.mts`;
8091
+ }
8092
+ function runModes(cwd, fallback) {
8093
+ const targets = projectTargets(cwd) ?? {};
8094
+ const declared = [];
8095
+ for (const [target, raw] of Object.entries(targets)) {
8096
+ const entry = raw;
8097
+ if (entry.executor !== JEST_EXECUTOR) continue;
8098
+ const jestConfig = entry.options?.jestConfig;
8099
+ if (jestConfig === void 0) continue;
8100
+ const name = configName(jestConfig);
8101
+ if (!existsSync28(join34(cwd, name))) continue;
8102
+ declared.push({
8103
+ target,
8104
+ jestConfig: name,
8105
+ vitestConfig: configFileFor(target),
8106
+ primary: target === PRIMARY_TARGET
8107
+ });
8108
+ }
8109
+ if (declared.length === 0) {
8110
+ const [only] = fallback;
8111
+ return only === void 0 ? [] : [
8112
+ {
8113
+ target: PRIMARY_TARGET,
8114
+ jestConfig: only,
8115
+ vitestConfig: PRIMARY_CONFIG_FILE,
8116
+ primary: true
8117
+ }
8118
+ ];
8119
+ }
8120
+ return declared.sort(
8121
+ (left, right) => left.primary === right.primary ? left.target.localeCompare(right.target) : Number(right.primary) - Number(left.primary)
8122
+ );
8123
+ }
8124
+
7580
8125
  // src/roles/test/jest-migration.ts
7581
8126
  var SPEC_TSCONFIG = "tsconfig.spec.json";
7582
8127
  var VITEST_GLOBAL_TYPES = "vitest/globals";
7583
8128
  function specTypesOperation(cwd) {
7584
- const path = join32(cwd, SPEC_TSCONFIG);
7585
- if (!existsSync26(path)) return void 0;
8129
+ const path = join35(cwd, SPEC_TSCONFIG);
8130
+ if (!existsSync29(path)) return void 0;
7586
8131
  let config;
7587
8132
  try {
7588
8133
  config = parseJsonc(
7589
- readFileSync26(path, "utf8"),
8134
+ readFileSync28(path, "utf8"),
7590
8135
  SPEC_TSCONFIG
7591
8136
  );
7592
8137
  } catch {
@@ -7617,23 +8162,32 @@ function blocked(reason) {
7617
8162
  return { operations: [], blocked: reason };
7618
8163
  }
7619
8164
  function planJestMigration(context, adoption) {
7620
- if (adoption.jestConfigs.length > 1) {
7621
- const [primary, ...rest] = adoption.jestConfigs;
7622
- return blocked(
7623
- `this module has a second run mode (${rest.join(", ")}) beside ${primary}, with its own target. One config file cannot stand for both, and how run modes are expressed under Vitest is an open question here, so this module is not migrated yet.`
7624
- );
7625
- }
7626
- const reading = readJestConfig(context.cwd);
7627
- if (reading === void 0) {
7628
- return blocked(`${adoption.jestConfigs.join(", ")} could not be read, so nothing was written.`);
7629
- }
7630
- const generated = generateNestTestConfig(context.cwd, reading);
7631
- if (generated.source === void 0) {
7632
- return blocked(
7633
- `this module's jest config does not transpose as it stands: ${generated.refusals.join("; ")}. Nothing was written; this needs a look before the migration runs.`
7634
- );
8165
+ const modes = runModes(context.cwd, adoption.jestConfigs);
8166
+ if (modes.length === 0) {
8167
+ return blocked(`no jest config could be found for this module, so nothing was written.`);
8168
+ }
8169
+ const prepared = [];
8170
+ for (const mode of modes) {
8171
+ const reading = readJestConfig(context.cwd, mode.jestConfig);
8172
+ if (reading === void 0) {
8173
+ return blocked(`${mode.jestConfig} could not be read, so nothing was written.`);
8174
+ }
8175
+ const generated = generateNestTestConfig(context.cwd, reading, mode.target);
8176
+ if (generated.source === void 0) {
8177
+ return blocked(
8178
+ `${mode.jestConfig} (the \`${mode.target}\` target) does not transpose as it stands: ${generated.refusals.join("; ")}. Nothing was written; this needs a look before the migration runs.`
8179
+ );
8180
+ }
8181
+ prepared.push({ mode, reading, source: generated.source });
7635
8182
  }
7636
- const migration = migrateModuleTests(context.cwd);
8183
+ const migration = migrateModuleTests(
8184
+ context.cwd,
8185
+ prepared.flatMap(({ reading }) => [
8186
+ ...reading.setupFiles ?? [],
8187
+ ...reading.setupFilesAfterEnv ?? [],
8188
+ ...reading.globalSetup ?? []
8189
+ ])
8190
+ );
7637
8191
  if (migration.blockers.length > 0) {
7638
8192
  const listed = migration.blockers.map((blocker) => `
7639
8193
  ${blocker.file}: ${blocker.reason}`).join("");
@@ -7641,32 +8195,41 @@ function planJestMigration(context, adoption) {
7641
8195
  `${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}`
7642
8196
  );
7643
8197
  }
7644
- const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
8198
+ const targets = projectTargets(context.cwd) ?? {};
8199
+ const existingFor = (name) => targets[name];
7645
8200
  const siblings = siblingScriptRewrites(context.cwd);
7646
8201
  const specTypes = specTypesOperation(context.cwd);
8202
+ const scripts = Object.fromEntries(
8203
+ prepared.map(({ mode }) => [
8204
+ mode.target,
8205
+ mode.primary ? SENTINEL_TEST_COMMAND : `${SENTINEL_TEST_COMMAND} --config ${mode.vitestConfig}`
8206
+ ])
8207
+ );
7647
8208
  const operations = [
7648
- { kind: "write", path: NEST_TEST_CONFIG_FILE, contents: generated.source },
8209
+ ...prepared.map(({ mode, source }) => ({
8210
+ kind: "write",
8211
+ path: mode.vitestConfig,
8212
+ contents: source
8213
+ })),
7649
8214
  ...migration.operations,
7650
8215
  ...specTypes === void 0 ? [] : [specTypes],
7651
- manifestOperation(context.cwd, { [TEST_SCRIPT_NAME]: SENTINEL_TEST_COMMAND, ...siblings }),
7652
- ...nxTargetOperations({
7653
- cwd: context.cwd,
7654
- targets: testTarget({
7655
- configFiles: [NEST_TEST_CONFIG_FILE],
7656
- outputs: existing?.outputs ?? [],
7657
- dependsOn: existing?.dependsOn ?? [],
7658
- configurations: existing?.configurations ?? {}
8216
+ manifestOperation(context.cwd, { ...scripts, ...siblings }),
8217
+ ...prepared.flatMap(
8218
+ ({ mode }) => nxTargetOperations({
8219
+ cwd: context.cwd,
8220
+ targets: testTarget({
8221
+ name: mode.target,
8222
+ configFiles: [mode.vitestConfig],
8223
+ outputs: existingFor(mode.target)?.outputs ?? [],
8224
+ dependsOn: existingFor(mode.target)?.dependsOn ?? [],
8225
+ configurations: existingFor(mode.target)?.configurations ?? {}
8226
+ })
7659
8227
  })
7660
- }),
7661
- ...adoption.jestConfigs.map((name) => ({ kind: "delete", path: name }))
8228
+ ),
8229
+ ...[.../* @__PURE__ */ new Set([...adoption.jestConfigs, ...prepared.map(({ mode }) => mode.jestConfig)])].map(
8230
+ (name) => ({ kind: "delete", path: name })
8231
+ )
7662
8232
  ];
7663
- if (existing !== void 0) {
7664
- operations.push({
7665
- kind: "remove-json-keys",
7666
- path: "project.json",
7667
- keys: [["targets", TEST_SCRIPT_NAME]]
7668
- });
7669
- }
7670
8233
  const notes = [
7671
8234
  `${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` : ""}.`,
7672
8235
  `${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.`
@@ -7679,6 +8242,9 @@ function planJestMigration(context, adoption) {
7679
8242
  notes.push(
7680
8243
  `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.`
7681
8244
  );
8245
+ notes.push(
8246
+ `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.`
8247
+ );
7682
8248
  if (Object.keys(siblings).length > 0) {
7683
8249
  notes.push(`rewrote ${Object.keys(siblings).join(", ")} to run through sentinel.`);
7684
8250
  }
@@ -7819,6 +8385,311 @@ function plan3(context) {
7819
8385
  return { operations, notes };
7820
8386
  }
7821
8387
 
8388
+ // src/roles/test/reference.ts
8389
+ import { execFileSync } from "child_process";
8390
+ import { existsSync as existsSync30, mkdtempSync, readFileSync as readFileSync29, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
8391
+ import { tmpdir } from "os";
8392
+ import { join as join36 } from "path";
8393
+
8394
+ // src/roles/test/baseline.ts
8395
+ function readSnapshot(report) {
8396
+ const files = report?.testResults ?? [];
8397
+ const passing = [];
8398
+ const failing = [];
8399
+ const unloadable = [];
8400
+ for (const file of files) {
8401
+ const assertions = file?.assertionResults ?? [];
8402
+ if (file?.status === "failed" && assertions.length === 0) {
8403
+ unloadable.push(typeof file.name === "string" ? file.name : "an unnamed file");
8404
+ }
8405
+ for (const assertion of assertions) {
8406
+ const name = assertion?.fullName;
8407
+ if (typeof name !== "string" || name === "") continue;
8408
+ if (assertion.status === "passed") passing.push(name);
8409
+ else if (assertion.status === "failed") failing.push(name);
8410
+ }
8411
+ }
8412
+ const claimed = report?.numFailedTests;
8413
+ return {
8414
+ files: files.length,
8415
+ passing: passing.sort(),
8416
+ failing: failing.sort(),
8417
+ unloadable: unloadable.sort(),
8418
+ claimedFailing: typeof claimed === "number" ? claimed : failing.length
8419
+ };
8420
+ }
8421
+ function difference(left, right) {
8422
+ const remaining = /* @__PURE__ */ new Map();
8423
+ for (const name of right) remaining.set(name, (remaining.get(name) ?? 0) + 1);
8424
+ const missing = [];
8425
+ for (const name of left) {
8426
+ const left_ = remaining.get(name) ?? 0;
8427
+ if (left_ === 0) {
8428
+ missing.push(name);
8429
+ continue;
8430
+ }
8431
+ remaining.set(name, left_ - 1);
8432
+ }
8433
+ return missing.sort();
8434
+ }
8435
+ var withoutRenderingQuotes = (name) => name.replaceAll(String.raw`\'`, "'").replaceAll(String.raw`\"`, '"').replaceAll(/['"`]\s*\+\s*['"`]/g, "").replaceAll(/(^|[\s:,[{(])(''|"")($|[\s,)\]}])/g, "$1<empty-string>$3").replaceAll(/['"`]/g, "");
8436
+ function withoutRenderingNoise(name) {
8437
+ const spelled = withSortedKeys(
8438
+ withoutUndefinedEntries(
8439
+ withoutErrorRendering(
8440
+ withoutNaN(
8441
+ withoutTimestamps(withoutRenderingQuotes(withoutClassBodies(withoutStackFrames(name))))
8442
+ )
8443
+ )
8444
+ )
8445
+ );
8446
+ return withoutArrayBrackets(
8447
+ 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, ",")
8448
+ );
8449
+ }
8450
+ function withoutArrayBrackets(name) {
8451
+ return name.replaceAll(
8452
+ /\[([^[\]]*)\]/g,
8453
+ (match, inner) => (
8454
+ // `[Object]` and `[Array]` are the formatter saying it STOPPED, not an array literal.
8455
+ // Stripping their brackets would erase the marker `collapsedOf` pairs on.
8456
+ /^(?:Object|Array)$/.test(inner) ? match : inner.replaceAll(/,\s+/g, ",")
8457
+ )
8458
+ );
8459
+ }
8460
+ var withoutClassBodies = (name) => name.replaceAll(
8461
+ /class\s+([A-Za-z_$][\w$]*)\s*\{\s*\}/g,
8462
+ (_match, className) => `class ${className}`
8463
+ ).replaceAll(
8464
+ /class\s+([A-Za-z_$][\w$]*)[\s\S]*?\n\}/g,
8465
+ (_match, className) => `class ${className}`
8466
+ );
8467
+ var withoutTimestamps = (name) => name.replaceAll(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, "<instant>");
8468
+ var withoutNaN = (name) => name.replaceAll(/\bNaN\b/g, "null");
8469
+ function entriesOf(inner) {
8470
+ const parts = [];
8471
+ let depth = 0;
8472
+ let start = 0;
8473
+ for (let index = 0; index < inner.length; index += 1) {
8474
+ const character = inner[index];
8475
+ if (character === "[" || character === "(") depth += 1;
8476
+ else if (character === "]" || character === ")") depth -= 1;
8477
+ else if (character === "," && depth === 0) {
8478
+ parts.push(inner.slice(start, index));
8479
+ start = index + 1;
8480
+ }
8481
+ if (depth < 0) return void 0;
8482
+ }
8483
+ if (depth !== 0) return void 0;
8484
+ parts.push(inner.slice(start));
8485
+ return parts.map((part) => part.trim()).filter(Boolean);
8486
+ }
8487
+ var sortedGroup = (match, inner) => {
8488
+ if (!inner.includes(":")) return match;
8489
+ const parts = entriesOf(inner);
8490
+ if (parts === void 0) return match;
8491
+ if (parts.length < 2 || !parts.every((part) => part.includes(":"))) return match;
8492
+ return `{${[...parts].sort().join(",")}}`;
8493
+ };
8494
+ var HELD = "\u241F";
8495
+ var withSortedKeys = (name) => {
8496
+ const held = [];
8497
+ let current = name;
8498
+ for (let depth = 0; depth < 24; depth += 1) {
8499
+ const next = current.replaceAll(/\{([^{}]*)\}/g, (match, inner) => {
8500
+ held.push(sortedGroup(match, inner));
8501
+ return `${HELD}${held.length - 1}${HELD}`;
8502
+ });
8503
+ if (next === current) break;
8504
+ current = next;
8505
+ }
8506
+ for (let depth = 0; depth < 24 && current.includes(HELD); depth += 1) {
8507
+ current = current.replaceAll(
8508
+ new RegExp(`${HELD}(\\d+)${HELD}`, "g"),
8509
+ (_match, index) => held[Number(index)]
8510
+ );
8511
+ }
8512
+ return current;
8513
+ };
8514
+ var withoutStackFrames = (name) => {
8515
+ const frame = name.search(/\n\s+at\s/);
8516
+ return frame === -1 ? name : name.slice(0, frame);
8517
+ };
8518
+ var withoutErrorRendering = (name) => name.replaceAll(/\[[A-Za-z]*(?:Error|Exception):[^\][]*\]/g, "{}");
8519
+ 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, "{}");
8520
+ function collapsedOf(candidate, name) {
8521
+ if (!/\[(?:Object|Array)\]/.test(candidate)) return false;
8522
+ const pattern = candidate.split(/\[(?:Object|Array)\]/).map((part) => part.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
8523
+ return new RegExp(`^${pattern}$`, "s").test(name);
8524
+ }
8525
+ function truncationOf(candidate, name) {
8526
+ const cut = candidate.indexOf("\u2026");
8527
+ if (cut === -1) return false;
8528
+ const head = candidate.slice(0, cut);
8529
+ const tail = candidate.slice(cut + 1);
8530
+ return name.startsWith(head) && name.endsWith(tail);
8531
+ }
8532
+ function pairRenamed(missing, added) {
8533
+ const remaining = [...added];
8534
+ const renamed = [];
8535
+ const unmatched = [];
8536
+ for (const name of missing) {
8537
+ const normalised = withoutRenderingNoise(name);
8538
+ const at = remaining.findIndex((candidate) => withoutRenderingNoise(candidate) === normalised);
8539
+ if (at === -1) {
8540
+ unmatched.push(name);
8541
+ continue;
8542
+ }
8543
+ renamed.push({ before: name, after: remaining[at] });
8544
+ remaining.splice(at, 1);
8545
+ }
8546
+ const lost = [];
8547
+ for (const name of unmatched) {
8548
+ const normalised = withoutRenderingNoise(name);
8549
+ const at = remaining.findIndex(
8550
+ (candidate) => truncationOf(withoutRenderingNoise(candidate), normalised) || collapsedOf(normalised, withoutRenderingNoise(candidate))
8551
+ );
8552
+ if (at === -1) {
8553
+ lost.push(name);
8554
+ continue;
8555
+ }
8556
+ renamed.push({ before: name, after: remaining[at] });
8557
+ remaining.splice(at, 1);
8558
+ }
8559
+ return { renamed, missing: lost, added: remaining };
8560
+ }
8561
+ function compareSnapshots(before, after) {
8562
+ const beforeAll = [...before.passing, ...before.failing];
8563
+ const afterAll = [...after.passing, ...after.failing];
8564
+ const paired = pairRenamed(difference(beforeAll, afterAll), difference(afterAll, beforeAll));
8565
+ const renamedBefore = new Set(paired.renamed.map((pair) => pair.before));
8566
+ const regressed = difference(before.passing, after.passing).filter(
8567
+ (name) => !paired.missing.includes(name) && !renamedBefore.has(name)
8568
+ );
8569
+ const failedBefore = new Set(before.failing);
8570
+ const recovered = after.passing.filter((name) => failedBefore.has(name));
8571
+ return {
8572
+ identical: paired.missing.length === 0 && paired.added.length === 0 && regressed.length === 0,
8573
+ missing: paired.missing,
8574
+ added: paired.added,
8575
+ regressed,
8576
+ renamed: paired.renamed,
8577
+ recovered,
8578
+ alreadyFailing: before.failing.length
8579
+ };
8580
+ }
8581
+ function unusableAsBaseline(snapshot) {
8582
+ if (snapshot.unloadable.length > 0) {
8583
+ 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`;
8584
+ }
8585
+ if (snapshot.claimedFailing > snapshot.failing.length) {
8586
+ 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`;
8587
+ }
8588
+ if (snapshot.passing.length === 0) {
8589
+ return `the reference run reports no passing test at all, so a green run after would prove nothing: there is no claim to preserve`;
8590
+ }
8591
+ return void 0;
8592
+ }
8593
+ function describeComparison(comparison) {
8594
+ const renamedNote = comparison.renamed.length > 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. "${comparison.renamed[0]?.before.slice(-40)}" -> "${comparison.renamed[0]?.after.slice(-40)}").` : "";
8595
+ 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.` : "";
8596
+ const recoveredNote = comparison.recovered.length > 0 ? ` ${comparison.recovered.length} test(s) were failing before and pass now.` : "";
8597
+ if (comparison.identical) {
8598
+ return `every test that ran before runs and passes now.${recoveredNote}${renamedNote}${alreadyRedNote}`;
8599
+ }
8600
+ const parts = [];
8601
+ if (comparison.recovered.length > 0) {
8602
+ parts.push(`${comparison.recovered.length} test(s) were failing before and pass now`);
8603
+ }
8604
+ if (comparison.missing.length > 0) {
8605
+ parts.push(
8606
+ `${comparison.missing.length} test(s) no longer exist: ${comparison.missing.slice(0, 3).join(", ")}`
8607
+ );
8608
+ }
8609
+ if (comparison.added.length > 0) {
8610
+ parts.push(
8611
+ `${comparison.added.length} test(s) appeared, which a migration should not do: ${comparison.added.slice(0, 3).join(", ")}`
8612
+ );
8613
+ }
8614
+ if (comparison.regressed.length > 0) {
8615
+ parts.push(
8616
+ `${comparison.regressed.length} test(s) passed before and do not now: ${comparison.regressed.slice(0, 3).join(", ")}`
8617
+ );
8618
+ }
8619
+ return `${parts.join("; ")}${renamedNote}${alreadyRedNote}`;
8620
+ }
8621
+
8622
+ // src/roles/test/reference.ts
8623
+ var REFERENCE_FILE = ".sentinel-test-reference.json";
8624
+ function runJest(cwd, config) {
8625
+ const bin = resolveBin(cwd, "jest");
8626
+ if (bin === void 0) return void 0;
8627
+ const root = findWorkspaceRoot(cwd) ?? cwd;
8628
+ const out = join36(mkdtempSync(join36(tmpdir(), "sentinel-reference-")), "jest.json");
8629
+ try {
8630
+ execFileSync(bin, ["-c", join36(cwd, config), "--json", `--outputFile=${out}`, "--silent"], {
8631
+ cwd: root,
8632
+ stdio: ["ignore", "ignore", "ignore"],
8633
+ maxBuffer: 1 << 28
8634
+ });
8635
+ } catch {
8636
+ }
8637
+ if (!existsSync30(out)) return void 0;
8638
+ try {
8639
+ return JSON.parse(readFileSync29(out, "utf8"));
8640
+ } catch {
8641
+ return void 0;
8642
+ }
8643
+ }
8644
+ function captureReference(cwd, config) {
8645
+ const report = runJest(cwd, config);
8646
+ if (report === void 0) {
8647
+ return {
8648
+ captured: false,
8649
+ 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.`
8650
+ };
8651
+ }
8652
+ const snapshot = readSnapshot(report);
8653
+ const unusable = unusableAsBaseline(snapshot);
8654
+ if (unusable !== void 0) {
8655
+ return {
8656
+ captured: false,
8657
+ message: `no reference recorded: ${unusable} The migration is applied anyway, unproved.`
8658
+ };
8659
+ }
8660
+ const stored = {
8661
+ runner: "jest",
8662
+ config,
8663
+ takenAt: (/* @__PURE__ */ new Date()).toISOString(),
8664
+ snapshot
8665
+ };
8666
+ writeFileSync3(join36(cwd, REFERENCE_FILE), `${JSON.stringify(stored, void 0, 2)}
8667
+ `);
8668
+ return {
8669
+ captured: true,
8670
+ message: `recorded ${snapshot.passing.length} passing test name(s) from ${config} as the reference. The next \`sentinel --run --test\` compares against it.`
8671
+ };
8672
+ }
8673
+ function hasReference(cwd) {
8674
+ return existsSync30(join36(cwd, REFERENCE_FILE));
8675
+ }
8676
+ function compareAgainstReference(cwd, report) {
8677
+ const path = join36(cwd, REFERENCE_FILE);
8678
+ let stored;
8679
+ try {
8680
+ stored = JSON.parse(readFileSync29(path, "utf8"));
8681
+ } catch {
8682
+ rmSync3(path, { force: true });
8683
+ return { ok: true, message: `the recorded reference could not be read, so nothing was proved.` };
8684
+ }
8685
+ const comparison = compareSnapshots(stored.snapshot, readSnapshot(report));
8686
+ rmSync3(path, { force: true });
8687
+ return {
8688
+ ok: comparison.identical,
8689
+ message: `${describeComparison(comparison)} (reference: ${stored.config}, ${stored.takenAt})`
8690
+ };
8691
+ }
8692
+
7822
8693
  // src/roles/test/adapters/vitest/vitest.adapter.ts
7823
8694
  function resolveVitest(cwd) {
7824
8695
  const fromSentinel = binFromOwnInstall("vitest", "vitest");
@@ -7832,6 +8703,22 @@ var VitestAdapter = class extends BaseAdapter {
7832
8703
  appliesTo(_preset) {
7833
8704
  return true;
7834
8705
  }
8706
+ /**
8707
+ * Record what this module's tests do TODAY, at the last moment it is still true.
8708
+ *
8709
+ * The plan about to be applied deletes the jest config, and with it any way of asking again. A
8710
+ * migration that cannot be compared to what came before is a migration nobody can check, which
8711
+ * is the whole reason this role has a baseline gate at all.
8712
+ */
8713
+ async beforeInit(ctx) {
8714
+ const adoption = readTestAdoption(ctx.cwd);
8715
+ if (adoption.state !== "jest") return Promise.resolve();
8716
+ const [config] = adoption.jestConfigs;
8717
+ if (config === void 0) return Promise.resolve();
8718
+ this.say(`recording the jest reference before migrating...`);
8719
+ this.say(captureReference(ctx.cwd, config).message);
8720
+ return Promise.resolve();
8721
+ }
7835
8722
  plan(context) {
7836
8723
  return plan3(context);
7837
8724
  }
@@ -7878,13 +8765,31 @@ var VitestAdapter = class extends BaseAdapter {
7878
8765
  }
7879
8766
  const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
7880
8767
  const watching = options.includes("--watch") || options.includes("-w");
7881
- const argv = watching ? [...options, ...paths] : ["run", ...options, ...paths];
8768
+ const proving = !watching && hasReference(ctx.cwd);
8769
+ const reportFile = proving ? join37(mkdtempSync2(join37(tmpdir2(), "sentinel-run-")), "vitest.json") : void 0;
8770
+ const reporters = reportFile === void 0 ? [] : ["--reporter=default", "--reporter=json", `--outputFile=${reportFile}`];
8771
+ const argv = watching ? [...options, ...paths] : ["run", ...reporters, ...options, ...paths];
7882
8772
  const result = spawnSync5(bin, argv, { cwd: ctx.cwd, stdio: "inherit" });
7883
8773
  if (result.error) {
7884
8774
  this.say(`could not run vitest (${result.error.message})`);
7885
8775
  return Promise.resolve({ ok: false, code: 1 });
7886
8776
  }
7887
8777
  const code = result.status ?? 1;
8778
+ if (reportFile !== void 0) {
8779
+ let report;
8780
+ try {
8781
+ report = JSON.parse(readFileSync30(reportFile, "utf8"));
8782
+ } catch {
8783
+ report = void 0;
8784
+ }
8785
+ if (report === void 0) {
8786
+ this.say(`vitest wrote no report, so this run proved nothing against the reference.`);
8787
+ } else {
8788
+ const outcome = compareAgainstReference(ctx.cwd, report);
8789
+ this.say(outcome.message);
8790
+ if (!outcome.ok) return Promise.resolve({ ok: false, code: code === 0 ? 1 : code });
8791
+ }
8792
+ }
7888
8793
  return Promise.resolve({ ok: code === 0, code });
7889
8794
  }
7890
8795
  say(message) {
@@ -7901,9 +8806,9 @@ function registerTest() {
7901
8806
 
7902
8807
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
7903
8808
  import { spawnSync as spawnSync6 } from "child_process";
7904
- import { existsSync as existsSync27, readFileSync as readFileSync28 } from "fs";
7905
- import { createRequire as createRequire4 } from "module";
7906
- import { join as join34 } from "path";
8809
+ import { existsSync as existsSync31, readFileSync as readFileSync32 } from "fs";
8810
+ import { createRequire as createRequire6 } from "module";
8811
+ import { join as join39 } from "path";
7907
8812
 
7908
8813
  // src/roles/typescript/presets/nest.json
7909
8814
  var nest_default2 = {
@@ -8084,8 +8989,8 @@ function readTsconfigAdoption(cwd) {
8084
8989
  }
8085
8990
 
8086
8991
  // src/roles/typescript/adapters/tsc/plan.ts
8087
- import { readFileSync as readFileSync27 } from "fs";
8088
- import { join as join33 } from "path";
8992
+ import { readFileSync as readFileSync31 } from "fs";
8993
+ import { join as join38 } from "path";
8089
8994
 
8090
8995
  // src/roles/typescript/typecheck-script.ts
8091
8996
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -8178,7 +9083,7 @@ function planAdoption(context) {
8178
9083
  };
8179
9084
  }
8180
9085
  const existing = parseJsonc(
8181
- readFileSync27(join33(context.cwd, target.path), "utf8"),
9086
+ readFileSync31(join38(context.cwd, target.path), "utf8"),
8182
9087
  target.path
8183
9088
  );
8184
9089
  const extendsChain = composeExtends(existing.extends, preset);
@@ -8316,7 +9221,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8316
9221
  let chain;
8317
9222
  try {
8318
9223
  const parsed = parseJsonc(
8319
- readFileSync28(join34(cwd, target.path), "utf8"),
9224
+ readFileSync32(join39(cwd, target.path), "utf8"),
8320
9225
  target.path
8321
9226
  );
8322
9227
  chain = parsed.extends;
@@ -8329,7 +9234,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8329
9234
  );
8330
9235
  if (preset === void 0) return void 0;
8331
9236
  try {
8332
- createRequire4(join34(cwd, "noop.js")).resolve(preset);
9237
+ createRequire6(join39(cwd, "noop.js")).resolve(preset);
8333
9238
  return void 0;
8334
9239
  } catch {
8335
9240
  return preset;
@@ -8425,7 +9330,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8425
9330
  referencedProjects(cwd, config) {
8426
9331
  try {
8427
9332
  const parsed = parseJsonc(
8428
- readFileSync28(join34(cwd, config), "utf8"),
9333
+ readFileSync32(join39(cwd, config), "utf8"),
8429
9334
  config
8430
9335
  );
8431
9336
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -8441,7 +9346,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
8441
9346
  * check.
8442
9347
  */
8443
9348
  typecheckTarget(cwd) {
8444
- if (existsSync27(join34(cwd, "tsconfig.json"))) return "tsconfig.json";
9349
+ if (existsSync31(join39(cwd, "tsconfig.json"))) return "tsconfig.json";
8445
9350
  const target = resolveTsconfigTarget(cwd);
8446
9351
  return target.reason === "none" ? null : target.path;
8447
9352
  }
@@ -8590,7 +9495,7 @@ function detectFramework(packageJson) {
8590
9495
  }
8591
9496
 
8592
9497
  // src/core/dispatch.ts
8593
- import { basename as basename2 } from "path";
9498
+ import { basename as basename3 } from "path";
8594
9499
 
8595
9500
  // src/shared/text.ts
8596
9501
  function ensureLines(current, lines) {
@@ -8646,7 +9551,7 @@ function replaceLines(current, replacements) {
8646
9551
  }
8647
9552
 
8648
9553
  // src/core/apply-plan.ts
8649
- import { existsSync as existsSync28, readFileSync as readFileSync29, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
9554
+ import { existsSync as existsSync32, readFileSync as readFileSync33, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
8650
9555
  import { resolve as resolve5, sep } from "path";
8651
9556
  import { applyEdits as applyEdits3, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
8652
9557
 
@@ -8665,7 +9570,7 @@ function resolveWithinRoot(cwd, relativePath) {
8665
9570
  return absolutePath;
8666
9571
  }
8667
9572
  function readIfExists(absolutePath) {
8668
- return existsSync28(absolutePath) ? readFileSync29(absolutePath, "utf8") : void 0;
9573
+ return existsSync32(absolutePath) ? readFileSync33(absolutePath, "utf8") : void 0;
8669
9574
  }
8670
9575
  function* leaves(value, prefix = []) {
8671
9576
  for (const [key, keyValue] of Object.entries(value)) {
@@ -8767,14 +9672,14 @@ function preparePlan(cwd, plan4) {
8767
9672
  }
8768
9673
  function writeFileAtomic(absolutePath, contents) {
8769
9674
  const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
8770
- writeFileSync3(tempPath, contents);
9675
+ writeFileSync4(tempPath, contents);
8771
9676
  renameSync(tempPath, absolutePath);
8772
9677
  }
8773
9678
  function applyPlan(cwd, plan4) {
8774
9679
  const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
8775
9680
  for (const file of changed) {
8776
9681
  if (file.deleted) {
8777
- rmSync3(file.absolutePath, { force: true });
9682
+ rmSync4(file.absolutePath, { force: true });
8778
9683
  continue;
8779
9684
  }
8780
9685
  writeFileAtomic(file.absolutePath, file.after);
@@ -8783,8 +9688,8 @@ function applyPlan(cwd, plan4) {
8783
9688
  }
8784
9689
 
8785
9690
  // src/core/config/preset-evidence.ts
8786
- import { existsSync as existsSync29, readdirSync as readdirSync7, readFileSync as readFileSync30 } from "fs";
8787
- import { join as join35 } from "path";
9691
+ import { existsSync as existsSync33, readdirSync as readdirSync8, readFileSync as readFileSync34 } from "fs";
9692
+ import { join as join40 } from "path";
8788
9693
  var PATH_SIGNALS = [
8789
9694
  {
8790
9695
  preset: "nest",
@@ -8812,18 +9717,18 @@ function sourceImports(cwd, pattern) {
8812
9717
  const dir = stack.pop();
8813
9718
  let entries;
8814
9719
  try {
8815
- entries = readdirSync7(dir, { withFileTypes: true });
9720
+ entries = readdirSync8(dir, { withFileTypes: true });
8816
9721
  } catch {
8817
9722
  continue;
8818
9723
  }
8819
9724
  for (const entry of entries) {
8820
9725
  if (entry.isDirectory()) {
8821
- if (!NOT_SCANNED.has(entry.name)) stack.push(join35(dir, entry.name));
9726
+ if (!NOT_SCANNED.has(entry.name)) stack.push(join40(dir, entry.name));
8822
9727
  continue;
8823
9728
  }
8824
9729
  if (!SOURCE_FILE2.test(entry.name)) continue;
8825
9730
  try {
8826
- if (pattern.test(readFileSync30(join35(dir, entry.name), "utf8"))) return true;
9731
+ if (pattern.test(readFileSync34(join40(dir, entry.name), "utf8"))) return true;
8827
9732
  } catch {
8828
9733
  continue;
8829
9734
  }
@@ -8832,11 +9737,11 @@ function sourceImports(cwd, pattern) {
8832
9737
  return false;
8833
9738
  }
8834
9739
  function dependencyNames(cwd) {
8835
- const path = join35(cwd, "package.json");
8836
- if (!existsSync29(path)) return [];
9740
+ const path = join40(cwd, "package.json");
9741
+ if (!existsSync33(path)) return [];
8837
9742
  try {
8838
9743
  const manifest = parseJsonc(
8839
- readFileSync30(path, "utf8"),
9744
+ readFileSync34(path, "utf8"),
8840
9745
  path
8841
9746
  );
8842
9747
  return [
@@ -8850,7 +9755,7 @@ function dependencyNames(cwd) {
8850
9755
  function declaresJsx(cwd) {
8851
9756
  let entries;
8852
9757
  try {
8853
- entries = readdirSync7(cwd).filter(
9758
+ entries = readdirSync8(cwd).filter(
8854
9759
  (name) => name.startsWith("tsconfig") && name.endsWith(".json")
8855
9760
  );
8856
9761
  } catch {
@@ -8859,7 +9764,7 @@ function declaresJsx(cwd) {
8859
9764
  for (const name of entries) {
8860
9765
  try {
8861
9766
  const config = parseJsonc(
8862
- readFileSync30(join35(cwd, name), "utf8"),
9767
+ readFileSync34(join40(cwd, name), "utf8"),
8863
9768
  name
8864
9769
  );
8865
9770
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -9004,6 +9909,15 @@ async function dispatch(opts) {
9004
9909
  if (opts.dryRun) {
9005
9910
  return previewPlan(opts, plan4);
9006
9911
  }
9912
+ if (adapter.beforeInit) {
9913
+ await adapter.beforeInit({
9914
+ module: basename3(opts.cwd),
9915
+ cwd: opts.cwd,
9916
+ preset,
9917
+ ci: false,
9918
+ fix: false
9919
+ });
9920
+ }
9007
9921
  for (const change of applyPlan(opts.cwd, plan4)) {
9008
9922
  process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
9009
9923
  `);
@@ -9014,7 +9928,7 @@ async function dispatch(opts) {
9014
9928
  process.stderr.write(` fixing what ${opts.target} can fix automatically...
9015
9929
  `);
9016
9930
  await adapter.afterInit({
9017
- module: basename2(opts.cwd),
9931
+ module: basename3(opts.cwd),
9018
9932
  cwd: opts.cwd,
9019
9933
  preset,
9020
9934
  ci: false,
@@ -9047,4 +9961,4 @@ export {
9047
9961
  detectFramework,
9048
9962
  dispatch
9049
9963
  };
9050
- //# sourceMappingURL=chunk-O3XCQGTJ.js.map
9964
+ //# sourceMappingURL=chunk-MUQWAZQN.js.map