@hublo/sentinel 1.4.0-alpha.23 → 1.4.0-alpha.24

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.
@@ -15,14 +15,12 @@ import {
15
15
  palette,
16
16
  registerAdapters,
17
17
  resolve,
18
- resolveBin
19
- } from "../chunk-7ADFV7P4.js";
18
+ resolveContext
19
+ } from "../chunk-U744PK2U.js";
20
20
  import {
21
- WORKSPACE_ROOT_MARKER,
22
21
  ensureWorkspacePrep,
23
22
  findWorkspaceRoot,
24
23
  inspectWorkspacePrep,
25
- moduleName,
26
24
  readOwnVersion,
27
25
  readProjectPackageJson
28
26
  } from "../chunk-NW6UHNYX.js";
@@ -30,102 +28,15 @@ import {
30
28
  // bin/sentinel.ts
31
29
  import { program } from "commander";
32
30
 
33
- // src/core/context.ts
34
- import { existsSync } from "node:fs";
35
- import { join as join2 } from "node:path";
36
-
37
- // src/core/discover-modules.ts
38
- import { execFileSync } from "node:child_process";
39
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
40
- import { tmpdir } from "node:os";
41
- import { join } from "node:path";
42
- function runNx(cwd2, args) {
43
- const nx = resolveBin(cwd2, "nx") ?? "nx";
44
- try {
45
- return execFileSync(nx, args, {
46
- cwd: cwd2,
47
- encoding: "utf8",
48
- env: { ...process.env, NX_DAEMON: "false" }
49
- });
50
- } catch (error) {
51
- const message = error instanceof Error ? error.message : String(error);
52
- throw new Error(
53
- `sentinel: could not run nx (${message}). Is nx installed in this workspace, and are you at its root?`,
54
- { cause: error }
55
- );
56
- }
57
- }
58
- function readGraph(cwd2) {
59
- const dir = mkdtempSync(join(tmpdir(), "sentinel-nx-"));
60
- const file = join(dir, "graph.json");
61
- try {
62
- runNx(cwd2, ["graph", "--file", file]);
63
- const parsed = JSON.parse(readFileSync(file, "utf8"));
64
- const nodes = parsed.graph?.nodes;
65
- if (!nodes || typeof nodes !== "object") {
66
- throw new Error(
67
- "sentinel: unexpected nx graph output (no graph.nodes); the installed nx version may be incompatible."
68
- );
69
- }
70
- return Object.entries(nodes).map(([name, node]) => ({
71
- name,
72
- root: join(cwd2, node.data.root)
73
- }));
74
- } finally {
75
- rmSync(dir, { recursive: true, force: true });
76
- }
77
- }
78
- function discoverModules(cwd2, options = {}) {
79
- const modules = readGraph(cwd2);
80
- if (!options.affected) return modules;
81
- const affected = new Set(
82
- JSON.parse(runNx(cwd2, ["show", "projects", "--affected", "--json"]))
83
- );
84
- return modules.filter((module) => affected.has(module.name));
85
- }
86
-
87
- // src/core/context.ts
88
- var MODULE_MARKERS = ["package.json", "project.json"];
89
- function isModuleDir(cwd2) {
90
- return MODULE_MARKERS.some((marker) => existsSync(join2(cwd2, marker)));
91
- }
92
- function resolveContext(cwd2, opts2) {
93
- const atRoot = existsSync(join2(cwd2, WORKSPACE_ROOT_MARKER));
94
- if (!atRoot && isModuleDir(cwd2)) {
95
- if (opts2.module) {
96
- throw new Error(
97
- "You are in a module directory: drop --module (the context is the current module)."
98
- );
99
- }
100
- if (opts2.ci) {
101
- throw new Error("--ci selects the affected set from the workspace root; run it there.");
102
- }
103
- const name = moduleName(cwd2);
104
- return { modules: [{ name, root: cwd2 }], scope: "cwd-module" };
105
- }
106
- if (atRoot) {
107
- if (opts2.module) {
108
- const found = discoverModules(cwd2).find((module) => module.name === opts2.module);
109
- if (!found) throw new Error(`module "${opts2.module}" not found in the workspace.`);
110
- return { modules: [found], scope: "named-module" };
111
- }
112
- if (opts2.ci) return { modules: discoverModules(cwd2, { affected: true }), scope: "affected" };
113
- return { modules: discoverModules(cwd2), scope: "all" };
114
- }
115
- throw new Error(
116
- `Run sentinel from a module directory or the workspace root (found neither ${MODULE_MARKERS.join("/")} nor ${WORKSPACE_ROOT_MARKER} here).`
117
- );
118
- }
119
-
120
31
  // src/roles/build/declined.ts
121
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
122
- import { join as join3 } from "node:path";
32
+ import { existsSync, readFileSync } from "node:fs";
33
+ import { join } from "node:path";
123
34
  var DEPRECATED_NX_BUILDER = "@nx/webpack:webpack";
124
35
  function buildsWithDeprecatedExecutor(cwd2) {
125
- const projectJson = join3(cwd2, "project.json");
126
- if (!existsSync2(projectJson)) return false;
36
+ const projectJson = join(cwd2, "project.json");
37
+ if (!existsSync(projectJson)) return false;
127
38
  try {
128
- const project = JSON.parse(readFileSync2(projectJson, "utf8"));
39
+ const project = JSON.parse(readFileSync(projectJson, "utf8"));
129
40
  return project.targets?.build?.executor === DEPRECATED_NX_BUILDER;
130
41
  } catch {
131
42
  return false;
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  DEFAULT_MAX_DIAGNOSTICS,
3
+ WORKSPACE_ROOT_MARKER,
3
4
  findWorkspaceRoot,
4
5
  moduleName,
5
6
  readNxProjectName,
@@ -561,9 +562,9 @@ function binFromOwnInstall(packageName, binName) {
561
562
  try {
562
563
  const manifest = require2.resolve(`${packageName}/package.json`);
563
564
  const bin = require2(manifest).bin;
564
- const relative8 = typeof bin === "string" ? bin : bin?.[binName];
565
- if (!relative8) return void 0;
566
- const executable = join5(dirname(manifest), relative8);
565
+ const relative9 = typeof bin === "string" ? bin : bin?.[binName];
566
+ if (!relative9) return void 0;
567
+ const executable = join5(dirname(manifest), relative9);
567
568
  return existsSync6(executable) ? executable : void 0;
568
569
  } catch {
569
570
  return void 0;
@@ -5406,7 +5407,7 @@ function registerLint() {
5406
5407
 
5407
5408
  // src/roles/test/adapters/vitest/vitest.adapter.ts
5408
5409
  import { spawnSync as spawnSync5 } from "node:child_process";
5409
- import { isAbsolute as isAbsolute3, resolve as resolve6 } from "node:path";
5410
+ import { isAbsolute as isAbsolute3, resolve as resolve9 } from "node:path";
5410
5411
 
5411
5412
  // src/roles/test/presets/toolchain.json
5412
5413
  var toolchain_default2 = {
@@ -5483,13 +5484,13 @@ function testTarget(options) {
5483
5484
  }
5484
5485
 
5485
5486
  // src/roles/test/jest-migration.ts
5486
- import { existsSync as existsSync30, readFileSync as readFileSync29, readdirSync as readdirSync8 } from "node:fs";
5487
+ import { existsSync as existsSync32, readFileSync as readFileSync31, readdirSync as readdirSync8 } from "node:fs";
5487
5488
  import { createRequire as createRequire6 } from "node:module";
5488
- import { dirname as dirname8, join as join37, relative as relative7 } from "node:path";
5489
+ import { dirname as dirname11, join as join40, relative as relative8 } from "node:path";
5489
5490
 
5490
5491
  // src/roles/test/codemod/migrate-module.ts
5491
- import { readdirSync as readdirSync6, readFileSync as readFileSync26, statSync as statSync6 } from "node:fs";
5492
- import { join as join32 } from "node:path";
5492
+ import { readdirSync as readdirSync6, readFileSync as readFileSync28, statSync as statSync6 } from "node:fs";
5493
+ import { join as join35 } from "node:path";
5493
5494
  import { parseSync as parseSync8 } from "oxc-parser";
5494
5495
 
5495
5496
  // src/roles/test/msw-lifecycle.ts
@@ -5635,15 +5636,16 @@ function readTestAdoption(cwd) {
5635
5636
  // src/roles/test/support-file-name.ts
5636
5637
  var JEST_PREFIX = "jest.";
5637
5638
  var VITEST_PREFIX = "vitest.";
5638
- function vitestSupportName(relative8) {
5639
- const cut = relative8.lastIndexOf("/");
5640
- const directory = cut === -1 ? "" : relative8.slice(0, cut + 1);
5641
- const base = relative8.slice(cut + 1);
5642
- if (!base.startsWith(JEST_PREFIX)) return relative8;
5639
+ function vitestSupportName(relative9) {
5640
+ const cut = relative9.lastIndexOf("/");
5641
+ const directory = cut === -1 ? "" : relative9.slice(0, cut + 1);
5642
+ const base = relative9.slice(cut + 1);
5643
+ if (!base.startsWith(JEST_PREFIX)) return relative9;
5643
5644
  return `${directory}${VITEST_PREFIX}${base.slice(JEST_PREFIX.length)}`;
5644
5645
  }
5645
5646
 
5646
5647
  // src/roles/test/codemod/migrate-file.ts
5648
+ import { dirname as dirname8 } from "node:path";
5647
5649
  import { parseSync as parseSync7 } from "oxc-parser";
5648
5650
 
5649
5651
  // src/roles/test/codemod/assertion-return-value.ts
@@ -6006,6 +6008,24 @@ var FAKEABLE = [
6006
6008
  "cancelIdleCallback",
6007
6009
  "performance"
6008
6010
  ];
6011
+ function withSeparator(source, options, property) {
6012
+ const properties = options.properties ?? [];
6013
+ const index = properties.indexOf(property);
6014
+ const start = property.start;
6015
+ const end = property.end;
6016
+ if (index < properties.length - 1) {
6017
+ const next = properties[index + 1]?.start;
6018
+ const between2 = next === void 0 ? "" : source.slice(end, next);
6019
+ const comma2 = between2.indexOf(",");
6020
+ if (comma2 >= 0) return { start, end: end + comma2 + 1 };
6021
+ return { start, end };
6022
+ }
6023
+ const previous = properties[index - 1]?.end;
6024
+ if (previous === void 0) return { start, end };
6025
+ const between = source.slice(previous, start);
6026
+ const comma = between.lastIndexOf(",");
6027
+ return comma >= 0 ? { start: previous + comma, end } : { start, end };
6028
+ }
6009
6029
  function stringLiterals2(node) {
6010
6030
  if (node.type !== "ArrayExpression") return void 0;
6011
6031
  const values = [];
@@ -6019,6 +6039,7 @@ function rewriteFakeTimerOptions(source, program) {
6019
6039
  const edits = [];
6020
6040
  const translated = [];
6021
6041
  const refusals = [];
6042
+ const warnings = [];
6022
6043
  walk(program, (node) => {
6023
6044
  const options = timerOptions(node);
6024
6045
  if (options === void 0) return;
@@ -6048,6 +6069,16 @@ function rewriteFakeTimerOptions(source, program) {
6048
6069
  });
6049
6070
  continue;
6050
6071
  }
6072
+ if (key.name === "legacyFakeTimers") {
6073
+ const asked = value.type === "Literal" && value.value === true;
6074
+ edits.push({ ...withSeparator(source, options, property), text: "" });
6075
+ if (asked) {
6076
+ warnings.push(
6077
+ "`useFakeTimers({ legacyFakeTimers: true })` was REMOVED, not translated: it selects jest's pre-27 timer implementation, and Vitest installs sinon's clock with no equivalent. The call now uses the modern clock, which is what jest itself defaults to. Re-read any test that depended on the old one."
6078
+ );
6079
+ }
6080
+ continue;
6081
+ }
6051
6082
  if (key.name === "doNotFake") {
6052
6083
  const excluded = stringLiterals2(value);
6053
6084
  if (excluded === void 0) {
@@ -6066,12 +6097,12 @@ function rewriteFakeTimerOptions(source, program) {
6066
6097
  }
6067
6098
  }
6068
6099
  });
6069
- if (edits.length === 0) return { source, translated, refusals };
6100
+ if (edits.length === 0) return { source, translated, refusals, warnings };
6070
6101
  let result = source;
6071
6102
  for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
6072
6103
  result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
6073
6104
  }
6074
- return { source: result, translated, refusals };
6105
+ return { source: result, translated, refusals, warnings };
6075
6106
  }
6076
6107
 
6077
6108
  // src/roles/test/codemod/scope.ts
@@ -6135,9 +6166,9 @@ function patternNames(pattern) {
6135
6166
  }
6136
6167
  function moduleBindings(program) {
6137
6168
  const bindings = /* @__PURE__ */ new Map();
6138
- for (const statement of program.body ?? []) {
6139
- if (statement.type !== "VariableDeclaration") continue;
6140
- for (const declarator of statement.declarations ?? []) {
6169
+ for (const statement2 of program.body ?? []) {
6170
+ if (statement2.type !== "VariableDeclaration") continue;
6171
+ for (const declarator of statement2.declarations ?? []) {
6141
6172
  const init = declarator.init ?? void 0;
6142
6173
  for (const name of patternNames(declarator.id)) bindings.set(name, init);
6143
6174
  }
@@ -6146,9 +6177,9 @@ function moduleBindings(program) {
6146
6177
  }
6147
6178
  function importedNames(program) {
6148
6179
  const names = /* @__PURE__ */ new Set();
6149
- for (const statement of program.body ?? []) {
6150
- if (statement.type !== "ImportDeclaration") continue;
6151
- for (const specifier of statement.specifiers ?? []) {
6180
+ for (const statement2 of program.body ?? []) {
6181
+ if (statement2.type !== "ImportDeclaration") continue;
6182
+ for (const specifier of statement2.specifiers ?? []) {
6152
6183
  const local = specifier.local;
6153
6184
  if (local?.type === "Identifier") names.add(local.name);
6154
6185
  }
@@ -6205,15 +6236,15 @@ function hoistable(name, bindings, imported, seen = /* @__PURE__ */ new Set()) {
6205
6236
  // src/roles/test/codemod/hoist.ts
6206
6237
  function simpleDeclarations(program) {
6207
6238
  const found = /* @__PURE__ */ new Map();
6208
- for (const statement of program.body ?? []) {
6209
- if (statement.type !== "VariableDeclaration") continue;
6210
- const declarations = statement.declarations ?? [];
6239
+ for (const statement2 of program.body ?? []) {
6240
+ if (statement2.type !== "VariableDeclaration") continue;
6241
+ const declarations = statement2.declarations ?? [];
6211
6242
  if (declarations.length !== 1) continue;
6212
6243
  const declarator = declarations[0];
6213
6244
  const id = declarator.id;
6214
6245
  const init = declarator.init;
6215
6246
  if (id?.type !== "Identifier" || init === void 0 || init === null) continue;
6216
- found.set(id.name, { statement, init, kind: statement.kind ?? "const" });
6247
+ found.set(id.name, { statement: statement2, init, kind: statement2.kind ?? "const" });
6217
6248
  }
6218
6249
  return found;
6219
6250
  }
@@ -6225,11 +6256,11 @@ function interdependent(names, bindings) {
6225
6256
  }
6226
6257
  return void 0;
6227
6258
  }
6228
- function statementSpan(source, statement) {
6229
- let end = statement.end;
6259
+ function statementSpan(source, statement2) {
6260
+ let end = statement2.end;
6230
6261
  if (source[end] === "\r") end++;
6231
6262
  if (source[end] === "\n") end++;
6232
- return { start: statement.start, end };
6263
+ return { start: statement2.start, end };
6233
6264
  }
6234
6265
  function hoistCapturedBindings(source, program, factories, bindings, imported) {
6235
6266
  const captured = [...new Set(factories.flatMap((factory) => capturedBindings(factory, bindings)))];
@@ -6284,8 +6315,8 @@ function hoistCapturedBindings(source, program, factories, bindings, imported) {
6284
6315
  `);
6285
6316
  }
6286
6317
  const hoistedLine = lines.join("");
6287
- const edits = ordered.map(({ statement }, index) => {
6288
- const span = statementSpan(source, statement);
6318
+ const edits = ordered.map(({ statement: statement2 }, index) => {
6319
+ const span = statementSpan(source, statement2);
6289
6320
  return { ...span, text: index === 0 ? hoistedLine : "" };
6290
6321
  });
6291
6322
  let rewritten = source;
@@ -7346,6 +7377,220 @@ function repointMswServer(fileName, source) {
7346
7377
  return { source: rewritten, repointed: true };
7347
7378
  }
7348
7379
 
7380
+ // src/roles/test/codemod/setup-static-imports.ts
7381
+ import { dirname as dirname7, resolve as resolve6 } from "node:path";
7382
+
7383
+ // src/core/context.ts
7384
+ import { existsSync as existsSync26 } from "node:fs";
7385
+ import { dirname as dirname6, join as join32, resolve as resolve4 } from "node:path";
7386
+
7387
+ // src/core/discover-modules.ts
7388
+ import { execFileSync } from "node:child_process";
7389
+ import { mkdtempSync, readFileSync as readFileSync25, rmSync as rmSync3 } from "node:fs";
7390
+ import { tmpdir } from "node:os";
7391
+ import { join as join31 } from "node:path";
7392
+ function runNx(cwd, args) {
7393
+ const nx = resolveBin(cwd, "nx") ?? "nx";
7394
+ try {
7395
+ return execFileSync(nx, args, {
7396
+ cwd,
7397
+ encoding: "utf8",
7398
+ env: { ...process.env, NX_DAEMON: "false" }
7399
+ });
7400
+ } catch (error) {
7401
+ const message = error instanceof Error ? error.message : String(error);
7402
+ throw new Error(
7403
+ `sentinel: could not run nx (${message}). Is nx installed in this workspace, and are you at its root?`,
7404
+ { cause: error }
7405
+ );
7406
+ }
7407
+ }
7408
+ function readGraph(cwd) {
7409
+ const dir = mkdtempSync(join31(tmpdir(), "sentinel-nx-"));
7410
+ const file = join31(dir, "graph.json");
7411
+ try {
7412
+ runNx(cwd, ["graph", "--file", file]);
7413
+ const parsed = JSON.parse(readFileSync25(file, "utf8"));
7414
+ const nodes = parsed.graph?.nodes;
7415
+ if (!nodes || typeof nodes !== "object") {
7416
+ throw new Error(
7417
+ "sentinel: unexpected nx graph output (no graph.nodes); the installed nx version may be incompatible."
7418
+ );
7419
+ }
7420
+ return Object.entries(nodes).map(([name, node]) => ({
7421
+ name,
7422
+ root: join31(cwd, node.data.root)
7423
+ }));
7424
+ } finally {
7425
+ rmSync3(dir, { recursive: true, force: true });
7426
+ }
7427
+ }
7428
+ function discoverModules(cwd, options = {}) {
7429
+ const modules = readGraph(cwd);
7430
+ if (!options.affected) return modules;
7431
+ const affected = new Set(
7432
+ JSON.parse(runNx(cwd, ["show", "projects", "--affected", "--json"]))
7433
+ );
7434
+ return modules.filter((module) => affected.has(module.name));
7435
+ }
7436
+
7437
+ // src/core/context.ts
7438
+ var MODULE_MARKERS = ["package.json", "project.json"];
7439
+ function isModuleDir(cwd) {
7440
+ return MODULE_MARKERS.some((marker) => existsSync26(join32(cwd, marker)));
7441
+ }
7442
+ function findModuleRoot(startDir, workspaceRoot) {
7443
+ let dir = resolve4(startDir);
7444
+ for (; ; ) {
7445
+ if (workspaceRoot !== void 0 && dir === resolve4(workspaceRoot)) return void 0;
7446
+ if (isModuleDir(dir)) return dir;
7447
+ const parent = dirname6(dir);
7448
+ if (parent === dir) return void 0;
7449
+ dir = parent;
7450
+ }
7451
+ }
7452
+ function resolveContext(cwd, opts) {
7453
+ const atRoot = existsSync26(join32(cwd, WORKSPACE_ROOT_MARKER));
7454
+ if (!atRoot && isModuleDir(cwd)) {
7455
+ if (opts.module) {
7456
+ throw new Error(
7457
+ "You are in a module directory: drop --module (the context is the current module)."
7458
+ );
7459
+ }
7460
+ if (opts.ci) {
7461
+ throw new Error("--ci selects the affected set from the workspace root; run it there.");
7462
+ }
7463
+ const name = moduleName(cwd);
7464
+ return { modules: [{ name, root: cwd }], scope: "cwd-module" };
7465
+ }
7466
+ if (atRoot) {
7467
+ if (opts.module) {
7468
+ const found = discoverModules(cwd).find((module) => module.name === opts.module);
7469
+ if (!found) throw new Error(`module "${opts.module}" not found in the workspace.`);
7470
+ return { modules: [found], scope: "named-module" };
7471
+ }
7472
+ if (opts.ci) return { modules: discoverModules(cwd, { affected: true }), scope: "affected" };
7473
+ return { modules: discoverModules(cwd), scope: "all" };
7474
+ }
7475
+ throw new Error(
7476
+ `Run sentinel from a module directory or the workspace root (found neither ${MODULE_MARKERS.join("/")} nor ${WORKSPACE_ROOT_MARKER} here).`
7477
+ );
7478
+ }
7479
+
7480
+ // src/roles/test/codemod/workspace-specifier.ts
7481
+ import { existsSync as existsSync27, readFileSync as readFileSync26 } from "node:fs";
7482
+ import { join as join33, relative as relative5, resolve as resolve5 } from "node:path";
7483
+ var EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", "/index.ts", "/index.js"];
7484
+ function pathEntries(workspaceRoot) {
7485
+ const file = join33(workspaceRoot, "tsconfig.base.json");
7486
+ if (!existsSync27(file)) return [];
7487
+ let paths;
7488
+ try {
7489
+ paths = parseJsonc(
7490
+ readFileSync26(file, "utf8"),
7491
+ "tsconfig.base.json"
7492
+ )?.compilerOptions?.paths;
7493
+ } catch {
7494
+ return [];
7495
+ }
7496
+ const entries = [];
7497
+ for (const [key, targets] of Object.entries(paths ?? {})) {
7498
+ const target = targets[0];
7499
+ if (target === void 0) continue;
7500
+ const wildcard = key.endsWith("/*") && target.endsWith("/*");
7501
+ entries.push({
7502
+ key: wildcard ? key.slice(0, -2) : key,
7503
+ target: resolve5(workspaceRoot, wildcard ? target.slice(0, -2) : target),
7504
+ wildcard
7505
+ });
7506
+ }
7507
+ return entries.sort((left, right) => right.target.length - left.target.length);
7508
+ }
7509
+ function workspaceSpecifier(workspaceRoot, file) {
7510
+ const absolute = resolve5(file);
7511
+ for (const entry of pathEntries(workspaceRoot)) {
7512
+ if (!entry.wildcard) {
7513
+ if (EXTENSIONS.some((extension) => `${entry.target}${extension}` === absolute)) {
7514
+ return entry.key;
7515
+ }
7516
+ continue;
7517
+ }
7518
+ const rest = relative5(entry.target, absolute);
7519
+ if (rest === "" || rest.startsWith("..") || rest.startsWith("/")) continue;
7520
+ const named = rest.replace(/\\/g, "/").replace(/\.(m|c)?(j|t)sx?$/, "");
7521
+ return `${entry.key}/${named}`;
7522
+ }
7523
+ return void 0;
7524
+ }
7525
+
7526
+ // src/roles/test/codemod/setup-static-imports.ts
7527
+ function requireCall(source, node) {
7528
+ if (node?.type !== "CallExpression") return void 0;
7529
+ const callee = node.callee;
7530
+ if (callee?.type !== "Identifier" || callee.name !== "require") return void 0;
7531
+ const args = node.arguments ?? [];
7532
+ const [only] = args;
7533
+ if (args.length !== 1 || only?.type !== "Literal") return void 0;
7534
+ const raw = source.slice(only.start, only.end);
7535
+ return raw.slice(1, -1);
7536
+ }
7537
+ function importable(written, fileName, workspaceRoot) {
7538
+ if (!written.startsWith(".")) return written;
7539
+ const fileDir = dirname7(fileName);
7540
+ const moduleRoot = findModuleRoot(fileDir, workspaceRoot) ?? fileDir;
7541
+ const target = resolve6(fileDir, written);
7542
+ if (target.startsWith(`${moduleRoot}/`)) return written;
7543
+ if (workspaceRoot === void 0) return void 0;
7544
+ return workspaceSpecifier(workspaceRoot, target);
7545
+ }
7546
+ function statement(fileName, source, node, specifier, written) {
7547
+ if (node.type === "ExpressionStatement") return `import '${specifier}'`;
7548
+ const declarations = node.declarations ?? [];
7549
+ const [only] = declarations;
7550
+ if (declarations.length !== 1 || only === void 0) return void 0;
7551
+ const id = only.id;
7552
+ if (id?.type === "Identifier") {
7553
+ const name = source.slice(id.start, id.end);
7554
+ return isCommonJsPackage(fileName, written) ? `import ${name} from '${specifier}'` : `import * as ${name} from '${specifier}'`;
7555
+ }
7556
+ if (id?.type === "ObjectPattern" && isPlainShorthand(id)) {
7557
+ const names = source.slice(id.start, id.end);
7558
+ return `import ${names} from '${specifier}'`;
7559
+ }
7560
+ return void 0;
7561
+ }
7562
+ function isPlainShorthand(pattern) {
7563
+ const properties = pattern.properties ?? [];
7564
+ if (properties.length === 0) return false;
7565
+ return properties.every(
7566
+ (property) => property.type === "Property" && property.shorthand === true && property.computed !== true && property.value?.type === "Identifier"
7567
+ );
7568
+ }
7569
+ function rewriteSetupStaticImports(fileName, source, program, workspaceRoot) {
7570
+ const body = program.body ?? [];
7571
+ const edits = [];
7572
+ const refusals = [];
7573
+ for (const node of body) {
7574
+ if (node.type === "ImportDeclaration") continue;
7575
+ const expression = node.type === "ExpressionStatement" ? node.expression : node.type === "VariableDeclaration" ? node.declarations?.[0]?.init : void 0;
7576
+ const written = requireCall(source, expression);
7577
+ if (written === void 0) break;
7578
+ const specifier = importable(written, fileName, workspaceRoot);
7579
+ if (specifier === void 0) {
7580
+ refusals.push(written);
7581
+ break;
7582
+ }
7583
+ const text = statement(fileName, source, node, specifier, written);
7584
+ if (text === void 0) break;
7585
+ edits.push({ start: node.start, end: node.end, text });
7586
+ }
7587
+ let result = source;
7588
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
7589
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
7590
+ }
7591
+ return { source: result, rewritten: edits.length, refusals };
7592
+ }
7593
+
7349
7594
  // src/roles/test/codemod/spy-instance-type.ts
7350
7595
  var SPY_TYPES = /* @__PURE__ */ new Set(["SpyInstance"]);
7351
7596
  var QUALIFIED_ONLY = /* @__PURE__ */ new Set(["Mock"]);
@@ -7417,6 +7662,7 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
7417
7662
  renamed: 0,
7418
7663
  hoisted: [],
7419
7664
  unhandled: [],
7665
+ warnings: [],
7420
7666
  refusals: [],
7421
7667
  rulesApplied: [],
7422
7668
  mswRepointed: false
@@ -7430,8 +7676,17 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
7430
7676
  }
7431
7677
  ]
7432
7678
  });
7433
- const original = parse2(fileName, source);
7434
- if (original === void 0) return unparsable("reading the file");
7679
+ const beforeStatic = parse2(fileName, source);
7680
+ if (beforeStatic === void 0) return unparsable("reading the file");
7681
+ const statics = rewriteSetupStaticImports(
7682
+ fileName,
7683
+ source,
7684
+ beforeStatic,
7685
+ findWorkspaceRoot(dirname8(fileName))
7686
+ );
7687
+ source = statics.source;
7688
+ const original = statics.rewritten === 0 ? beforeStatic : parse2(fileName, source);
7689
+ if (original === void 0) return unparsable("hoisting the setup requires");
7435
7690
  const titles = rewriteEachTitles(source, original);
7436
7691
  const tables = rewriteEachTableRows(titles.source, original);
7437
7692
  const afterTables = parse2(fileName, tables.source);
@@ -7516,23 +7771,34 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
7516
7771
  ...fakeTimers.translated
7517
7772
  ].sort(),
7518
7773
  refusals: [
7774
+ /*
7775
+ * A leading require that leaves its module and that no `paths` entry covers. Reported rather
7776
+ * than written in a shape we know nx refuses, which is Héla's bound: we refuse only where the
7777
+ * constraint is beyond us, and inventing a workspace alias is. Measured on this repo: 3 such
7778
+ * requires across 136 setup files, all 3 covered, so this list stays empty here.
7779
+ */
7780
+ ...statics.refusals.map((specifier) => ({
7781
+ binding: specifier,
7782
+ reason: `\`require('${specifier}')\` reaches another project, and \`tsconfig.base.json\` declares no path for it. A static import of a relative path is refused by nx, and \`await import()\` would make it a dynamic dependency, which forbids every static import of that project across this module. Add a \`paths\` entry for it, or move the file inside this project.`
7783
+ })),
7519
7784
  ...fakeTimers.refusals,
7520
7785
  ...hoist.refusals,
7521
7786
  ...mocks.refusals.map((refusal) => ({ binding: refusal.binding, reason: refusal.reason })),
7522
7787
  ...constructible.refusals,
7523
7788
  ...isolated.refusals
7524
7789
  ],
7790
+ warnings: fakeTimers.warnings,
7525
7791
  mswRepointed: repointed.repointed
7526
7792
  };
7527
7793
  }
7528
7794
 
7529
7795
  // src/roles/test/codemod/test-support-files.ts
7530
- import { existsSync as existsSync26, readFileSync as readFileSync25, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
7531
- import { dirname as dirname6, join as join31, relative as relative5, resolve as resolve4 } from "node:path";
7796
+ import { existsSync as existsSync28, readFileSync as readFileSync27, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
7797
+ import { dirname as dirname9, join as join34, relative as relative6, resolve as resolve7 } from "node:path";
7532
7798
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
7533
7799
  var TEST_FILE2 = /\.(spec|test)\.[cm]?[jt]sx?$/;
7534
7800
  var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
7535
- var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"];
7801
+ var EXTENSIONS2 = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"];
7536
7802
  function relativeSpecifiers(source) {
7537
7803
  const found = [];
7538
7804
  const patterns = [
@@ -7547,14 +7813,14 @@ function relativeSpecifiers(source) {
7547
7813
  return found;
7548
7814
  }
7549
7815
  function resolveSpecifier(fromFile, specifier) {
7550
- const base = resolve4(dirname6(fromFile), specifier);
7551
- if (existsSync26(base) && statSync5(base).isFile()) return base;
7552
- for (const extension of EXTENSIONS) {
7553
- if (existsSync26(`${base}${extension}`)) return `${base}${extension}`;
7816
+ const base = resolve7(dirname9(fromFile), specifier);
7817
+ if (existsSync28(base) && statSync5(base).isFile()) return base;
7818
+ for (const extension of EXTENSIONS2) {
7819
+ if (existsSync28(`${base}${extension}`)) return `${base}${extension}`;
7554
7820
  }
7555
- for (const extension of EXTENSIONS) {
7556
- const index = join31(base, `index${extension}`);
7557
- if (existsSync26(index)) return index;
7821
+ for (const extension of EXTENSIONS2) {
7822
+ const index = join34(base, `index${extension}`);
7823
+ if (existsSync28(index)) return index;
7558
7824
  }
7559
7825
  return void 0;
7560
7826
  }
@@ -7571,9 +7837,9 @@ function moduleSources(cwd) {
7571
7837
  }
7572
7838
  for (const entry of entries) {
7573
7839
  if (entry.isDirectory()) {
7574
- if (!SKIPPED.has(entry.name)) stack.push(join31(dir, entry.name));
7840
+ if (!SKIPPED.has(entry.name)) stack.push(join34(dir, entry.name));
7575
7841
  } else if (SOURCE_FILE.test(entry.name)) {
7576
- found.push(join31(dir, entry.name));
7842
+ found.push(join34(dir, entry.name));
7577
7843
  }
7578
7844
  }
7579
7845
  }
@@ -7581,7 +7847,7 @@ function moduleSources(cwd) {
7581
7847
  }
7582
7848
  function read(path) {
7583
7849
  try {
7584
- return readFileSync25(path, "utf8");
7850
+ return readFileSync27(path, "utf8");
7585
7851
  } catch {
7586
7852
  return void 0;
7587
7853
  }
@@ -7590,10 +7856,10 @@ function setupFilesInside(cwd, named) {
7590
7856
  const kept = [];
7591
7857
  for (const raw of named) {
7592
7858
  const withoutToken = raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", ".");
7593
- const full = resolve4(cwd, withoutToken);
7594
- const inside = relative5(cwd, full);
7859
+ const full = resolve7(cwd, withoutToken);
7860
+ const inside = relative6(cwd, full);
7595
7861
  if (inside.startsWith("..") || inside === "") continue;
7596
- if (!existsSync26(full)) continue;
7862
+ if (!existsSync28(full)) continue;
7597
7863
  kept.push(inside.replaceAll("\\", "/"));
7598
7864
  }
7599
7865
  return kept;
@@ -7628,7 +7894,7 @@ function listTestSupportFiles(cwd, setupFiles = []) {
7628
7894
  queue.push(target);
7629
7895
  }
7630
7896
  }
7631
- const reachable = [...testOnly].filter((file) => /\brequire\s*\(/.test(contents.get(file) ?? "")).map((file) => relative5(cwd, file).replaceAll("\\", "/"));
7897
+ const reachable = [...testOnly].filter((file) => /\brequire\s*\(/.test(contents.get(file) ?? "")).map((file) => relative6(cwd, file).replaceAll("\\", "/"));
7632
7898
  return [.../* @__PURE__ */ new Set([...reachable, ...setupFilesInside(cwd, setupFiles)])].sort();
7633
7899
  }
7634
7900
 
@@ -7639,7 +7905,7 @@ function parse3(source) {
7639
7905
  }
7640
7906
  function read2(path) {
7641
7907
  try {
7642
- return readFileSync26(path, "utf8");
7908
+ return readFileSync28(path, "utf8");
7643
7909
  } catch {
7644
7910
  return void 0;
7645
7911
  }
@@ -7657,7 +7923,7 @@ function defaultImportedSpecifiers(cwd) {
7657
7923
  }
7658
7924
  for (const entry of entries) {
7659
7925
  if (SKIPPED2.has(entry)) continue;
7660
- const path = join32(dir, entry);
7926
+ const path = join35(dir, entry);
7661
7927
  let directory;
7662
7928
  try {
7663
7929
  directory = statSync6(path).isDirectory();
@@ -7681,27 +7947,28 @@ function migrateModuleTests(cwd, setupFiles = []) {
7681
7947
  const modulesOwnMsw = ownsMswLifecycle(cwd, setupFiles);
7682
7948
  const operations = [];
7683
7949
  const blockers = [];
7950
+ const collected = [];
7684
7951
  let files = 0;
7685
7952
  let renamed = 0;
7686
7953
  let hoisted = 0;
7687
7954
  const defaultImported = defaultImportedSpecifiers(cwd);
7688
7955
  const asyncHelpers = /* @__PURE__ */ new Set();
7689
- for (const relative8 of listTestSupportFiles(cwd, setupFiles)) {
7690
- const source = read2(join32(cwd, relative8));
7956
+ for (const relative9 of listTestSupportFiles(cwd, setupFiles)) {
7957
+ const source = read2(join35(cwd, relative9));
7691
7958
  if (source === void 0) {
7692
- blockers.push({ file: relative8, reason: "this file could not be read" });
7959
+ blockers.push({ file: relative9, reason: "this file could not be read" });
7693
7960
  continue;
7694
7961
  }
7695
7962
  const migrated = migrateTestFile(
7696
- join32(cwd, relative8),
7963
+ join35(cwd, relative9),
7697
7964
  source,
7698
7965
  defaultImported,
7699
7966
  /* @__PURE__ */ new Set(),
7700
7967
  modulesOwnMsw
7701
7968
  );
7702
- const renamedTo = vitestSupportName(relative8);
7703
- if (!migrated.changed && renamedTo === relative8) continue;
7704
- if (renamedTo !== relative8) operations.push({ kind: "delete", path: relative8 });
7969
+ const renamedTo = vitestSupportName(relative9);
7970
+ if (!migrated.changed && renamedTo === relative9) continue;
7971
+ if (renamedTo !== relative9) operations.push({ kind: "delete", path: relative9 });
7705
7972
  const before = parse3(source);
7706
7973
  const after = parse3(migrated.source);
7707
7974
  if (before !== void 0 && after !== void 0) {
@@ -7714,14 +7981,14 @@ function migrateModuleTests(cwd, setupFiles = []) {
7714
7981
  renamed += migrated.renamed;
7715
7982
  operations.push({ kind: "write", path: renamedTo, contents: migrated.source });
7716
7983
  }
7717
- for (const relative8 of listTestFiles(cwd)) {
7718
- const source = read2(join32(cwd, relative8));
7984
+ for (const relative9 of listTestFiles(cwd)) {
7985
+ const source = read2(join35(cwd, relative9));
7719
7986
  if (source === void 0) {
7720
- blockers.push({ file: relative8, reason: "this file could not be read" });
7987
+ blockers.push({ file: relative9, reason: "this file could not be read" });
7721
7988
  continue;
7722
7989
  }
7723
7990
  const migrated = migrateTestFile(
7724
- join32(cwd, relative8),
7991
+ join35(cwd, relative9),
7725
7992
  source,
7726
7993
  defaultImported,
7727
7994
  asyncHelpers,
@@ -7729,25 +7996,27 @@ function migrateModuleTests(cwd, setupFiles = []) {
7729
7996
  );
7730
7997
  for (const member of migrated.unhandled) {
7731
7998
  blockers.push({
7732
- file: `${relative8}:${member.line}`,
7999
+ file: `${relative9}:${member.line}`,
7733
8000
  reason: member.rule === void 0 ? `${member.member} has no rule, and guessing one is how a suite goes green for the wrong reason` : `${member.member} needs ${member.rule}`
7734
8001
  });
7735
8002
  }
8003
+ collected.push(...migrated.warnings);
7736
8004
  for (const refusal of migrated.refusals) {
7737
- blockers.push({ file: relative8, reason: refusal.reason });
8005
+ blockers.push({ file: relative9, reason: refusal.reason });
7738
8006
  }
7739
8007
  if (!migrated.changed) continue;
7740
8008
  files++;
7741
8009
  renamed += migrated.renamed;
7742
8010
  if (migrated.hoisted.length > 0) hoisted++;
7743
- operations.push({ kind: "write", path: relative8, contents: migrated.source });
8011
+ operations.push({ kind: "write", path: relative9, contents: migrated.source });
7744
8012
  }
7745
- return blockers.length > 0 ? { operations: [], files, renamed, hoisted, blockers } : { operations, files, renamed, hoisted, blockers: [] };
8013
+ const warnings = [...new Set(collected)];
8014
+ return blockers.length > 0 ? { operations: [], files, renamed, hoisted, blockers, warnings } : { operations, files, renamed, hoisted, blockers: [], warnings };
7746
8015
  }
7747
8016
 
7748
8017
  // src/roles/test/nest/generate-config.ts
7749
- import { readdirSync as readdirSync7, readFileSync as readFileSync27, statSync as statSync7 } from "node:fs";
7750
- import { join as join33, relative as relative6 } from "node:path";
8018
+ import { readdirSync as readdirSync7, readFileSync as readFileSync29, statSync as statSync7 } from "node:fs";
8019
+ import { join as join36, relative as relative7 } from "node:path";
7751
8020
  var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
7752
8021
  var REACT_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
7753
8022
  function flavourOf(reading) {
@@ -7757,7 +8026,7 @@ var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/jest-parity";
7757
8026
  var SENTINEL_MSW_SETUP = "@hublo/sentinel/test/setup/msw-lifecycle";
7758
8027
  var SENTINEL_WORKSPACE_SETUP = "@hublo/sentinel/test/setup/workspace";
7759
8028
  function hopsToRoot(cwd, workspaceRoot) {
7760
- const up = relative6(cwd, workspaceRoot).replaceAll("\\", "/");
8029
+ const up = relative7(cwd, workspaceRoot).replaceAll("\\", "/");
7761
8030
  return up === "" ? "." : up;
7762
8031
  }
7763
8032
  function inlineBlock(packages) {
@@ -7791,7 +8060,7 @@ function readModuleTraits(cwd) {
7791
8060
  }
7792
8061
  for (const entry of entries) {
7793
8062
  if (SKIPPED3.has(entry)) continue;
7794
- const path = join33(dir, entry);
8063
+ const path = join36(dir, entry);
7795
8064
  let directory;
7796
8065
  try {
7797
8066
  directory = statSync7(path).isDirectory();
@@ -7805,7 +8074,7 @@ function readModuleTraits(cwd) {
7805
8074
  if (!/\.tsx?$/.test(entry)) continue;
7806
8075
  let source;
7807
8076
  try {
7808
- source = readFileSync27(path, "utf8");
8077
+ source = readFileSync29(path, "utf8");
7809
8078
  } catch {
7810
8079
  continue;
7811
8080
  }
@@ -7936,8 +8205,8 @@ ${overrides.join("\n")}
7936
8205
  }
7937
8206
 
7938
8207
  // src/roles/test/nest/read-jest-config.ts
7939
- import { existsSync as existsSync27, readFileSync as readFileSync28 } from "node:fs";
7940
- import { join as join34 } from "node:path";
8208
+ import { existsSync as existsSync29, readFileSync as readFileSync30 } from "node:fs";
8209
+ import { join as join37 } from "node:path";
7941
8210
  var CARRIED = [
7942
8211
  "testEnvironment",
7943
8212
  "coverageDirectory",
@@ -8127,7 +8396,7 @@ function extendedConfig(source, cwd) {
8127
8396
  (name) => name.replace(/^\.\//, "")
8128
8397
  );
8129
8398
  const file = candidates.find(
8130
- (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync27(join34(cwd, name))
8399
+ (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync29(join37(cwd, name))
8131
8400
  );
8132
8401
  if (file === void 0) continue;
8133
8402
  if (!new RegExp(String.raw`\{\s*\.\.\.${binding}\b`).test(source)) continue;
@@ -8154,11 +8423,11 @@ function sideEffects(source, extended) {
8154
8423
  return found;
8155
8424
  }
8156
8425
  function readJestConfig(cwd, named) {
8157
- const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync27(join34(cwd, name)));
8158
- if (file === void 0 || !existsSync27(join34(cwd, file))) return void 0;
8426
+ const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync29(join37(cwd, name)));
8427
+ if (file === void 0 || !existsSync29(join37(cwd, file))) return void 0;
8159
8428
  let source;
8160
8429
  try {
8161
- source = readFileSync28(join34(cwd, file), "utf8");
8430
+ source = readFileSync30(join37(cwd, file), "utf8");
8162
8431
  } catch {
8163
8432
  return { file, refusals: [`${file} could not be read`], notes: [] };
8164
8433
  }
@@ -8239,8 +8508,8 @@ function readJestConfig(cwd, named) {
8239
8508
  }
8240
8509
 
8241
8510
  // src/roles/test/nest/run-modes.ts
8242
- import { existsSync as existsSync28 } from "node:fs";
8243
- import { basename as basename2, join as join35 } from "node:path";
8511
+ import { existsSync as existsSync30 } from "node:fs";
8512
+ import { basename as basename2, join as join38 } from "node:path";
8244
8513
  var JEST_EXECUTOR = "@nx/jest:jest";
8245
8514
  var PRIMARY_CONFIG_FILE = "vitest.config.mts";
8246
8515
  var PRIMARY_TARGET = "test";
@@ -8261,7 +8530,7 @@ function runModes(cwd, fallback) {
8261
8530
  const jestConfig = entry.options?.jestConfig;
8262
8531
  if (jestConfig === void 0) continue;
8263
8532
  const name = configName(jestConfig);
8264
- if (!existsSync28(join35(cwd, name))) continue;
8533
+ if (!existsSync30(join38(cwd, name))) continue;
8265
8534
  declared.push({
8266
8535
  target,
8267
8536
  jestConfig: name,
@@ -8286,8 +8555,8 @@ function runModes(cwd, fallback) {
8286
8555
  }
8287
8556
 
8288
8557
  // src/roles/test/written-file-formatter.ts
8289
- import { existsSync as existsSync29 } from "node:fs";
8290
- import { dirname as dirname7, join as join36 } from "node:path";
8558
+ import { existsSync as existsSync31 } from "node:fs";
8559
+ import { dirname as dirname10, join as join39 } from "node:path";
8291
8560
  var PRETTIER_CONFIGS = [
8292
8561
  ".prettierrc",
8293
8562
  ".prettierrc.json",
@@ -8306,9 +8575,9 @@ function prettierConfigFrom(cwd) {
8306
8575
  let dir = cwd;
8307
8576
  for (; ; ) {
8308
8577
  for (const name of PRETTIER_CONFIGS) {
8309
- if (existsSync29(join36(dir, name))) return join36(dir, name);
8578
+ if (existsSync31(join39(dir, name))) return join39(dir, name);
8310
8579
  }
8311
- const parent = dirname7(dir);
8580
+ const parent = dirname10(dir);
8312
8581
  if (parent === dir) return void 0;
8313
8582
  dir = parent;
8314
8583
  }
@@ -8336,12 +8605,12 @@ function writtenFileFormatter(cwd) {
8336
8605
  var SPEC_TSCONFIG = "tsconfig.spec.json";
8337
8606
  var VITEST_GLOBAL_TYPES = "vitest/globals";
8338
8607
  function specTypesOperation(cwd) {
8339
- const path = join37(cwd, SPEC_TSCONFIG);
8340
- if (!existsSync30(path)) return void 0;
8608
+ const path = join40(cwd, SPEC_TSCONFIG);
8609
+ if (!existsSync32(path)) return void 0;
8341
8610
  let config;
8342
8611
  try {
8343
8612
  config = parseJsonc(
8344
- readFileSync29(path, "utf8"),
8613
+ readFileSync31(path, "utf8"),
8345
8614
  SPEC_TSCONFIG
8346
8615
  );
8347
8616
  } catch {
@@ -8361,10 +8630,10 @@ function specTypesOperation(cwd) {
8361
8630
  }
8362
8631
  function shippedTypesPath(cwd, declaration) {
8363
8632
  try {
8364
- const manifest = createRequire6(join37(cwd, "noop.cjs")).resolve("@hublo/sentinel/package.json");
8365
- const file = join37(dirname8(manifest), declaration);
8366
- if (existsSync30(file)) {
8367
- const path = relative7(cwd, file);
8633
+ const manifest = createRequire6(join40(cwd, "noop.cjs")).resolve("@hublo/sentinel/package.json");
8634
+ const file = join40(dirname11(manifest), declaration);
8635
+ if (existsSync32(file)) {
8636
+ const path = relative8(cwd, file);
8368
8637
  return path.startsWith(".") ? path : `./${path}`;
8369
8638
  }
8370
8639
  } catch {
@@ -8383,7 +8652,7 @@ function tsconfigFileListOperations(cwd) {
8383
8652
  let config;
8384
8653
  try {
8385
8654
  config = parseJsonc(
8386
- readFileSync29(join37(cwd, name), "utf8"),
8655
+ readFileSync31(join40(cwd, name), "utf8"),
8387
8656
  name
8388
8657
  );
8389
8658
  } catch {
@@ -8511,6 +8780,10 @@ function planJestMigration(context, adoption) {
8511
8780
  `${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` : ""}.`,
8512
8781
  `${prepared.map(({ mode }) => mode.vitestConfig).join(", ")} written from what ${prepared.map(({ mode }) => mode.jestConfig).join(", ")} declared, and that config deleted.` + (escaping.length === 0 ? ` Every specifier in the new one is a package name: nothing in it reaches outside this module.` : ` \u26A0\uFE0F ${escaping.length} alias(es) carried from that config reach OUTSIDE this module: ${escaping.slice(0, 3).join(", ")}. They came from its \`moduleNameMapper\` and are kept as they were, but a module that reaches out cannot be moved on its own.`)
8513
8782
  ];
8783
+ for (const warning of migration.warnings) notes.push(`\u26A0\uFE0F ${warning}`);
8784
+ notes.push(
8785
+ `the config files this wrote are named \`vitest.*\` where they were \`jest.*\`. An ignore written by NAME no longer covers them: if your lint or format config ignores \`jest.*\`, add the \`vitest.*\` equivalent, or accept that these files are now checked.`
8786
+ );
8514
8787
  if (specTypes !== void 0) {
8515
8788
  notes.push(
8516
8789
  `${SPEC_TSCONFIG} now declares \`${VITEST_GLOBAL_TYPES}\` instead of \`jest\`. Without it the module keeps depending on @types/jest and \`jest.Mock\` still resolves, so the migration would look finished while the dependency it cuts is still declared.`
@@ -8670,11 +8943,11 @@ function plan3(context) {
8670
8943
  }
8671
8944
 
8672
8945
  // src/roles/test/reference.ts
8673
- import { execFileSync } from "node:child_process";
8946
+ import { execFileSync as execFileSync2 } from "node:child_process";
8674
8947
  import { createHash } from "node:crypto";
8675
- import { existsSync as existsSync31, mkdirSync, mkdtempSync, readFileSync as readFileSync30, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
8676
- import { tmpdir } from "node:os";
8677
- import { join as join38, resolve as resolve5 } from "node:path";
8948
+ import { existsSync as existsSync33, mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync32, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "node:fs";
8949
+ import { tmpdir as tmpdir2 } from "node:os";
8950
+ import { join as join41, resolve as resolve8 } from "node:path";
8678
8951
 
8679
8952
  // src/roles/test/baseline.ts
8680
8953
  function readSnapshot(report) {
@@ -8718,25 +8991,25 @@ function unusableAsBaseline(snapshot) {
8718
8991
 
8719
8992
  // src/roles/test/reference.ts
8720
8993
  function referencePath(cwd) {
8721
- const key = createHash("sha256").update(resolve5(cwd)).digest("hex").slice(0, 16);
8722
- return join38(tmpdir(), "sentinel-test-reference", `${key}.json`);
8994
+ const key = createHash("sha256").update(resolve8(cwd)).digest("hex").slice(0, 16);
8995
+ return join41(tmpdir2(), "sentinel-test-reference", `${key}.json`);
8723
8996
  }
8724
8997
  function runJest(cwd, config) {
8725
8998
  const bin = resolveBin(cwd, "jest");
8726
8999
  if (bin === void 0) return void 0;
8727
9000
  const root = findWorkspaceRoot(cwd) ?? cwd;
8728
- const out = join38(mkdtempSync(join38(tmpdir(), "sentinel-reference-")), "jest.json");
9001
+ const out = join41(mkdtempSync2(join41(tmpdir2(), "sentinel-reference-")), "jest.json");
8729
9002
  try {
8730
- execFileSync(bin, ["-c", join38(cwd, config), "--json", `--outputFile=${out}`, "--silent"], {
9003
+ execFileSync2(bin, ["-c", join41(cwd, config), "--json", `--outputFile=${out}`, "--silent"], {
8731
9004
  cwd: root,
8732
9005
  stdio: ["ignore", "ignore", "ignore"],
8733
9006
  maxBuffer: 1 << 28
8734
9007
  });
8735
9008
  } catch {
8736
9009
  }
8737
- if (!existsSync31(out)) return void 0;
9010
+ if (!existsSync33(out)) return void 0;
8738
9011
  try {
8739
- return JSON.parse(readFileSync30(out, "utf8"));
9012
+ return JSON.parse(readFileSync32(out, "utf8"));
8740
9013
  } catch {
8741
9014
  return void 0;
8742
9015
  }
@@ -8764,7 +9037,7 @@ function captureReference(cwd, config) {
8764
9037
  snapshot
8765
9038
  };
8766
9039
  const path = referencePath(cwd);
8767
- mkdirSync(join38(path, ".."), { recursive: true });
9040
+ mkdirSync(join41(path, ".."), { recursive: true });
8768
9041
  writeFileSync3(path, `${JSON.stringify(stored, void 0, 2)}
8769
9042
  `);
8770
9043
  return {
@@ -8944,7 +9217,7 @@ function fromWorkspaceRoot(moduleDir, options, paths) {
8944
9217
  if (root === void 0 || root === moduleDir) {
8945
9218
  return { cwd: moduleDir, options: [...options], paths: [...paths] };
8946
9219
  }
8947
- const absolute = (value) => isAbsolute3(value) ? value : resolve6(moduleDir, value);
9220
+ const absolute = (value) => isAbsolute3(value) ? value : resolve9(moduleDir, value);
8948
9221
  const moved = [];
8949
9222
  let previousWasConfig = false;
8950
9223
  for (const option of options) {
@@ -8973,9 +9246,9 @@ function registerTest() {
8973
9246
 
8974
9247
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
8975
9248
  import { spawnSync as spawnSync6 } from "node:child_process";
8976
- import { existsSync as existsSync32, readFileSync as readFileSync32 } from "node:fs";
9249
+ import { existsSync as existsSync34, readFileSync as readFileSync34 } from "node:fs";
8977
9250
  import { createRequire as createRequire7 } from "node:module";
8978
- import { join as join40 } from "node:path";
9251
+ import { join as join43 } from "node:path";
8979
9252
 
8980
9253
  // src/roles/typescript/presets/nest.json
8981
9254
  var nest_default2 = {
@@ -9156,8 +9429,8 @@ function readTsconfigAdoption(cwd) {
9156
9429
  }
9157
9430
 
9158
9431
  // src/roles/typescript/adapters/tsc/plan.ts
9159
- import { readFileSync as readFileSync31 } from "node:fs";
9160
- import { join as join39 } from "node:path";
9432
+ import { readFileSync as readFileSync33 } from "node:fs";
9433
+ import { join as join42 } from "node:path";
9161
9434
 
9162
9435
  // src/roles/typescript/typecheck-script.ts
9163
9436
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -9250,7 +9523,7 @@ function planAdoption(context) {
9250
9523
  };
9251
9524
  }
9252
9525
  const existing = parseJsonc(
9253
- readFileSync31(join39(context.cwd, target.path), "utf8"),
9526
+ readFileSync33(join42(context.cwd, target.path), "utf8"),
9254
9527
  target.path
9255
9528
  );
9256
9529
  const extendsChain = composeExtends(existing.extends, preset);
@@ -9388,7 +9661,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
9388
9661
  let chain;
9389
9662
  try {
9390
9663
  const parsed = parseJsonc(
9391
- readFileSync32(join40(cwd, target.path), "utf8"),
9664
+ readFileSync34(join43(cwd, target.path), "utf8"),
9392
9665
  target.path
9393
9666
  );
9394
9667
  chain = parsed.extends;
@@ -9401,7 +9674,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
9401
9674
  );
9402
9675
  if (preset === void 0) return void 0;
9403
9676
  try {
9404
- createRequire7(join40(cwd, "noop.js")).resolve(preset);
9677
+ createRequire7(join43(cwd, "noop.js")).resolve(preset);
9405
9678
  return void 0;
9406
9679
  } catch {
9407
9680
  return preset;
@@ -9497,7 +9770,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
9497
9770
  referencedProjects(cwd, config) {
9498
9771
  try {
9499
9772
  const parsed = parseJsonc(
9500
- readFileSync32(join40(cwd, config), "utf8"),
9773
+ readFileSync34(join43(cwd, config), "utf8"),
9501
9774
  config
9502
9775
  );
9503
9776
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -9513,7 +9786,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
9513
9786
  * check.
9514
9787
  */
9515
9788
  typecheckTarget(cwd) {
9516
- if (existsSync32(join40(cwd, "tsconfig.json"))) return "tsconfig.json";
9789
+ if (existsSync34(join43(cwd, "tsconfig.json"))) return "tsconfig.json";
9517
9790
  const target = resolveTsconfigTarget(cwd);
9518
9791
  return target.reason === "none" ? null : target.path;
9519
9792
  }
@@ -9718,8 +9991,8 @@ function replaceLines(current, replacements) {
9718
9991
  }
9719
9992
 
9720
9993
  // src/core/apply-plan.ts
9721
- import { existsSync as existsSync33, readFileSync as readFileSync33, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
9722
- import { resolve as resolve7, sep } from "node:path";
9994
+ import { existsSync as existsSync35, readFileSync as readFileSync35, renameSync, rmSync as rmSync5, writeFileSync as writeFileSync4 } from "node:fs";
9995
+ import { resolve as resolve10, sep } from "node:path";
9723
9996
  import { applyEdits as applyEdits3, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
9724
9997
 
9725
9998
  // src/shared/deep-merge.ts
@@ -9729,15 +10002,15 @@ function isPlainObject(value) {
9729
10002
 
9730
10003
  // src/core/apply-plan.ts
9731
10004
  function resolveWithinRoot(cwd, relativePath) {
9732
- const root = resolve7(cwd);
9733
- const absolutePath = resolve7(root, relativePath);
10005
+ const root = resolve10(cwd);
10006
+ const absolutePath = resolve10(root, relativePath);
9734
10007
  if (absolutePath !== root && !absolutePath.startsWith(root + sep)) {
9735
10008
  throw new Error(`Refusing to write outside the module root: "${relativePath}".`);
9736
10009
  }
9737
10010
  return absolutePath;
9738
10011
  }
9739
10012
  function readIfExists(absolutePath) {
9740
- return existsSync33(absolutePath) ? readFileSync33(absolutePath, "utf8") : void 0;
10013
+ return existsSync35(absolutePath) ? readFileSync35(absolutePath, "utf8") : void 0;
9741
10014
  }
9742
10015
  function* leaves(value, prefix = []) {
9743
10016
  for (const [key, keyValue] of Object.entries(value)) {
@@ -9846,7 +10119,7 @@ function applyPlan(cwd, plan4) {
9846
10119
  const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
9847
10120
  for (const file of changed) {
9848
10121
  if (file.deleted) {
9849
- rmSync4(file.absolutePath, { force: true });
10122
+ rmSync5(file.absolutePath, { force: true });
9850
10123
  continue;
9851
10124
  }
9852
10125
  writeFileAtomic(file.absolutePath, file.after);
@@ -9855,8 +10128,8 @@ function applyPlan(cwd, plan4) {
9855
10128
  }
9856
10129
 
9857
10130
  // src/core/config/preset-evidence.ts
9858
- import { existsSync as existsSync34, readdirSync as readdirSync9, readFileSync as readFileSync34 } from "node:fs";
9859
- import { join as join41 } from "node:path";
10131
+ import { existsSync as existsSync36, readdirSync as readdirSync9, readFileSync as readFileSync36 } from "node:fs";
10132
+ import { join as join44 } from "node:path";
9860
10133
  var PATH_SIGNALS = [
9861
10134
  {
9862
10135
  preset: "nest",
@@ -9890,12 +10163,12 @@ function sourceImports(cwd, pattern) {
9890
10163
  }
9891
10164
  for (const entry of entries) {
9892
10165
  if (entry.isDirectory()) {
9893
- if (!NOT_SCANNED.has(entry.name)) stack.push(join41(dir, entry.name));
10166
+ if (!NOT_SCANNED.has(entry.name)) stack.push(join44(dir, entry.name));
9894
10167
  continue;
9895
10168
  }
9896
10169
  if (!SOURCE_FILE2.test(entry.name)) continue;
9897
10170
  try {
9898
- if (pattern.test(readFileSync34(join41(dir, entry.name), "utf8"))) return true;
10171
+ if (pattern.test(readFileSync36(join44(dir, entry.name), "utf8"))) return true;
9899
10172
  } catch {
9900
10173
  continue;
9901
10174
  }
@@ -9904,11 +10177,11 @@ function sourceImports(cwd, pattern) {
9904
10177
  return false;
9905
10178
  }
9906
10179
  function dependencyNames(cwd) {
9907
- const path = join41(cwd, "package.json");
9908
- if (!existsSync34(path)) return [];
10180
+ const path = join44(cwd, "package.json");
10181
+ if (!existsSync36(path)) return [];
9909
10182
  try {
9910
10183
  const manifest = parseJsonc(
9911
- readFileSync34(path, "utf8"),
10184
+ readFileSync36(path, "utf8"),
9912
10185
  path
9913
10186
  );
9914
10187
  return [
@@ -9931,7 +10204,7 @@ function declaresJsx(cwd) {
9931
10204
  for (const name of entries) {
9932
10205
  try {
9933
10206
  const config = parseJsonc(
9934
- readFileSync34(join41(cwd, name), "utf8"),
10207
+ readFileSync36(join44(cwd, name), "utf8"),
9935
10208
  name
9936
10209
  );
9937
10210
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -10118,13 +10391,13 @@ export {
10118
10391
  resolve,
10119
10392
  buildConfigFile,
10120
10393
  BaseAdapter,
10121
- resolveBin,
10122
10394
  VERBS,
10123
10395
  TARGETS,
10124
10396
  LONG_RUNNING_TARGETS,
10125
10397
  SWEEPABLE_TARGETS,
10126
10398
  PRESET_NAMES,
10127
10399
  palette,
10400
+ resolveContext,
10128
10401
  registerAdapters,
10129
10402
  correctingSignal,
10130
10403
  describeFramework,
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-7ADFV7P4.js";
10
+ } from "./chunk-U744PK2U.js";
11
11
  import "./chunk-NW6UHNYX.js";
12
12
  export {
13
13
  BaseAdapter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.4.0-alpha.23",
3
+ "version": "1.4.0-alpha.24",
4
4
  "description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,3 +39,39 @@
39
39
  * type of what actually executes rather than the type of the runner it is leaving.
40
40
  */
41
41
  declare const jest: (typeof import('vitest'))['vi']
42
+
43
+ /**
44
+ * The same answer for TYPE positions, which the const above does not reach.
45
+ *
46
+ * `declare const jest` makes `jest.fn()` compile and says nothing about `jest.Mock`, because a
47
+ * const has no type members. A helper that annotates with one gets:
48
+ *
49
+ * libs/front/tests/src/mocks/libs/analytics.mock.ts(2,22)
50
+ * error TS2694: Namespace 'jest' has no exported member 'Mock'.
51
+ *
52
+ * Measured on `libs/front/components`, whose typecheck was green before it migrated: 8 of these,
53
+ * every one of them in `libs/front/tests`, a project this module reads and does not own. Repo-wide
54
+ * the whole vocabulary is eight names, and here is each one's Vitest counterpart.
55
+ *
56
+ * ⚠️ `Mock` and `SpyInstance` take JEST's generics here, not Vitest's, and the difference is not
57
+ * cosmetic: jest's first generic is the RETURN type, Vitest's is the whole SIGNATURE. These
58
+ * declarations serve files that are still written for jest, so they must read jest's spelling;
59
+ * `codemod/spy-instance-type.ts` is the other half, and it converts the spelling in the files the
60
+ * migration does rewrite.
61
+ *
62
+ * A type-only namespace declares no value, which is why it merges with the const above instead of
63
+ * colliding with it.
64
+ */
65
+ declare namespace jest {
66
+ type Mock<T = any, Y extends any[] = any[]> = import('vitest').Mock<(...args: Y) => T>
67
+ type Mocked<T> = import('vitest').Mocked<T>
68
+ type MockedObject<T> = import('vitest').MockedObject<T>
69
+ type MockedClass<T extends abstract new (...args: any[]) => any> = import('vitest').MockedClass<T>
70
+ type MockedFunction<T extends (...args: any[]) => any> = import('vitest').MockedFunction<T>
71
+ /** jest's own alias for `MockedFunction`. */
72
+ type MockedFn<T extends (...args: any[]) => any> = import('vitest').MockedFunction<T>
73
+ type SpyInstance<T = any, Y extends any[] = any[]> = import('vitest').MockInstance<
74
+ (...args: Y) => T
75
+ >
76
+ type SpiedFunction<T extends (...args: any[]) => any> = import('vitest').MockInstance<T>
77
+ }