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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/bin/sentinel.d.ts +1 -0
  2. package/dist/bin/sentinel.js +43 -102
  3. package/dist/chunk-2XLX6PFR.js.map +1 -0
  4. package/dist/{chunk-3TDUIKVQ.js → chunk-3ZKTXODW.js} +6 -6
  5. package/dist/chunk-3ZKTXODW.js.map +1 -0
  6. package/dist/chunk-GCZKG4B2.js +4743 -0
  7. package/dist/{chunk-WLFE5RUU.js → chunk-NW6UHNYX.js} +6 -6
  8. package/dist/chunk-NW6UHNYX.js.map +1 -0
  9. package/dist/chunk-PWV3BMDA.js.map +1 -0
  10. package/dist/{chunk-CPCUPK4J.js → chunk-TKDVAYYV.js} +4 -4
  11. package/dist/chunk-TKDVAYYV.js.map +1 -0
  12. package/dist/{chunk-MUQWAZQN.js → chunk-Y2EKU6OE.js} +294 -4133
  13. package/dist/{chunk-SX5CBE3Z.js → chunk-ZV5KMOS7.js} +133 -16
  14. package/dist/chunk-ZV5KMOS7.js.map +1 -0
  15. package/dist/index.d.ts +28 -5
  16. package/dist/index.js +5 -4
  17. package/dist/roles/build/nest/toolchain.js +7 -7
  18. package/dist/roles/test/nest/toolchain.d.ts +1 -1
  19. package/dist/roles/test/nest/toolchain.js +2 -2
  20. package/dist/roles/test/react/toolchain.d.ts +1 -1
  21. package/dist/roles/test/react/toolchain.js +2 -2
  22. package/dist/roles/test/setup/a11y.d.ts +45 -0
  23. package/dist/roles/test/setup/a11y.js +72 -0
  24. package/dist/roles/test/setup/a11y.js.map +1 -0
  25. package/dist/roles/test/setup/{nest.js → jest-parity.js} +5 -5
  26. package/dist/roles/test/setup/jest-parity.js.map +1 -0
  27. package/dist/roles/test/setup/w3c.d.ts +75 -0
  28. package/dist/roles/test/setup/w3c.js +49 -0
  29. package/dist/roles/test/setup/w3c.js.map +1 -0
  30. package/dist/roles/test/setup/workspace-entry.js +2 -2
  31. package/dist/roles/test/setup/workspace-entry.js.map +1 -1
  32. package/dist/{shared-test-config-CGxvMlmk.d.ts → roles/test/shared-test-config.d.ts} +5 -1
  33. package/dist/roles/test/shared-test-config.js +8 -0
  34. package/dist/roles/test/shared-test-config.js.map +1 -0
  35. package/dist/roles/test/tools/msw.d.ts +1 -0
  36. package/dist/roles/test/tools/msw.js +3 -0
  37. package/dist/roles/test/tools/msw.js.map +1 -0
  38. package/dist/validate-ONAQPYJB.js +129 -0
  39. package/docs/test-adoption.md +188 -10
  40. package/docs/using-sentinel.md +25 -10
  41. package/package.json +24 -7
  42. package/types/jest-global.d.ts +85 -0
  43. package/types/mock-extended.d.ts +52 -0
  44. package/dist/roles/test/setup/nest.js.map +0 -1
  45. /package/dist/roles/test/setup/{nest.d.ts → jest-parity.d.ts} +0 -0
@@ -0,0 +1,4743 @@
1
+ import {
2
+ WORKSPACE_ROOT_MARKER,
3
+ findWorkspaceRoot,
4
+ moduleName,
5
+ readNxProjectName,
6
+ readOwnPackage,
7
+ readProjectPackageJson
8
+ } from "./chunk-NW6UHNYX.js";
9
+
10
+ // src/core/base-adapter.ts
11
+ var BaseAdapter = class {
12
+ inspect(_ctx) {
13
+ throw new Error(`${this.runner}: --inspect not implemented yet`);
14
+ }
15
+ report(_ctx) {
16
+ throw new Error(`${this.runner}: --report not implemented yet`);
17
+ }
18
+ status(_ctx) {
19
+ throw new Error(`${this.runner}: --status not implemented yet`);
20
+ }
21
+ };
22
+
23
+ // src/roles/test/read-adoption.ts
24
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "node:fs";
25
+ import { join as join2, relative } from "node:path";
26
+
27
+ // src/core/config/manifest.ts
28
+ import { existsSync, readFileSync } from "node:fs";
29
+ import { basename, join } from "node:path";
30
+ function moduleScripts(cwd) {
31
+ try {
32
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
33
+ return pkg.scripts ?? {};
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+ function manifestTargets(cwd) {
39
+ try {
40
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
41
+ return pkg.nx?.targets ?? {};
42
+ } catch {
43
+ return {};
44
+ }
45
+ }
46
+ function moduleName2(cwd) {
47
+ try {
48
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
49
+ return pkg.name;
50
+ } catch {
51
+ return void 0;
52
+ }
53
+ }
54
+ function isSelfAdoption(cwd) {
55
+ return moduleName2(cwd) === readOwnPackage().name;
56
+ }
57
+ function selfCommand(cwd, flags) {
58
+ const own = readOwnPackage();
59
+ if (!isSelfAdoption(cwd) || !own.bin) return void 0;
60
+ return `node ${own.bin.replace(/^\.\//, "")} ${flags}`;
61
+ }
62
+ function adoptedPackageName(cwd) {
63
+ return readProjectPackageJson(cwd).name ?? readNxProjectName(cwd) ?? basename(cwd);
64
+ }
65
+ function manifestOperation(cwd, scripts) {
66
+ const own = readOwnPackage();
67
+ const value = {
68
+ scripts,
69
+ devDependencies: { [own.name]: isSelfAdoption(cwd) ? "link:." : own.version }
70
+ };
71
+ if (existsSync(join(cwd, "package.json"))) {
72
+ return { kind: "merge-json", path: "package.json", value };
73
+ }
74
+ return {
75
+ kind: "merge-json",
76
+ path: "package.json",
77
+ value: { name: adoptedPackageName(cwd), private: true, ...value }
78
+ };
79
+ }
80
+
81
+ // src/roles/test/presets/toolchain.json
82
+ var toolchain_default = {
83
+ $comment: "The test toolchain sentinel owns. Same shape and same rule as the build one: a single table drives what `--init` removes from a module's package.json, which import specifiers move, and the name each default import is re-exported under, so the three cannot disagree.",
84
+ owned: [
85
+ {
86
+ package: "vitest",
87
+ specifier: "vitest/config",
88
+ named: ["defineConfig", "mergeConfig"],
89
+ types: ["ConfigEnv", "TestUserConfig", "ViteUserConfig"],
90
+ why: "the runner itself, declared at the workspace ROOT today, which is what stops a module being testable on its own. Note the package is `vitest` and the specifier is `vitest/config`: the same split as nitro on the build side, and the reason one table holds both."
91
+ },
92
+ {
93
+ package: "vite",
94
+ specifier: "vite",
95
+ named: ["loadEnv"],
96
+ why: "`loadEnv` has no equivalent in `vitest/config`, and two of the six configs import it from vite to build their test env. Re-exported here rather than claimed: the BUILD role owns the `vite` package, and two roles owning one package is how a version conflict gets written down twice."
97
+ }
98
+ ]
99
+ };
100
+
101
+ // src/roles/test/preset-data.ts
102
+ var OWNED_TEST_PACKAGES = toolchain_default.owned;
103
+
104
+ // src/roles/test/config-policy.ts
105
+ var TEST_PRESET_SPECIFIERS = {
106
+ react: "@hublo/sentinel/test/react",
107
+ nest: "@hublo/sentinel/test/nest"
108
+ };
109
+ function testPresetSpecifier(preset) {
110
+ return TEST_PRESET_SPECIFIERS[preset] ?? TEST_PRESET_SPECIFIERS.react;
111
+ }
112
+ var ALL_TEST_PRESET_SPECIFIERS = Object.values(TEST_PRESET_SPECIFIERS);
113
+ var TEST_PRESET_SPECIFIER = TEST_PRESET_SPECIFIERS.react;
114
+ var TEST_CONFIG_FILES = [
115
+ "vitest.config.ts",
116
+ "vitest.config.mts",
117
+ "vitest.config.js",
118
+ "vitest.config.mjs"
119
+ ];
120
+ var TEST_OWNED_PACKAGES = OWNED_TEST_PACKAGES;
121
+ var SENTINEL_OWNED_TEST_PACKAGES = TEST_OWNED_PACKAGES.filter(
122
+ (entry) => entry.package !== "vite"
123
+ ).map((entry) => entry.package);
124
+ function testToolchain(preset) {
125
+ return { presetSpecifier: testPresetSpecifier(preset), owned: TEST_OWNED_PACKAGES };
126
+ }
127
+ var TEST_SCRIPT_NAME = "test";
128
+ var SENTINEL_TEST_COMMAND = "sentinel --run --test --";
129
+ var VITEST_INVOCATION = /(^|&&\s*|\|\|\s*|;\s*)(?:npx\s+|pnpm\s+(?:exec\s+|dlx\s+)?)?vitest\b([^&|;]*)/g;
130
+ function rewriteVitestScript(command) {
131
+ return command.replace(VITEST_INVOCATION, (_match, lead, tail) => {
132
+ const args = tail.trim().split(/\s+/).filter(Boolean);
133
+ const oneShot = args[0] === "run" || args.includes("--run");
134
+ const rest = args.filter((arg, index) => !(index === 0 && arg === "run") && arg !== "--run");
135
+ const passthrough = oneShot ? rest : ["--watch", ...rest];
136
+ const suffix = passthrough.length > 0 ? ` -- ${passthrough.join(" ")}` : "";
137
+ const trailing = /\s$/.test(tail) ? " " : "";
138
+ return `${lead}sentinel --run --test${suffix}${trailing}`;
139
+ });
140
+ }
141
+ function testTarget(options) {
142
+ const { name, configFiles, outputs, dependsOn, configurations } = options;
143
+ const cached = outputs !== void 0 && outputs.length > 0;
144
+ return {
145
+ [name ?? TEST_SCRIPT_NAME]: {
146
+ cache: cached,
147
+ inputs: ["default", "^default", ...configFiles.map((name2) => `{projectRoot}/${name2}`)],
148
+ ...cached ? { outputs: [...outputs] } : {},
149
+ ...dependsOn !== void 0 && dependsOn.length > 0 ? { dependsOn: [...dependsOn] } : {},
150
+ ...configurations !== void 0 && Object.keys(configurations).length > 0 ? { configurations: { ...configurations } } : {}
151
+ }
152
+ };
153
+ }
154
+
155
+ // src/roles/test/read-adoption.ts
156
+ var JEST_CONFIG_FILES = [
157
+ "jest.config.ts",
158
+ "jest.config.js",
159
+ "jest.prisma.config.ts",
160
+ "jest.functional.config.ts",
161
+ "jest.integration-config.ts"
162
+ ];
163
+ function testConfigFile(cwd) {
164
+ return TEST_CONFIG_FILES.find((name) => existsSync2(join2(cwd, name)));
165
+ }
166
+ function jestConfigFiles(cwd) {
167
+ return JEST_CONFIG_FILES.filter((name) => existsSync2(join2(cwd, name)));
168
+ }
169
+ function rootJestConfigFiles(cwd) {
170
+ const root = findWorkspaceRoot(cwd);
171
+ if (root === void 0 || root === cwd) return [];
172
+ return JEST_CONFIG_FILES.filter((name) => existsSync2(join2(root, name)));
173
+ }
174
+ var TEST_FILE = /[.-](spec|test)\.[cm]?[jt]sx?$/;
175
+ var NOT_WALKED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo"]);
176
+ function hasTestFiles(cwd) {
177
+ return walkTestFiles(cwd, true).length > 0;
178
+ }
179
+ function listTestFiles(cwd) {
180
+ return walkTestFiles(cwd, false).sort();
181
+ }
182
+ function walkTestFiles(cwd, stopAtFirst) {
183
+ const found = [];
184
+ const stack = [cwd];
185
+ while (stack.length > 0) {
186
+ const dir = stack.pop();
187
+ let entries;
188
+ try {
189
+ entries = readdirSync(dir, { withFileTypes: true });
190
+ } catch {
191
+ continue;
192
+ }
193
+ for (const entry of entries) {
194
+ if (entry.isDirectory()) {
195
+ if (!NOT_WALKED.has(entry.name)) stack.push(join2(dir, entry.name));
196
+ } else if (TEST_FILE.test(entry.name)) {
197
+ found.push(relative(cwd, join2(dir, entry.name)).replaceAll("\\", "/"));
198
+ if (stopAtFirst) return found;
199
+ }
200
+ }
201
+ }
202
+ return found;
203
+ }
204
+ function readTestConfigSource(cwd, fileName) {
205
+ try {
206
+ return readFileSync2(join2(cwd, fileName), "utf8");
207
+ } catch {
208
+ return "";
209
+ }
210
+ }
211
+ function importsPreset(source) {
212
+ return ALL_TEST_PRESET_SPECIFIERS.some((specifier) => {
213
+ const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
214
+ return new RegExp(String.raw`(?:from|import)\s*\(?\s*['"]${escaped}['"]`).test(source);
215
+ });
216
+ }
217
+ function readTestAdoption(cwd) {
218
+ const manifest = readProjectPackageJson(cwd);
219
+ const jestConfigs = jestConfigFiles(cwd);
220
+ const configFile = testConfigFile(cwd);
221
+ const declared = { ...manifest.dependencies, ...manifest.devDependencies };
222
+ const ownDeclarations = ["vitest", "jest", "@nx/jest", "jest-mock-extended", "@swc/jest"].filter((name) => declared[name] !== void 0).map((name) => ({ name, version: declared[name] }));
223
+ if (configFile === void 0) {
224
+ const hasTests = hasTestFiles(cwd);
225
+ const inheritsRootConfig = hasTests && jestConfigs.length === 0 && rootJestConfigFiles(cwd).length > 0;
226
+ return {
227
+ configFile: null,
228
+ preset: null,
229
+ adopted: false,
230
+ conformant: false,
231
+ drift: [],
232
+ unreadable: null,
233
+ state: hasTests ? "jest" : "no-tests",
234
+ runner: hasTests ? "jest" : "none",
235
+ jestConfigs,
236
+ inheritsRootConfig,
237
+ ownDeclarations
238
+ };
239
+ }
240
+ const source = readTestConfigSource(cwd, configFile);
241
+ if (source === "") {
242
+ return {
243
+ configFile,
244
+ preset: null,
245
+ adopted: false,
246
+ conformant: false,
247
+ drift: [],
248
+ unreadable: `${configFile} could not be read`,
249
+ state: "vitest-own-toolchain",
250
+ runner: "vitest",
251
+ jestConfigs,
252
+ inheritsRootConfig: false,
253
+ // it has a config of its own, readable or not
254
+ ownDeclarations
255
+ };
256
+ }
257
+ const adopted = importsPreset(source);
258
+ const script = moduleScripts(cwd)["test"];
259
+ return {
260
+ configFile,
261
+ preset: adopted ? "react" : null,
262
+ adopted,
263
+ conformant: adopted && script !== void 0 && jestConfigs.length === 0,
264
+ drift: [],
265
+ unreadable: null,
266
+ state: adopted ? "adopted" : "vitest-own-toolchain",
267
+ runner: "vitest",
268
+ jestConfigs,
269
+ inheritsRootConfig: false,
270
+ // local always wins: this module is out of the root's reach
271
+ ownDeclarations
272
+ };
273
+ }
274
+
275
+ // src/roles/test/module-env.ts
276
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
277
+ import { join as join3 } from "node:path";
278
+ var CASCADE = [".env", ".env.test"];
279
+ var MODULE_CASCADE = [".env", ".env.local", ".env.test"];
280
+ function parse(path) {
281
+ const found = {};
282
+ let contents;
283
+ try {
284
+ contents = readFileSync3(path, "utf8");
285
+ } catch {
286
+ return found;
287
+ }
288
+ for (const line of contents.split("\n")) {
289
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
290
+ if (match === null) continue;
291
+ const [, name, raw] = match;
292
+ if (name === void 0 || raw === void 0) continue;
293
+ const trimmed = raw.trim();
294
+ const unquoted = trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'") ? trimmed.slice(1, -1) : trimmed.split(" #")[0]?.trimEnd();
295
+ found[name] = unquoted ?? "";
296
+ }
297
+ return found;
298
+ }
299
+ function moduleEnvironment(cwd) {
300
+ const root = findWorkspaceRoot(cwd);
301
+ const loaded = [];
302
+ const added = {};
303
+ const files = [
304
+ ...root === void 0 ? [] : CASCADE.map((name) => [join3(root, name), name]),
305
+ ...MODULE_CASCADE.map((name) => [join3(cwd, name), `<module>/${name}`])
306
+ ];
307
+ for (const [path, label] of files) {
308
+ if (!existsSync3(path)) continue;
309
+ loaded.push(label);
310
+ Object.assign(added, parse(path));
311
+ }
312
+ const env = { ...added, ...process.env };
313
+ return { env, loaded };
314
+ }
315
+
316
+ // src/roles/test/reference.ts
317
+ import { execFileSync } from "node:child_process";
318
+ import { createHash } from "node:crypto";
319
+ import { existsSync as existsSync5, mkdirSync, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync } from "node:fs";
320
+ import { tmpdir } from "node:os";
321
+ import { join as join5, resolve } from "node:path";
322
+
323
+ // src/shared/resolve-bin.ts
324
+ import { existsSync as existsSync4 } from "node:fs";
325
+ import { createRequire } from "node:module";
326
+ import { delimiter, dirname, join as join4 } from "node:path";
327
+ var require2 = createRequire(import.meta.url);
328
+ function resolveBin(fromDir, name) {
329
+ let dir = fromDir;
330
+ for (; ; ) {
331
+ const candidate = join4(dir, "node_modules", ".bin", name);
332
+ if (existsSync4(candidate)) return candidate;
333
+ const parent = dirname(dir);
334
+ if (parent === dir) return void 0;
335
+ dir = parent;
336
+ }
337
+ }
338
+ function binFromOwnInstall(packageName, binName) {
339
+ try {
340
+ const manifest = require2.resolve(`${packageName}/package.json`);
341
+ const bin = require2(manifest).bin;
342
+ const relative6 = typeof bin === "string" ? bin : bin?.[binName];
343
+ if (!relative6) return void 0;
344
+ const executable = join4(dirname(manifest), relative6);
345
+ return existsSync4(executable) ? executable : void 0;
346
+ } catch {
347
+ return void 0;
348
+ }
349
+ }
350
+ function binSearchPath(cwd) {
351
+ return [`${cwd}${delimiter}node_modules/.bin (and parents)`, "sentinel's own install"].join(
352
+ " then "
353
+ );
354
+ }
355
+
356
+ // src/roles/test/baseline.ts
357
+ function readSnapshot(report) {
358
+ const files = report?.testResults ?? [];
359
+ const passing = [];
360
+ const failing = [];
361
+ const unloadable = [];
362
+ const skipped = [];
363
+ for (const file of files) {
364
+ const assertions = file?.assertionResults ?? [];
365
+ if (file?.status === "failed" && assertions.length === 0) {
366
+ unloadable.push(typeof file.name === "string" ? file.name : "an unnamed file");
367
+ }
368
+ for (const assertion of assertions) {
369
+ const name = assertion?.fullName;
370
+ if (typeof name !== "string" || name === "") continue;
371
+ if (assertion.status === "passed") passing.push(name);
372
+ else if (assertion.status === "failed") failing.push(name);
373
+ else skipped.push(name);
374
+ }
375
+ }
376
+ const claimed = report?.numFailedTests;
377
+ return {
378
+ files: files.length,
379
+ passing: passing.sort(),
380
+ failing: failing.sort(),
381
+ unloadable: unloadable.sort(),
382
+ skipped: skipped.sort(),
383
+ claimedFailing: typeof claimed === "number" ? claimed : failing.length
384
+ };
385
+ }
386
+ function difference(left, right) {
387
+ const remaining = /* @__PURE__ */ new Map();
388
+ for (const name of right) remaining.set(name, (remaining.get(name) ?? 0) + 1);
389
+ const missing = [];
390
+ for (const name of left) {
391
+ const left_ = remaining.get(name) ?? 0;
392
+ if (left_ === 0) {
393
+ missing.push(name);
394
+ continue;
395
+ }
396
+ remaining.set(name, left_ - 1);
397
+ }
398
+ return missing.sort();
399
+ }
400
+ var withoutRenderingQuotes = (name) => name.replaceAll(String.raw`\'`, "'").replaceAll(String.raw`\"`, '"').replaceAll(/['"`]\s*\+\s*['"`]/g, "").replaceAll(/(^|[\s:,[{(])(''|"")($|[\s,)\]}])/g, "$1<empty-string>$3").replaceAll(/['"`]/g, "");
401
+ function withoutRenderingNoise(name) {
402
+ const spelled = withSortedKeys(
403
+ withoutUndefinedEntries(
404
+ withoutErrorRendering(
405
+ withoutNaN(
406
+ withoutTimestamps(withoutRenderingQuotes(withoutClassBodies(withoutStackFrames(name))))
407
+ )
408
+ )
409
+ )
410
+ );
411
+ return withoutArrayBrackets(
412
+ 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, ",")
413
+ );
414
+ }
415
+ function withoutArrayBrackets(name) {
416
+ return name.replaceAll(
417
+ /\[([^[\]]*)\]/g,
418
+ (match, inner) => (
419
+ // `[Object]` and `[Array]` are the formatter saying it STOPPED, not an array literal.
420
+ // Stripping their brackets would erase the marker `collapsedOf` pairs on.
421
+ /^(?:Object|Array)$/.test(inner) ? match : inner.replaceAll(/,\s+/g, ",")
422
+ )
423
+ );
424
+ }
425
+ var withoutClassBodies = (name) => name.replaceAll(
426
+ /class\s+([A-Za-z_$][\w$]*)\s*\{\s*\}/g,
427
+ (_match, className) => `class ${className}`
428
+ ).replaceAll(
429
+ /class\s+([A-Za-z_$][\w$]*)[\s\S]*?\n\}/g,
430
+ (_match, className) => `class ${className}`
431
+ );
432
+ var withoutTimestamps = (name) => name.replaceAll(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, "<instant>");
433
+ var withoutNaN = (name) => name.replaceAll(/\bNaN\b/g, "null");
434
+ function entriesOf(inner) {
435
+ const parts = [];
436
+ let depth = 0;
437
+ let start = 0;
438
+ for (let index = 0; index < inner.length; index += 1) {
439
+ const character = inner[index];
440
+ if (character === "[" || character === "(") depth += 1;
441
+ else if (character === "]" || character === ")") depth -= 1;
442
+ else if (character === "," && depth === 0) {
443
+ parts.push(inner.slice(start, index));
444
+ start = index + 1;
445
+ }
446
+ if (depth < 0) return void 0;
447
+ }
448
+ if (depth !== 0) return void 0;
449
+ parts.push(inner.slice(start));
450
+ return parts.map((part) => part.trim()).filter(Boolean);
451
+ }
452
+ var sortedGroup = (match, inner) => {
453
+ if (!inner.includes(":")) return match;
454
+ const parts = entriesOf(inner);
455
+ if (parts === void 0) return match;
456
+ if (parts.length < 2 || !parts.every((part) => part.includes(":"))) return match;
457
+ return `{${[...parts].sort().join(",")}}`;
458
+ };
459
+ var HELD = "\u241F";
460
+ var withSortedKeys = (name) => {
461
+ const held = [];
462
+ let current = name;
463
+ for (let depth = 0; depth < 24; depth += 1) {
464
+ const next = current.replaceAll(/\{([^{}]*)\}/g, (match, inner) => {
465
+ held.push(sortedGroup(match, inner));
466
+ return `${HELD}${held.length - 1}${HELD}`;
467
+ });
468
+ if (next === current) break;
469
+ current = next;
470
+ }
471
+ for (let depth = 0; depth < 24 && current.includes(HELD); depth += 1) {
472
+ current = current.replaceAll(
473
+ new RegExp(`${HELD}(\\d+)${HELD}`, "g"),
474
+ (_match, index) => held[Number(index)]
475
+ );
476
+ }
477
+ return current;
478
+ };
479
+ var withoutStackFrames = (name) => {
480
+ const frame = name.search(/\n\s+at\s/);
481
+ return frame === -1 ? name : name.slice(0, frame);
482
+ };
483
+ var withoutErrorRendering = (name) => name.replaceAll(/\[[A-Za-z]*(?:Error|Exception):[^\][]*\]/g, "{}");
484
+ 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, "{}");
485
+ function collapsedOf(candidate, name) {
486
+ if (!/\[(?:Object|Array)\]/.test(candidate)) return false;
487
+ const pattern = candidate.split(/\[(?:Object|Array)\]/).map((part) => part.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
488
+ return new RegExp(`^${pattern}$`, "s").test(name);
489
+ }
490
+ function truncationOf(candidate, name) {
491
+ const cut = candidate.indexOf("\u2026");
492
+ if (cut === -1) return false;
493
+ const head = candidate.slice(0, cut);
494
+ const tail = candidate.slice(cut + 1);
495
+ return name.startsWith(head) && name.endsWith(tail);
496
+ }
497
+ function pairRenamed(missing, added) {
498
+ const remaining = [...added];
499
+ const renamed = [];
500
+ const unmatched = [];
501
+ for (const name of missing) {
502
+ const normalised = withoutRenderingNoise(name);
503
+ const at = remaining.findIndex((candidate) => withoutRenderingNoise(candidate) === normalised);
504
+ if (at === -1) {
505
+ unmatched.push(name);
506
+ continue;
507
+ }
508
+ renamed.push({ before: name, after: remaining[at] });
509
+ remaining.splice(at, 1);
510
+ }
511
+ const lost = [];
512
+ for (const name of unmatched) {
513
+ const normalised = withoutRenderingNoise(name);
514
+ const at = remaining.findIndex(
515
+ (candidate) => truncationOf(withoutRenderingNoise(candidate), normalised) || collapsedOf(normalised, withoutRenderingNoise(candidate))
516
+ );
517
+ if (at === -1) {
518
+ lost.push(name);
519
+ continue;
520
+ }
521
+ renamed.push({ before: name, after: remaining[at] });
522
+ remaining.splice(at, 1);
523
+ }
524
+ return { renamed, missing: lost, added: remaining };
525
+ }
526
+ function compareSnapshots(before, after) {
527
+ const beforeAll = [...before.passing, ...before.failing];
528
+ const afterAll = [...after.passing, ...after.failing];
529
+ const paired = pairRenamed(difference(beforeAll, afterAll), difference(afterAll, beforeAll));
530
+ const renamedBefore = new Set(paired.renamed.map((pair) => pair.before));
531
+ const regressed = difference(before.passing, after.passing).filter(
532
+ (name) => !paired.missing.includes(name) && !renamedBefore.has(name)
533
+ );
534
+ const failedBefore = new Set(before.failing);
535
+ const recovered = after.passing.filter((name) => failedBefore.has(name));
536
+ const skippedAfter = new Set(after.skipped ?? []);
537
+ const missingBecauseSkipped = paired.missing.filter((name) => skippedAfter.has(name));
538
+ return {
539
+ missingBecauseSkipped,
540
+ identical: paired.missing.length === 0 && paired.added.length === 0 && regressed.length === 0,
541
+ missing: paired.missing,
542
+ added: paired.added,
543
+ regressed,
544
+ renamed: paired.renamed,
545
+ recovered,
546
+ alreadyFailing: before.failing.length
547
+ };
548
+ }
549
+ function unusableAsBaseline(snapshot) {
550
+ if (snapshot.unloadable.length > 0) {
551
+ 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`;
552
+ }
553
+ if (snapshot.claimedFailing > snapshot.failing.length) {
554
+ 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`;
555
+ }
556
+ if (snapshot.passing.length === 0) {
557
+ return `the reference run reports no passing test at all, so a green run after would prove nothing: there is no claim to preserve`;
558
+ }
559
+ return void 0;
560
+ }
561
+ function renamedExample(before, after) {
562
+ let shared = 0;
563
+ while (shared < before.length && shared < after.length && before[shared] === after[shared]) {
564
+ shared += 1;
565
+ }
566
+ const boundary = before.lastIndexOf(" ", shared);
567
+ const from = boundary > 0 ? boundary + 1 : 0;
568
+ const ellipsis = from > 0 ? "\u2026" : "";
569
+ return `"${ellipsis}${before.slice(from)}" -> "${ellipsis}${after.slice(from)}"`;
570
+ }
571
+ function describeComparison(comparison) {
572
+ const firstRename = comparison.renamed[0];
573
+ const renamedNote = firstRename === void 0 ? "" : ` ${comparison.renamed.length} test(s) kept their identity and changed name, because the runners render an each title's interpolated value differently (e.g. ${renamedExample(firstRename.before, firstRename.after)}).`;
574
+ 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.` : "";
575
+ const recoveredNote = comparison.recovered.length > 0 ? ` ${comparison.recovered.length} test(s) were failing before and pass now.` : "";
576
+ if (comparison.identical) {
577
+ return `every test that ran before runs and passes now.${recoveredNote}${renamedNote}${alreadyRedNote}`;
578
+ }
579
+ const parts = [];
580
+ if (comparison.recovered.length > 0) {
581
+ parts.push(`${comparison.recovered.length} test(s) were failing before and pass now`);
582
+ }
583
+ if (comparison.missing.length > 0) {
584
+ const because = comparison.missingBecauseSkipped.length === 0 ? "" : ` ${comparison.missingBecauseSkipped.length} of them are reported by the new run and run by nobody, which is what a \`beforeAll\` that throws does: jest counted the same test as FAILED, Vitest counts it as skipped, so the name leaves both lists. Look at that file's suite setup, not at the test.`;
585
+ parts.push(
586
+ `${comparison.missing.length} test(s) no longer exist: ${comparison.missing.slice(0, 3).join(", ")}.${because}`
587
+ );
588
+ }
589
+ if (comparison.added.length > 0) {
590
+ parts.push(
591
+ `${comparison.added.length} test(s) appeared, which a migration should not do: ${comparison.added.slice(0, 3).join(", ")}`
592
+ );
593
+ }
594
+ if (comparison.regressed.length > 0) {
595
+ parts.push(
596
+ `${comparison.regressed.length} test(s) passed before and do not now: ${comparison.regressed.slice(0, 3).join(", ")}`
597
+ );
598
+ }
599
+ return `${parts.join("; ")}.${renamedNote}${alreadyRedNote}`;
600
+ }
601
+
602
+ // src/roles/test/reference.ts
603
+ function referencePath(cwd, suite) {
604
+ const key = createHash("sha256").update(`${resolve(cwd)}\0${suite}`).digest("hex").slice(0, 16);
605
+ return join5(tmpdir(), "sentinel-test-reference", `${key}.json`);
606
+ }
607
+ function suiteOf(config) {
608
+ const base = config.replace(/^(jest|vitest)\./, "").replace(/\.[cm]?[jt]s$/, "");
609
+ const name = base.replace(/[.-]?config$/, "");
610
+ return name === "" ? "main" : name;
611
+ }
612
+ function runJest(cwd, config) {
613
+ const bin = resolveBin(cwd, "jest");
614
+ if (bin === void 0) return void 0;
615
+ const root = findWorkspaceRoot(cwd) ?? cwd;
616
+ const out = join5(mkdtempSync(join5(tmpdir(), "sentinel-reference-")), "jest.json");
617
+ try {
618
+ execFileSync(bin, ["-c", join5(cwd, config), "--json", `--outputFile=${out}`, "--silent"], {
619
+ cwd: root,
620
+ /*
621
+ * ⚠️ The module's environment, not the shell's, and this is what made the reference lie.
622
+ * Measured on `institution`: the task runner loads the repo's dotenv files before starting
623
+ * the task, a typed command does not, and the difference was 314 env names against 49. The
624
+ * reference recorded 2958 passing where the module's own target sees 2957. See
625
+ * `module-env.ts`.
626
+ */
627
+ env: moduleEnvironment(cwd).env,
628
+ stdio: ["ignore", "ignore", "ignore"],
629
+ maxBuffer: 1 << 28
630
+ });
631
+ } catch {
632
+ }
633
+ if (!existsSync5(out)) return void 0;
634
+ try {
635
+ return JSON.parse(readFileSync4(out, "utf8"));
636
+ } catch {
637
+ return void 0;
638
+ }
639
+ }
640
+ function captureReference(cwd, config) {
641
+ const report = runJest(cwd, config);
642
+ if (report === void 0) {
643
+ return {
644
+ captured: false,
645
+ 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.`
646
+ };
647
+ }
648
+ const snapshot = readSnapshot(report);
649
+ const unusable = unusableAsBaseline(snapshot);
650
+ if (unusable !== void 0) {
651
+ return {
652
+ captured: false,
653
+ message: `no reference recorded: ${unusable} The migration is applied anyway, unproved.`
654
+ };
655
+ }
656
+ const stored = {
657
+ runner: "jest",
658
+ config,
659
+ takenAt: (/* @__PURE__ */ new Date()).toISOString(),
660
+ snapshot
661
+ };
662
+ const path = referencePath(cwd, suiteOf(config));
663
+ mkdirSync(join5(path, ".."), { recursive: true });
664
+ writeFileSync(path, `${JSON.stringify(stored, void 0, 2)}
665
+ `);
666
+ return {
667
+ captured: true,
668
+ message: `recorded ${snapshot.passing.length} passing test name(s) from ${config} as the reference. Migrate, then \`sentinel --validate --test\` again to compare against it.`
669
+ };
670
+ }
671
+ function hasReference(cwd, suite) {
672
+ return existsSync5(referencePath(cwd, suite));
673
+ }
674
+ function compareAgainstReference(cwd, suite, report) {
675
+ const path = referencePath(cwd, suite);
676
+ let stored;
677
+ try {
678
+ stored = JSON.parse(readFileSync4(path, "utf8"));
679
+ } catch {
680
+ rmSync(path, { force: true });
681
+ return { ok: true, message: `the recorded reference could not be read, so nothing was proved.` };
682
+ }
683
+ const comparison = compareSnapshots(stored.snapshot, readSnapshot(report));
684
+ if (comparison.identical) rmSync(path, { force: true });
685
+ const kept = comparison.identical ? "" : ` The reference is KEPT until a run passes it, so this does not go green by running again. Fix the suite and re-run, or delete ${path} to abandon the proof on purpose.`;
686
+ return {
687
+ ok: comparison.identical,
688
+ message: `${describeComparison(comparison)} (reference: ${stored.config}, ${stored.takenAt})${kept}`
689
+ };
690
+ }
691
+
692
+ // src/roles/test/resolve-vitest.ts
693
+ function resolveVitest(cwd) {
694
+ const fromSentinel = binFromOwnInstall("vitest", "vitest");
695
+ if (fromSentinel !== void 0) return { bin: fromSentinel, origin: "sentinel" };
696
+ const fromModule = resolveBin(cwd, "vitest");
697
+ return fromModule !== void 0 ? { bin: fromModule, origin: "module" } : { bin: void 0, origin: "module" };
698
+ }
699
+
700
+ // src/roles/test/adapters/vitest/vitest.adapter.ts
701
+ import { spawnSync } from "node:child_process";
702
+ import { isAbsolute as isAbsolute2, resolve as resolve7 } from "node:path";
703
+
704
+ // src/core/config/tool-args.ts
705
+ import { existsSync as existsSync6 } from "node:fs";
706
+ import { isAbsolute, resolve as resolve2 } from "node:path";
707
+ function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
708
+ const takesValue = new Set(valueFlags);
709
+ const options = [];
710
+ const paths = [];
711
+ let previousTakesValue = false;
712
+ for (const arg of toolArgs) {
713
+ const looksLikeOption = arg.startsWith("-");
714
+ const target = isAbsolute(arg) ? arg : resolve2(cwd, arg);
715
+ if (!previousTakesValue && !looksLikeOption && existsSync6(target)) paths.push(arg);
716
+ else options.push(arg);
717
+ previousTakesValue = !arg.includes("=") && takesValue.has(arg);
718
+ }
719
+ return { options, paths };
720
+ }
721
+
722
+ // src/core/config/nx-target.ts
723
+ import { existsSync as existsSync7 } from "node:fs";
724
+ import { join as join6 } from "node:path";
725
+ function nxTargetMetadataOperations(options) {
726
+ const { targets } = options;
727
+ if (Object.keys(targets).length === 0) return [];
728
+ return [{ kind: "merge-json", path: "package.json", value: { nx: { targets } } }];
729
+ }
730
+ function nxTargetOperations(options) {
731
+ const { cwd, targets } = options;
732
+ const names = Object.keys(targets);
733
+ if (names.length === 0) return [];
734
+ const operations = [];
735
+ if (existsSync7(join6(cwd, "project.json"))) {
736
+ operations.push({
737
+ kind: "remove-json-keys",
738
+ path: "project.json",
739
+ keys: names.map((name) => ["targets", name])
740
+ });
741
+ }
742
+ operations.push(...nxTargetMetadataOperations(options));
743
+ return operations;
744
+ }
745
+
746
+ // src/core/config/project-json.ts
747
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "node:fs";
748
+ import { join as join7 } from "node:path";
749
+ function projectTargets(cwd) {
750
+ const path = join7(cwd, "project.json");
751
+ if (!existsSync8(path)) return void 0;
752
+ try {
753
+ return JSON.parse(readFileSync5(path, "utf8")).targets;
754
+ } catch {
755
+ return void 0;
756
+ }
757
+ }
758
+
759
+ // src/core/config/rewrite-imports.ts
760
+ import { parseSync } from "oxc-parser";
761
+ var movedSpecifiers = (toolchain) => new Set(toolchain.owned.map((entry) => entry.specifier));
762
+ var defaultExportNames = (toolchain) => Object.fromEntries(
763
+ toolchain.owned.filter((entry) => entry.default !== void 0).map((entry) => [entry.specifier, entry.default])
764
+ );
765
+ function renderImport(bindings, presetSpecifier, semi) {
766
+ const render = (list) => list.map((b) => b.exported === b.local ? b.exported : `${b.exported} as ${b.local}`).sort((left, right) => left.localeCompare(right)).join(", ");
767
+ const end = semi ? ";" : "";
768
+ const values = bindings.filter((b) => !b.typeOnly);
769
+ const types = bindings.filter((b) => b.typeOnly);
770
+ const lines = [];
771
+ if (values.length > 0) lines.push(`import { ${render(values)} } from '${presetSpecifier}'${end}`);
772
+ if (types.length > 0) {
773
+ lines.push(`import type { ${render(types)} } from '${presetSpecifier}'${end}`);
774
+ }
775
+ return lines.join("\n");
776
+ }
777
+ function rewriteToolchainImports(toolchain, source, fileName) {
778
+ const moved = movedSpecifiers(toolchain);
779
+ const defaults = defaultExportNames(toolchain);
780
+ const { program, errors } = parseSync(fileName, source, { sourceType: "module" });
781
+ if (errors.length > 0) {
782
+ return {
783
+ kind: "blocked",
784
+ why: `${fileName} does not parse: ${errors[0]?.message ?? "unknown"}`
785
+ };
786
+ }
787
+ const imports = program.body.filter(
788
+ (node) => node.type === "ImportDeclaration"
789
+ );
790
+ if (imports.some((node) => node.source.value === toolchain.presetSpecifier)) {
791
+ return { kind: "unchanged", why: "this config already imports its toolchain from sentinel" };
792
+ }
793
+ const moving = imports.filter((node) => moved.has(node.source.value));
794
+ if (moving.length === 0) {
795
+ return {
796
+ kind: "unchanged",
797
+ why: "this config imports none of the packages sentinel owns, so there is nothing to move"
798
+ };
799
+ }
800
+ const bindings = [];
801
+ for (const node of moving) {
802
+ const from = node.source.value;
803
+ const specifiers = node.specifiers ?? [];
804
+ if (specifiers.length === 0) {
805
+ return { kind: "blocked", why: `\`import '${from}'\` has no bindings to move` };
806
+ }
807
+ for (const spec of specifiers) {
808
+ const typeOnly = node.importKind === "type" || spec.importKind === "type";
809
+ if (spec.type === "ImportDefaultSpecifier") {
810
+ const exported = defaults[from];
811
+ if (!exported) {
812
+ return {
813
+ kind: "blocked",
814
+ why: `no sentinel export for the default import of \`${from}\``
815
+ };
816
+ }
817
+ bindings.push({ exported, local: spec.local.name, typeOnly });
818
+ continue;
819
+ }
820
+ if (spec.type === "ImportSpecifier") {
821
+ bindings.push({
822
+ exported: spec.imported?.name ?? spec.imported?.value ?? spec.local.name,
823
+ local: spec.local.name,
824
+ typeOnly
825
+ });
826
+ continue;
827
+ }
828
+ return {
829
+ kind: "blocked",
830
+ why: `\`import * as ${spec.local.name} from '${from}'\` cannot be mapped onto sentinel's named exports`
831
+ };
832
+ }
833
+ }
834
+ const spans = moving.map((node) => ({ start: node.start, end: node.end })).sort((left, right) => left.start - right.start);
835
+ const first = spans[0];
836
+ const rest = spans.slice(1);
837
+ let out = source;
838
+ for (const span of rest.slice().reverse()) {
839
+ const after = out.slice(span.end).match(/^\r?\n/)?.[0].length ?? 0;
840
+ out = out.slice(0, span.start) + out.slice(span.end + after);
841
+ }
842
+ const semi = source.slice(first.start, first.end).trimEnd().endsWith(";");
843
+ out = out.slice(0, first.start) + renderImport(bindings, toolchain.presetSpecifier, semi) + out.slice(first.end);
844
+ return {
845
+ kind: "rewritten",
846
+ source: out,
847
+ moved: [...new Set(moving.map((node) => node.source.value))].sort()
848
+ };
849
+ }
850
+
851
+ // src/roles/test/jest-migration.ts
852
+ import { existsSync as existsSync18, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "node:fs";
853
+ import { createRequire as createRequire5 } from "node:module";
854
+ import { dirname as dirname10, join as join22, relative as relative5 } from "node:path";
855
+
856
+ // src/shared/jsonc.ts
857
+ import { parse as parse2, printParseErrorCode } from "jsonc-parser";
858
+ function parseJsonc(text, source = "config") {
859
+ const errors = [];
860
+ const value = parse2(text, errors, { allowTrailingComma: true });
861
+ if (errors.length > 0) {
862
+ const details = errors.map((error) => printParseErrorCode(error.error)).join(", ");
863
+ throw new Error(`${source}: malformed JSONC (${details}).`);
864
+ }
865
+ return value;
866
+ }
867
+
868
+ // src/roles/test/codemod/migrate-module.ts
869
+ import { readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
870
+ import { join as join15 } from "node:path";
871
+ import { parseSync as parseSync7 } from "oxc-parser";
872
+
873
+ // src/roles/test/msw-lifecycle.ts
874
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
875
+ import { join as join8 } from "node:path";
876
+ var ROOT_SETUP = "jest.setup.after.env.js";
877
+ function ownsMswLifecycle(cwd, setupFiles) {
878
+ for (const raw of setupFiles) {
879
+ if (raw.endsWith(ROOT_SETUP)) continue;
880
+ const file = join8(cwd, raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", "."));
881
+ if (!existsSync9(file)) continue;
882
+ try {
883
+ if (/\.listen\s*\(/.test(readFileSync6(file, "utf8"))) return true;
884
+ } catch {
885
+ continue;
886
+ }
887
+ }
888
+ return false;
889
+ }
890
+
891
+ // src/roles/test/support-file-name.ts
892
+ var JEST_PREFIX = "jest.";
893
+ var VITEST_PREFIX = "vitest.";
894
+ function vitestSupportName(relative6) {
895
+ const cut = relative6.lastIndexOf("/");
896
+ const directory = cut === -1 ? "" : relative6.slice(0, cut + 1);
897
+ const base = relative6.slice(cut + 1);
898
+ if (!base.startsWith(JEST_PREFIX)) return relative6;
899
+ return `${directory}${VITEST_PREFIX}${base.slice(JEST_PREFIX.length)}`;
900
+ }
901
+
902
+ // src/roles/test/codemod/migrate-file.ts
903
+ import { dirname as dirname6 } from "node:path";
904
+ import { parseSync as parseSync6 } from "oxc-parser";
905
+
906
+ // src/shared/ast.ts
907
+ var SKIP = /* @__PURE__ */ Symbol("skip-subtree");
908
+ function walk(node, visit) {
909
+ if (node === null || typeof node !== "object") return;
910
+ if (Array.isArray(node)) {
911
+ for (const entry of node) walk(entry, visit);
912
+ return;
913
+ }
914
+ const current = node;
915
+ if (typeof current.type === "string" && visit(current) === SKIP) return;
916
+ for (const value of Object.values(current)) {
917
+ if (value !== null && typeof value === "object") walk(value, visit);
918
+ }
919
+ }
920
+
921
+ // src/roles/test/codemod/assertion-return-value.ts
922
+ var MEMBER_TYPES = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
923
+ function rootedAtExpect(node) {
924
+ let current = node;
925
+ for (let depth = 0; current !== void 0 && depth < 12; depth += 1) {
926
+ if (current.type === "CallExpression") {
927
+ const callee = current.callee;
928
+ if (callee?.type === "Identifier") return callee.name === "expect";
929
+ current = callee;
930
+ continue;
931
+ }
932
+ if (MEMBER_TYPES.has(current.type)) {
933
+ current = current.object;
934
+ continue;
935
+ }
936
+ return false;
937
+ }
938
+ return false;
939
+ }
940
+ function capture(node) {
941
+ if (node.type !== "VariableDeclaration") return void 0;
942
+ const declarations = node.declarations ?? [];
943
+ if (declarations.length !== 1) return void 0;
944
+ const only = declarations[0];
945
+ const id = only.id;
946
+ const init = only.init;
947
+ if (id?.type !== "Identifier" || init?.type !== "AwaitExpression") return void 0;
948
+ if (!rootedAtExpect(init.argument)) return void 0;
949
+ return { name: id.name, init };
950
+ }
951
+ function undefinedAssertionOf(node, name) {
952
+ if (node.type !== "CallExpression") return false;
953
+ const callee = node.callee;
954
+ if (callee === void 0 || !MEMBER_TYPES.has(callee.type)) return false;
955
+ const property = callee.property;
956
+ if (property?.type !== "Identifier" || property.name !== "toBeUndefined") return false;
957
+ const object = callee.object;
958
+ if (object?.type !== "CallExpression") return false;
959
+ if (object.callee?.type !== "Identifier") return false;
960
+ if (object.callee.name !== "expect") return false;
961
+ const [argument] = object.arguments ?? [];
962
+ return argument?.type === "Identifier" && argument.name === name;
963
+ }
964
+ function rewriteAssertionReturnValue(source, program) {
965
+ const captures = [];
966
+ walk(program, (node) => {
967
+ const found = capture(node);
968
+ if (found === void 0) return;
969
+ captures.push({
970
+ name: found.name,
971
+ declarationStart: node.start,
972
+ initStart: found.init.start
973
+ });
974
+ });
975
+ const notReferences = [];
976
+ walk(program, (node) => {
977
+ if (node.type === "Property" || node.type === "ObjectProperty" || node.type === "PropertyDefinition") {
978
+ if (node.computed === true) return;
979
+ const key = node.key;
980
+ if (key !== void 0)
981
+ notReferences.push({ start: key.start, end: key.end });
982
+ return;
983
+ }
984
+ if (MEMBER_TYPES.has(node.type) && node.computed !== true) {
985
+ const property = node.property;
986
+ if (property !== void 0) {
987
+ notReferences.push({ start: property.start, end: property.end });
988
+ }
989
+ }
990
+ });
991
+ const functions = [];
992
+ walk(program, (node) => {
993
+ if (/Function|ArrowFunctionExpression/.test(node.type)) functions.push(node);
994
+ });
995
+ const scopeOf = (position) => {
996
+ const containing = functions.filter((fn) => position >= fn.start && position <= fn.end).sort(
997
+ (a, b) => a.end - a.start - (b.end - b.start)
998
+ );
999
+ const nearest = containing[0];
1000
+ return nearest === void 0 ? { start: 0, end: source.length } : { start: nearest.start, end: nearest.end };
1001
+ };
1002
+ const edits = [];
1003
+ const refusals = [];
1004
+ for (const found of captures) {
1005
+ const readings = [];
1006
+ let readElsewhere = false;
1007
+ const scope = scopeOf(found.declarationStart);
1008
+ walk(program, (node) => {
1009
+ const start = node.start;
1010
+ if (start < scope.start || node.end > scope.end) return;
1011
+ if (undefinedAssertionOf(node, found.name)) {
1012
+ readings.push(node);
1013
+ return;
1014
+ }
1015
+ if (node.type !== "Identifier" || node.name !== found.name) return;
1016
+ if (notReferences.some((span) => start >= span.start && node.end <= span.end)) {
1017
+ return;
1018
+ }
1019
+ const inside = readings.some(
1020
+ (reading) => start >= reading.start && node.end <= reading.end
1021
+ );
1022
+ const isBinding = start < found.initStart;
1023
+ if (!inside && !isBinding) readElsewhere = true;
1024
+ });
1025
+ if (readings.length === 0 || readElsewhere) {
1026
+ if (readings.length > 0) refusals.push(found.name);
1027
+ continue;
1028
+ }
1029
+ edits.push({ start: found.declarationStart, end: found.initStart, text: "" });
1030
+ for (const reading of readings) {
1031
+ const from = reading.start;
1032
+ let to = reading.end;
1033
+ while (to < source.length && /[ \t;]/.test(source[to])) to += 1;
1034
+ let head = from;
1035
+ while (head > 0 && /[ \t]/.test(source[head - 1])) head -= 1;
1036
+ if (head > 0 && source[head - 1] === "\n") head -= 1;
1037
+ edits.push({ start: head, end: to, text: "" });
1038
+ }
1039
+ }
1040
+ let result = source;
1041
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
1042
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
1043
+ }
1044
+ return { source: result, rewritten: captures.length - refusals.length, refusals };
1045
+ }
1046
+
1047
+ // src/roles/test/codemod/bare-mock-factory.ts
1048
+ var MEMBER_TYPES2 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
1049
+ function isMockFactory(node) {
1050
+ if (node === void 0 || !MEMBER_TYPES2.has(node.type)) return false;
1051
+ const object = node.object;
1052
+ const property = node.property;
1053
+ if (object?.type !== "Identifier") return false;
1054
+ if (object.name !== "vi" && object.name !== "jest") return false;
1055
+ return property?.type === "Identifier" && property.name === "fn";
1056
+ }
1057
+ function invokeBareMockFactories(source, program) {
1058
+ const edits = [];
1059
+ walk(program, (node) => {
1060
+ if (node.type !== "CallExpression") return;
1061
+ for (const argument of node.arguments ?? []) {
1062
+ if (isMockFactory(argument)) edits.push(argument.end);
1063
+ }
1064
+ });
1065
+ if (edits.length === 0) return { source, rewritten: 0 };
1066
+ let result = source;
1067
+ for (const end of [...edits].sort((left, right) => right - left)) {
1068
+ result = `${result.slice(0, end)}()${result.slice(end)}`;
1069
+ }
1070
+ return { source: result, rewritten: edits.length };
1071
+ }
1072
+
1073
+ // src/roles/test/codemod/constructible-mocks.ts
1074
+ var MEMBER_TYPES3 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
1075
+ function mockImplementationArrow(node) {
1076
+ if (node.type !== "CallExpression") return void 0;
1077
+ const callee = node.callee;
1078
+ if (callee === void 0 || !MEMBER_TYPES3.has(callee.type)) return void 0;
1079
+ const property = callee.property;
1080
+ if (property?.type !== "Identifier" || property.name !== "mockImplementation") return void 0;
1081
+ const [argument] = node.arguments ?? [];
1082
+ return argument?.type === "ArrowFunctionExpression" ? argument : void 0;
1083
+ }
1084
+ function assertedMockedClass(source, node) {
1085
+ if (node.type !== "CallExpression") return false;
1086
+ const callee = node.callee;
1087
+ if (callee === void 0 || !MEMBER_TYPES3.has(callee.type)) return false;
1088
+ const object = callee.object;
1089
+ if (object === void 0) return false;
1090
+ return /\bMockedClass\b/.test(source.slice(object.start, object.end));
1091
+ }
1092
+ function readsThis(node) {
1093
+ let found = false;
1094
+ walk(node, (inner) => {
1095
+ if (inner.type === "ThisExpression") found = true;
1096
+ });
1097
+ return found;
1098
+ }
1099
+ function rewriteConstructibleMocks(source, program, factories) {
1100
+ const edits = [];
1101
+ const refusals = [];
1102
+ let rewritten = 0;
1103
+ const seen = /* @__PURE__ */ new Set();
1104
+ const subtrees = [...factories];
1105
+ walk(program, (node) => {
1106
+ if (assertedMockedClass(source, node)) subtrees.push(node);
1107
+ });
1108
+ for (const subtree of subtrees) {
1109
+ walk(subtree, (node) => {
1110
+ const arrow = mockImplementationArrow(node);
1111
+ if (arrow === void 0 || seen.has(arrow)) return;
1112
+ seen.add(arrow);
1113
+ if (readsThis(arrow)) {
1114
+ refusals.push({
1115
+ binding: "mockImplementation",
1116
+ reason: `a mock implementation reads \`this\`, so it cannot become a function expression, and an arrow cannot be constructed by Vitest. This one needs a human.`
1117
+ });
1118
+ return;
1119
+ }
1120
+ const params = arrow.params ?? [];
1121
+ const paramsText = params.length === 0 ? "" : source.slice(params[0]?.start, params[params.length - 1]?.end);
1122
+ const body = arrow.body;
1123
+ const bodyText = source.slice(body.start, body.end);
1124
+ const text = body.type === "BlockStatement" ? `function (${paramsText}) ${bodyText}` : `function (${paramsText}) {
1125
+ return ${bodyText}
1126
+ }`;
1127
+ edits.push({ start: arrow.start, end: arrow.end, text });
1128
+ rewritten += 1;
1129
+ });
1130
+ }
1131
+ let result = source;
1132
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
1133
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
1134
+ }
1135
+ return { source: result, rewritten, refusals };
1136
+ }
1137
+
1138
+ // src/roles/test/codemod/deep-imports.ts
1139
+ var DEEP_ENTRIES = {
1140
+ "jest-mock-extended/lib/Mock": { root: "jest-mock-extended", defaultExport: "mock" }
1141
+ };
1142
+ function normaliseDeepImports(source) {
1143
+ let result = source;
1144
+ let rewritten = 0;
1145
+ for (const [deep, { root, defaultExport }] of Object.entries(DEEP_ENTRIES)) {
1146
+ const quoted = deep.replaceAll("/", "\\/");
1147
+ const defaultImport = new RegExp(`import\\s+(\\w+)\\s+from\\s+(['"])${quoted}\\2`, "g");
1148
+ result = result.replace(defaultImport, (_whole, binding, quote) => {
1149
+ rewritten += 1;
1150
+ const named = binding === defaultExport ? defaultExport : `${defaultExport} as ${binding}`;
1151
+ return `import { ${named} } from ${quote}${root}${quote}`;
1152
+ });
1153
+ const anyImport = new RegExp(`(from\\s+)(['"])${quoted}\\2`, "g");
1154
+ result = result.replace(anyImport, (_whole, from, quote) => {
1155
+ rewritten += 1;
1156
+ return `${from}${quote}${root}${quote}`;
1157
+ });
1158
+ }
1159
+ return { source: result, rewritten };
1160
+ }
1161
+
1162
+ // src/roles/test/codemod/each-table-rows.ts
1163
+ var MEMBER_TYPES4 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
1164
+ var RUNNERS = /* @__PURE__ */ new Set(["it", "test", "describe"]);
1165
+ function eachTable(node) {
1166
+ if (node.type !== "CallExpression") return void 0;
1167
+ const callee = node.callee;
1168
+ if (callee === void 0 || !MEMBER_TYPES4.has(callee.type)) return void 0;
1169
+ const property = callee.property;
1170
+ if (property?.type !== "Identifier" || property.name !== "each") return void 0;
1171
+ let object = callee.object;
1172
+ while (object !== void 0 && MEMBER_TYPES4.has(object.type)) {
1173
+ object = object.object;
1174
+ }
1175
+ if (object?.type !== "Identifier" || !RUNNERS.has(object.name)) return void 0;
1176
+ const [table] = node.arguments ?? [];
1177
+ return table?.type === "ArrayExpression" ? table : void 0;
1178
+ }
1179
+ function rewriteEachTableRows(source, program) {
1180
+ const edits = [];
1181
+ walk(program, (node) => {
1182
+ const table = eachTable(node);
1183
+ if (table === void 0) return;
1184
+ const rows = table.elements ?? [];
1185
+ if (rows.length === 0 || rows.some((row) => row === null)) return;
1186
+ const arrays = rows.filter((row) => row.type === "ArrayExpression").length;
1187
+ if (arrays === 0 || arrays === rows.length) return;
1188
+ const wrapped = rows.map((row) => source.slice(row.start, row.end)).map((text) => `[${text}]`).join(", ");
1189
+ edits.push({ start: table.start, end: table.end, text: `[${wrapped}]` });
1190
+ });
1191
+ let result = source;
1192
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
1193
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
1194
+ }
1195
+ return { source: result, rewritten: edits.length };
1196
+ }
1197
+
1198
+ // src/roles/test/codemod/each-title.ts
1199
+ var MEMBER_TYPES5 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
1200
+ function eachTitle(node) {
1201
+ if (node.type !== "CallExpression") return void 0;
1202
+ const inner = node.callee;
1203
+ if (inner?.type !== "CallExpression") return void 0;
1204
+ const member = inner.callee;
1205
+ if (member === void 0 || !MEMBER_TYPES5.has(member.type)) return void 0;
1206
+ const property = member.property;
1207
+ if (property?.type !== "Identifier" || property.name !== "each") return void 0;
1208
+ const [title] = node.arguments ?? [];
1209
+ if (title === void 0) return void 0;
1210
+ return title.type === "StringLiteral" || title.type === "Literal" || title.type === "TemplateLiteral" ? title : void 0;
1211
+ }
1212
+ function rewriteSpecifiers(text) {
1213
+ let result = "";
1214
+ let rewritten = 0;
1215
+ for (let index = 0; index < text.length; index += 1) {
1216
+ if (text[index] !== "%") {
1217
+ result += text[index];
1218
+ continue;
1219
+ }
1220
+ const next = text[index + 1];
1221
+ if (next === "%") {
1222
+ result += "%%";
1223
+ index += 1;
1224
+ continue;
1225
+ }
1226
+ if (next === "p") {
1227
+ result += "%j";
1228
+ rewritten += 1;
1229
+ index += 1;
1230
+ continue;
1231
+ }
1232
+ result += "%";
1233
+ }
1234
+ return { text: result, rewritten };
1235
+ }
1236
+ function rewriteEachTitles(source, program) {
1237
+ const edits = [];
1238
+ let rewritten = 0;
1239
+ walk(program, (node) => {
1240
+ const title = eachTitle(node);
1241
+ if (title === void 0) return;
1242
+ const start = title.start;
1243
+ const end = title.end;
1244
+ const replacement = rewriteSpecifiers(source.slice(start, end));
1245
+ if (replacement.rewritten === 0) return;
1246
+ edits.push({ start, end, text: replacement.text });
1247
+ rewritten += replacement.rewritten;
1248
+ });
1249
+ let result = source;
1250
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
1251
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
1252
+ }
1253
+ return { source: result, rewritten };
1254
+ }
1255
+
1256
+ // src/roles/test/codemod/empty-mock-implementation.ts
1257
+ var METHODS = /* @__PURE__ */ new Set(["mockImplementation", "mockImplementationOnce"]);
1258
+ var MEMBER_TYPES6 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
1259
+ function rewriteEmptyMockImplementation(source, program) {
1260
+ const insertAt = [];
1261
+ walk(program, (node) => {
1262
+ if (node.type !== "CallExpression") return;
1263
+ if ((node.arguments ?? []).length > 0) return;
1264
+ const callee = node.callee;
1265
+ if (callee === void 0 || !MEMBER_TYPES6.has(callee.type)) return;
1266
+ const property = callee.property;
1267
+ if (property?.type !== "Identifier" || !METHODS.has(property.name)) return;
1268
+ insertAt.push(node.end - 1);
1269
+ });
1270
+ let result = source;
1271
+ for (const position of [...insertAt].sort((left, right) => right - left)) {
1272
+ result = `${result.slice(0, position)}() => undefined${result.slice(position)}`;
1273
+ }
1274
+ return { source: result, rewritten: insertAt.length };
1275
+ }
1276
+
1277
+ // src/roles/test/codemod/fake-timer-options.ts
1278
+ var MEMBER_TYPES7 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
1279
+ var TIMER_CALLS = /* @__PURE__ */ new Set(["useFakeTimers"]);
1280
+ function timerOptions(node) {
1281
+ if (node.type !== "CallExpression") return void 0;
1282
+ const callee = node.callee;
1283
+ if (callee === void 0 || !MEMBER_TYPES7.has(callee.type)) return void 0;
1284
+ const object = callee.object;
1285
+ const property = callee.property;
1286
+ if (object?.type !== "Identifier") return void 0;
1287
+ if (object.name !== "jest" && object.name !== "vi") return void 0;
1288
+ if (property?.type !== "Identifier" || !TIMER_CALLS.has(property.name)) return void 0;
1289
+ const [options] = node.arguments ?? [];
1290
+ return options?.type === "ObjectExpression" ? options : void 0;
1291
+ }
1292
+ var FAKEABLE = [
1293
+ "setTimeout",
1294
+ "clearTimeout",
1295
+ "setImmediate",
1296
+ "clearImmediate",
1297
+ "setInterval",
1298
+ "clearInterval",
1299
+ "Date",
1300
+ "hrtime",
1301
+ "requestAnimationFrame",
1302
+ "cancelAnimationFrame",
1303
+ "requestIdleCallback",
1304
+ "cancelIdleCallback",
1305
+ "performance"
1306
+ ];
1307
+ function withSeparator(source, options, property) {
1308
+ const properties = options.properties ?? [];
1309
+ const index = properties.indexOf(property);
1310
+ const start = property.start;
1311
+ const end = property.end;
1312
+ if (index < properties.length - 1) {
1313
+ const next = properties[index + 1]?.start;
1314
+ const between2 = next === void 0 ? "" : source.slice(end, next);
1315
+ const comma2 = between2.indexOf(",");
1316
+ if (comma2 >= 0) return { start, end: end + comma2 + 1 };
1317
+ return { start, end };
1318
+ }
1319
+ const previous = properties[index - 1]?.end;
1320
+ if (previous === void 0) return { start, end };
1321
+ const between = source.slice(previous, start);
1322
+ const comma = between.lastIndexOf(",");
1323
+ return comma >= 0 ? { start: previous + comma, end } : { start, end };
1324
+ }
1325
+ function stringLiterals(node) {
1326
+ if (node.type !== "ArrayExpression") return void 0;
1327
+ const values = [];
1328
+ for (const element of node.elements ?? []) {
1329
+ if (element?.type !== "Literal" || typeof element.value !== "string") return void 0;
1330
+ values.push(element.value);
1331
+ }
1332
+ return values;
1333
+ }
1334
+ function rewriteFakeTimerOptions(source, program) {
1335
+ const edits = [];
1336
+ const translated = [];
1337
+ const refusals = [];
1338
+ const warnings = [];
1339
+ walk(program, (node) => {
1340
+ const options = timerOptions(node);
1341
+ if (options === void 0) return;
1342
+ for (const property of options.properties ?? []) {
1343
+ const key = property.key;
1344
+ if (key?.type !== "Identifier") continue;
1345
+ const value = property.value;
1346
+ if (value === void 0) continue;
1347
+ const span = { start: property.start, end: property.end };
1348
+ if (key.name === "advanceTimers") {
1349
+ if (value.type === "Literal" && value.value === true) {
1350
+ edits.push({ ...span, text: "shouldAdvanceTime: true" });
1351
+ translated.push("advanceTimers -> shouldAdvanceTime");
1352
+ continue;
1353
+ }
1354
+ if (value.type === "Literal" && typeof value.value === "number") {
1355
+ edits.push({
1356
+ ...span,
1357
+ text: `shouldAdvanceTime: true, advanceTimeDelta: ${value.value}`
1358
+ });
1359
+ translated.push("advanceTimers -> shouldAdvanceTime + advanceTimeDelta");
1360
+ continue;
1361
+ }
1362
+ refusals.push({
1363
+ binding: "advanceTimers",
1364
+ 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."
1365
+ });
1366
+ continue;
1367
+ }
1368
+ if (key.name === "legacyFakeTimers") {
1369
+ const asked = value.type === "Literal" && value.value === true;
1370
+ edits.push({ ...withSeparator(source, options, property), text: "" });
1371
+ if (asked) {
1372
+ warnings.push(
1373
+ "`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."
1374
+ );
1375
+ }
1376
+ continue;
1377
+ }
1378
+ if (key.name === "doNotFake") {
1379
+ const excluded = stringLiterals(value);
1380
+ if (excluded === void 0) {
1381
+ refusals.push({
1382
+ binding: "doNotFake",
1383
+ 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."
1384
+ });
1385
+ continue;
1386
+ }
1387
+ const kept = FAKEABLE.filter((name) => !excluded.includes(name));
1388
+ edits.push({
1389
+ ...span,
1390
+ text: `toFake: [${kept.map((name) => `'${name}'`).join(", ")}]`
1391
+ });
1392
+ translated.push("doNotFake -> toFake");
1393
+ }
1394
+ }
1395
+ });
1396
+ if (edits.length === 0) return { source, translated, refusals, warnings };
1397
+ let result = source;
1398
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
1399
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
1400
+ }
1401
+ return { source: result, translated, refusals, warnings };
1402
+ }
1403
+
1404
+ // src/roles/test/codemod/scope.ts
1405
+ function readIdentifiers(node, runsLater) {
1406
+ const names = [];
1407
+ walk(node, (current) => {
1408
+ if (current !== node && runsLater?.(current) === true) return SKIP;
1409
+ if (current.type === "Property" || current.type === "ObjectProperty") {
1410
+ if (current.shorthand === true) {
1411
+ names.push(...readIdentifiers(current.value, runsLater));
1412
+ return SKIP;
1413
+ }
1414
+ if (current.computed === true) return;
1415
+ const key = current.key;
1416
+ if (key?.type !== "Identifier") return;
1417
+ names.push(...readIdentifiers(current.value, runsLater));
1418
+ return SKIP;
1419
+ }
1420
+ if (current.type === "MemberExpression" || current.type === "StaticMemberExpression") {
1421
+ if (current.computed === true) return;
1422
+ names.push(...readIdentifiers(current.object, runsLater));
1423
+ return SKIP;
1424
+ }
1425
+ if (current.type === "Identifier") {
1426
+ names.push(current.name);
1427
+ return SKIP;
1428
+ }
1429
+ return void 0;
1430
+ });
1431
+ return names;
1432
+ }
1433
+ function patternNames(pattern) {
1434
+ const names = [];
1435
+ const visit = (node) => {
1436
+ if (node === null || typeof node !== "object") return;
1437
+ const current = node;
1438
+ switch (current.type) {
1439
+ case "Identifier":
1440
+ names.push(current.name);
1441
+ return;
1442
+ case "ObjectPattern":
1443
+ for (const property of current.properties ?? []) {
1444
+ visit(property.type === "RestElement" ? property.argument : property.value);
1445
+ }
1446
+ return;
1447
+ case "ArrayPattern":
1448
+ for (const element of current.elements ?? []) visit(element);
1449
+ return;
1450
+ case "AssignmentPattern":
1451
+ visit(current.left);
1452
+ return;
1453
+ case "RestElement":
1454
+ visit(current.argument);
1455
+ return;
1456
+ default:
1457
+ return;
1458
+ }
1459
+ };
1460
+ visit(pattern);
1461
+ return names;
1462
+ }
1463
+ function moduleBindings(program) {
1464
+ const bindings = /* @__PURE__ */ new Map();
1465
+ for (const statement2 of program.body ?? []) {
1466
+ if (statement2.type !== "VariableDeclaration") continue;
1467
+ for (const declarator of statement2.declarations ?? []) {
1468
+ const init = declarator.init ?? void 0;
1469
+ for (const name of patternNames(declarator.id)) bindings.set(name, init);
1470
+ }
1471
+ }
1472
+ return bindings;
1473
+ }
1474
+ function importedNames(program) {
1475
+ const names = /* @__PURE__ */ new Set();
1476
+ for (const statement2 of program.body ?? []) {
1477
+ if (statement2.type !== "ImportDeclaration") continue;
1478
+ for (const specifier of statement2.specifiers ?? []) {
1479
+ const local = specifier.local;
1480
+ if (local?.type === "Identifier") names.add(local.name);
1481
+ }
1482
+ }
1483
+ return names;
1484
+ }
1485
+ function declaredInside(factory) {
1486
+ const names = /* @__PURE__ */ new Set();
1487
+ walk(factory, (node) => {
1488
+ if (node.type === "VariableDeclarator" || node.type === "FunctionDeclaration") {
1489
+ for (const name of patternNames(node.id)) names.add(name);
1490
+ }
1491
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") {
1492
+ for (const parameter of node.params ?? []) {
1493
+ for (const name of patternNames(parameter)) names.add(name);
1494
+ }
1495
+ }
1496
+ });
1497
+ return names;
1498
+ }
1499
+ var FUNCTION_TYPES = /* @__PURE__ */ new Set([
1500
+ "ArrowFunctionExpression",
1501
+ "FunctionExpression",
1502
+ "FunctionDeclaration"
1503
+ ]);
1504
+ function capturedBindings(factory, bindings) {
1505
+ const own = declaredInside(factory);
1506
+ const invoked = /* @__PURE__ */ new Set();
1507
+ walk(factory, (node) => {
1508
+ if (node.type !== "CallExpression") return;
1509
+ let callee = node.callee;
1510
+ while (callee?.type === "ParenthesizedExpression") callee = callee.expression;
1511
+ if (callee !== void 0 && FUNCTION_TYPES.has(callee.type)) invoked.add(callee);
1512
+ });
1513
+ const runsLater = (candidate) => FUNCTION_TYPES.has(candidate.type) && !invoked.has(candidate);
1514
+ const captured = /* @__PURE__ */ new Set();
1515
+ for (const name of readIdentifiers(factory, runsLater)) {
1516
+ if (bindings.has(name) && !own.has(name)) captured.add(name);
1517
+ }
1518
+ return [...captured];
1519
+ }
1520
+ function hoistable(name, bindings, imported, seen = /* @__PURE__ */ new Set()) {
1521
+ if (seen.has(name)) return true;
1522
+ seen.add(name);
1523
+ const init = bindings.get(name);
1524
+ if (init === void 0) return false;
1525
+ for (const read3 of readIdentifiers(init)) {
1526
+ if (imported.has(read3)) return false;
1527
+ if (bindings.has(read3) && !hoistable(read3, bindings, imported, seen)) return false;
1528
+ }
1529
+ return true;
1530
+ }
1531
+
1532
+ // src/roles/test/codemod/hoist.ts
1533
+ function simpleDeclarations(program) {
1534
+ const found = /* @__PURE__ */ new Map();
1535
+ for (const statement2 of program.body ?? []) {
1536
+ if (statement2.type !== "VariableDeclaration") continue;
1537
+ const declarations = statement2.declarations ?? [];
1538
+ if (declarations.length !== 1) continue;
1539
+ const declarator = declarations[0];
1540
+ const id = declarator.id;
1541
+ const init = declarator.init;
1542
+ if (id?.type !== "Identifier" || init === void 0 || init === null) continue;
1543
+ found.set(id.name, { statement: statement2, init, kind: statement2.kind ?? "const" });
1544
+ }
1545
+ return found;
1546
+ }
1547
+ function interdependent(names, bindings) {
1548
+ for (const name of names) {
1549
+ for (const read3 of readIdentifiers(bindings.get(name))) {
1550
+ if (read3 !== name && names.includes(read3)) return `${name} reads ${read3}`;
1551
+ }
1552
+ }
1553
+ return void 0;
1554
+ }
1555
+ function statementSpan(source, statement2) {
1556
+ let end = statement2.end;
1557
+ if (source[end] === "\r") end++;
1558
+ if (source[end] === "\n") end++;
1559
+ return { start: statement2.start, end };
1560
+ }
1561
+ function hoistCapturedBindings(source, program, factories, bindings, imported) {
1562
+ const captured = [...new Set(factories.flatMap((factory) => capturedBindings(factory, bindings)))];
1563
+ if (captured.length === 0) return { source, hoisted: [], refusals: [] };
1564
+ const refusals = [];
1565
+ for (const name of captured) {
1566
+ if (hoistable(name, bindings, imported)) continue;
1567
+ refusals.push({
1568
+ binding: name,
1569
+ 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`
1570
+ });
1571
+ }
1572
+ const declarations = simpleDeclarations(program);
1573
+ for (const name of captured) {
1574
+ if (!declarations.has(name) && !refusals.some((refusal) => refusal.binding === name)) {
1575
+ refusals.push({
1576
+ binding: name,
1577
+ reason: `it is not declared as a plain \`${name} = ...\` at module scope`
1578
+ });
1579
+ }
1580
+ }
1581
+ const cycle = interdependent(captured, bindings);
1582
+ if (cycle !== void 0) {
1583
+ refusals.push({
1584
+ binding: captured.join(", "),
1585
+ reason: `${cycle}, and an object literal cannot refer to its own keys`
1586
+ });
1587
+ }
1588
+ if (refusals.length > 0) return { source, hoisted: [], refusals };
1589
+ const ordered = captured.map((name) => ({
1590
+ name,
1591
+ ...declarations.get(name)
1592
+ })).sort((left, right) => left.statement.start - right.statement.start);
1593
+ const first = ordered[0];
1594
+ const semi = source.slice(first.statement.start, first.statement.end).trimEnd().endsWith(";");
1595
+ const end = semi ? ";" : "";
1596
+ const initOf = ({ init }) => source.slice(init.start, init.end);
1597
+ const constants = ordered.filter(({ kind }) => kind === "const");
1598
+ const objectLine = constants.length === 0 ? void 0 : `const { ${constants.map(({ name }) => name).join(", ")} } = vi.hoisted(() => ({ ${constants.map((entry) => `${entry.name}: ${initOf(entry)}`).join(", ")} }))${end}
1599
+ `;
1600
+ const lines = [];
1601
+ let objectEmitted = false;
1602
+ for (const entry of ordered) {
1603
+ if (entry.kind === "const") {
1604
+ if (!objectEmitted && objectLine !== void 0) {
1605
+ lines.push(objectLine);
1606
+ objectEmitted = true;
1607
+ }
1608
+ continue;
1609
+ }
1610
+ lines.push(`${entry.kind} ${entry.name} = vi.hoisted(() => ${initOf(entry)})${end}
1611
+ `);
1612
+ }
1613
+ const hoistedLine = lines.join("");
1614
+ const edits = ordered.map(({ statement: statement2 }, index) => {
1615
+ const span = statementSpan(source, statement2);
1616
+ return { ...span, text: index === 0 ? hoistedLine : "" };
1617
+ });
1618
+ let rewritten = source;
1619
+ for (const edit of edits.sort((left, right) => right.start - left.start)) {
1620
+ rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
1621
+ }
1622
+ return { source: rewritten, hoisted: ordered.map(({ name }) => name), refusals: [] };
1623
+ }
1624
+
1625
+ // src/roles/test/codemod/hook-return-value.ts
1626
+ var HOOKS = /* @__PURE__ */ new Set(["beforeEach", "afterEach", "beforeAll", "afterAll"]);
1627
+ var MEMBER_TYPES8 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
1628
+ function hookCallback(node) {
1629
+ if (node.type !== "CallExpression") return void 0;
1630
+ const callee = node.callee;
1631
+ if (callee?.type !== "Identifier" || !HOOKS.has(callee.name)) return void 0;
1632
+ const [callback] = node.arguments ?? [];
1633
+ return callback?.type === "ArrowFunctionExpression" ? callback : void 0;
1634
+ }
1635
+ function configuresAMock(body) {
1636
+ if (body.type !== "CallExpression") return false;
1637
+ const callee = body.callee;
1638
+ if (callee === void 0 || !MEMBER_TYPES8.has(callee.type)) return false;
1639
+ const property = callee.property;
1640
+ return property?.type === "Identifier" && /^mock[A-Z]/.test(property.name);
1641
+ }
1642
+ function rewriteHookReturnValue(source, program) {
1643
+ const edits = [];
1644
+ walk(program, (node) => {
1645
+ const callback = hookCallback(node);
1646
+ if (callback === void 0 || callback.async === true) return;
1647
+ const body = callback.body;
1648
+ if (body === void 0 || body.type === "BlockStatement") return;
1649
+ if (!configuresAMock(body)) return;
1650
+ const start = body.start;
1651
+ const end = body.end;
1652
+ edits.push({ start, end, text: `{
1653
+ ${source.slice(start, end)}
1654
+ }` });
1655
+ });
1656
+ let result = source;
1657
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
1658
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
1659
+ }
1660
+ return { source: result, rewritten: edits.length };
1661
+ }
1662
+
1663
+ // src/roles/test/codemod/inject-type-import.ts
1664
+ import { parseSync as parseSync3 } from "oxc-parser";
1665
+
1666
+ // src/roles/test/codemod/rename.ts
1667
+ import { parseSync as parseSync2 } from "oxc-parser";
1668
+ var VITEST_TYPE_SOURCE = "vitest";
1669
+ var RUNTIME_RENAMES = /* @__PURE__ */ new Set([
1670
+ // Mocks
1671
+ "clearAllMocks",
1672
+ "doMock",
1673
+ "fn",
1674
+ "mock",
1675
+ "mocked",
1676
+ "resetAllMocks",
1677
+ "resetModules",
1678
+ "restoreAllMocks",
1679
+ "spyOn",
1680
+ "unmock",
1681
+ /*
1682
+ * Timers, as a family rather than as the two the corpus happens to use today. Running this over
1683
+ * the repo reported `jest.runAllTimers` as unknown, which was a hole in the table rather than a
1684
+ * finding about the code. Each name here was then checked against a running Vitest, so adding the
1685
+ * siblings is not speculation: a file gaining one tomorrow gets the same answer as one using it
1686
+ * today, instead of a report about a member that was always going to be a rename.
1687
+ */
1688
+ "advanceTimersByTime",
1689
+ "advanceTimersByTimeAsync",
1690
+ "advanceTimersToNextTimer",
1691
+ "clearAllTimers",
1692
+ "getRealSystemTime",
1693
+ "getTimerCount",
1694
+ "runAllTimers",
1695
+ "runAllTimersAsync",
1696
+ "runOnlyPendingTimers",
1697
+ "setSystemTime",
1698
+ "useFakeTimers",
1699
+ "useRealTimers"
1700
+ ]);
1701
+ var TYPE_RENAMES = {
1702
+ Mock: "Mock",
1703
+ MockedClass: "MockedClass",
1704
+ MockedFunction: "MockedFunction",
1705
+ MockedObject: "MockedObject",
1706
+ Mocked: "Mocked",
1707
+ SpiedFunction: "MockInstance",
1708
+ SpyInstance: "MockInstance"
1709
+ };
1710
+ var NAMED_RULE = {
1711
+ requireActual: `await vi.importActual(...), which makes the surrounding factory async`,
1712
+ requireMock: `await vi.importMock(...), which makes the surrounding factory async`,
1713
+ setTimeout: `vi.setConfig({ testTimeout: ... }), which is configuration rather than a call`
1714
+ };
1715
+ var OWNED_BY_A_LATER_PASS = /* @__PURE__ */ new Set(["isolateModules", "isolateModulesAsync"]);
1716
+ function lineOf(source, offset) {
1717
+ let line = 1;
1718
+ for (let index = 0; index < offset && index < source.length; index++) {
1719
+ if (source[index] === "\n") line++;
1720
+ }
1721
+ return line;
1722
+ }
1723
+ function valueEdit(node, source, outcome) {
1724
+ const object = node.object;
1725
+ const property = node.property;
1726
+ if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
1727
+ if (property?.type !== "Identifier") return void 0;
1728
+ const member = property.name;
1729
+ if (RUNTIME_RENAMES.has(member)) {
1730
+ outcome.rewritten++;
1731
+ return { start: object.start, end: object.end, text: "vi" };
1732
+ }
1733
+ if (OWNED_BY_A_LATER_PASS.has(member)) return void 0;
1734
+ outcome.unhandled.push({
1735
+ member: `jest.${member}`,
1736
+ line: lineOf(source, object.start),
1737
+ ...NAMED_RULE[member] === void 0 ? {} : { rule: NAMED_RULE[member] }
1738
+ });
1739
+ return void 0;
1740
+ }
1741
+ function typeEdit(node, source, outcome, needed) {
1742
+ const left = node.left;
1743
+ const right = node.right;
1744
+ if (left?.type !== "Identifier" || left.name !== "jest") return void 0;
1745
+ if (right?.type !== "Identifier") return void 0;
1746
+ const member = right.name;
1747
+ const replacement = TYPE_RENAMES[member];
1748
+ if (replacement === void 0) {
1749
+ outcome.unhandled.push({ member: `jest.${member}`, line: lineOf(source, node.start) });
1750
+ return void 0;
1751
+ }
1752
+ needed.add(replacement);
1753
+ outcome.rewritten++;
1754
+ return { start: node.start, end: node.end, text: replacement };
1755
+ }
1756
+ function renameJestToVitest(fileName, source) {
1757
+ const outcome = { source, typeImports: [], rewritten: 0, unhandled: [] };
1758
+ const { program, errors } = parseSync2(fileName, source, { sourceType: "module" });
1759
+ if (errors.length > 0) return outcome;
1760
+ const edits = [];
1761
+ const needed = /* @__PURE__ */ new Set();
1762
+ walk(program, (node) => {
1763
+ if (node.type === "MemberExpression" || node.type === "StaticMemberExpression") {
1764
+ const edit = valueEdit(node, source, outcome);
1765
+ if (edit !== void 0) edits.push(edit);
1766
+ return;
1767
+ }
1768
+ if (node.type === "TSQualifiedName") {
1769
+ const edit = typeEdit(node, source, outcome, needed);
1770
+ if (edit !== void 0) edits.push(edit);
1771
+ }
1772
+ });
1773
+ outcome.typeImports = [...needed].sort();
1774
+ let rewritten = source;
1775
+ for (const edit of edits.sort((a, b) => b.start - a.start)) {
1776
+ rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
1777
+ }
1778
+ outcome.source = rewritten;
1779
+ return outcome;
1780
+ }
1781
+
1782
+ // src/roles/test/codemod/inject-type-import.ts
1783
+ function renderTypeImport(names, semi) {
1784
+ return `import type { ${names.join(", ")} } from '${VITEST_TYPE_SOURCE}'${semi ? ";" : ""}`;
1785
+ }
1786
+ function importDeclarations(program) {
1787
+ return (program.body ?? []).filter((node) => node.type === "ImportDeclaration");
1788
+ }
1789
+ function usesSemicolons(source, imports) {
1790
+ const first = imports[0];
1791
+ if (first === void 0) return false;
1792
+ return source.slice(first.start, first.end).trimEnd().endsWith(";");
1793
+ }
1794
+ function existingVitestImport(imports) {
1795
+ return imports.find(
1796
+ (node) => node.source?.value === VITEST_TYPE_SOURCE
1797
+ );
1798
+ }
1799
+ function alreadyImported(declaration) {
1800
+ return (declaration.specifiers ?? []).filter((specifier) => specifier.type === "ImportSpecifier").map((specifier) => specifier.imported?.name).filter((name) => name !== void 0);
1801
+ }
1802
+ function injectTypeImport(fileName, source, typeNames) {
1803
+ if (typeNames.length === 0) return source;
1804
+ const { program, errors } = parseSync3(fileName, source, { sourceType: "module" });
1805
+ if (errors.length > 0) return source;
1806
+ const imports = importDeclarations(program);
1807
+ const semi = usesSemicolons(source, imports);
1808
+ const existing = existingVitestImport(imports);
1809
+ if (existing !== void 0) {
1810
+ const merged = [.../* @__PURE__ */ new Set([...alreadyImported(existing), ...typeNames])].sort();
1811
+ if (merged.length === alreadyImported(existing).length) return source;
1812
+ return source.slice(0, existing.start) + renderTypeImport(merged, semi) + source.slice(existing.end);
1813
+ }
1814
+ const line = renderTypeImport([...new Set(typeNames)].sort(), semi);
1815
+ const first = imports[0];
1816
+ if (first === void 0) return `${line}
1817
+
1818
+ ${source}`;
1819
+ return source.slice(0, first.start) + line + "\n" + source.slice(first.start);
1820
+ }
1821
+
1822
+ // src/roles/test/codemod/named-rules.ts
1823
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
1824
+ import { createRequire as createRequire2 } from "node:module";
1825
+ import { dirname as dirname2, join as join9 } from "node:path";
1826
+ function isCommonJsPackage(fromFile, specifier) {
1827
+ if (specifier.startsWith(".") || specifier.startsWith("/")) return false;
1828
+ let resolved;
1829
+ try {
1830
+ resolved = createRequire2(fromFile).resolve(specifier);
1831
+ } catch {
1832
+ return false;
1833
+ }
1834
+ if (!resolved.includes(`${"/"}node_modules${"/"}`)) return false;
1835
+ if (resolved.endsWith(".mjs")) return false;
1836
+ if (resolved.endsWith(".cjs")) return true;
1837
+ let dir = dirname2(resolved);
1838
+ for (let depth = 0; depth < 12; depth += 1) {
1839
+ const manifest = join9(dir, "package.json");
1840
+ if (existsSync10(manifest)) {
1841
+ try {
1842
+ return JSON.parse(readFileSync7(manifest, "utf8")).type !== "module";
1843
+ } catch {
1844
+ return false;
1845
+ }
1846
+ }
1847
+ const parent = dirname2(dir);
1848
+ if (parent === dir) break;
1849
+ dir = parent;
1850
+ }
1851
+ return false;
1852
+ }
1853
+ var MEMBER_TYPES9 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
1854
+ var ASYNC_REPLACEMENTS = {
1855
+ requireActual: "importActual",
1856
+ requireMock: "importMock"
1857
+ };
1858
+ function mockedSpecifiers(program) {
1859
+ const found = /* @__PURE__ */ new Set();
1860
+ walk(program, (node) => {
1861
+ if (node.type !== "CallExpression") return;
1862
+ const callee = node.callee;
1863
+ if (callee === void 0 || !MEMBER_TYPES9.has(callee.type)) return;
1864
+ const object = callee.object;
1865
+ const property = callee.property;
1866
+ if (object?.type !== "Identifier") return;
1867
+ if (object.name !== "jest" && object.name !== "vi") return;
1868
+ if (property?.type !== "Identifier" || property.name !== "mock") return;
1869
+ const [specifier] = node.arguments ?? [];
1870
+ if (specifier?.type === "StringLiteral" || specifier?.type === "Literal") {
1871
+ found.add(String(specifier.value));
1872
+ }
1873
+ });
1874
+ return found;
1875
+ }
1876
+ var FUNCTIONS = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
1877
+ function walkWithEnclosingFunction(node, enclosing, visit) {
1878
+ if (node === null || typeof node !== "object") return;
1879
+ if (Array.isArray(node)) {
1880
+ for (const entry of node) walkWithEnclosingFunction(entry, enclosing, visit);
1881
+ return;
1882
+ }
1883
+ const current = node;
1884
+ const next = FUNCTIONS.has(current.type) ? current : enclosing;
1885
+ if (typeof current.type === "string") visit(current, enclosing);
1886
+ for (const value of Object.values(current)) {
1887
+ if (value !== null && typeof value === "object") walkWithEnclosingFunction(value, next, visit);
1888
+ }
1889
+ }
1890
+ function bareRequire(node) {
1891
+ if (node.type !== "CallExpression") return false;
1892
+ const callee = node.callee;
1893
+ if (callee?.type !== "Identifier" || callee.name !== "require") return false;
1894
+ const args = node.arguments ?? [];
1895
+ return args.length === 1 && args[0] !== void 0;
1896
+ }
1897
+ function mockFactoryReturn(node) {
1898
+ if (node.type !== "CallExpression") return void 0;
1899
+ const callee = node.callee;
1900
+ if (callee === void 0 || !MEMBER_TYPES9.has(callee.type)) return void 0;
1901
+ const object = callee.object;
1902
+ const property = callee.property;
1903
+ if (object?.type !== "Identifier") return void 0;
1904
+ if (!["jest", "vi", "vitest"].includes(object.name)) return void 0;
1905
+ if (property?.name !== "mock") return void 0;
1906
+ const factory = (node.arguments ?? [])[1];
1907
+ if (factory?.type !== "ArrowFunctionExpression" && factory?.type !== "FunctionExpression") {
1908
+ return void 0;
1909
+ }
1910
+ let body = factory.body;
1911
+ while (body?.type === "ParenthesizedExpression") body = body.expression;
1912
+ if (body === void 0) return void 0;
1913
+ if (body.type !== "BlockStatement") return body;
1914
+ const statements = body.body ?? [];
1915
+ if (statements.length !== 1) return void 0;
1916
+ const only = statements[0];
1917
+ if (only?.type !== "ReturnStatement") return void 0;
1918
+ let argument = only.argument;
1919
+ while (argument?.type === "ParenthesizedExpression") {
1920
+ argument = argument.expression;
1921
+ }
1922
+ return argument;
1923
+ }
1924
+ function ownedCall(node) {
1925
+ if (node.type !== "CallExpression") return void 0;
1926
+ const callee = node.callee;
1927
+ if (callee?.type !== "MemberExpression" && callee?.type !== "StaticMemberExpression") {
1928
+ return void 0;
1929
+ }
1930
+ const object = callee.object;
1931
+ const property = callee.property;
1932
+ if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
1933
+ return property?.type === "Identifier" ? property.name : void 0;
1934
+ }
1935
+ function argumentText(source, call) {
1936
+ const args = call.arguments ?? [];
1937
+ if (args.length === 0) return "";
1938
+ const first = args[0];
1939
+ const last = args[args.length - 1];
1940
+ return source.slice(first.start, last.end);
1941
+ }
1942
+ function applyEdits(source, edits) {
1943
+ let result = source;
1944
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
1945
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
1946
+ }
1947
+ return result;
1948
+ }
1949
+ function applyNamedRules(source, program, fileName = "") {
1950
+ const edits = [];
1951
+ const applied = /* @__PURE__ */ new Set();
1952
+ const asyncify = /* @__PURE__ */ new Set();
1953
+ const mocked = mockedSpecifiers(program);
1954
+ const wholeStatement = /* @__PURE__ */ new Map();
1955
+ walk(program, (node) => {
1956
+ if (node.type !== "ExpressionStatement") return;
1957
+ const expression = node.expression;
1958
+ if (expression !== void 0) wholeStatement.set(expression, node);
1959
+ });
1960
+ const standsAlone = /* @__PURE__ */ new Set();
1961
+ const alone = (node) => {
1962
+ if (node !== void 0) standsAlone.add(node);
1963
+ };
1964
+ walk(program, (node) => {
1965
+ switch (node.type) {
1966
+ case "VariableDeclarator":
1967
+ return alone(node.init);
1968
+ case "AssignmentExpression":
1969
+ return alone(node.right);
1970
+ case "ReturnStatement":
1971
+ case "ExportDefaultDeclaration":
1972
+ return alone(node.argument ?? node.declaration);
1973
+ case "ArrowFunctionExpression":
1974
+ return node.expression === true ? alone(node.body) : void 0;
1975
+ case "ConditionalExpression":
1976
+ alone(node.consequent);
1977
+ return alone(node.alternate);
1978
+ case "ArrayExpression":
1979
+ return (node.elements ?? []).forEach((e) => alone(e ?? void 0));
1980
+ case "Property":
1981
+ case "ObjectProperty":
1982
+ return node.shorthand === true ? void 0 : alone(node.value);
1983
+ case "CallExpression":
1984
+ case "NewExpression":
1985
+ return (node.arguments ?? []).forEach((a) => alone(a));
1986
+ default:
1987
+ return;
1988
+ }
1989
+ });
1990
+ const placed = (node, expression, trailing = "") => {
1991
+ if (trailing !== "") return `(${expression})${trailing}`;
1992
+ if (wholeStatement.has(node)) return expression;
1993
+ return standsAlone.has(node) ? expression : `(${expression})`;
1994
+ };
1995
+ const handedToTheRunnerAsAModule = /* @__PURE__ */ new Set();
1996
+ walk(program, (node) => {
1997
+ const returned = mockFactoryReturn(node);
1998
+ if (returned !== void 0) handedToTheRunnerAsAModule.add(returned);
1999
+ });
2000
+ const readsANameOffIt = /* @__PURE__ */ new Set();
2001
+ walk(program, (node) => {
2002
+ if (node.type === "VariableDeclarator") {
2003
+ const id = node.id;
2004
+ const init = node.init ?? void 0;
2005
+ if (id !== void 0 && id.type !== "Identifier" && init !== void 0) {
2006
+ readsANameOffIt.add(init);
2007
+ }
2008
+ return;
2009
+ }
2010
+ if (!MEMBER_TYPES9.has(node.type)) return;
2011
+ const object = node.object;
2012
+ if (object !== void 0) readsANameOffIt.add(object);
2013
+ });
2014
+ walkWithEnclosingFunction(program, void 0, (node, enclosing) => {
2015
+ const member = ownedCall(node);
2016
+ if (member === void 0 && !bareRequire(node)) return;
2017
+ if (member === "requireMock") {
2018
+ const [specifier] = node.arguments ?? [];
2019
+ const literal = specifier?.type === "StringLiteral" || specifier?.type === "Literal" ? String(specifier.value) : void 0;
2020
+ if (literal !== void 0 && mocked.has(literal)) {
2021
+ edits.push({
2022
+ start: node.start,
2023
+ end: node.end,
2024
+ text: placed(node, `await import(${argumentText(source, node)})`)
2025
+ });
2026
+ applied.add("jest.requireMock");
2027
+ if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
2028
+ return;
2029
+ }
2030
+ }
2031
+ const asyncReplacement = member === void 0 ? void 0 : ASYNC_REPLACEMENTS[member];
2032
+ if (asyncReplacement !== void 0) {
2033
+ edits.push({
2034
+ start: node.start,
2035
+ end: node.end,
2036
+ text: placed(node, `await vi.${asyncReplacement}(${argumentText(source, node)})`)
2037
+ });
2038
+ applied.add(`jest.${member}`);
2039
+ if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
2040
+ return;
2041
+ }
2042
+ if (bareRequire(node)) {
2043
+ const [specifier] = node.arguments ?? [];
2044
+ const literal = specifier?.type === "StringLiteral" || specifier?.type === "Literal" ? String(specifier.value) : void 0;
2045
+ const interop = literal !== void 0 && !readsANameOffIt.has(node) && !handedToTheRunnerAsAModule.has(node) && !wholeStatement.has(node) && isCommonJsPackage(fileName, literal) ? ".default" : "";
2046
+ edits.push({
2047
+ start: node.start,
2048
+ end: node.end,
2049
+ text: placed(node, `await import(${argumentText(source, node)})`, interop)
2050
+ });
2051
+ applied.add("require");
2052
+ if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
2053
+ return;
2054
+ }
2055
+ if (member === "mock" && enclosing !== void 0) {
2056
+ const callee = node.callee;
2057
+ edits.push({
2058
+ start: callee.start,
2059
+ end: callee.end,
2060
+ text: "vi.doMock"
2061
+ });
2062
+ applied.add("jest.mock inside a function");
2063
+ return;
2064
+ }
2065
+ if (member === "setTimeout") {
2066
+ edits.push({
2067
+ start: node.start,
2068
+ end: node.end,
2069
+ text: `vi.setConfig({ testTimeout: ${argumentText(source, node)} })`
2070
+ });
2071
+ applied.add("jest.setTimeout");
2072
+ }
2073
+ });
2074
+ for (const fn of asyncify) {
2075
+ edits.push({ start: fn.start, end: fn.start, text: "async " });
2076
+ }
2077
+ return { source: applyEdits(source, edits), applied: [...applied].sort() };
2078
+ }
2079
+
2080
+ // src/roles/test/codemod/isolate-modules.ts
2081
+ var MEMBER_TYPES10 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
2082
+ var ISOLATORS = /* @__PURE__ */ new Set(["isolateModules", "isolateModulesAsync"]);
2083
+ var FUNCTIONS2 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression"]);
2084
+ function isolatingCall(node) {
2085
+ if (node.type !== "CallExpression") return void 0;
2086
+ const callee = node.callee;
2087
+ if (callee === void 0 || !MEMBER_TYPES10.has(callee.type)) return void 0;
2088
+ const object = callee.object;
2089
+ const property = callee.property;
2090
+ if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
2091
+ if (property?.type !== "Identifier" || !ISOLATORS.has(property.name)) return void 0;
2092
+ const [callback] = node.arguments ?? [];
2093
+ return callback !== void 0 && FUNCTIONS2.has(callback.type) ? { call: node, callback } : void 0;
2094
+ }
2095
+ function unusableIsolatingCall(node) {
2096
+ if (node.type !== "CallExpression") return void 0;
2097
+ const callee = node.callee;
2098
+ if (callee === void 0 || !MEMBER_TYPES10.has(callee.type)) return void 0;
2099
+ const object = callee.object;
2100
+ const property = callee.property;
2101
+ if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
2102
+ if (property?.type !== "Identifier" || !ISOLATORS.has(property.name)) return void 0;
2103
+ const [callback] = node.arguments ?? [];
2104
+ return callback === void 0 || FUNCTIONS2.has(callback.type) ? void 0 : `jest.${property.name}`;
2105
+ }
2106
+ function indentationAt(source, offset) {
2107
+ const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
2108
+ return /^[ \t]*/.exec(source.slice(lineStart, offset))?.[0] ?? "";
2109
+ }
2110
+ function rewriteIsolateModules(source, program) {
2111
+ const edits = [];
2112
+ const refusals = [];
2113
+ const asyncify = /* @__PURE__ */ new Set();
2114
+ let rewritten = 0;
2115
+ walkWithEnclosingFunction(program, void 0, (node, enclosing) => {
2116
+ const unusable = unusableIsolatingCall(node);
2117
+ if (unusable !== void 0) {
2118
+ refusals.push({
2119
+ binding: unusable,
2120
+ reason: `${unusable} is called with something other than a function, so the callback cannot be invoked against a reset registry. This one needs a human.`
2121
+ });
2122
+ return;
2123
+ }
2124
+ const found = isolatingCall(node);
2125
+ if (found === void 0) return;
2126
+ const callbackText = source.slice(found.callback.start, found.callback.end);
2127
+ const callback = found.callback.async === true ? callbackText : `async ${callbackText}`;
2128
+ const indent = indentationAt(source, found.call.start);
2129
+ edits.push({
2130
+ start: found.call.start,
2131
+ end: found.call.end,
2132
+ text: `vi.resetModules()
2133
+ ${indent}await (${callback})()`
2134
+ });
2135
+ rewritten += 1;
2136
+ if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
2137
+ });
2138
+ for (const fn of asyncify) {
2139
+ edits.push({ start: fn.start, end: fn.start, text: "async " });
2140
+ }
2141
+ let result = source;
2142
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
2143
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
2144
+ }
2145
+ return { source: result, rewritten, refusals };
2146
+ }
2147
+
2148
+ // src/roles/test/codemod/mock-as-value.ts
2149
+ var MEMBER_TYPES11 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
2150
+ function isViMockCall(node) {
2151
+ if (node?.type !== "CallExpression") return false;
2152
+ const callee = node.callee;
2153
+ if (callee === void 0 || !MEMBER_TYPES11.has(callee.type)) return false;
2154
+ const object = callee.object;
2155
+ const property = callee.property;
2156
+ return object?.type === "Identifier" && object.name === "vi" && property?.type === "Identifier" && (property.name === "mock" || property.name === "doMock");
2157
+ }
2158
+ function mockBinding(node) {
2159
+ if (node.type !== "VariableDeclaration") return void 0;
2160
+ const declarations = node.declarations ?? [];
2161
+ if (declarations.length !== 1) return void 0;
2162
+ const [declarator] = declarations;
2163
+ const id = declarator?.id;
2164
+ const init = declarator?.init;
2165
+ if (id?.type !== "Identifier" || !isViMockCall(init)) return void 0;
2166
+ return { name: id.name, call: init };
2167
+ }
2168
+ function rewriteMockAsValue(source, program) {
2169
+ const bindings = /* @__PURE__ */ new Map();
2170
+ walk(program, (node) => {
2171
+ const found = mockBinding(node);
2172
+ if (found !== void 0) bindings.set(found.name, { declaration: node, call: found.call });
2173
+ });
2174
+ if (bindings.size === 0) return { source, rewritten: 0, refusals: [] };
2175
+ const memberUses = /* @__PURE__ */ new Map();
2176
+ const otherUses = /* @__PURE__ */ new Map();
2177
+ walk(program, (node) => {
2178
+ if (MEMBER_TYPES11.has(node.type)) {
2179
+ const object = node.object;
2180
+ if (object?.type === "Identifier" && bindings.has(object.name)) {
2181
+ const list = memberUses.get(object.name) ?? [];
2182
+ list.push(object);
2183
+ memberUses.set(object.name, list);
2184
+ return;
2185
+ }
2186
+ }
2187
+ if (node.type !== "Identifier" || !bindings.has(node.name)) return;
2188
+ const declared = bindings.get(node.name);
2189
+ const insideDeclaration = node.start >= declared?.declaration.start && node.end <= declared?.declaration.end;
2190
+ if (insideDeclaration) return;
2191
+ const seen = memberUses.get(node.name) ?? [];
2192
+ if (seen.some((use) => use.start === node.start)) return;
2193
+ otherUses.set(node.name, (otherUses.get(node.name) ?? 0) + 1);
2194
+ });
2195
+ const edits = [];
2196
+ const refusals = [];
2197
+ let rewritten = 0;
2198
+ for (const [name, { declaration, call }] of bindings) {
2199
+ if ((otherUses.get(name) ?? 0) > 0) {
2200
+ refusals.push({
2201
+ binding: name,
2202
+ reason: `\`${name}\` is \`jest.mock()\`'s return value used as a value elsewhere, not only as \`${name}.member(...)\`. vi.mock() returns nothing, so this needs a human.`
2203
+ });
2204
+ continue;
2205
+ }
2206
+ edits.push({
2207
+ start: declaration.start,
2208
+ end: declaration.end,
2209
+ text: source.slice(call.start, call.end)
2210
+ });
2211
+ for (const use of memberUses.get(name) ?? []) {
2212
+ edits.push({ start: use.start, end: use.end, text: "vi" });
2213
+ }
2214
+ rewritten += 1;
2215
+ }
2216
+ let result = source;
2217
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
2218
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
2219
+ }
2220
+ return { source: result, rewritten, refusals };
2221
+ }
2222
+
2223
+ // src/roles/test/codemod/mock-factory-default.ts
2224
+ var MEMBER_TYPES12 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
2225
+ var BINDING = "sentinelMockedModule";
2226
+ function mockCall(node) {
2227
+ if (node.type !== "CallExpression") return void 0;
2228
+ const callee = node.callee;
2229
+ if (callee === void 0 || !MEMBER_TYPES12.has(callee.type)) return void 0;
2230
+ const property = callee.property;
2231
+ if (property?.type !== "Identifier" || property.name !== "mock") return void 0;
2232
+ const object = callee.object;
2233
+ if (object?.type !== "Identifier" || !["vi", "vitest"].includes(object.name)) {
2234
+ return void 0;
2235
+ }
2236
+ const [specifier, factory] = node.arguments ?? [];
2237
+ if (specifier?.type !== "Literal" || typeof specifier.value !== "string") return void 0;
2238
+ if (factory === void 0) return void 0;
2239
+ return { specifier: specifier.value, factory };
2240
+ }
2241
+ function returnedExpression(factory) {
2242
+ const object = returnedObject(factory);
2243
+ if (object !== void 0) return { node: object, isObject: true };
2244
+ if (factory.type !== "ArrowFunctionExpression") return void 0;
2245
+ let body = factory.body;
2246
+ while (body?.type === "ParenthesizedExpression") body = body.expression;
2247
+ if (body === void 0) return void 0;
2248
+ if (body.type !== "BlockStatement") return { node: body, isObject: false, concise: true };
2249
+ const statements = body.body ?? [];
2250
+ if (statements.length !== 1) return void 0;
2251
+ const only = statements[0];
2252
+ if (only?.type !== "ReturnStatement") return void 0;
2253
+ const argument = only.argument;
2254
+ return argument === void 0 ? void 0 : { node: argument, isObject: false };
2255
+ }
2256
+ function returnedObject(factory) {
2257
+ if (factory.type !== "ArrowFunctionExpression") return void 0;
2258
+ let body = factory.body;
2259
+ while (body?.type === "ParenthesizedExpression") body = body.expression;
2260
+ if (body?.type === "ObjectExpression") return body;
2261
+ const statements = body?.body ?? [];
2262
+ if (statements.length !== 1) return void 0;
2263
+ const only = statements[0];
2264
+ if (only?.type === "ExpressionStatement" && only.expression?.type === "ObjectExpression") {
2265
+ return only.expression;
2266
+ }
2267
+ if (only?.type === "ReturnStatement" && only.argument?.type === "ObjectExpression") {
2268
+ return only.argument;
2269
+ }
2270
+ return void 0;
2271
+ }
2272
+ function isModuleNamespace(node) {
2273
+ let current = node;
2274
+ while (current?.type === "ParenthesizedExpression") {
2275
+ current = current.expression;
2276
+ }
2277
+ if (current?.type === "AwaitExpression") {
2278
+ let argument = current.argument;
2279
+ while (argument?.type === "ParenthesizedExpression") {
2280
+ argument = argument.expression;
2281
+ }
2282
+ current = argument;
2283
+ }
2284
+ return current?.type === "ImportExpression";
2285
+ }
2286
+ function namesDefault(object) {
2287
+ return (object.properties ?? []).some((property) => {
2288
+ const key = property.key;
2289
+ if (key?.type === "Identifier") return key.name === "default";
2290
+ if (key?.type === "Literal") return key.value === "default";
2291
+ return false;
2292
+ });
2293
+ }
2294
+ function rewriteMockFactoryDefault(source, program, defaultImported) {
2295
+ const edits = [];
2296
+ walk(program, (node) => {
2297
+ const call = mockCall(node);
2298
+ if (call === void 0) return;
2299
+ const returned = returnedExpression(call.factory);
2300
+ if (returned === void 0) return;
2301
+ if (returned.isObject && namesDefault(returned.node)) return;
2302
+ if (returned.isObject && !defaultImported.has(call.specifier)) return;
2303
+ if (!returned.isObject && isModuleNamespace(returned.node)) return;
2304
+ if (!returned.isObject) {
2305
+ const expression = source.slice(returned.node.start, returned.node.end);
2306
+ edits.push({
2307
+ start: returned.node.start,
2308
+ end: returned.node.end,
2309
+ // Parenthesised when it is the arrow's own body, or it is read as a block, not an object.
2310
+ literal: returned.concise === true ? `({ default: ${expression} })` : `{ default: ${expression} }`,
2311
+ specifier: call.specifier,
2312
+ wrap: false
2313
+ });
2314
+ return;
2315
+ }
2316
+ const object = returned.node;
2317
+ const literal = source.slice(object.start, object.end);
2318
+ if (literal.includes(BINDING)) return;
2319
+ const body = call.factory.body;
2320
+ edits.push({
2321
+ start: body.start,
2322
+ end: body.end,
2323
+ literal,
2324
+ specifier: call.specifier,
2325
+ wrap: true
2326
+ });
2327
+ });
2328
+ let result = source;
2329
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
2330
+ const text = edit.wrap ? `{ const ${BINDING} = ${edit.literal}; return { ...${BINDING}, default: ${BINDING} } }` : edit.literal;
2331
+ result = `${result.slice(0, edit.start)}${text}${result.slice(edit.end)}`;
2332
+ }
2333
+ return {
2334
+ source: result,
2335
+ rewritten: edits.length,
2336
+ specifiers: [...new Set(edits.map((edit) => edit.specifier))]
2337
+ };
2338
+ }
2339
+
2340
+ // src/roles/test/codemod/mock-promise-resolve.ts
2341
+ var MOCK_FACTORY = /* @__PURE__ */ new Set(["fn", "spyOn"]);
2342
+ function isMockFactory2(source, node) {
2343
+ if (node.type !== "CallExpression") return false;
2344
+ const callee = node.callee;
2345
+ if (callee?.type !== "MemberExpression") return false;
2346
+ const object = callee.object;
2347
+ const property = callee.property;
2348
+ if (object?.type !== "Identifier" || property?.type !== "Identifier") return false;
2349
+ const runner = source.slice(object.start, object.end);
2350
+ const method = source.slice(property.start, property.end);
2351
+ return (runner === "jest" || runner === "vi") && MOCK_FACTORY.has(method);
2352
+ }
2353
+ function resolvedValue(source, executor) {
2354
+ if (executor.type !== "ArrowFunctionExpression" && executor.type !== "FunctionExpression") {
2355
+ return void 0;
2356
+ }
2357
+ const params = executor.params;
2358
+ if (params?.length !== 1) return void 0;
2359
+ const parameter = params[0];
2360
+ if (parameter.type !== "Identifier") return void 0;
2361
+ const resolveName = source.slice(parameter.start, parameter.end);
2362
+ const body = executor.body;
2363
+ if (body === void 0) return void 0;
2364
+ let call;
2365
+ if (body.type === "BlockStatement") {
2366
+ const statements = body.body;
2367
+ if (statements?.length !== 1) return void 0;
2368
+ const only = statements[0];
2369
+ if (only.type !== "ExpressionStatement") return void 0;
2370
+ call = only.expression;
2371
+ } else {
2372
+ call = body;
2373
+ }
2374
+ if (call?.type !== "CallExpression") return void 0;
2375
+ const callee = call.callee;
2376
+ if (callee?.type !== "Identifier") return void 0;
2377
+ if (source.slice(callee.start, callee.end) !== resolveName) return void 0;
2378
+ const args = call.arguments;
2379
+ if (args?.length !== 1) return void 0;
2380
+ return args[0];
2381
+ }
2382
+ function rewriteMockPromiseResolve(source, program) {
2383
+ const edits = [];
2384
+ walk(program, (node) => {
2385
+ if (!isMockFactory2(source, node)) return;
2386
+ for (const argument of node.arguments ?? []) {
2387
+ walk(argument, (inner) => {
2388
+ if (inner.type !== "NewExpression") return;
2389
+ const callee = inner.callee;
2390
+ if (callee?.type !== "Identifier") return;
2391
+ if (source.slice(callee.start, callee.end) !== "Promise") return;
2392
+ if ((inner.typeArguments ?? inner.typeParameters) !== void 0) return;
2393
+ const executors = inner.arguments ?? [];
2394
+ if (executors.length !== 1) return;
2395
+ const value = resolvedValue(source, executors[0]);
2396
+ if (value === void 0) return;
2397
+ edits.push({
2398
+ start: inner.start,
2399
+ end: inner.end,
2400
+ text: `Promise.resolve(${source.slice(value.start, value.end)})`
2401
+ });
2402
+ });
2403
+ }
2404
+ });
2405
+ let result = source;
2406
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
2407
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
2408
+ }
2409
+ return { source: result, rewritten: edits.length };
2410
+ }
2411
+
2412
+ // src/roles/test/codemod/mocked-module-binding.ts
2413
+ var MEMBER_TYPES13 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
2414
+ function mockedSpecifier(source, node) {
2415
+ if (node.type !== "CallExpression") return void 0;
2416
+ const callee = node.callee;
2417
+ if (callee === void 0 || !MEMBER_TYPES13.has(callee.type)) return void 0;
2418
+ const object = callee.object;
2419
+ const property = callee.property;
2420
+ if (object?.type !== "Identifier") return void 0;
2421
+ if (object.name !== "vi" && object.name !== "jest") return void 0;
2422
+ if (property?.type !== "Identifier" || property.name !== "mock") return void 0;
2423
+ const [first] = node.arguments ?? [];
2424
+ if (first?.type !== "Literal") return void 0;
2425
+ return source.slice(first.start, first.end).slice(1, -1);
2426
+ }
2427
+ function awaitedImport(source, node) {
2428
+ if (node?.type !== "AwaitExpression") return void 0;
2429
+ const call = node.argument;
2430
+ if (call?.type !== "ImportExpression") return void 0;
2431
+ const specifier = call.source ?? call.arguments?.[0];
2432
+ if (specifier?.type !== "Literal") return void 0;
2433
+ return source.slice(specifier.start, specifier.end).slice(1, -1);
2434
+ }
2435
+ function annotateMockedBindings(source, program) {
2436
+ const mocked = /* @__PURE__ */ new Set();
2437
+ walk(program, (node) => {
2438
+ const specifier = mockedSpecifier(source, node);
2439
+ if (specifier !== void 0) mocked.add(specifier);
2440
+ });
2441
+ if (mocked.size === 0) return { source, rewritten: 0, typeImports: [] };
2442
+ const edits = [];
2443
+ for (const statement2 of program.body ?? []) {
2444
+ if (statement2.type !== "VariableDeclaration") continue;
2445
+ const declarations = statement2.declarations ?? [];
2446
+ if (declarations.length !== 1) continue;
2447
+ const [only] = declarations;
2448
+ if (only === void 0) continue;
2449
+ if (only.id?.type !== "Identifier") continue;
2450
+ const init = only.init;
2451
+ const specifier = awaitedImport(source, init);
2452
+ if (specifier === void 0 || !mocked.has(specifier)) continue;
2453
+ const start = init?.start;
2454
+ const end = init?.end;
2455
+ const expression = source.slice(start, end);
2456
+ if (source.slice(end, end + 4).trimStart().startsWith("as"))
2457
+ continue;
2458
+ edits.push({
2459
+ start,
2460
+ end,
2461
+ text: `(${expression}) as unknown as Record<string, Mock>`
2462
+ });
2463
+ }
2464
+ if (edits.length === 0) return { source, rewritten: 0, typeImports: [] };
2465
+ let result = source;
2466
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
2467
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
2468
+ }
2469
+ return { source: result, rewritten: edits.length, typeImports: ["Mock"] };
2470
+ }
2471
+
2472
+ // src/roles/test/codemod/propagate-await.ts
2473
+ import { parseSync as parseSync4 } from "oxc-parser";
2474
+ var FUNCTIONS3 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
2475
+ var MEMBER_TYPES14 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
2476
+ var ALREADY_A_PROMISE = /* @__PURE__ */ new Set(["then", "catch", "finally"]);
2477
+ function asyncFunctionNames(program) {
2478
+ const names = /* @__PURE__ */ new Set();
2479
+ walk(program, (node) => {
2480
+ if (node.type === "FunctionDeclaration" && node.async === true) {
2481
+ const id2 = node.id;
2482
+ if (id2?.type === "Identifier") names.add(id2.name);
2483
+ return;
2484
+ }
2485
+ if (node.type !== "VariableDeclarator") return;
2486
+ const init = node.init ?? void 0;
2487
+ const id = node.id;
2488
+ if (init === void 0 || !FUNCTIONS3.has(init.type) || init.async !== true) return;
2489
+ if (id?.type === "Identifier") names.add(id.name);
2490
+ });
2491
+ return names;
2492
+ }
2493
+ function returningFunctionNames(program, changed) {
2494
+ const names = /* @__PURE__ */ new Set();
2495
+ const returnsChanged = (fn) => {
2496
+ const body = fn.body;
2497
+ if (body === void 0) return false;
2498
+ if (callTo(body, changed) !== void 0) return true;
2499
+ let found = false;
2500
+ walk(body, (node) => {
2501
+ if (node.type !== "ReturnStatement") return;
2502
+ const argument = node.argument;
2503
+ if (argument !== void 0 && callTo(argument, changed) !== void 0) found = true;
2504
+ });
2505
+ return found;
2506
+ };
2507
+ walk(program, (node) => {
2508
+ if (node.type === "FunctionDeclaration" && returnsChanged(node)) {
2509
+ const id2 = node.id;
2510
+ if (id2?.type === "Identifier") names.add(id2.name);
2511
+ return;
2512
+ }
2513
+ if (node.type !== "VariableDeclarator") return;
2514
+ const init = node.init ?? void 0;
2515
+ const id = node.id;
2516
+ if (init === void 0 || !FUNCTIONS3.has(init.type)) return;
2517
+ if (id?.type === "Identifier" && returnsChanged(init)) names.add(id.name);
2518
+ });
2519
+ return names;
2520
+ }
2521
+ function callTo(node, names) {
2522
+ if (node.type !== "CallExpression") return void 0;
2523
+ const callee = node.callee;
2524
+ if (callee?.type !== "Identifier") return void 0;
2525
+ const name = callee.name;
2526
+ return names.has(name) ? name : void 0;
2527
+ }
2528
+ function walkWithContext(node, enclosing, parent, visit) {
2529
+ if (node === null || typeof node !== "object") return;
2530
+ if (Array.isArray(node)) {
2531
+ for (const item of node) walkWithContext(item, enclosing, parent, visit);
2532
+ return;
2533
+ }
2534
+ const current = node;
2535
+ const isFunction = typeof current.type === "string" && FUNCTIONS3.has(current.type);
2536
+ if (typeof current.type === "string") visit(current, enclosing, parent);
2537
+ const nextEnclosing = isFunction ? current : enclosing;
2538
+ const nextParent = typeof current.type === "string" ? current : parent;
2539
+ for (const value of Object.values(current)) {
2540
+ if (value !== null && typeof value === "object") {
2541
+ walkWithContext(value, nextEnclosing, nextParent, visit);
2542
+ }
2543
+ }
2544
+ }
2545
+ function alreadyHandled(call, parent) {
2546
+ if (parent === void 0) return false;
2547
+ if (parent.type === "AwaitExpression") return true;
2548
+ if (parent.type === "ReturnStatement" || parent.type === "ArrowFunctionExpression") return true;
2549
+ if (MEMBER_TYPES14.has(parent.type) && parent.object === call) {
2550
+ const property = parent.property;
2551
+ return property?.type === "Identifier" && ALREADY_A_PROMISE.has(property.name);
2552
+ }
2553
+ return false;
2554
+ }
2555
+ function applyEdits2(source, edits) {
2556
+ let result = source;
2557
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
2558
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
2559
+ }
2560
+ return result;
2561
+ }
2562
+ function propagateAwait(fileName, source, original, elsewhere = /* @__PURE__ */ new Set()) {
2563
+ const wasAsync = asyncFunctionNames(original);
2564
+ let current = source;
2565
+ let rewritten = 0;
2566
+ for (let round = 0; round < 12; round += 1) {
2567
+ let parsed;
2568
+ try {
2569
+ parsed = parseSync4(fileName, current);
2570
+ } catch {
2571
+ return { source: current, rewritten };
2572
+ }
2573
+ const program = parsed.program;
2574
+ const changed = new Set(elsewhere);
2575
+ for (const name of asyncFunctionNames(program)) {
2576
+ if (!wasAsync.has(name)) changed.add(name);
2577
+ }
2578
+ if (changed.size === 0) return { source: current, rewritten };
2579
+ for (const name of returningFunctionNames(program, changed)) changed.add(name);
2580
+ const edits = [];
2581
+ const asyncify = /* @__PURE__ */ new Set();
2582
+ walkWithContext(program, void 0, void 0, (node, enclosing, parent) => {
2583
+ if (callTo(node, changed) === void 0) return;
2584
+ if (alreadyHandled(node, parent)) return;
2585
+ edits.push({ start: node.start, end: node.start, text: "await " });
2586
+ if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
2587
+ });
2588
+ if (edits.length === 0) return { source: current, rewritten };
2589
+ for (const fn of asyncify) {
2590
+ edits.push({ start: fn.start, end: fn.start, text: "async " });
2591
+ }
2592
+ rewritten += edits.length - asyncify.size;
2593
+ current = applyEdits2(current, edits);
2594
+ }
2595
+ return { source: current, rewritten };
2596
+ }
2597
+
2598
+ // src/roles/test/codemod/reexport-barrel-mock.ts
2599
+ import { readFileSync as readFileSync8, readdirSync as readdirSync2, statSync } from "node:fs";
2600
+ import { createRequire as createRequire3 } from "node:module";
2601
+ import { dirname as dirname3, join as join10 } from "node:path";
2602
+ var MEMBER_TYPES15 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
2603
+ var PACKAGE_SUBPATH = /^(@[^/]+\/[^/]+|[^@.][^/]*)\/(.+)$/;
2604
+ var PURE_REEXPORT = /^module\.exports\s*=\s*require\(\s*(['"])([^'"]+)\1\s*\)\s*;?$/;
2605
+ function mockCalls(program) {
2606
+ const found = [];
2607
+ walk(program, (node) => {
2608
+ if (node.type !== "CallExpression") return;
2609
+ const callee = node.callee;
2610
+ if (callee === void 0 || !MEMBER_TYPES15.has(callee.type)) return;
2611
+ const object = callee.object;
2612
+ const property = callee.property;
2613
+ if (object?.type !== "Identifier") return;
2614
+ if (object.name !== "vi" && object.name !== "jest") return;
2615
+ if (property?.name !== "mock") return;
2616
+ const first = (node.arguments ?? [])[0];
2617
+ if (first?.type !== "Literal" || typeof first.value !== "string") return;
2618
+ found.push({
2619
+ specifier: first.value,
2620
+ start: node.start,
2621
+ end: node.end,
2622
+ literalStart: first.start,
2623
+ literalEnd: first.end
2624
+ });
2625
+ });
2626
+ return found;
2627
+ }
2628
+ function packageRoot(fromFile, name) {
2629
+ try {
2630
+ return dirname3(createRequire3(fromFile).resolve(`${name}/package.json`));
2631
+ } catch {
2632
+ return void 0;
2633
+ }
2634
+ }
2635
+ function resolveFrom(fromFile, specifier) {
2636
+ try {
2637
+ return createRequire3(fromFile).resolve(specifier);
2638
+ } catch {
2639
+ return void 0;
2640
+ }
2641
+ }
2642
+ function barrelsFor(fromFile, name, target) {
2643
+ const root = packageRoot(fromFile, name);
2644
+ if (root === void 0) return [];
2645
+ let entries;
2646
+ try {
2647
+ entries = readdirSync2(root);
2648
+ } catch {
2649
+ return [];
2650
+ }
2651
+ const aliases = [];
2652
+ for (const entry of entries) {
2653
+ if (!entry.endsWith(".js")) continue;
2654
+ const path = join10(root, entry);
2655
+ let body;
2656
+ try {
2657
+ if (!statSync(path).isFile()) continue;
2658
+ body = readFileSync8(path, "utf8");
2659
+ } catch {
2660
+ continue;
2661
+ }
2662
+ const match = PURE_REEXPORT.exec(body.trim());
2663
+ if (match === null) continue;
2664
+ if (resolveFrom(path, match[2]) !== target) continue;
2665
+ aliases.push(`${name}/${entry.slice(0, -".js".length)}`);
2666
+ }
2667
+ return aliases.sort();
2668
+ }
2669
+ function rewriteReexportBarrelMocks(fromFile, source, program) {
2670
+ const calls = mockCalls(program);
2671
+ if (calls.length === 0) return { source, added: [] };
2672
+ const alreadyMocked = new Set(calls.map((call) => call.specifier));
2673
+ const added = [];
2674
+ const insertions = [];
2675
+ for (const call of calls) {
2676
+ const parts = PACKAGE_SUBPATH.exec(call.specifier);
2677
+ if (parts === null) continue;
2678
+ const target = resolveFrom(fromFile, call.specifier);
2679
+ if (target === void 0) continue;
2680
+ for (const alias of barrelsFor(fromFile, parts[1], target)) {
2681
+ if (alreadyMocked.has(alias)) continue;
2682
+ alreadyMocked.add(alias);
2683
+ added.push(alias);
2684
+ const text = source.slice(call.start, call.literalStart) + `'${alias}'` + source.slice(call.literalEnd, call.end);
2685
+ insertions.push({ at: call.end, text: `
2686
+ ${text}` });
2687
+ }
2688
+ }
2689
+ if (insertions.length === 0) return { source, added: [] };
2690
+ let rewritten = source;
2691
+ for (const insertion of insertions.sort((a, b) => b.at - a.at)) {
2692
+ rewritten = rewritten.slice(0, insertion.at) + insertion.text + rewritten.slice(insertion.at);
2693
+ }
2694
+ return { source: rewritten, added };
2695
+ }
2696
+
2697
+ // src/roles/test/codemod/repoint-msw.ts
2698
+ import { parseSync as parseSync5 } from "oxc-parser";
2699
+ var SENTINEL_MSW_SERVER = "@hublo/sentinel/test/msw";
2700
+ var SENTINEL_MSW_API = "@hublo/sentinel/test/tools/msw";
2701
+ var MSW_PACKAGE = /^msw$/;
2702
+ var MSW_SERVER_MODULE = /(^|\/)msw\/server$/;
2703
+ function importsServer(declaration) {
2704
+ return (declaration.specifiers ?? []).some((specifier) => {
2705
+ const imported = specifier.imported;
2706
+ const local = specifier.local;
2707
+ return imported?.name === "server" || imported === void 0 && local?.name === "server";
2708
+ });
2709
+ }
2710
+ function repointMswServer(fileName, source) {
2711
+ const { program, errors } = parseSync5(fileName, source, { sourceType: "module" });
2712
+ if (errors.length > 0) return { source, repointed: false };
2713
+ const edits = [];
2714
+ walk(program, (node) => {
2715
+ if (node.type !== "ImportDeclaration") return;
2716
+ const specifier = node.source;
2717
+ if (specifier?.value === void 0) return;
2718
+ const isServer = MSW_SERVER_MODULE.test(specifier.value) && importsServer(node);
2719
+ const isPackage2 = MSW_PACKAGE.test(specifier.value);
2720
+ if (!isServer && !isPackage2) return;
2721
+ edits.push({
2722
+ start: specifier.start,
2723
+ end: specifier.end,
2724
+ to: isServer ? SENTINEL_MSW_SERVER : SENTINEL_MSW_API
2725
+ });
2726
+ });
2727
+ if (edits.length === 0) return { source, repointed: false };
2728
+ let rewritten = source;
2729
+ for (const edit of edits.sort((left, right) => right.start - left.start)) {
2730
+ const quote = source[edit.start] === '"' ? '"' : "'";
2731
+ rewritten = rewritten.slice(0, edit.start) + `${quote}${edit.to}${quote}` + rewritten.slice(edit.end);
2732
+ }
2733
+ return { source: rewritten, repointed: true };
2734
+ }
2735
+
2736
+ // src/roles/test/codemod/setup-static-imports.ts
2737
+ import { dirname as dirname5, resolve as resolve5 } from "node:path";
2738
+
2739
+ // src/core/context.ts
2740
+ import { existsSync as existsSync11 } from "node:fs";
2741
+ import { dirname as dirname4, join as join12, resolve as resolve3 } from "node:path";
2742
+
2743
+ // src/core/discover-modules.ts
2744
+ import { execFileSync as execFileSync2 } from "node:child_process";
2745
+ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync9, rmSync as rmSync2 } from "node:fs";
2746
+ import { tmpdir as tmpdir2 } from "node:os";
2747
+ import { join as join11 } from "node:path";
2748
+ function runNx(cwd, args) {
2749
+ const nx = resolveBin(cwd, "nx") ?? "nx";
2750
+ try {
2751
+ return execFileSync2(nx, args, {
2752
+ cwd,
2753
+ encoding: "utf8",
2754
+ env: { ...process.env, NX_DAEMON: "false" }
2755
+ });
2756
+ } catch (error) {
2757
+ const message = error instanceof Error ? error.message : String(error);
2758
+ throw new Error(
2759
+ `sentinel: could not run nx (${message}). Is nx installed in this workspace, and are you at its root?`,
2760
+ { cause: error }
2761
+ );
2762
+ }
2763
+ }
2764
+ function readGraph(cwd) {
2765
+ const dir = mkdtempSync2(join11(tmpdir2(), "sentinel-nx-"));
2766
+ const file = join11(dir, "graph.json");
2767
+ try {
2768
+ runNx(cwd, ["graph", "--file", file]);
2769
+ const parsed = JSON.parse(readFileSync9(file, "utf8"));
2770
+ const nodes = parsed.graph?.nodes;
2771
+ if (!nodes || typeof nodes !== "object") {
2772
+ throw new Error(
2773
+ "sentinel: unexpected nx graph output (no graph.nodes); the installed nx version may be incompatible."
2774
+ );
2775
+ }
2776
+ return Object.entries(nodes).map(([name, node]) => ({
2777
+ name,
2778
+ root: join11(cwd, node.data.root)
2779
+ }));
2780
+ } finally {
2781
+ rmSync2(dir, { recursive: true, force: true });
2782
+ }
2783
+ }
2784
+ function discoverModules(cwd, options = {}) {
2785
+ const modules = readGraph(cwd);
2786
+ if (!options.affected) return modules;
2787
+ const affected = new Set(
2788
+ JSON.parse(runNx(cwd, ["show", "projects", "--affected", "--json"]))
2789
+ );
2790
+ return modules.filter((module) => affected.has(module.name));
2791
+ }
2792
+
2793
+ // src/core/context.ts
2794
+ var MODULE_MARKERS = ["package.json", "project.json"];
2795
+ function isModuleDir(cwd) {
2796
+ return MODULE_MARKERS.some((marker) => existsSync11(join12(cwd, marker)));
2797
+ }
2798
+ function findModuleRoot(startDir, workspaceRoot) {
2799
+ let dir = resolve3(startDir);
2800
+ for (; ; ) {
2801
+ if (workspaceRoot !== void 0 && dir === resolve3(workspaceRoot)) return void 0;
2802
+ if (isModuleDir(dir)) return dir;
2803
+ const parent = dirname4(dir);
2804
+ if (parent === dir) return void 0;
2805
+ dir = parent;
2806
+ }
2807
+ }
2808
+ function resolveContext(cwd, opts) {
2809
+ const atRoot = existsSync11(join12(cwd, WORKSPACE_ROOT_MARKER));
2810
+ if (!atRoot && isModuleDir(cwd)) {
2811
+ if (opts.module) {
2812
+ throw new Error(
2813
+ "You are in a module directory: drop --module (the context is the current module)."
2814
+ );
2815
+ }
2816
+ if (opts.ci) {
2817
+ throw new Error("--ci selects the affected set from the workspace root; run it there.");
2818
+ }
2819
+ const name = moduleName(cwd);
2820
+ return { modules: [{ name, root: cwd }], scope: "cwd-module" };
2821
+ }
2822
+ if (atRoot) {
2823
+ if (opts.module) {
2824
+ const found = discoverModules(cwd).find((module) => module.name === opts.module);
2825
+ if (!found) throw new Error(`module "${opts.module}" not found in the workspace.`);
2826
+ return { modules: [found], scope: "named-module" };
2827
+ }
2828
+ if (opts.ci) return { modules: discoverModules(cwd, { affected: true }), scope: "affected" };
2829
+ return { modules: discoverModules(cwd), scope: "all" };
2830
+ }
2831
+ throw new Error(
2832
+ `Run sentinel from a module directory or the workspace root (found neither ${MODULE_MARKERS.join("/")} nor ${WORKSPACE_ROOT_MARKER} here).`
2833
+ );
2834
+ }
2835
+
2836
+ // src/roles/test/codemod/workspace-specifier.ts
2837
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
2838
+ import { join as join13, relative as relative2, resolve as resolve4 } from "node:path";
2839
+ var EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", "/index.ts", "/index.js"];
2840
+ function pathEntries(workspaceRoot) {
2841
+ const file = join13(workspaceRoot, "tsconfig.base.json");
2842
+ if (!existsSync12(file)) return [];
2843
+ let paths;
2844
+ try {
2845
+ paths = parseJsonc(
2846
+ readFileSync10(file, "utf8"),
2847
+ "tsconfig.base.json"
2848
+ )?.compilerOptions?.paths;
2849
+ } catch {
2850
+ return [];
2851
+ }
2852
+ const entries = [];
2853
+ for (const [key, targets] of Object.entries(paths ?? {})) {
2854
+ const target = targets[0];
2855
+ if (target === void 0) continue;
2856
+ const wildcard = key.endsWith("/*") && target.endsWith("/*");
2857
+ entries.push({
2858
+ key: wildcard ? key.slice(0, -2) : key,
2859
+ target: resolve4(workspaceRoot, wildcard ? target.slice(0, -2) : target),
2860
+ wildcard
2861
+ });
2862
+ }
2863
+ return entries.sort((left, right) => right.target.length - left.target.length);
2864
+ }
2865
+ function workspaceSpecifier(workspaceRoot, file) {
2866
+ const absolute = resolve4(file);
2867
+ for (const entry of pathEntries(workspaceRoot)) {
2868
+ if (!entry.wildcard) {
2869
+ if (EXTENSIONS.some((extension) => `${entry.target}${extension}` === absolute)) {
2870
+ return entry.key;
2871
+ }
2872
+ continue;
2873
+ }
2874
+ const rest = relative2(entry.target, absolute);
2875
+ if (rest === "" || rest.startsWith("..") || rest.startsWith("/")) continue;
2876
+ const named = rest.replace(/\\/g, "/").replace(/\.(m|c)?(j|t)sx?$/, "");
2877
+ return `${entry.key}/${named}`;
2878
+ }
2879
+ return void 0;
2880
+ }
2881
+
2882
+ // src/roles/test/codemod/setup-static-imports.ts
2883
+ function requireCall(source, node) {
2884
+ if (node?.type !== "CallExpression") return void 0;
2885
+ const callee = node.callee;
2886
+ if (callee?.type !== "Identifier" || callee.name !== "require") return void 0;
2887
+ const args = node.arguments ?? [];
2888
+ const [only] = args;
2889
+ if (args.length !== 1 || only?.type !== "Literal") return void 0;
2890
+ const raw = source.slice(only.start, only.end);
2891
+ return raw.slice(1, -1);
2892
+ }
2893
+ function importable(written, fileName, workspaceRoot) {
2894
+ if (!written.startsWith(".")) return written;
2895
+ const fileDir = dirname5(fileName);
2896
+ const moduleRoot = findModuleRoot(fileDir, workspaceRoot) ?? fileDir;
2897
+ const target = resolve5(fileDir, written);
2898
+ if (target.startsWith(`${moduleRoot}/`)) return written;
2899
+ if (workspaceRoot === void 0) return void 0;
2900
+ return workspaceSpecifier(workspaceRoot, target);
2901
+ }
2902
+ function statement(fileName, source, node, specifier, written) {
2903
+ if (node.type === "ExpressionStatement") return `import '${specifier}'`;
2904
+ const declarations = node.declarations ?? [];
2905
+ const [only] = declarations;
2906
+ if (declarations.length !== 1 || only === void 0) return void 0;
2907
+ const id = only.id;
2908
+ if (id?.type === "Identifier") {
2909
+ const name = source.slice(id.start, id.end);
2910
+ return isCommonJsPackage(fileName, written) ? `import ${name} from '${specifier}'` : `import * as ${name} from '${specifier}'`;
2911
+ }
2912
+ if (id?.type === "ObjectPattern" && isPlainShorthand(id)) {
2913
+ const names = source.slice(id.start, id.end);
2914
+ return `import ${names} from '${specifier}'`;
2915
+ }
2916
+ return void 0;
2917
+ }
2918
+ function isPlainShorthand(pattern) {
2919
+ const properties = pattern.properties ?? [];
2920
+ if (properties.length === 0) return false;
2921
+ return properties.every(
2922
+ (property) => property.type === "Property" && property.shorthand === true && property.computed !== true && property.value?.type === "Identifier"
2923
+ );
2924
+ }
2925
+ function rewriteSetupStaticImports(fileName, source, program, workspaceRoot) {
2926
+ const body = program.body ?? [];
2927
+ const edits = [];
2928
+ const refusals = [];
2929
+ for (const node of body) {
2930
+ if (node.type === "ImportDeclaration") continue;
2931
+ const expression = node.type === "ExpressionStatement" ? node.expression : node.type === "VariableDeclaration" ? node.declarations?.[0]?.init : void 0;
2932
+ const written = requireCall(source, expression);
2933
+ if (written === void 0) break;
2934
+ const specifier = importable(written, fileName, workspaceRoot);
2935
+ if (specifier === void 0) {
2936
+ refusals.push(written);
2937
+ break;
2938
+ }
2939
+ const text = statement(fileName, source, node, specifier, written);
2940
+ if (text === void 0) break;
2941
+ edits.push({ start: node.start, end: node.end, text });
2942
+ }
2943
+ let result = source;
2944
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
2945
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
2946
+ }
2947
+ return { source: result, rewritten: edits.length, refusals };
2948
+ }
2949
+
2950
+ // src/roles/test/codemod/spy-instance-type.ts
2951
+ var SPY_TYPES = /* @__PURE__ */ new Set(["SpyInstance"]);
2952
+ var QUALIFIED_ONLY = /* @__PURE__ */ new Set(["Mock"]);
2953
+ var VITEST_NAME = { SpyInstance: "MockInstance", Mock: "Mock" };
2954
+ function typeNameOf(source, node) {
2955
+ const name = node.typeName;
2956
+ if (name === void 0) return void 0;
2957
+ return source.slice(name.start, name.end);
2958
+ }
2959
+ function rewriteSpyInstanceType(source, program) {
2960
+ const edits = [];
2961
+ walk(program, (node) => {
2962
+ if (node.type !== "TSTypeReference") return;
2963
+ const written = typeNameOf(source, node);
2964
+ if (written === void 0) return;
2965
+ const qualified = written.startsWith("jest.");
2966
+ const bare = qualified ? written.slice("jest.".length) : written;
2967
+ if (!SPY_TYPES.has(bare) && !(qualified && QUALIFIED_ONLY.has(bare))) return;
2968
+ const args = (node.typeArguments ?? node.typeParameters)?.params;
2969
+ if (args === void 0 || args.length === 0) return;
2970
+ const text = (argument) => source.slice(argument.start, argument.end);
2971
+ const returns = text(args[0]);
2972
+ const parameters = args.length > 1 ? `...args: ${text(args[1])}` : "...args: never[]";
2973
+ const target = VITEST_NAME[bare];
2974
+ edits.push({
2975
+ start: node.start,
2976
+ end: node.end,
2977
+ text: `${target}<(${parameters}) => ${returns}>`,
2978
+ imports: target
2979
+ });
2980
+ });
2981
+ let result = source;
2982
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
2983
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
2984
+ }
2985
+ return {
2986
+ source: result,
2987
+ rewritten: edits.length,
2988
+ // Only the names actually written, so a file does not import a type it never uses.
2989
+ typeImports: [...new Set(edits.map((edit) => edit.imports))]
2990
+ };
2991
+ }
2992
+
2993
+ // src/roles/test/codemod/migrate-file.ts
2994
+ function mockFactories(program) {
2995
+ const factories = [];
2996
+ walk(program, (node) => {
2997
+ if (node.type !== "CallExpression") return;
2998
+ const callee = node.callee;
2999
+ if (callee?.type !== "MemberExpression" && callee?.type !== "StaticMemberExpression") return;
3000
+ const object = callee.object;
3001
+ const property = callee.property;
3002
+ if (object?.type !== "Identifier") return;
3003
+ if (object.name !== "jest" && object.name !== "vi") return;
3004
+ if (property?.name !== "mock") return;
3005
+ const args = node.arguments ?? [];
3006
+ if (args.length > 1) factories.push(args[1]);
3007
+ });
3008
+ return factories;
3009
+ }
3010
+ function parse3(fileName, source) {
3011
+ const { program, errors } = parseSync6(fileName, source, { sourceType: "module" });
3012
+ return errors.length > 0 ? void 0 : program;
3013
+ }
3014
+ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new Set(), asyncHelpers = /* @__PURE__ */ new Set(), modulesOwnMsw = false) {
3015
+ const untouched = {
3016
+ source,
3017
+ changed: false,
3018
+ renamed: 0,
3019
+ hoisted: [],
3020
+ unhandled: [],
3021
+ warnings: [],
3022
+ refusals: [],
3023
+ rulesApplied: [],
3024
+ mswRepointed: false
3025
+ };
3026
+ const unparsable = (step) => ({
3027
+ ...untouched,
3028
+ refusals: [
3029
+ {
3030
+ binding: step,
3031
+ 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.`
3032
+ }
3033
+ ]
3034
+ });
3035
+ const beforeStatic = parse3(fileName, source);
3036
+ if (beforeStatic === void 0) return unparsable("reading the file");
3037
+ const statics = rewriteSetupStaticImports(
3038
+ fileName,
3039
+ source,
3040
+ beforeStatic,
3041
+ findWorkspaceRoot(dirname6(fileName))
3042
+ );
3043
+ source = statics.source;
3044
+ const original = statics.rewritten === 0 ? beforeStatic : parse3(fileName, source);
3045
+ if (original === void 0) return unparsable("hoisting the setup requires");
3046
+ const titles = rewriteEachTitles(source, original);
3047
+ const tables = rewriteEachTableRows(titles.source, original);
3048
+ const afterTables = parse3(fileName, tables.source);
3049
+ if (afterTables === void 0) return unparsable("each table rows");
3050
+ const spyTypes = rewriteSpyInstanceType(tables.source, afterTables);
3051
+ const afterSpyTypes = parse3(fileName, spyTypes.source);
3052
+ if (afterSpyTypes === void 0) return unparsable("SpyInstance types");
3053
+ const ruled = applyNamedRules(spyTypes.source, afterSpyTypes, fileName);
3054
+ const renamed = renameJestToVitest(fileName, ruled.source);
3055
+ const afterRename = parse3(fileName, renamed.source);
3056
+ if (afterRename === void 0) return unparsable("jest to vi rename");
3057
+ const fakeTimers = rewriteFakeTimerOptions(renamed.source, afterRename);
3058
+ const afterFakeTimers = parse3(fileName, fakeTimers.source);
3059
+ if (afterFakeTimers === void 0) return unparsable("fake timer options");
3060
+ const mocks = rewriteMockAsValue(fakeTimers.source, afterFakeTimers);
3061
+ const afterMocks = parse3(fileName, mocks.source);
3062
+ if (afterMocks === void 0) return unparsable("mock as a value");
3063
+ const constructible = rewriteConstructibleMocks(
3064
+ mocks.source,
3065
+ afterMocks,
3066
+ mockFactories(afterMocks)
3067
+ );
3068
+ const afterConstructible = parse3(fileName, constructible.source);
3069
+ if (afterConstructible === void 0) return unparsable("constructible mocks");
3070
+ const factoryDefault = rewriteMockFactoryDefault(
3071
+ constructible.source,
3072
+ afterConstructible,
3073
+ defaultImported
3074
+ );
3075
+ const beforeReturnValue = parse3(fileName, factoryDefault.source);
3076
+ if (beforeReturnValue === void 0) return unparsable("mock factory default");
3077
+ const returnValue = rewriteAssertionReturnValue(factoryDefault.source, beforeReturnValue);
3078
+ const afterReturnValue = parse3(fileName, returnValue.source);
3079
+ if (afterReturnValue === void 0) return unparsable("assertion return value");
3080
+ const isolated = rewriteIsolateModules(returnValue.source, afterReturnValue);
3081
+ const afterIsolate = parse3(fileName, isolated.source);
3082
+ if (afterIsolate === void 0) return unparsable("isolate modules");
3083
+ const emptyImplementations = rewriteEmptyMockImplementation(isolated.source, afterIsolate);
3084
+ const afterEmpty = parse3(fileName, emptyImplementations.source);
3085
+ if (afterEmpty === void 0) return unparsable("empty mock implementation");
3086
+ const hooks = rewriteHookReturnValue(emptyImplementations.source, afterEmpty);
3087
+ const afterHooks = parse3(fileName, hooks.source);
3088
+ if (afterHooks === void 0) return unparsable("hook return value");
3089
+ const promises = rewriteMockPromiseResolve(hooks.source, afterHooks);
3090
+ const awaited = propagateAwait(fileName, promises.source, original, asyncHelpers);
3091
+ const program = parse3(fileName, awaited.source);
3092
+ if (program === void 0) return unparsable("hook body");
3093
+ const hoist = hoistCapturedBindings(
3094
+ awaited.source,
3095
+ program,
3096
+ mockFactories(program),
3097
+ moduleBindings(program),
3098
+ importedNames(program)
3099
+ );
3100
+ const afterHoist = parse3(fileName, hoist.source);
3101
+ if (afterHoist === void 0) return unparsable("hoisting captured bindings");
3102
+ const mockedBindings = annotateMockedBindings(hoist.source, afterHoist);
3103
+ const afterBindings = parse3(fileName, mockedBindings.source);
3104
+ if (afterBindings === void 0) return unparsable("mocked module bindings");
3105
+ const bareFactories = invokeBareMockFactories(mockedBindings.source, afterBindings);
3106
+ const withImport = injectTypeImport(fileName, bareFactories.source, [
3107
+ ...renamed.typeImports,
3108
+ ...spyTypes.typeImports,
3109
+ ...mockedBindings.typeImports
3110
+ ]);
3111
+ const deep = normaliseDeepImports(withImport);
3112
+ const repointed = modulesOwnMsw ? { source: deep.source, repointed: false } : repointMswServer(fileName, deep.source);
3113
+ const finalProgram = parse3(fileName, repointed.source);
3114
+ if (finalProgram === void 0) return unparsable("msw server import");
3115
+ const barrels = rewriteReexportBarrelMocks(fileName, repointed.source, finalProgram);
3116
+ return {
3117
+ source: barrels.source,
3118
+ changed: barrels.source !== source,
3119
+ renamed: renamed.rewritten,
3120
+ hoisted: hoist.hoisted,
3121
+ unhandled: renamed.unhandled,
3122
+ rulesApplied: [
3123
+ ...ruled.applied,
3124
+ ...titles.rewritten > 0 ? ["each title %p"] : [],
3125
+ ...mocks.rewritten > 0 ? ["vi.mock as a value"] : [],
3126
+ ...deep.rewritten > 0 ? ["deep import normalised"] : [],
3127
+ ...constructible.rewritten > 0 ? ["constructible mock implementation"] : [],
3128
+ ...isolated.rewritten > 0 ? ["jest.isolateModules"] : [],
3129
+ ...emptyImplementations.rewritten > 0 ? ["empty mockImplementation"] : [],
3130
+ ...hooks.rewritten > 0 ? ["hook body, so it does not return its mock"] : [],
3131
+ ...spyTypes.rewritten > 0 ? ["SpyInstance<T> as a signature, not a return type"] : [],
3132
+ ...mockedBindings.rewritten > 0 ? ["a mocked module binding, typed as mocked"] : [],
3133
+ ...bareFactories.rewritten > 0 ? ["vi.fn passed as a value, invoked"] : [],
3134
+ ...awaited.rewritten > 0 ? ["await propagated to a helper that became async"] : [],
3135
+ ...barrels.added.map((alias) => `mock carried to ${alias}, which re-exports the mocked file`),
3136
+ ...fakeTimers.translated
3137
+ ].sort(),
3138
+ refusals: [
3139
+ /*
3140
+ * A leading require that leaves its module and that no `paths` entry covers. Reported rather
3141
+ * than written in a shape we know nx refuses, which is Héla's bound: we refuse only where the
3142
+ * constraint is beyond us, and inventing a workspace alias is. Measured on this repo: 3 such
3143
+ * requires across 136 setup files, all 3 covered, so this list stays empty here.
3144
+ */
3145
+ ...statics.refusals.map((specifier) => ({
3146
+ binding: specifier,
3147
+ 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.`
3148
+ })),
3149
+ ...fakeTimers.refusals,
3150
+ ...hoist.refusals,
3151
+ ...mocks.refusals.map((refusal) => ({ binding: refusal.binding, reason: refusal.reason })),
3152
+ ...constructible.refusals,
3153
+ ...isolated.refusals
3154
+ ],
3155
+ warnings: fakeTimers.warnings,
3156
+ mswRepointed: repointed.repointed
3157
+ };
3158
+ }
3159
+
3160
+ // src/roles/test/codemod/test-support-files.ts
3161
+ import { existsSync as existsSync13, readFileSync as readFileSync11, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
3162
+ import { dirname as dirname7, join as join14, relative as relative3, resolve as resolve6 } from "node:path";
3163
+ var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
3164
+ var TEST_FILE2 = /\.(spec|test)\.[cm]?[jt]sx?$/;
3165
+ var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
3166
+ var EXTENSIONS2 = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"];
3167
+ function relativeSpecifiers(source) {
3168
+ const found = [];
3169
+ const patterns = [
3170
+ /\bfrom\s+['"](\.[^'"]*)['"]/g,
3171
+ /\bimport\s+['"](\.[^'"]*)['"]/g,
3172
+ /\bimport\s*\(\s*['"](\.[^'"]*)['"]\s*\)/g,
3173
+ /\brequire\s*\(\s*['"](\.[^'"]*)['"]\s*\)/g
3174
+ ];
3175
+ for (const pattern of patterns) {
3176
+ for (const match of source.matchAll(pattern)) found.push(match[1]);
3177
+ }
3178
+ return found;
3179
+ }
3180
+ function resolveSpecifier(fromFile, specifier) {
3181
+ const base = resolve6(dirname7(fromFile), specifier);
3182
+ if (existsSync13(base) && statSync2(base).isFile()) return base;
3183
+ for (const extension of EXTENSIONS2) {
3184
+ if (existsSync13(`${base}${extension}`)) return `${base}${extension}`;
3185
+ }
3186
+ for (const extension of EXTENSIONS2) {
3187
+ const index = join14(base, `index${extension}`);
3188
+ if (existsSync13(index)) return index;
3189
+ }
3190
+ return void 0;
3191
+ }
3192
+ function moduleSources(cwd) {
3193
+ const found = [];
3194
+ const stack = [cwd];
3195
+ while (stack.length > 0) {
3196
+ const dir = stack.pop();
3197
+ let entries;
3198
+ try {
3199
+ entries = readdirSync3(dir, { withFileTypes: true });
3200
+ } catch {
3201
+ continue;
3202
+ }
3203
+ for (const entry of entries) {
3204
+ if (entry.isDirectory()) {
3205
+ if (!SKIPPED.has(entry.name)) stack.push(join14(dir, entry.name));
3206
+ } else if (SOURCE_FILE.test(entry.name)) {
3207
+ found.push(join14(dir, entry.name));
3208
+ }
3209
+ }
3210
+ }
3211
+ return found;
3212
+ }
3213
+ function read(path) {
3214
+ try {
3215
+ return readFileSync11(path, "utf8");
3216
+ } catch {
3217
+ return void 0;
3218
+ }
3219
+ }
3220
+ function setupFilesInside(cwd, named) {
3221
+ const kept = [];
3222
+ for (const raw of named) {
3223
+ const withoutToken = raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", ".");
3224
+ const full = resolve6(cwd, withoutToken);
3225
+ const inside = relative3(cwd, full);
3226
+ if (inside.startsWith("..") || inside === "") continue;
3227
+ if (!existsSync13(full)) continue;
3228
+ kept.push(inside.replaceAll("\\", "/"));
3229
+ }
3230
+ return kept;
3231
+ }
3232
+ function listTestSupportFiles(cwd, setupFiles = []) {
3233
+ const sources = moduleSources(cwd);
3234
+ const contents = /* @__PURE__ */ new Map();
3235
+ for (const file of sources) {
3236
+ const source = read(file);
3237
+ if (source !== void 0) contents.set(file, source);
3238
+ }
3239
+ const importedBy = /* @__PURE__ */ new Map();
3240
+ for (const [file, source] of contents) {
3241
+ for (const specifier of relativeSpecifiers(source)) {
3242
+ const target = resolveSpecifier(file, specifier);
3243
+ if (target === void 0 || !contents.has(target)) continue;
3244
+ importedBy.set(target, [...importedBy.get(target) ?? [], file]);
3245
+ }
3246
+ }
3247
+ const testOnly = /* @__PURE__ */ new Set();
3248
+ const queue = sources.filter((file) => TEST_FILE2.test(file));
3249
+ const isTestOnly = (file) => (importedBy.get(file) ?? []).every(
3250
+ (importer) => TEST_FILE2.test(importer) || testOnly.has(importer)
3251
+ );
3252
+ while (queue.length > 0) {
3253
+ const file = queue.shift();
3254
+ for (const specifier of relativeSpecifiers(contents.get(file) ?? "")) {
3255
+ const target = resolveSpecifier(file, specifier);
3256
+ if (target === void 0 || TEST_FILE2.test(target) || testOnly.has(target)) continue;
3257
+ if (!isTestOnly(target)) continue;
3258
+ testOnly.add(target);
3259
+ queue.push(target);
3260
+ }
3261
+ }
3262
+ const reachable = [...testOnly].filter((file) => /\brequire\s*\(/.test(contents.get(file) ?? "")).map((file) => relative3(cwd, file).replaceAll("\\", "/"));
3263
+ return [.../* @__PURE__ */ new Set([...reachable, ...setupFilesInside(cwd, setupFiles)])].sort();
3264
+ }
3265
+
3266
+ // src/roles/test/codemod/migrate-module.ts
3267
+ function parse4(source) {
3268
+ const { program, errors } = parseSync7("file.ts", source, { sourceType: "module" });
3269
+ return errors.length > 0 ? void 0 : program;
3270
+ }
3271
+ function read2(path) {
3272
+ try {
3273
+ return readFileSync12(path, "utf8");
3274
+ } catch {
3275
+ return void 0;
3276
+ }
3277
+ }
3278
+ var SKIPPED2 = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
3279
+ function defaultImportedSpecifiers(cwd) {
3280
+ const found = /* @__PURE__ */ new Set();
3281
+ const pattern = /^\s*import\s+[A-Za-z_$][\w$]*\s*(?:,\s*\{[^}]*\}\s*)?from\s+['"]([^'"]+)['"]/gm;
3282
+ const walk2 = (dir) => {
3283
+ let entries;
3284
+ try {
3285
+ entries = readdirSync4(dir);
3286
+ } catch {
3287
+ return;
3288
+ }
3289
+ for (const entry of entries) {
3290
+ if (SKIPPED2.has(entry)) continue;
3291
+ const path = join15(dir, entry);
3292
+ let directory;
3293
+ try {
3294
+ directory = statSync3(path).isDirectory();
3295
+ } catch {
3296
+ continue;
3297
+ }
3298
+ if (directory) {
3299
+ walk2(path);
3300
+ continue;
3301
+ }
3302
+ if (!/\.tsx?$/.test(entry)) continue;
3303
+ const source = read2(path);
3304
+ if (source === void 0) continue;
3305
+ for (const match of source.matchAll(pattern)) found.add(match[1]);
3306
+ }
3307
+ };
3308
+ walk2(cwd);
3309
+ return found;
3310
+ }
3311
+ function migrateModuleTests(cwd, setupFiles = []) {
3312
+ const modulesOwnMsw = ownsMswLifecycle(cwd, setupFiles);
3313
+ const operations = [];
3314
+ const blockers = [];
3315
+ const collected = [];
3316
+ let files = 0;
3317
+ let renamed = 0;
3318
+ let hoisted = 0;
3319
+ const defaultImported = defaultImportedSpecifiers(cwd);
3320
+ const asyncHelpers = /* @__PURE__ */ new Set();
3321
+ for (const relative6 of listTestSupportFiles(cwd, setupFiles)) {
3322
+ const source = read2(join15(cwd, relative6));
3323
+ if (source === void 0) {
3324
+ blockers.push({ file: relative6, reason: "this file could not be read" });
3325
+ continue;
3326
+ }
3327
+ const migrated = migrateTestFile(
3328
+ join15(cwd, relative6),
3329
+ source,
3330
+ defaultImported,
3331
+ /* @__PURE__ */ new Set(),
3332
+ modulesOwnMsw
3333
+ );
3334
+ const renamedTo = vitestSupportName(relative6);
3335
+ if (!migrated.changed && renamedTo === relative6) continue;
3336
+ if (renamedTo !== relative6) operations.push({ kind: "delete", path: relative6 });
3337
+ const before = parse4(source);
3338
+ const after = parse4(migrated.source);
3339
+ if (before !== void 0 && after !== void 0) {
3340
+ const wasAsync = asyncFunctionNames(before);
3341
+ for (const name of asyncFunctionNames(after)) {
3342
+ if (!wasAsync.has(name)) asyncHelpers.add(name);
3343
+ }
3344
+ }
3345
+ files++;
3346
+ renamed += migrated.renamed;
3347
+ operations.push({ kind: "write", path: renamedTo, contents: migrated.source });
3348
+ }
3349
+ for (const relative6 of listTestFiles(cwd)) {
3350
+ const source = read2(join15(cwd, relative6));
3351
+ if (source === void 0) {
3352
+ blockers.push({ file: relative6, reason: "this file could not be read" });
3353
+ continue;
3354
+ }
3355
+ const migrated = migrateTestFile(
3356
+ join15(cwd, relative6),
3357
+ source,
3358
+ defaultImported,
3359
+ asyncHelpers,
3360
+ modulesOwnMsw
3361
+ );
3362
+ for (const member of migrated.unhandled) {
3363
+ blockers.push({
3364
+ file: `${relative6}:${member.line}`,
3365
+ 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}`
3366
+ });
3367
+ }
3368
+ collected.push(...migrated.warnings);
3369
+ for (const refusal of migrated.refusals) {
3370
+ blockers.push({ file: relative6, reason: refusal.reason });
3371
+ }
3372
+ if (!migrated.changed) continue;
3373
+ files++;
3374
+ renamed += migrated.renamed;
3375
+ if (migrated.hoisted.length > 0) hoisted++;
3376
+ operations.push({ kind: "write", path: relative6, contents: migrated.source });
3377
+ }
3378
+ const warnings = [...new Set(collected)];
3379
+ return blockers.length > 0 ? { operations: [], files, renamed, hoisted, blockers, warnings } : { operations, files, renamed, hoisted, blockers: [], warnings };
3380
+ }
3381
+
3382
+ // src/roles/test/nest/generate-config.ts
3383
+ import { readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
3384
+ import { join as join16, relative as relative4 } from "node:path";
3385
+ var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
3386
+ var REACT_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
3387
+ function flavourOf(reading) {
3388
+ return reading.testEnvironment === "jsdom" ? "react" : "nest";
3389
+ }
3390
+ var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/jest-parity";
3391
+ var SENTINEL_MSW_SETUP = "@hublo/sentinel/test/setup/msw-lifecycle";
3392
+ var SENTINEL_WORKSPACE_SETUP = "@hublo/sentinel/test/setup/workspace";
3393
+ var SENTINEL_A11Y_SETUP = "@hublo/sentinel/test/setup/a11y";
3394
+ var SENTINEL_W3C_SETUP = "@hublo/sentinel/test/setup/w3c";
3395
+ function hopsToRoot(cwd, workspaceRoot) {
3396
+ const up = relative4(cwd, workspaceRoot).replaceAll("\\", "/");
3397
+ return up === "" ? "." : up;
3398
+ }
3399
+ function inlineBlock(packages) {
3400
+ if (packages.length === 0) return "";
3401
+ const list = packages.map((name) => `'${name}'`).join(", ");
3402
+ return ` server: { deps: { inline: [${list}] } },`;
3403
+ }
3404
+ function coverageBlock(reading) {
3405
+ return reading.coverageDirectory === void 0 ? "" : ` coverage: { reportsDirectory: '${reading.coverageDirectory}' },`;
3406
+ }
3407
+ function environmentBlock(reading) {
3408
+ return reading.testEnvironment === void 0 ? "" : ` environment: '${reading.testEnvironment}',`;
3409
+ }
3410
+ var SKIPPED3 = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
3411
+ var BARE_AUTOMOCK = /(?:vi|vitest|jest)\.mock\(\s*['"]([^'".][^'"]*)['"]\s*\)/g;
3412
+ var MENTIONS_MSW = /from\s+['"](msw(\/[^'"]*)?|@hublo\/test\/msw\/server|@hublo\/sentinel\/test\/msw)['"]/;
3413
+ var DECORATED_ABSTRACT_MEMBER = /@[A-Za-z_$][\w$]*\s*(?:\([^)]*\))?\s*(?:\r?\n\s*(?:@[A-Za-z_$][\w$]*\s*(?:\([^)]*\))?\s*)*)*\r?\n\s*(?:public |protected |private |readonly )*abstract\s+[A-Za-z_$][\w$]*\s*[?!]?\s*[:(]/;
3414
+ function readModuleTraits(cwd) {
3415
+ const traits = {
3416
+ msw: false,
3417
+ decoratedAbstractMember: false,
3418
+ automockedPackages: []
3419
+ };
3420
+ const automocked = /* @__PURE__ */ new Set();
3421
+ const walk2 = (dir) => {
3422
+ let entries;
3423
+ try {
3424
+ entries = readdirSync5(dir);
3425
+ } catch {
3426
+ return;
3427
+ }
3428
+ for (const entry of entries) {
3429
+ if (SKIPPED3.has(entry)) continue;
3430
+ const path = join16(dir, entry);
3431
+ let directory;
3432
+ try {
3433
+ directory = statSync4(path).isDirectory();
3434
+ } catch {
3435
+ continue;
3436
+ }
3437
+ if (directory) {
3438
+ walk2(path);
3439
+ continue;
3440
+ }
3441
+ if (!/\.tsx?$/.test(entry)) continue;
3442
+ let source;
3443
+ try {
3444
+ source = readFileSync13(path, "utf8");
3445
+ } catch {
3446
+ continue;
3447
+ }
3448
+ if (!traits.msw && MENTIONS_MSW.test(source)) traits.msw = true;
3449
+ if (!traits.decoratedAbstractMember && DECORATED_ABSTRACT_MEMBER.test(source)) {
3450
+ traits.decoratedAbstractMember = true;
3451
+ }
3452
+ BARE_AUTOMOCK.lastIndex = 0;
3453
+ for (const match of source.matchAll(BARE_AUTOMOCK)) automocked.add(match[1]);
3454
+ }
3455
+ };
3456
+ walk2(cwd);
3457
+ traits.automockedPackages = [...automocked].sort();
3458
+ return traits;
3459
+ }
3460
+ var withoutRootDir = (value) => value.replaceAll("<rootDir>/", "./").replaceAll("<rootDir>", ".");
3461
+ function ownsMswLifecycle2(cwd, reading) {
3462
+ return ownsMswLifecycle(cwd, [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []]);
3463
+ }
3464
+ function setupBlock(cwd, reading, msw) {
3465
+ const dom = flavourOf(reading) === "react";
3466
+ const declared = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []];
3467
+ const namedTheRootSetup = declared.some((file) => file.endsWith("jest.setup.after.env.js"));
3468
+ const own = declared.filter((file) => !file.endsWith("jest.setup.after.env.js")).map((file) => `'${vitestSupportName(withoutRootDir(file))}'`);
3469
+ const sentinel = [
3470
+ `'${SENTINEL_TEST_SETUP}'`,
3471
+ ...dom ? [`'${SENTINEL_A11Y_SETUP}'`, `'${SENTINEL_W3C_SETUP}'`] : [],
3472
+ ...namedTheRootSetup ? [`'${SENTINEL_WORKSPACE_SETUP}'`] : [],
3473
+ ...msw && namedTheRootSetup && !ownsMswLifecycle2(cwd, reading) ? [`'${SENTINEL_MSW_SETUP}'`] : []
3474
+ ];
3475
+ return ` setupFiles: [${[...sentinel, ...own].join(", ")}],`;
3476
+ }
3477
+ function envBlock(reading) {
3478
+ if (reading.env === void 0 || reading.env.length === 0) return "";
3479
+ const entries = reading.env.map((entry) => ` ${entry.name}: ${entry.value},`);
3480
+ return [" env: {", ...entries, " },"].join("\n");
3481
+ }
3482
+ function concurrencyBlock(cwd, reading, target) {
3483
+ const declared = reading.maxWorkers;
3484
+ if (declared !== void 0) {
3485
+ return declared === "1" ? ` fileParallelism: false,` : ` maxWorkers: ${declared},`;
3486
+ }
3487
+ const nxTarget = projectTargets(cwd)?.[target];
3488
+ for (const level of [nxTarget?.options, nxTarget?.configurations?.ci]) {
3489
+ if (level === void 0) continue;
3490
+ if (level.runInBand === true) return ` fileParallelism: false,`;
3491
+ if (typeof level.maxWorkers === "number") {
3492
+ return level.maxWorkers === 1 ? ` fileParallelism: false,` : ` maxWorkers: ${level.maxWorkers},`;
3493
+ }
3494
+ }
3495
+ return "";
3496
+ }
3497
+ function timeoutBlock(reading) {
3498
+ return reading.testTimeout === void 0 ? "" : ` testTimeout: ${reading.testTimeout},`;
3499
+ }
3500
+ function globalSetupBlock(reading) {
3501
+ if (reading.globalSetup === void 0) return "";
3502
+ const files = reading.globalSetup.map((file) => `'${vitestSupportName(withoutRootDir(file))}'`).join(", ");
3503
+ return ` globalSetup: [${files}],`;
3504
+ }
3505
+ function includeBlock(reading) {
3506
+ if (reading.testMatch === void 0) return "";
3507
+ const globs = reading.testMatch.map((glob) => `'${withoutRootDir(glob)}'`).join(", ");
3508
+ return ` include: [${globs}],`;
3509
+ }
3510
+ function excludeBlock(reading) {
3511
+ if (reading.excludeGlobs === void 0 || reading.excludeGlobs.length === 0) return "";
3512
+ const globs = reading.excludeGlobs.map((glob) => `'${glob}'`).join(", ");
3513
+ return ` exclude: [...configDefaults.exclude, ${globs}],`;
3514
+ }
3515
+ function isPackage(replacement) {
3516
+ return !replacement.startsWith(".") && !replacement.startsWith("/") && !replacement.toLowerCase().includes("<rootdir>");
3517
+ }
3518
+ function aliasBlock(reading) {
3519
+ if (reading.aliases === void 0 || reading.aliases.length === 0) return "";
3520
+ const entries = reading.aliases.map((alias) => {
3521
+ const pattern = alias.find.replaceAll(String.raw`\\`, "\\").replaceAll("/", String.raw`\/`);
3522
+ const deadToken = /<rootdir>/i.test(alias.replacement) && !alias.replacement.includes("<rootDir>");
3523
+ const target = isPackage(alias.replacement) ? `'${alias.replacement}'` : deadToken ? `'${alias.replacement}'` : `path.resolve(import.meta.dirname, '${withoutRootDir(alias.replacement)}')`;
3524
+ return ` { find: /${pattern}/, replacement: ${target} },`;
3525
+ });
3526
+ return [" resolve: {", " alias: [", ...entries, " ],", " },"].join("\n");
3527
+ }
3528
+ function generateNestTestConfig(cwd, reading, target = "test") {
3529
+ if (reading.refusals.length > 0) return { refusals: reading.refusals };
3530
+ const workspaceRoot = findWorkspaceRoot(cwd);
3531
+ if (workspaceRoot === void 0) {
3532
+ return { refusals: ["the workspace root could not be found from this module"] };
3533
+ }
3534
+ const traits = readModuleTraits(cwd);
3535
+ const up = hopsToRoot(cwd, workspaceRoot);
3536
+ const test = [
3537
+ environmentBlock(reading),
3538
+ setupBlock(cwd, reading, traits.msw),
3539
+ envBlock(reading),
3540
+ timeoutBlock(reading),
3541
+ concurrencyBlock(cwd, reading, target),
3542
+ globalSetupBlock(reading),
3543
+ includeBlock(reading),
3544
+ excludeBlock(reading),
3545
+ inlineBlock(traits.automockedPackages),
3546
+ coverageBlock(reading)
3547
+ ].filter((line) => line !== "");
3548
+ const needsDefaults = test.some((line) => line.includes("configDefaults"));
3549
+ const imports = [
3550
+ `import path from 'node:path'`,
3551
+ "",
3552
+ ...needsDefaults ? [`import { configDefaults } from 'vitest/config'`, ""] : [],
3553
+ flavourOf(reading) === "react" ? `import { defineConfig, reactTestConfig } from '${REACT_TEST_PRESET_SPECIFIER}'` : `import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
3554
+ ].join("\n");
3555
+ const overrides = [aliasBlock(reading), " test: {", ...test, " },"].filter(
3556
+ (line) => line !== ""
3557
+ );
3558
+ const decoratorLine = traits.decoratedAbstractMember ? "\n lowerDecoratorsWithTypeScript: true," : "";
3559
+ return {
3560
+ refusals: [],
3561
+ source: `${imports}
3562
+
3563
+ export default defineConfig(
3564
+ ${flavourOf(reading) === "react" ? "reactTestConfig" : "nestTestConfig"}({
3565
+ root: import.meta.dirname,
3566
+ workspaceRoot: path.resolve(import.meta.dirname, '${up}'),${decoratorLine}
3567
+ overrides: {
3568
+ ${overrides.join("\n")}
3569
+ },
3570
+ }),
3571
+ )
3572
+ `
3573
+ };
3574
+ }
3575
+
3576
+ // src/roles/test/nest/read-jest-config.ts
3577
+ import { existsSync as existsSync14, readFileSync as readFileSync14 } from "node:fs";
3578
+ import { join as join17 } from "node:path";
3579
+ var CARRIED = [
3580
+ "testEnvironment",
3581
+ "coverageDirectory",
3582
+ "setupFilesAfterEnv",
3583
+ "setupFiles",
3584
+ "testTimeout",
3585
+ "maxWorkers",
3586
+ "globalSetup",
3587
+ "testMatch",
3588
+ "collectCoverageFrom",
3589
+ "testPathIgnorePatterns",
3590
+ "moduleNameMapper"
3591
+ ];
3592
+ var DISCARDED = [
3593
+ "preset",
3594
+ "displayName",
3595
+ "moduleFileExtensions",
3596
+ "transform",
3597
+ "extensionsToTreatAsEsm"
3598
+ ];
3599
+ var STRING = String.raw`['"\`]([^'"\`]*)['"\`]`;
3600
+ function readString(body, key) {
3601
+ const value = new RegExp(String.raw`\b${key}\s*:\s*${STRING}`).exec(body)?.[1];
3602
+ return value?.includes("${") === true ? void 0 : value;
3603
+ }
3604
+ function readNumber(body, key) {
3605
+ return new RegExp(String.raw`\b${key}\s*:\s*(\d+)`).exec(body)?.[1];
3606
+ }
3607
+ function objectBody(source, key) {
3608
+ const at = new RegExp(String.raw`\b${key}\s*:\s*\{`).exec(source);
3609
+ if (at === null) return void 0;
3610
+ const from = at.index + at[0].length - 1;
3611
+ let depth = 0;
3612
+ for (let i = from; i < source.length; i += 1) {
3613
+ if (source[i] === "{") depth += 1;
3614
+ else if (source[i] === "}") {
3615
+ depth -= 1;
3616
+ if (depth === 0) return source.slice(from + 1, i);
3617
+ }
3618
+ }
3619
+ return void 0;
3620
+ }
3621
+ function objectBodyAfter(source, offset) {
3622
+ const open = source.indexOf("{", offset);
3623
+ if (open === -1) return void 0;
3624
+ let depth = 0;
3625
+ for (let i = open; i < source.length; i += 1) {
3626
+ if (source[i] === "{") depth += 1;
3627
+ else if (source[i] === "}") {
3628
+ depth -= 1;
3629
+ if (depth === 0) return source.slice(open + 1, i);
3630
+ }
3631
+ }
3632
+ return void 0;
3633
+ }
3634
+ function readEnvironment(source) {
3635
+ const entries = [];
3636
+ const at = /process\.env\s*=\s*Object\.assign\(\s*process\.env\s*,/.exec(source);
3637
+ if (at !== null) {
3638
+ const block = objectBody(source.slice(at.index), "assign") ?? objectBodyAfter(source, at.index);
3639
+ for (const raw of (block ?? "").split(/,(?![^[\]{}]*[\]}])/)) {
3640
+ const entry = /^\s*(?:\/\/[^\n]*\n\s*)?([A-Za-z_][\w]*)\s*:\s*([\s\S]+?)\s*$/.exec(raw);
3641
+ if (entry !== null) entries.push({ name: entry[1], value: entry[2] });
3642
+ }
3643
+ }
3644
+ for (const match of source.matchAll(/process\.env\.(\w+)\s*\?\?=\s*(.+?),?\s*$/gm)) {
3645
+ entries.push({ name: match[1], value: match[2] });
3646
+ }
3647
+ return entries;
3648
+ }
3649
+ function readCoverageFrom(body) {
3650
+ const globs = readStringArray(body, "collectCoverageFrom");
3651
+ if (globs === void 0) return void 0;
3652
+ return {
3653
+ include: globs.filter((glob) => !glob.startsWith("!")),
3654
+ exclude: globs.filter((glob) => glob.startsWith("!")).map((glob) => glob.slice(1))
3655
+ };
3656
+ }
3657
+ function translateIgnorePattern(raw) {
3658
+ const pattern = raw.replaceAll("\\\\", "\\");
3659
+ if (pattern === "/node_modules/" || pattern === "node_modules") return {};
3660
+ const suffix = /^\\\.([\w.\\-]+)\$$/.exec(pattern);
3661
+ if (suffix !== null) return { glob: `**/*.${suffix[1].replaceAll("\\.", ".")}` };
3662
+ const directory = /^\/([\w\-/]+)\/$/.exec(pattern);
3663
+ if (directory !== null) return { glob: `**/${directory[1]}/**` };
3664
+ return {
3665
+ refusal: `its \`testPathIgnorePatterns\` entry \`${raw}\` is a regex shape this reader does not translate. Vitest excludes by GLOB, and a regex copied across matches nothing, so the files it names would all run`
3666
+ };
3667
+ }
3668
+ function readModuleNameMapper(body) {
3669
+ const block = objectBody(body, "moduleNameMapper");
3670
+ if (block === void 0) return { aliases: [], dropped: [] };
3671
+ const aliases = [];
3672
+ const dropped = [];
3673
+ for (const entry of block.matchAll(
3674
+ new RegExp(String.raw`${STRING}\s*:\s*\n?\s*${STRING}`, "g")
3675
+ )) {
3676
+ const find = entry[1];
3677
+ const replacement = entry[2];
3678
+ const named = find.replace(/^\^/, "").replace(/\$$/, "").replaceAll("\\", "");
3679
+ if (named !== "" && replacement.includes(`node_modules/${named}/`)) {
3680
+ dropped.push(find);
3681
+ continue;
3682
+ }
3683
+ aliases.push({ find, replacement: replacement.replace(/^<rootDir>\//, "") });
3684
+ }
3685
+ return { aliases, dropped };
3686
+ }
3687
+ function readStringArray(body, key) {
3688
+ const raw = new RegExp(String.raw`\b${key}\s*:\s*\[([^\]]*)\]`).exec(body)?.[1];
3689
+ if (raw === void 0) return void 0;
3690
+ return [...raw.matchAll(new RegExp(STRING, "g"))].map((match) => match[1]);
3691
+ }
3692
+ function configBody(source) {
3693
+ const start = /(?:const\s+\w+(?::\s*\w+)?\s*=|export\s+default)\s*\{/.exec(source);
3694
+ if (start === null) return void 0;
3695
+ let depth = 0;
3696
+ const from = start.index + start[0].length - 1;
3697
+ for (let i = from; i < source.length; i += 1) {
3698
+ const char = source[i];
3699
+ if (char === "{") depth += 1;
3700
+ else if (char === "}") {
3701
+ depth -= 1;
3702
+ if (depth === 0) return source.slice(from + 1, i);
3703
+ }
3704
+ }
3705
+ return void 0;
3706
+ }
3707
+ function declaredKeys(body) {
3708
+ const keys = [];
3709
+ let depth = 0;
3710
+ let quote;
3711
+ let token = "";
3712
+ for (let i = 0; i < body.length; i += 1) {
3713
+ const char = body[i];
3714
+ if (quote !== void 0) {
3715
+ if (char === quote && body[i - 1] !== "\\") quote = void 0;
3716
+ continue;
3717
+ }
3718
+ if (char === "'" || char === '"' || char === "`") {
3719
+ quote = char;
3720
+ continue;
3721
+ }
3722
+ if (char === "/" && body[i + 1] === "/") {
3723
+ const end = body.indexOf("\n", i);
3724
+ i = end === -1 ? body.length : end;
3725
+ token = "";
3726
+ continue;
3727
+ }
3728
+ if (char === "/" && body[i + 1] === "*") {
3729
+ const end = body.indexOf("*/", i + 2);
3730
+ i = end === -1 ? body.length : end + 1;
3731
+ token = "";
3732
+ continue;
3733
+ }
3734
+ if (char === "{" || char === "[" || char === "(") {
3735
+ depth += 1;
3736
+ token = "";
3737
+ continue;
3738
+ }
3739
+ if (char === "}" || char === "]" || char === ")") {
3740
+ depth -= 1;
3741
+ token = "";
3742
+ continue;
3743
+ }
3744
+ if (depth === 0 && char === ":") {
3745
+ const key = /([A-Za-z_$][\w$]*)\s*$/.exec(token)?.[1];
3746
+ if (key !== void 0) keys.push(key);
3747
+ token = "";
3748
+ continue;
3749
+ }
3750
+ if (depth === 0 && char === ",") {
3751
+ token = "";
3752
+ continue;
3753
+ }
3754
+ token += char;
3755
+ }
3756
+ return keys;
3757
+ }
3758
+ function extendedConfig(source, cwd) {
3759
+ if (cwd === void 0) return void 0;
3760
+ for (const line of source.split("\n")) {
3761
+ const match = /^\s*import\s+(?!type\b)(\w+)\s+from\s+['"](\.[^'"]*)['"]/.exec(line);
3762
+ if (match === null) continue;
3763
+ const [, binding, specifier] = match;
3764
+ const candidates = [specifier, `${specifier}.ts`, `${specifier}.js`].map(
3765
+ (name) => name.replace(/^\.\//, "")
3766
+ );
3767
+ const file = candidates.find(
3768
+ (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync14(join17(cwd, name))
3769
+ );
3770
+ if (file === void 0) continue;
3771
+ if (!new RegExp(String.raw`\{\s*\.\.\.${binding}\b`).test(source)) continue;
3772
+ return { binding, file };
3773
+ }
3774
+ return void 0;
3775
+ }
3776
+ function sideEffects(source, extended) {
3777
+ const found = [];
3778
+ for (const line of source.split("\n")) {
3779
+ const valueImport = /^\s*import\s+(?!type\b)(.*)$/.exec(line);
3780
+ if (valueImport === null || /from\s+['"]jest['"]/.test(line)) continue;
3781
+ const bindings = [
3782
+ ...(/\{([^}]*)\}/.exec(valueImport[1])?.[1] ?? "").split(","),
3783
+ /^\s*(\w+)\s*(?:,|from)/.exec(valueImport[1])?.[1] ?? ""
3784
+ ].map((name) => name.split(" as ").pop()?.trim() ?? "").filter((name) => name !== "");
3785
+ if (extended !== void 0 && bindings.includes(extended.binding)) continue;
3786
+ const used = bindings.some((name) => {
3787
+ const uses = source.match(new RegExp(String.raw`\b${name}\b`, "g")) ?? [];
3788
+ return uses.length > 1;
3789
+ });
3790
+ if (used) found.push(`it imports a value it uses (\`${line.trim()}\`)`);
3791
+ }
3792
+ return found;
3793
+ }
3794
+ function readJestConfig(cwd, named) {
3795
+ const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync14(join17(cwd, name)));
3796
+ if (file === void 0 || !existsSync14(join17(cwd, file))) return void 0;
3797
+ let source;
3798
+ try {
3799
+ source = readFileSync14(join17(cwd, file), "utf8");
3800
+ } catch {
3801
+ return { file, refusals: [`${file} could not be read`], notes: [] };
3802
+ }
3803
+ const extended = extendedConfig(source, cwd);
3804
+ const refusals = sideEffects(source, extended);
3805
+ const body = configBody(source);
3806
+ if (body === void 0) {
3807
+ return { file, refusals: [...refusals, "its config object could not be located"], notes: [] };
3808
+ }
3809
+ const known = /* @__PURE__ */ new Set([...CARRIED, ...DISCARDED]);
3810
+ for (const key of declaredKeys(body)) {
3811
+ if (!known.has(key)) refusals.push(`it declares \`${key}\`, which is not transposed`);
3812
+ }
3813
+ const notes = [];
3814
+ const afterEnv = readStringArray(body, "setupFilesAfterEnv");
3815
+ const before = readStringArray(body, "setupFiles");
3816
+ if (afterEnv !== void 0 && before !== void 0) {
3817
+ notes.push(
3818
+ "jest runs `setupFiles` before its test framework is installed and `setupFilesAfterEnv` after it; Vitest has one `setupFiles`, so both land there in that order. A setup file that relied on the framework being ABSENT will now find it present"
3819
+ );
3820
+ }
3821
+ const ignore = readStringArray(body, "testPathIgnorePatterns") ?? [];
3822
+ const excludeGlobs = [];
3823
+ for (const pattern of ignore) {
3824
+ const { glob, refusal } = translateIgnorePattern(pattern);
3825
+ if (refusal !== void 0) refusals.push(refusal);
3826
+ else if (glob !== void 0) excludeGlobs.push(glob);
3827
+ }
3828
+ const mapper = readModuleNameMapper(body);
3829
+ for (const find of mapper.dropped) {
3830
+ notes.push(
3831
+ `\`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`
3832
+ );
3833
+ }
3834
+ for (const alias of mapper.aliases) {
3835
+ if (/<rootdir>/i.test(alias.replacement) && !alias.replacement.includes("<rootDir>")) {
3836
+ notes.push(
3837
+ `\`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`
3838
+ );
3839
+ }
3840
+ }
3841
+ const coverageFrom = readCoverageFrom(body);
3842
+ const env = readEnvironment(source);
3843
+ const globalSetup = readString(body, "globalSetup");
3844
+ const testTimeout = readNumber(body, "testTimeout");
3845
+ const maxWorkers = readNumber(body, "maxWorkers");
3846
+ const testMatch = readStringArray(body, "testMatch");
3847
+ const base = extended === void 0 ? void 0 : readJestConfig(cwd, extended.file);
3848
+ const inherited = base === void 0 ? {} : { ...base, refusals: [], notes: [] };
3849
+ for (const [jestKey, field] of [
3850
+ ["testPathIgnorePatterns", "excludeGlobs"],
3851
+ ["setupFilesAfterEnv", "setupFilesAfterEnv"],
3852
+ ["setupFiles", "setupFiles"],
3853
+ ["testMatch", "testMatch"],
3854
+ ["collectCoverageFrom", "coverageFrom"],
3855
+ ["moduleNameMapper", "aliases"]
3856
+ ]) {
3857
+ if (declaredKeys(body).includes(jestKey)) delete inherited[field];
3858
+ }
3859
+ return {
3860
+ ...inherited,
3861
+ file,
3862
+ ...readString(body, "testEnvironment") !== void 0 ? { testEnvironment: readString(body, "testEnvironment") } : {},
3863
+ ...readString(body, "coverageDirectory") !== void 0 ? { coverageDirectory: readString(body, "coverageDirectory") } : {},
3864
+ ...afterEnv !== void 0 ? { setupFilesAfterEnv: afterEnv } : {},
3865
+ ...before !== void 0 ? { setupFiles: before } : {},
3866
+ ...testTimeout !== void 0 ? { testTimeout } : {},
3867
+ ...maxWorkers !== void 0 ? { maxWorkers } : {},
3868
+ ...globalSetup !== void 0 ? { globalSetup: [globalSetup] } : {},
3869
+ ...testMatch !== void 0 ? { testMatch } : {},
3870
+ ...coverageFrom !== void 0 ? { coverageFrom } : {},
3871
+ ...excludeGlobs.length > 0 ? { excludeGlobs } : {},
3872
+ ...mapper.aliases.length > 0 ? { aliases: mapper.aliases } : {},
3873
+ ...env.length > 0 ? { env } : {},
3874
+ refusals: [...base?.refusals ?? [], ...refusals],
3875
+ notes: [...base?.notes ?? [], ...notes]
3876
+ };
3877
+ }
3878
+
3879
+ // src/roles/test/nest/run-modes.ts
3880
+ import { existsSync as existsSync15 } from "node:fs";
3881
+ import { basename as basename2, join as join18 } from "node:path";
3882
+ var JEST_EXECUTOR = "@nx/jest:jest";
3883
+ var PRIMARY_CONFIG_FILE = "vitest.config.mts";
3884
+ var PRIMARY_TARGET = "test";
3885
+ function configName(declared) {
3886
+ return basename2(declared.replaceAll("\\", "/"));
3887
+ }
3888
+ function configFileFor(target) {
3889
+ if (target === PRIMARY_TARGET) return PRIMARY_CONFIG_FILE;
3890
+ const suffix = target.startsWith("test-") ? target.slice("test-".length) : target;
3891
+ return `vitest.${suffix}.config.mts`;
3892
+ }
3893
+ function runModes(cwd, fallback) {
3894
+ const targets = projectTargets(cwd) ?? {};
3895
+ const declared = [];
3896
+ for (const [target, raw] of Object.entries(targets)) {
3897
+ const entry = raw;
3898
+ if (entry.executor !== JEST_EXECUTOR) continue;
3899
+ const jestConfig = entry.options?.jestConfig;
3900
+ if (jestConfig === void 0) continue;
3901
+ const name = configName(jestConfig);
3902
+ if (!existsSync15(join18(cwd, name))) continue;
3903
+ declared.push({
3904
+ target,
3905
+ jestConfig: name,
3906
+ vitestConfig: configFileFor(target),
3907
+ primary: target === PRIMARY_TARGET
3908
+ });
3909
+ }
3910
+ if (declared.length === 0) {
3911
+ const [only] = fallback;
3912
+ return only === void 0 ? [] : [
3913
+ {
3914
+ target: PRIMARY_TARGET,
3915
+ jestConfig: only,
3916
+ vitestConfig: PRIMARY_CONFIG_FILE,
3917
+ primary: true
3918
+ }
3919
+ ];
3920
+ }
3921
+ return declared.sort(
3922
+ (left, right) => left.primary === right.primary ? left.target.localeCompare(right.target) : Number(right.primary) - Number(left.primary)
3923
+ );
3924
+ }
3925
+
3926
+ // src/roles/test/written-file-formatter.ts
3927
+ import { existsSync as existsSync17 } from "node:fs";
3928
+ import { dirname as dirname9, join as join21 } from "node:path";
3929
+
3930
+ // src/roles/format/read-adoption.ts
3931
+ import { existsSync as existsSync16, readFileSync as readFileSync15 } from "node:fs";
3932
+ import { join as join19 } from "node:path";
3933
+
3934
+ // src/roles/format/config-policy.ts
3935
+ var FORMAT_CONFIG_FILE = ".oxfmtrc.json";
3936
+ var FORMAT_SCRIPT_NAME = "format";
3937
+ var PROVENANCE_KEY = "$sentinel";
3938
+ var PERMITTED_LOCAL_KEYS = [
3939
+ "useTabs",
3940
+ "tabWidth",
3941
+ "printWidth",
3942
+ "semi",
3943
+ "singleQuote",
3944
+ "jsxSingleQuote",
3945
+ "trailingComma",
3946
+ "quoteProps",
3947
+ "bracketSpacing",
3948
+ "bracketSameLine",
3949
+ "arrowParens",
3950
+ "endOfLine",
3951
+ "objectWrap",
3952
+ "proseWrap",
3953
+ "experimentalOperatorPosition"
3954
+ ];
3955
+ var ALWAYS_LOCAL_KEYS = ["ignorePatterns", "overrides"];
3956
+ var FORMAT_DEFAULT_IGNORES = ["**/*.toml"];
3957
+ var PRETTIER_CONFIG_FILES = [
3958
+ ".prettierrc",
3959
+ ".prettierrc.json",
3960
+ ".prettierrc.json5",
3961
+ ".prettierrc.yml",
3962
+ ".prettierrc.yaml",
3963
+ ".prettierrc.js",
3964
+ ".prettierrc.cjs",
3965
+ ".prettierrc.mjs",
3966
+ "prettier.config.js",
3967
+ "prettier.config.cjs",
3968
+ "prettier.config.mjs"
3969
+ ];
3970
+ function formatTargets() {
3971
+ return {
3972
+ [FORMAT_SCRIPT_NAME]: {
3973
+ cache: true,
3974
+ inputs: ["default", `{projectRoot}/${FORMAT_CONFIG_FILE}`]
3975
+ },
3976
+ [`${FORMAT_SCRIPT_NAME}:fix`]: { cache: false }
3977
+ };
3978
+ }
3979
+
3980
+ // src/roles/format/presets/base.json
3981
+ var base_default = {
3982
+ printWidth: 80,
3983
+ semi: false,
3984
+ singleQuote: true,
3985
+ trailingComma: "all",
3986
+ sortPackageJson: false,
3987
+ sortImports: false
3988
+ };
3989
+
3990
+ // src/roles/format/presets/nest-imports.json
3991
+ var nest_imports_default = {
3992
+ customGroups: [
3993
+ {
3994
+ groupName: "workspace",
3995
+ elementNamePattern: ["@hublo/**", "@shared/**", "@front/**", "@network/**"]
3996
+ }
3997
+ ],
3998
+ groups: ["builtin", "external", "workspace", ["parent", "index"], "sibling"],
3999
+ newlinesBetween: true,
4000
+ order: "asc",
4001
+ ignoreCase: false
4002
+ };
4003
+
4004
+ // src/roles/format/preset-data.ts
4005
+ var FORMAT_PRESETS = ["base", "svelte", "nest"];
4006
+ function hasFormatPreset(name) {
4007
+ return FORMAT_PRESETS.includes(name);
4008
+ }
4009
+ function formatPresetName(declared) {
4010
+ if (declared === "svelte") return "svelte";
4011
+ if (declared === "nest") return "nest";
4012
+ return "base";
4013
+ }
4014
+ function formatPresetFor(preset) {
4015
+ const base = base_default;
4016
+ const name = formatPresetName(preset);
4017
+ if (name === "svelte") return { ...base, svelte: true };
4018
+ if (name === "nest") return { ...base, sortImports: nest_imports_default };
4019
+ return base;
4020
+ }
4021
+
4022
+ // src/roles/format/read-adoption.ts
4023
+ var NOT_ADOPTED = (configFile, unreadable = null) => ({
4024
+ configFile,
4025
+ preset: null,
4026
+ adopted: false,
4027
+ conformant: false,
4028
+ drift: [],
4029
+ local: {},
4030
+ presetVersion: null,
4031
+ effective: {},
4032
+ unreadable
4033
+ });
4034
+ function sameValue(a, b) {
4035
+ return JSON.stringify(a) === JSON.stringify(b);
4036
+ }
4037
+ function readFormatAdoption(cwd) {
4038
+ const path = join19(cwd, FORMAT_CONFIG_FILE);
4039
+ if (!existsSync16(path)) return NOT_ADOPTED(null);
4040
+ let parsed;
4041
+ try {
4042
+ parsed = parseJsonc(readFileSync15(path, "utf8"), FORMAT_CONFIG_FILE);
4043
+ } catch (error) {
4044
+ const reason = error instanceof Error ? error.message : String(error);
4045
+ return NOT_ADOPTED(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
4046
+ }
4047
+ const provenance = parsed[PROVENANCE_KEY];
4048
+ if (!provenance || typeof provenance.preset !== "string") return NOT_ADOPTED(FORMAT_CONFIG_FILE);
4049
+ if (!hasFormatPreset(provenance.preset)) {
4050
+ return NOT_ADOPTED(
4051
+ FORMAT_CONFIG_FILE,
4052
+ `${FORMAT_CONFIG_FILE} declares the format preset "${provenance.preset}", which sentinel does not ship (shipped: ${FORMAT_PRESETS.join(", ")}). Re-run \`sentinel --init --format\`.`
4053
+ );
4054
+ }
4055
+ const declaredLocal = Array.isArray(provenance.local) ? provenance.local : [];
4056
+ const preset = formatPresetFor(provenance.preset);
4057
+ const drift = [];
4058
+ const local = {};
4059
+ for (const [key, expected] of Object.entries(preset)) {
4060
+ if (declaredLocal.includes(key)) {
4061
+ local[key] = parsed[key];
4062
+ continue;
4063
+ }
4064
+ if (!(key in parsed) || !sameValue(parsed[key], expected)) drift.push(key);
4065
+ }
4066
+ for (const key of declaredLocal) {
4067
+ if (!(key in preset)) local[key] = parsed[key];
4068
+ }
4069
+ for (const key of ALWAYS_LOCAL_KEYS) {
4070
+ if (key in parsed) local[key] = parsed[key];
4071
+ }
4072
+ return {
4073
+ configFile: FORMAT_CONFIG_FILE,
4074
+ preset: provenance.preset,
4075
+ adopted: true,
4076
+ conformant: drift.length === 0,
4077
+ drift,
4078
+ local,
4079
+ presetVersion: typeof provenance.version === "string" ? provenance.version : null,
4080
+ effective: parsed,
4081
+ unreadable: null
4082
+ };
4083
+ }
4084
+
4085
+ // src/roles/format/resolve-oxfmt.ts
4086
+ import { readFileSync as readFileSync16 } from "node:fs";
4087
+ import { createRequire as createRequire4 } from "node:module";
4088
+ import { dirname as dirname8, join as join20 } from "node:path";
4089
+ function resolveOxfmt(cwd) {
4090
+ return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
4091
+ }
4092
+ function oxfmtRejects(key, value) {
4093
+ const schema = configSchema();
4094
+ const property = schema?.properties?.[key];
4095
+ if (!property) return false;
4096
+ const definition = resolveRef(schema, property);
4097
+ if (Array.isArray(definition.enum)) return !definition.enum.includes(value);
4098
+ if (definition.type === "boolean") return typeof value !== "boolean";
4099
+ if (definition.type === "integer" || definition.type === "number")
4100
+ return typeof value !== "number";
4101
+ return false;
4102
+ }
4103
+ function resolveRef(schema, node) {
4104
+ const ref = node.$ref ?? node.allOf?.[0]?.$ref;
4105
+ const name = ref?.startsWith("#/definitions/") ? ref.slice("#/definitions/".length) : void 0;
4106
+ return (name ? schema.definitions?.[name] : void 0) ?? node;
4107
+ }
4108
+ var cachedSchema;
4109
+ function configSchema() {
4110
+ if (cachedSchema !== void 0) return cachedSchema ?? void 0;
4111
+ cachedSchema = readConfigSchema() ?? null;
4112
+ return cachedSchema ?? void 0;
4113
+ }
4114
+ function readConfigSchema() {
4115
+ try {
4116
+ const manifest = createRequire4(import.meta.url).resolve("oxfmt/package.json");
4117
+ const schemaPath = join20(dirname8(manifest), "configuration_schema.json");
4118
+ return JSON.parse(readFileSync16(schemaPath, "utf8"));
4119
+ } catch {
4120
+ return void 0;
4121
+ }
4122
+ }
4123
+
4124
+ // src/roles/test/written-file-formatter.ts
4125
+ var PRETTIER_CONFIGS = [
4126
+ ".prettierrc",
4127
+ ".prettierrc.json",
4128
+ ".prettierrc.json5",
4129
+ ".prettierrc.yaml",
4130
+ ".prettierrc.yml",
4131
+ ".prettierrc.js",
4132
+ ".prettierrc.cjs",
4133
+ ".prettierrc.mjs",
4134
+ ".prettierrc.toml",
4135
+ "prettier.config.js",
4136
+ "prettier.config.cjs",
4137
+ "prettier.config.mjs"
4138
+ ];
4139
+ function prettierConfigFrom(cwd) {
4140
+ let dir = cwd;
4141
+ for (; ; ) {
4142
+ for (const name of PRETTIER_CONFIGS) {
4143
+ if (existsSync17(join21(dir, name))) return join21(dir, name);
4144
+ }
4145
+ const parent = dirname9(dir);
4146
+ if (parent === dir) return void 0;
4147
+ dir = parent;
4148
+ }
4149
+ }
4150
+ function writtenFileFormatter(cwd) {
4151
+ if (readFormatAdoption(cwd).configFile !== null) {
4152
+ const oxfmt = resolveOxfmt(cwd);
4153
+ if (oxfmt !== void 0) {
4154
+ return { bin: oxfmt, args: ["--write"], origin: "this module's own oxfmt config" };
4155
+ }
4156
+ }
4157
+ const config = prettierConfigFrom(cwd);
4158
+ const prettier = resolveBin(cwd, "prettier");
4159
+ if (config !== void 0 && prettier !== void 0) {
4160
+ return {
4161
+ bin: prettier,
4162
+ args: ["--write", "--log-level", "warn"],
4163
+ origin: `the workspace's Prettier`
4164
+ };
4165
+ }
4166
+ return void 0;
4167
+ }
4168
+
4169
+ // src/roles/test/jest-migration.ts
4170
+ var SPEC_TSCONFIG = "tsconfig.spec.json";
4171
+ var VITEST_GLOBAL_TYPES = "vitest/globals";
4172
+ function specTypesOperation(cwd) {
4173
+ const path = join22(cwd, SPEC_TSCONFIG);
4174
+ if (!existsSync18(path)) return void 0;
4175
+ let config;
4176
+ try {
4177
+ config = parseJsonc(
4178
+ readFileSync17(path, "utf8"),
4179
+ SPEC_TSCONFIG
4180
+ );
4181
+ } catch {
4182
+ return void 0;
4183
+ }
4184
+ const types = config?.compilerOptions?.types;
4185
+ if (!Array.isArray(types) || !types.includes("jest")) return void 0;
4186
+ return {
4187
+ kind: "merge-json",
4188
+ path: SPEC_TSCONFIG,
4189
+ value: {
4190
+ compilerOptions: {
4191
+ types: types.map((entry) => entry === "jest" ? VITEST_GLOBAL_TYPES : entry)
4192
+ }
4193
+ }
4194
+ };
4195
+ }
4196
+ function shippedTypesPath(cwd, declaration) {
4197
+ try {
4198
+ const manifest = createRequire5(join22(cwd, "noop.cjs")).resolve("@hublo/sentinel/package.json");
4199
+ const file = join22(dirname10(manifest), declaration);
4200
+ if (existsSync18(file)) {
4201
+ const path = relative5(cwd, file);
4202
+ return path.startsWith(".") ? path : `./${path}`;
4203
+ }
4204
+ } catch {
4205
+ }
4206
+ return `./node_modules/@hublo/sentinel/${declaration}`;
4207
+ }
4208
+ function tsconfigFileListOperations(cwd) {
4209
+ const operations = [];
4210
+ let names;
4211
+ try {
4212
+ names = readdirSync6(cwd).filter((name) => TSCONFIG_FILE.test(name));
4213
+ } catch {
4214
+ return operations;
4215
+ }
4216
+ for (const name of names) {
4217
+ let config;
4218
+ try {
4219
+ config = parseJsonc(
4220
+ readFileSync17(join22(cwd, name), "utf8"),
4221
+ name
4222
+ );
4223
+ } catch {
4224
+ continue;
4225
+ }
4226
+ const include = Array.isArray(config?.include) ? rewriteFileList(config.include) : void 0;
4227
+ const exclude = Array.isArray(config?.exclude) ? rewriteFileList(config.exclude) : void 0;
4228
+ const files = Array.isArray(config?.files) ? rewriteFileList(config.files) : void 0;
4229
+ const withTypes = name === SPEC_TSCONFIG ? withShippedTypes(cwd, files ?? config?.files) : void 0;
4230
+ if (include === void 0 && exclude === void 0 && files === void 0 && withTypes === void 0)
4231
+ continue;
4232
+ operations.push({
4233
+ kind: "merge-json",
4234
+ path: name,
4235
+ value: {
4236
+ ...include === void 0 ? {} : { include },
4237
+ ...exclude === void 0 ? {} : { exclude },
4238
+ ...withTypes === void 0 ? files === void 0 ? {} : { files } : { files: withTypes }
4239
+ }
4240
+ });
4241
+ }
4242
+ return operations;
4243
+ }
4244
+ function rewriteFileList(entries) {
4245
+ const rewritten = entries.filter((entry) => typeof entry !== "string" || !JEST_CONFIG_ENTRY.test(entry)).map((entry) => typeof entry === "string" ? vitestSupportName(entry) : entry);
4246
+ const unchanged = rewritten.length === entries.length && rewritten.every((entry, index) => entry === entries[index]);
4247
+ return unchanged ? void 0 : rewritten;
4248
+ }
4249
+ function withShippedTypes(cwd, files) {
4250
+ const entries = Array.isArray(files) ? files : [];
4251
+ const missing = SHIPPED_TYPES.filter(
4252
+ (declaration) => !entries.some((entry) => entry.endsWith(declaration))
4253
+ );
4254
+ if (missing.length === 0) return void 0;
4255
+ return [...entries, ...missing.map((declaration) => shippedTypesPath(cwd, declaration))];
4256
+ }
4257
+ var SHIPPED_TYPES = ["types/mock-extended.d.ts", "types/jest-global.d.ts"];
4258
+ var JEST_CONFIG_ENTRY = /(^|\/)jest\.[\w.-]*config\.[cm]?[jt]s$/;
4259
+ var TSCONFIG_FILE = /^tsconfig[\w.-]*\.json$/;
4260
+ function siblingScriptRewrites(cwd) {
4261
+ const rewrites = {};
4262
+ for (const [name, command] of Object.entries(moduleScripts(cwd))) {
4263
+ if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
4264
+ const rewritten = rewriteVitestScript(command);
4265
+ if (rewritten !== command) rewrites[name] = rewritten;
4266
+ }
4267
+ return rewrites;
4268
+ }
4269
+ function blocked(reason) {
4270
+ return { operations: [], blocked: reason };
4271
+ }
4272
+ function planJestMigration(context, adoption) {
4273
+ const modes = runModes(context.cwd, adoption.jestConfigs);
4274
+ if (modes.length === 0) {
4275
+ return blocked(`no jest config could be found for this module, so nothing was written.`);
4276
+ }
4277
+ const prepared = [];
4278
+ for (const mode of modes) {
4279
+ const reading = readJestConfig(context.cwd, mode.jestConfig);
4280
+ if (reading === void 0) {
4281
+ return blocked(`${mode.jestConfig} could not be read, so nothing was written.`);
4282
+ }
4283
+ const generated = generateNestTestConfig(context.cwd, reading, mode.target);
4284
+ if (generated.source === void 0) {
4285
+ return blocked(
4286
+ `${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.`
4287
+ );
4288
+ }
4289
+ prepared.push({ mode, reading, source: generated.source });
4290
+ }
4291
+ const migration = migrateModuleTests(
4292
+ context.cwd,
4293
+ prepared.flatMap(({ reading }) => [
4294
+ ...reading.setupFiles ?? [],
4295
+ ...reading.setupFilesAfterEnv ?? [],
4296
+ ...reading.globalSetup ?? []
4297
+ ])
4298
+ );
4299
+ if (migration.blockers.length > 0) {
4300
+ const listed = migration.blockers.map((blocker) => `
4301
+ ${blocker.file}: ${blocker.reason}`).join("");
4302
+ return blocked(
4303
+ `${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}`
4304
+ );
4305
+ }
4306
+ const fromProject = projectTargets(context.cwd) ?? {};
4307
+ const fromManifest = manifestTargets(context.cwd);
4308
+ const existingFor = (name) => fromProject[name] ?? fromManifest[name];
4309
+ const siblings = siblingScriptRewrites(context.cwd);
4310
+ const specTypes = specTypesOperation(context.cwd);
4311
+ const scripts = Object.fromEntries(
4312
+ prepared.map(({ mode }) => [
4313
+ mode.target,
4314
+ mode.primary ? SENTINEL_TEST_COMMAND : `${SENTINEL_TEST_COMMAND} --config ${mode.vitestConfig}`
4315
+ ])
4316
+ );
4317
+ const operations = [
4318
+ ...prepared.map(({ mode, source }) => ({
4319
+ kind: "write",
4320
+ path: mode.vitestConfig,
4321
+ contents: source
4322
+ })),
4323
+ ...migration.operations,
4324
+ ...specTypes === void 0 ? [] : [specTypes],
4325
+ ...tsconfigFileListOperations(context.cwd),
4326
+ manifestOperation(context.cwd, { ...scripts, ...siblings }),
4327
+ ...prepared.flatMap(
4328
+ ({ mode }) => nxTargetOperations({
4329
+ cwd: context.cwd,
4330
+ targets: testTarget({
4331
+ name: mode.target,
4332
+ configFiles: [mode.vitestConfig],
4333
+ outputs: existingFor(mode.target)?.outputs ?? [],
4334
+ dependsOn: existingFor(mode.target)?.dependsOn ?? [],
4335
+ configurations: existingFor(mode.target)?.configurations ?? {}
4336
+ })
4337
+ })
4338
+ ),
4339
+ ...[.../* @__PURE__ */ new Set([...adoption.jestConfigs, ...prepared.map(({ mode }) => mode.jestConfig)])].map(
4340
+ (name) => ({ kind: "delete", path: name })
4341
+ )
4342
+ ];
4343
+ const escaping = (prepared[0]?.reading.aliases ?? []).map((alias) => alias.replacement).filter((replacement) => replacement.includes(".."));
4344
+ const notes = [
4345
+ `${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` : ""}.`,
4346
+ `${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.`)
4347
+ ];
4348
+ for (const warning of migration.warnings) notes.push(`\u26A0\uFE0F ${warning}`);
4349
+ notes.push(
4350
+ `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.`
4351
+ );
4352
+ if (specTypes !== void 0) {
4353
+ notes.push(
4354
+ `${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.`
4355
+ );
4356
+ }
4357
+ const formatter = writtenFileFormatter(context.cwd);
4358
+ if (formatter !== void 0) {
4359
+ notes.push(
4360
+ `the files this rewrote were run through ${formatter.origin} afterwards. Two things needed it: the Vitest type imports land beside the existing ones, and a rewritten call is often SHORTER than the one it replaced, so the line breaks around it are no longer the ones your config would pick. Neither is a defect, and both would have failed your format check. What no formatter does is ORDER imports, so if your lint enforces \`import/order\`, run its fixer too.`
4361
+ );
4362
+ } else {
4363
+ notes.push(
4364
+ `no style is declared here, by this module or its workspace, so what was written keeps the shape it was written in, and the Vitest type imports sit beside the existing ones rather than where your \`import/order\` wants them. That rule is classified by a prefix list this package cannot read, so it is left to the tool that owns it: run your module's own lint fixer (\`nx run <module>:lint --fix\` here) and its formatter before committing.`
4365
+ );
4366
+ }
4367
+ notes.push(
4368
+ `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.`
4369
+ );
4370
+ if (Object.keys(siblings).length > 0) {
4371
+ notes.push(`rewrote ${Object.keys(siblings).join(", ")} to run through sentinel.`);
4372
+ }
4373
+ return { operations, notes };
4374
+ }
4375
+
4376
+ // src/roles/test/plan.ts
4377
+ function ownedTestDependencies(cwd, declared) {
4378
+ return declared.filter((entry) => SENTINEL_OWNED_TEST_PACKAGES.includes(entry.name)).flatMap((entry) => [
4379
+ ["dependencies", entry.name],
4380
+ ["devDependencies", entry.name]
4381
+ ]).filter(([section]) => section !== void 0);
4382
+ }
4383
+ var DEFERRED = {
4384
+ svelte: `the svelte modules here carry no svelte test at all, so adopting them would prove nothing about svelte, and their owners have a framework migration under way that moves these versions anyway`
4385
+ };
4386
+ function plan(context) {
4387
+ const deferred = DEFERRED[context.preset];
4388
+ if (deferred !== void 0) {
4389
+ return {
4390
+ operations: [],
4391
+ skipped: `the test role defers ${context.preset} for now. It defers because ${deferred}.`
4392
+ };
4393
+ }
4394
+ const configFile = testConfigFile(context.cwd);
4395
+ const adoption = readTestAdoption(context.cwd);
4396
+ if (adoption.state === "no-tests") {
4397
+ const operations2 = adoption.jestConfigs.map((name) => ({
4398
+ kind: "delete",
4399
+ path: name
4400
+ }));
4401
+ const owned2 = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
4402
+ if (owned2.length > 0) {
4403
+ operations2.push({ kind: "remove-json-keys", path: "package.json", keys: owned2 });
4404
+ }
4405
+ const hasTarget = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME] !== void 0;
4406
+ if (hasTarget) {
4407
+ operations2.push({
4408
+ kind: "remove-json-keys",
4409
+ path: "project.json",
4410
+ keys: [["targets", TEST_SCRIPT_NAME]]
4411
+ });
4412
+ }
4413
+ if (operations2.length === 0) {
4414
+ return {
4415
+ operations: [],
4416
+ skipped: `this module has no test and no jest leftovers, so there is nothing to do here.`
4417
+ };
4418
+ }
4419
+ return {
4420
+ operations: operations2,
4421
+ notes: [
4422
+ // Names what is actually removed, never a shape this module does not have: half of these
4423
+ // modules carry a config with no target behind it, and claiming otherwise would describe
4424
+ // an operation the plan never performs.
4425
+ `this module has no test file, so this is a cleanup rather than an adoption: ${[...adoption.jestConfigs, ...hasTarget ? ["the jest target it feeds"] : []].join(" and ")} removed, so nothing here breaks the day jest leaves the workspace. No test config is written in exchange, because there is nothing to configure.`
4426
+ ]
4427
+ };
4428
+ }
4429
+ if (configFile === void 0) {
4430
+ if (adoption.state === "jest") return planJestMigration(context, adoption);
4431
+ return {
4432
+ operations: [],
4433
+ skipped: `this module has no test config and no test files, so there is nothing to point at sentinel. What it needs is its jest leftovers removed, which is a cleanup rather than an adoption.`
4434
+ };
4435
+ }
4436
+ const operations = [];
4437
+ const notes = [];
4438
+ const rewrite = rewriteToolchainImports(
4439
+ testToolchain(context.preset),
4440
+ readTestConfigSource(context.cwd, configFile),
4441
+ configFile
4442
+ );
4443
+ if (rewrite.kind === "blocked") {
4444
+ return {
4445
+ operations: [],
4446
+ skipped: `${configFile} could not be pointed at sentinel, so nothing was written: ${rewrite.why}. Nothing here is broken; this needs a look before the role applies.`
4447
+ };
4448
+ }
4449
+ if (rewrite.kind === "rewritten") {
4450
+ operations.push({ kind: "write", path: configFile, contents: rewrite.source });
4451
+ notes.push(
4452
+ `${configFile} now imports ${rewrite.moved.join(", ")} from sentinel. Only the import lines changed: every alias, setup file, timeout and coverage path is untouched, so this suite runs what it ran before.`
4453
+ );
4454
+ }
4455
+ const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME] ?? manifestTargets(context.cwd)[TEST_SCRIPT_NAME];
4456
+ const outputs = existing?.outputs ?? [];
4457
+ const dependsOn = existing?.dependsOn ?? [];
4458
+ const configurations = existing?.configurations ?? {};
4459
+ if (dependsOn.length > 0) {
4460
+ notes.push(`carried \`dependsOn\` from the replaced target: ${dependsOn.join(", ")}.`);
4461
+ }
4462
+ if (Object.keys(configurations).length > 0) {
4463
+ notes.push(
4464
+ `carried the \`${Object.keys(configurations).join("`, `")}\` configuration(s): CI invokes \`--configuration=ci\`, and a target that no longer offers it fails the caller, not the module.`
4465
+ );
4466
+ }
4467
+ if (outputs.length === 0) {
4468
+ notes.push(
4469
+ `this target declares no \`outputs\`, so it is left UNCACHED. Caching a run that restores nothing is the defect where nx reports a hit and the coverage directory is empty; if this module writes coverage, declare where and the target can cache.`
4470
+ );
4471
+ }
4472
+ const siblingScripts = {};
4473
+ for (const [name, command] of Object.entries(moduleScripts(context.cwd))) {
4474
+ if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
4475
+ const rewritten = rewriteVitestScript(command);
4476
+ if (rewritten !== command) siblingScripts[name] = rewritten;
4477
+ }
4478
+ if (Object.keys(siblingScripts).length > 0) {
4479
+ notes.push(
4480
+ `rewrote ${Object.keys(siblingScripts).join(", ")} to run through sentinel: they called vitest directly, and the dependency they relied on is removed below. A watch script keeps watching, through \`-- --watch\`.`
4481
+ );
4482
+ }
4483
+ operations.push(
4484
+ manifestOperation(context.cwd, {
4485
+ [TEST_SCRIPT_NAME]: SENTINEL_TEST_COMMAND,
4486
+ ...siblingScripts
4487
+ })
4488
+ );
4489
+ operations.push(
4490
+ ...nxTargetOperations({
4491
+ cwd: context.cwd,
4492
+ targets: testTarget({ configFiles: [configFile], outputs, dependsOn, configurations })
4493
+ })
4494
+ );
4495
+ const owned = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
4496
+ if (owned.length > 0) {
4497
+ operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
4498
+ notes.push(
4499
+ `removed ${[...new Set(owned.map(([, name]) => name))].join(", ")} from this module: sentinel owns the runner now, and two copies of Vitest in one run give the plugins a different Vite than the one running them.`
4500
+ );
4501
+ }
4502
+ if (adoption.jestConfigs.length > 0) {
4503
+ notes.push(
4504
+ `${adoption.jestConfigs.join(", ")} is still here beside the Vitest config. It is not this step's to delete, since something may still run it, but the workspace cannot drop jest while it exists.`
4505
+ );
4506
+ }
4507
+ return { operations, notes };
4508
+ }
4509
+
4510
+ // src/roles/test/adapters/vitest/vitest.adapter.ts
4511
+ var FORMATTABLE = /\.(m|c)?(j|t)sx?$|\.json$/;
4512
+ var FORMAT_BATCH = 25;
4513
+ var VitestAdapter = class extends BaseAdapter {
4514
+ target = "test";
4515
+ runner = "vitest";
4516
+ appliesTo(_preset) {
4517
+ return true;
4518
+ }
4519
+ /**
4520
+ * Say, once, that this migration is about to become unprovable if nobody recorded a reference.
4521
+ *
4522
+ * ⚠️ It used to RECORD one here, which welded a measurement to an act that does not own it.
4523
+ * Héla, 2026-09-25: the verification phase is isolated behind its own verb, so it extends
4524
+ * without destroying. Two things were wrong with recording here, and they are one thing seen
4525
+ * twice: a migration that could not be proved was still reported as done, because the
4526
+ * measurement was a side effect; and the reference was taken the way the migration found
4527
+ * convenient rather than the way the module runs its tests, which is how it came to record 2958
4528
+ * passing on a module whose own target sees 2957.
4529
+ *
4530
+ * So this warns and writes nothing. The plan about to be applied deletes the jest config, and
4531
+ * with it any way of asking again, which is precisely why the warning is here and not later.
4532
+ */
4533
+ beforeInit(ctx) {
4534
+ const adoption = readTestAdoption(ctx.cwd);
4535
+ if (adoption.state !== "jest") return Promise.resolve();
4536
+ if (adoption.jestConfigs.every((config) => hasReference(ctx.cwd, suiteOf(config)))) {
4537
+ return Promise.resolve();
4538
+ }
4539
+ this.say(
4540
+ `no reference is recorded for this module, so this migration will not be provable: the jest config this deletes is the only place the old suite could be read from. Record one first with \`sentinel --validate --test\`, then migrate. Proceeding without it.`
4541
+ );
4542
+ return Promise.resolve();
4543
+ }
4544
+ /**
4545
+ * Hand what was just written to the module's OWN formatter.
4546
+ *
4547
+ * ⚠️ Not a style sentinel imposes: the module adopted `.oxfmtrc.json`, and this runs that
4548
+ * config over the files this role created. Without it, `--init --test` leaves a module whose
4549
+ * own `--run --format` fails on a file the module never wrote.
4550
+ *
4551
+ * Measured on `apps/nest/microservices/agency`, in this repository's CI:
4552
+ *
4553
+ * agency:format 12 files, including vitest.config.mts and
4554
+ * vitest.integration.config.mts, both generated here
4555
+ *
4556
+ * The generator cannot avoid it by wrapping at a fixed width, because the width is the
4557
+ * MODULE's decision: `printWidth` lives in its own config, and 12 modules already set their
4558
+ * own. Guessing it is how a tool starts owning a setting it does not own.
4559
+ *
4560
+ * The same delegation the build role asks a human to do by hand, done.
4561
+ *
4562
+ * ## ⚠️ Two boundaries, both learned by crossing them
4563
+ *
4564
+ * ### Only a module that has a formatter CONFIG
4565
+ *
4566
+ * The question is not "can a formatter be found". It always can: sentinel depends on oxfmt and
4567
+ * ships the binary, so `resolveOxfmt` falls back to its own install and never returns undefined
4568
+ * once sentinel is installed. Asking it that way meant a module with no oxfmt config was
4569
+ * formatted with oxfmt's DEFAULTS, which is a style nobody here chose.
4570
+ *
4571
+ * So the CONFIG is read. A module that wrote its own `.oxfmtrc.json` without adopting the role
4572
+ * has still stated a style, and running it is delegation; running oxfmt with no config is us
4573
+ * choosing one.
4574
+ *
4575
+ * Measured on `apps/nest/microservices/institution`, which has not adopted format and whose lint
4576
+ * forbids semicolons: `institution:lint` went from green to failing, thousands of `semi`, on a
4577
+ * module whose 2958 tests pass. So the adoption is read, not the binary.
4578
+ *
4579
+ * ### Only the files this just WROTE
4580
+ *
4581
+ * `--write .` is a whole-directory pass, and that is not a tidy-up. Same measurement: 1813 files
4582
+ * changed, 47333 insertions, 55506 deletions, in a module whose migration touched 268. A
4583
+ * migration diff is read by a person.
4584
+ *
4585
+ * A module that has not adopted format keeps what was written exactly as written, and is told so.
4586
+ * The codemod rewrites tokens in place, so those files keep the shape they already had.
4587
+ */
4588
+ async afterInit(ctx) {
4589
+ const written = (ctx.written ?? []).filter((file) => FORMATTABLE.test(file));
4590
+ if (written.length === 0) return Promise.resolve();
4591
+ const formatter = writtenFileFormatter(ctx.cwd);
4592
+ if (formatter === void 0) {
4593
+ this.say(
4594
+ `no style is declared here, by this module or its workspace, so the ${written.length} file(s) this wrote keep the shape they were written in. Run your own formatter over them before committing.`
4595
+ );
4596
+ return Promise.resolve();
4597
+ }
4598
+ let failed = 0;
4599
+ for (let at = 0; at < written.length; at += FORMAT_BATCH) {
4600
+ const batch = written.slice(at, at + FORMAT_BATCH);
4601
+ const pass = spawnSync(formatter.bin, [...formatter.args, ...batch], {
4602
+ cwd: ctx.cwd,
4603
+ encoding: "utf8"
4604
+ });
4605
+ if (pass.status !== 0) failed += batch.length;
4606
+ }
4607
+ this.say(
4608
+ failed === 0 ? `formatted the ${written.length} file(s) this wrote with ${formatter.origin}, so a format check passes on files it did not write itself.` : (
4609
+ // Adoption SUCCEEDED; only the tidy-up did not. Failing here would revert a migration
4610
+ // over a formatting pass, which is the wrong trade.
4611
+ `${formatter.origin} could not run over ${failed} of the ${written.length} file(s) this wrote. Adoption itself is applied; run your formatter before committing.`
4612
+ )
4613
+ );
4614
+ return Promise.resolve();
4615
+ }
4616
+ plan(context) {
4617
+ return plan(context);
4618
+ }
4619
+ async status(ctx) {
4620
+ return Promise.resolve(readTestAdoption(ctx.cwd));
4621
+ }
4622
+ async inspect(ctx) {
4623
+ const adoption = readTestAdoption(ctx.cwd);
4624
+ return Promise.resolve({
4625
+ state: adoption.state,
4626
+ runner: adoption.runner,
4627
+ configFile: adoption.configFile,
4628
+ jestConfigs: adoption.jestConfigs,
4629
+ adopted: adoption.adopted,
4630
+ conformant: adoption.conformant,
4631
+ drift: adoption.drift,
4632
+ unreadable: adoption.unreadable
4633
+ });
4634
+ }
4635
+ /**
4636
+ * Run the module's tests.
4637
+ *
4638
+ * A module that has not adopted is reported and PASSES, the stance every role takes: `--run`
4639
+ * with no named target sweeps the workspace, so failing here would exit non-zero on the 107
4640
+ * modules still on jest, which says nothing anyone can act on.
4641
+ */
4642
+ async run(ctx) {
4643
+ const adoption = readTestAdoption(ctx.cwd);
4644
+ if (!adoption.adopted) {
4645
+ this.say(
4646
+ adoption.state === "jest" ? `this module tests with jest (${adoption.jestConfigs.join(", ")}); run \`sentinel --init --test\` to migrate it.` : adoption.state === "no-tests" ? `this module has no tests.` : `no sentinel preset in this module's Vitest config; run \`sentinel --init --test\` to adopt.`
4647
+ );
4648
+ return Promise.resolve({ ok: true, code: 0 });
4649
+ }
4650
+ const { bin, origin } = resolveVitest(ctx.cwd);
4651
+ if (bin === void 0) {
4652
+ this.say(`could not find the vitest binary. Run \`pnpm install\` in the module.`);
4653
+ return Promise.resolve({ ok: false, code: 1 });
4654
+ }
4655
+ if (origin === "module") {
4656
+ this.say(
4657
+ `using the MODULE's vitest, not sentinel's. An adopted config gets its plugins from sentinel, and two copies fail in ways that never mention a version. Remove vitest from this module's package.json.`
4658
+ );
4659
+ }
4660
+ const { options: typedOptions, paths: typedPaths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
4661
+ valueFlags: ["--config", "-c"]
4662
+ });
4663
+ const { cwd: runFrom, options, paths } = fromWorkspaceRoot(ctx.cwd, typedOptions, typedPaths);
4664
+ const watching = options.includes("--watch") || options.includes("-w");
4665
+ const argv = watching ? [...options, ...paths] : ["run", ...options, ...paths];
4666
+ const result = spawnSync(bin, argv, { cwd: runFrom, stdio: "inherit" });
4667
+ if (result.error) {
4668
+ this.say(`could not run vitest (${result.error.message})`);
4669
+ return Promise.resolve({ ok: false, code: 1 });
4670
+ }
4671
+ const code = result.status ?? 1;
4672
+ return Promise.resolve({ ok: code === 0, code });
4673
+ }
4674
+ say(message) {
4675
+ process.stderr.write(`sentinel test(vitest): ${message}
4676
+ `);
4677
+ }
4678
+ };
4679
+ function fromWorkspaceRoot(moduleDir, options, paths) {
4680
+ const root = findWorkspaceRoot(moduleDir);
4681
+ if (root === void 0 || root === moduleDir) {
4682
+ return { cwd: moduleDir, options: [...options], paths: [...paths] };
4683
+ }
4684
+ const absolute = (value) => isAbsolute2(value) ? value : resolve7(moduleDir, value);
4685
+ const moved = [];
4686
+ let previousWasConfig = false;
4687
+ for (const option of options) {
4688
+ if (previousWasConfig) {
4689
+ moved.push(absolute(option));
4690
+ previousWasConfig = false;
4691
+ continue;
4692
+ }
4693
+ if (option.startsWith("--config=")) moved.push(`--config=${absolute(option.slice(9))}`);
4694
+ else moved.push(option);
4695
+ previousWasConfig = option === "--config" || option === "-c";
4696
+ }
4697
+ return {
4698
+ cwd: root,
4699
+ // `--root` LAST, so a caller cannot be silently overridden by our own flag.
4700
+ options: [...moved, "--root", moduleDir],
4701
+ paths: paths.map((path) => absolute(path))
4702
+ };
4703
+ }
4704
+
4705
+ export {
4706
+ moduleScripts,
4707
+ selfCommand,
4708
+ adoptedPackageName,
4709
+ manifestOperation,
4710
+ projectTargets,
4711
+ BaseAdapter,
4712
+ splitToolArgs,
4713
+ resolveBin,
4714
+ binFromOwnInstall,
4715
+ binSearchPath,
4716
+ parseJsonc,
4717
+ nxTargetOperations,
4718
+ FORMAT_CONFIG_FILE,
4719
+ FORMAT_SCRIPT_NAME,
4720
+ PROVENANCE_KEY,
4721
+ PERMITTED_LOCAL_KEYS,
4722
+ ALWAYS_LOCAL_KEYS,
4723
+ FORMAT_DEFAULT_IGNORES,
4724
+ PRETTIER_CONFIG_FILES,
4725
+ formatTargets,
4726
+ formatPresetName,
4727
+ formatPresetFor,
4728
+ readFormatAdoption,
4729
+ rewriteToolchainImports,
4730
+ resolveOxfmt,
4731
+ oxfmtRejects,
4732
+ walk,
4733
+ readTestAdoption,
4734
+ resolveContext,
4735
+ moduleEnvironment,
4736
+ suiteOf,
4737
+ captureReference,
4738
+ hasReference,
4739
+ compareAgainstReference,
4740
+ resolveVitest,
4741
+ VitestAdapter,
4742
+ fromWorkspaceRoot
4743
+ };