@hublo/sentinel 1.3.0 → 1.4.0-alpha.2
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.
- package/README.md +8 -4
- package/dist/bin/sentinel.d.ts +0 -1
- package/dist/bin/sentinel.js +22 -8
- package/dist/chunk-2XLX6PFR.js +132 -0
- package/dist/chunk-3TDUIKVQ.js +178 -0
- package/dist/{chunk-676GBPMS.js → chunk-4UIZJ3TR.js} +3399 -549
- package/dist/chunk-CPCUPK4J.js +70 -0
- package/dist/chunk-NX4GHIHF.js +25 -0
- package/dist/chunk-PWV3BMDA.js +15 -0
- package/dist/chunk-WLFE5RUU.js +264 -0
- package/dist/index.js +2 -1
- package/dist/roles/build/nest/toolchain.d.ts +4 -36
- package/dist/roles/build/nest/toolchain.js +10 -178
- package/dist/roles/build/nest/toolchain.js.map +1 -0
- package/dist/roles/build/toolchain.js.map +1 -0
- package/dist/roles/test/nest/toolchain.d.ts +29 -0
- package/dist/roles/test/nest/toolchain.js +289 -0
- package/dist/roles/test/nest/toolchain.js.map +1 -0
- package/dist/roles/test/react/toolchain.d.ts +62 -0
- package/dist/roles/test/react/toolchain.js +5 -0
- package/dist/roles/test/react/toolchain.js.map +1 -0
- package/dist/roles/test/setup/mock-extended.d.ts +46 -0
- package/dist/roles/test/setup/mock-extended.js +65 -0
- package/dist/roles/test/setup/mock-extended.js.map +1 -0
- package/dist/roles/test/setup/msw-lifecycle.d.ts +16 -0
- package/dist/roles/test/setup/msw-lifecycle.js +12 -0
- package/dist/roles/test/setup/msw-lifecycle.js.map +1 -0
- package/dist/roles/test/setup/msw-server.d.ts +3 -0
- package/dist/roles/test/setup/msw-server.js +10 -0
- package/dist/roles/test/setup/msw-server.js.map +1 -0
- package/dist/roles/test/setup/nest.d.ts +2 -0
- package/dist/roles/test/setup/nest.js +159 -0
- package/dist/roles/test/setup/nest.js.map +1 -0
- package/dist/roles/test/setup/workspace-entry.d.ts +2 -0
- package/dist/roles/test/setup/workspace-entry.js +8 -0
- package/dist/roles/test/setup/workspace-entry.js.map +1 -0
- package/dist/tsconfig-aliases-Ce6axdJ4.d.ts +36 -0
- package/docs/.gitkeep +0 -0
- package/docs/build-adoption.md +521 -0
- package/docs/format-adoption.md +321 -0
- package/docs/lint-adoption.md +290 -0
- package/docs/performance.md +49 -0
- package/docs/test-adoption.md +175 -0
- package/docs/typescript-adoption.md +184 -0
- package/docs/typescript-traces.md +798 -0
- package/docs/using-sentinel.md +180 -0
- package/docs/validating-a-change.md +101 -0
- package/package.json +32 -5
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_MAX_DIAGNOSTICS,
|
|
3
|
+
findWorkspaceRoot,
|
|
4
|
+
moduleName,
|
|
5
|
+
readNxProjectName,
|
|
6
|
+
readOwnPackage,
|
|
7
|
+
readProjectPackageJson
|
|
8
|
+
} from "./chunk-WLFE5RUU.js";
|
|
9
|
+
|
|
1
10
|
// src/core/registry.ts
|
|
2
11
|
var adapters = [];
|
|
3
12
|
var PresetUnsupportedError = class extends Error {
|
|
@@ -67,58 +76,6 @@ function resolve(target, preset, runner) {
|
|
|
67
76
|
return matching[0];
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
// src/shared/package-json.ts
|
|
71
|
-
import { existsSync, readFileSync } from "fs";
|
|
72
|
-
import { basename, dirname, join } from "path";
|
|
73
|
-
import { fileURLToPath } from "url";
|
|
74
|
-
function readOwnPackage() {
|
|
75
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
76
|
-
for (; ; ) {
|
|
77
|
-
const pkgPath = join(dir, "package.json");
|
|
78
|
-
if (existsSync(pkgPath)) {
|
|
79
|
-
try {
|
|
80
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
81
|
-
if (typeof pkg.version === "string") {
|
|
82
|
-
const bin = typeof pkg.bin === "string" ? pkg.bin : Object.values(pkg.bin ?? {})[0];
|
|
83
|
-
return { name: pkg.name ?? "@hublo/sentinel", version: pkg.version, bin };
|
|
84
|
-
}
|
|
85
|
-
} catch {
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const parent = dirname(dir);
|
|
89
|
-
if (parent === dir) return { name: "@hublo/sentinel", version: "0.0.0" };
|
|
90
|
-
dir = parent;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
function readOwnVersion() {
|
|
94
|
-
return readOwnPackage().version;
|
|
95
|
-
}
|
|
96
|
-
function readProjectPackageJson(dir) {
|
|
97
|
-
const path = join(dir, "package.json");
|
|
98
|
-
if (!existsSync(path)) return {};
|
|
99
|
-
try {
|
|
100
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
101
|
-
} catch {
|
|
102
|
-
process.stderr.write(`sentinel: could not parse ${path}; ignoring for detection.
|
|
103
|
-
`);
|
|
104
|
-
return {};
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function readNxProjectName(dir) {
|
|
108
|
-
const path = join(dir, "project.json");
|
|
109
|
-
if (!existsSync(path)) return void 0;
|
|
110
|
-
try {
|
|
111
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
112
|
-
return typeof parsed.name === "string" ? parsed.name : void 0;
|
|
113
|
-
} catch {
|
|
114
|
-
return void 0;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function moduleName(dir) {
|
|
118
|
-
const declared = readProjectPackageJson(dir).name;
|
|
119
|
-
return readNxProjectName(dir) ?? (declared === void 0 ? basename(dir) : declared);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
79
|
// src/core/base-adapter.ts
|
|
123
80
|
var BaseAdapter = class {
|
|
124
81
|
inspect(_ctx) {
|
|
@@ -166,15 +123,15 @@ function palette(stream) {
|
|
|
166
123
|
}
|
|
167
124
|
|
|
168
125
|
// src/roles/build/nest/nx-target.ts
|
|
169
|
-
import { existsSync
|
|
170
|
-
import { join
|
|
126
|
+
import { existsSync, readFileSync } from "fs";
|
|
127
|
+
import { join, relative } from "path";
|
|
171
128
|
var DEPRECATED_NX_BUILDER = "@nx/webpack:webpack";
|
|
172
129
|
var TRANSLATABLE_WEBPACK_CONFIG = "tools/webpack-configs/update.webpack.config.js";
|
|
173
130
|
var readProjectJson = (cwd) => {
|
|
174
|
-
const path =
|
|
175
|
-
if (!
|
|
131
|
+
const path = join(cwd, "project.json");
|
|
132
|
+
if (!existsSync(path)) return void 0;
|
|
176
133
|
try {
|
|
177
|
-
return JSON.parse(
|
|
134
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
178
135
|
} catch {
|
|
179
136
|
return void 0;
|
|
180
137
|
}
|
|
@@ -217,7 +174,7 @@ function liveFileReplacements(cwd, target) {
|
|
|
217
174
|
const live = [];
|
|
218
175
|
for (const configuration of Object.values(configurations)) {
|
|
219
176
|
for (const replacement of configuration.fileReplacements ?? []) {
|
|
220
|
-
const exists =
|
|
177
|
+
const exists = existsSync(join(workspaceRoot, replacement.replace)) || existsSync(join(workspaceRoot, replacement.with));
|
|
221
178
|
if (exists) live.push(replacement.replace);
|
|
222
179
|
}
|
|
223
180
|
}
|
|
@@ -238,8 +195,8 @@ function workspaceRootHops(cwd, target) {
|
|
|
238
195
|
}
|
|
239
196
|
|
|
240
197
|
// src/roles/build/read-adoption.ts
|
|
241
|
-
import { existsSync as
|
|
242
|
-
import { join as
|
|
198
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
199
|
+
import { join as join4 } from "path";
|
|
243
200
|
|
|
244
201
|
// src/core/config/module-type.ts
|
|
245
202
|
function esmReadiness(fileName, packageType) {
|
|
@@ -253,11 +210,11 @@ function esmReadiness(fileName, packageType) {
|
|
|
253
210
|
}
|
|
254
211
|
|
|
255
212
|
// src/core/config/manifest.ts
|
|
256
|
-
import { existsSync as
|
|
257
|
-
import { basename
|
|
213
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
214
|
+
import { basename, join as join2 } from "path";
|
|
258
215
|
function moduleScripts(cwd) {
|
|
259
216
|
try {
|
|
260
|
-
const pkg = JSON.parse(
|
|
217
|
+
const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
|
|
261
218
|
return pkg.scripts ?? {};
|
|
262
219
|
} catch {
|
|
263
220
|
return {};
|
|
@@ -265,7 +222,7 @@ function moduleScripts(cwd) {
|
|
|
265
222
|
}
|
|
266
223
|
function moduleName2(cwd) {
|
|
267
224
|
try {
|
|
268
|
-
const pkg = JSON.parse(
|
|
225
|
+
const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
|
|
269
226
|
return pkg.name;
|
|
270
227
|
} catch {
|
|
271
228
|
return void 0;
|
|
@@ -280,7 +237,7 @@ function selfCommand(cwd, flags) {
|
|
|
280
237
|
return `node ${own.bin.replace(/^\.\//, "")} ${flags}`;
|
|
281
238
|
}
|
|
282
239
|
function adoptedPackageName(cwd) {
|
|
283
|
-
return readProjectPackageJson(cwd).name ?? readNxProjectName(cwd) ??
|
|
240
|
+
return readProjectPackageJson(cwd).name ?? readNxProjectName(cwd) ?? basename(cwd);
|
|
284
241
|
}
|
|
285
242
|
function manifestOperation(cwd, scripts) {
|
|
286
243
|
const own = readOwnPackage();
|
|
@@ -288,7 +245,7 @@ function manifestOperation(cwd, scripts) {
|
|
|
288
245
|
scripts,
|
|
289
246
|
devDependencies: { [own.name]: isSelfAdoption(cwd) ? "link:." : own.version }
|
|
290
247
|
};
|
|
291
|
-
if (
|
|
248
|
+
if (existsSync2(join2(cwd, "package.json"))) {
|
|
292
249
|
return { kind: "merge-json", path: "package.json", value };
|
|
293
250
|
}
|
|
294
251
|
return {
|
|
@@ -298,6 +255,19 @@ function manifestOperation(cwd, scripts) {
|
|
|
298
255
|
};
|
|
299
256
|
}
|
|
300
257
|
|
|
258
|
+
// src/core/config/project-json.ts
|
|
259
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
260
|
+
import { join as join3 } from "path";
|
|
261
|
+
function projectTargets(cwd) {
|
|
262
|
+
const path = join3(cwd, "project.json");
|
|
263
|
+
if (!existsSync3(path)) return void 0;
|
|
264
|
+
try {
|
|
265
|
+
return JSON.parse(readFileSync3(path, "utf8")).targets;
|
|
266
|
+
} catch {
|
|
267
|
+
return void 0;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
301
271
|
// src/roles/build/presets/requirements.json
|
|
302
272
|
var requirements_default = {
|
|
303
273
|
svelte: {
|
|
@@ -413,19 +383,6 @@ var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
|
|
|
413
383
|
(entry) => entry.package
|
|
414
384
|
);
|
|
415
385
|
|
|
416
|
-
// src/roles/build/project-json.ts
|
|
417
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
418
|
-
import { join as join4 } from "path";
|
|
419
|
-
function projectTargets(cwd) {
|
|
420
|
-
const path = join4(cwd, "project.json");
|
|
421
|
-
if (!existsSync4(path)) return void 0;
|
|
422
|
-
try {
|
|
423
|
-
return JSON.parse(readFileSync4(path, "utf8")).targets;
|
|
424
|
-
} catch {
|
|
425
|
-
return void 0;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
386
|
// src/roles/build/adoption-drift.ts
|
|
430
387
|
function buildAdoptionDrift(cwd) {
|
|
431
388
|
const drift = [];
|
|
@@ -488,7 +445,7 @@ var NOT_ADOPTED = (configFile, unreadable = null) => ({
|
|
|
488
445
|
unreadable
|
|
489
446
|
});
|
|
490
447
|
function buildConfigFile(cwd) {
|
|
491
|
-
return BUILD_CONFIG_FILES.find((name) =>
|
|
448
|
+
return BUILD_CONFIG_FILES.find((name) => existsSync4(join4(cwd, name)));
|
|
492
449
|
}
|
|
493
450
|
function importsSpecifier(source, specifier) {
|
|
494
451
|
const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -514,7 +471,7 @@ function readBuildConfigSource(cwd) {
|
|
|
514
471
|
const configFile = buildConfigFile(cwd);
|
|
515
472
|
if (!configFile) return void 0;
|
|
516
473
|
try {
|
|
517
|
-
return
|
|
474
|
+
return readFileSync4(join4(cwd, configFile), "utf8");
|
|
518
475
|
} catch {
|
|
519
476
|
return void 0;
|
|
520
477
|
}
|
|
@@ -524,7 +481,7 @@ function readBuildAdoption(cwd) {
|
|
|
524
481
|
if (!configFile) return NOT_ADOPTED(null);
|
|
525
482
|
let source;
|
|
526
483
|
try {
|
|
527
|
-
source =
|
|
484
|
+
source = readFileSync4(join4(cwd, configFile), "utf8");
|
|
528
485
|
} catch (error) {
|
|
529
486
|
return NOT_ADOPTED(configFile, error instanceof Error ? error.message : String(error));
|
|
530
487
|
}
|
|
@@ -560,7 +517,7 @@ function readBuildAdoption(cwd) {
|
|
|
560
517
|
import { spawnSync } from "child_process";
|
|
561
518
|
|
|
562
519
|
// src/core/config/tool-args.ts
|
|
563
|
-
import { existsSync as
|
|
520
|
+
import { existsSync as existsSync5 } from "fs";
|
|
564
521
|
import { isAbsolute, resolve as resolve2 } from "path";
|
|
565
522
|
function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
|
|
566
523
|
const takesValue = new Set(valueFlags);
|
|
@@ -570,7 +527,7 @@ function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
|
|
|
570
527
|
for (const arg of toolArgs) {
|
|
571
528
|
const looksLikeOption = arg.startsWith("-");
|
|
572
529
|
const target = isAbsolute(arg) ? arg : resolve2(cwd, arg);
|
|
573
|
-
if (!previousTakesValue && !looksLikeOption &&
|
|
530
|
+
if (!previousTakesValue && !looksLikeOption && existsSync5(target)) paths.push(arg);
|
|
574
531
|
else options.push(arg);
|
|
575
532
|
previousTakesValue = !arg.includes("=") && takesValue.has(arg);
|
|
576
533
|
}
|
|
@@ -578,16 +535,16 @@ function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
|
|
|
578
535
|
}
|
|
579
536
|
|
|
580
537
|
// src/shared/resolve-bin.ts
|
|
581
|
-
import { existsSync as
|
|
538
|
+
import { existsSync as existsSync6 } from "fs";
|
|
582
539
|
import { createRequire } from "module";
|
|
583
|
-
import { delimiter, dirname
|
|
540
|
+
import { delimiter, dirname, join as join5 } from "path";
|
|
584
541
|
var require2 = createRequire(import.meta.url);
|
|
585
542
|
function resolveBin(fromDir, name) {
|
|
586
543
|
let dir = fromDir;
|
|
587
544
|
for (; ; ) {
|
|
588
|
-
const candidate =
|
|
589
|
-
if (
|
|
590
|
-
const parent =
|
|
545
|
+
const candidate = join5(dir, "node_modules", ".bin", name);
|
|
546
|
+
if (existsSync6(candidate)) return candidate;
|
|
547
|
+
const parent = dirname(dir);
|
|
591
548
|
if (parent === dir) return void 0;
|
|
592
549
|
dir = parent;
|
|
593
550
|
}
|
|
@@ -596,10 +553,10 @@ function binFromOwnInstall(packageName, binName) {
|
|
|
596
553
|
try {
|
|
597
554
|
const manifest = require2.resolve(`${packageName}/package.json`);
|
|
598
555
|
const bin = require2(manifest).bin;
|
|
599
|
-
const
|
|
600
|
-
if (!
|
|
601
|
-
const executable =
|
|
602
|
-
return
|
|
556
|
+
const relative7 = typeof bin === "string" ? bin : bin?.[binName];
|
|
557
|
+
if (!relative7) return void 0;
|
|
558
|
+
const executable = join5(dirname(manifest), relative7);
|
|
559
|
+
return existsSync6(executable) ? executable : void 0;
|
|
603
560
|
} catch {
|
|
604
561
|
return void 0;
|
|
605
562
|
}
|
|
@@ -611,12 +568,12 @@ function binSearchPath(cwd) {
|
|
|
611
568
|
}
|
|
612
569
|
|
|
613
570
|
// src/roles/build/plan.ts
|
|
614
|
-
import { existsSync as
|
|
615
|
-
import { join as
|
|
571
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
572
|
+
import { join as join10 } from "path";
|
|
616
573
|
|
|
617
574
|
// src/core/config/existing-command.ts
|
|
618
|
-
import { readFileSync as
|
|
619
|
-
import { join as
|
|
575
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
576
|
+
import { join as join6 } from "path";
|
|
620
577
|
|
|
621
578
|
// src/shared/jsonc.ts
|
|
622
579
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
@@ -635,7 +592,7 @@ function nxTargetCommand(cwd, target) {
|
|
|
635
592
|
let project;
|
|
636
593
|
try {
|
|
637
594
|
project = parseJsonc(
|
|
638
|
-
|
|
595
|
+
readFileSync5(join6(cwd, "project.json"), "utf8"),
|
|
639
596
|
"project.json"
|
|
640
597
|
);
|
|
641
598
|
} catch {
|
|
@@ -666,8 +623,8 @@ function existingCommand(cwd, target) {
|
|
|
666
623
|
}
|
|
667
624
|
|
|
668
625
|
// src/core/config/nx-target.ts
|
|
669
|
-
import { existsSync as
|
|
670
|
-
import { join as
|
|
626
|
+
import { existsSync as existsSync7 } from "fs";
|
|
627
|
+
import { join as join7 } from "path";
|
|
671
628
|
function nxTargetMetadataOperations(options) {
|
|
672
629
|
const { targets } = options;
|
|
673
630
|
if (Object.keys(targets).length === 0) return [];
|
|
@@ -678,7 +635,7 @@ function nxTargetOperations(options) {
|
|
|
678
635
|
const names = Object.keys(targets);
|
|
679
636
|
if (names.length === 0) return [];
|
|
680
637
|
const operations = [];
|
|
681
|
-
if (
|
|
638
|
+
if (existsSync7(join7(cwd, "project.json"))) {
|
|
682
639
|
operations.push({
|
|
683
640
|
kind: "remove-json-keys",
|
|
684
641
|
path: "project.json",
|
|
@@ -732,8 +689,8 @@ function keepsOtherCommands(script, sentinelCommand) {
|
|
|
732
689
|
}
|
|
733
690
|
|
|
734
691
|
// src/roles/format/read-adoption.ts
|
|
735
|
-
import { existsSync as
|
|
736
|
-
import { join as
|
|
692
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
693
|
+
import { join as join8 } from "path";
|
|
737
694
|
|
|
738
695
|
// src/roles/format/config-policy.ts
|
|
739
696
|
var FORMAT_CONFIG_FILE = ".oxfmtrc.json";
|
|
@@ -839,11 +796,11 @@ function sameValue(a, b) {
|
|
|
839
796
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
840
797
|
}
|
|
841
798
|
function readFormatAdoption(cwd) {
|
|
842
|
-
const path =
|
|
843
|
-
if (!
|
|
799
|
+
const path = join8(cwd, FORMAT_CONFIG_FILE);
|
|
800
|
+
if (!existsSync8(path)) return NOT_ADOPTED2(null);
|
|
844
801
|
let parsed;
|
|
845
802
|
try {
|
|
846
|
-
parsed = parseJsonc(
|
|
803
|
+
parsed = parseJsonc(readFileSync6(path, "utf8"), FORMAT_CONFIG_FILE);
|
|
847
804
|
} catch (error) {
|
|
848
805
|
const reason = error instanceof Error ? error.message : String(error);
|
|
849
806
|
return NOT_ADOPTED2(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
|
|
@@ -944,8 +901,8 @@ function devTargetOperations(cwd, devScript) {
|
|
|
944
901
|
}
|
|
945
902
|
|
|
946
903
|
// src/roles/build/nest/adopt.ts
|
|
947
|
-
import { existsSync as
|
|
948
|
-
import { join as
|
|
904
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
905
|
+
import { join as join9 } from "path";
|
|
949
906
|
var NEST_CONFIG_FILE = "vite.config.mts";
|
|
950
907
|
function asSource(value, depth) {
|
|
951
908
|
const pad = " ".repeat(depth);
|
|
@@ -994,9 +951,9 @@ function nestBuildOutputs(target) {
|
|
|
994
951
|
}
|
|
995
952
|
var OUT_DIR_IN_CONFIG = /^\s*outDir:\s*'([^']+)'/m;
|
|
996
953
|
function nestOutputsFromConfig(cwd) {
|
|
997
|
-
const configPath =
|
|
998
|
-
if (!
|
|
999
|
-
const outDir = OUT_DIR_IN_CONFIG.exec(
|
|
954
|
+
const configPath = join9(cwd, NEST_CONFIG_FILE);
|
|
955
|
+
if (!existsSync9(configPath)) return void 0;
|
|
956
|
+
const outDir = OUT_DIR_IN_CONFIG.exec(readFileSync7(configPath, "utf8"))?.[1];
|
|
1000
957
|
return outDir === void 0 ? void 0 : [`{workspaceRoot}/${outDir}`];
|
|
1001
958
|
}
|
|
1002
959
|
function nestAdoptionOperations(cwd, target) {
|
|
@@ -1005,9 +962,9 @@ function nestAdoptionOperations(cwd, target) {
|
|
|
1005
962
|
|
|
1006
963
|
// src/roles/build/requirements.ts
|
|
1007
964
|
function atLeast(declared, needed) {
|
|
1008
|
-
const
|
|
1009
|
-
const left =
|
|
1010
|
-
const right =
|
|
965
|
+
const parse4 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
966
|
+
const left = parse4(declared);
|
|
967
|
+
const right = parse4(needed);
|
|
1011
968
|
for (let at = 0; at < Math.max(left.length, right.length); at++) {
|
|
1012
969
|
const a = left[at] ?? 0;
|
|
1013
970
|
const b = right[at] ?? 0;
|
|
@@ -1037,14 +994,15 @@ var movedSpecifiers = (toolchain) => new Set(toolchain.owned.map((entry) => entr
|
|
|
1037
994
|
var defaultExportNames = (toolchain) => Object.fromEntries(
|
|
1038
995
|
toolchain.owned.filter((entry) => entry.default !== void 0).map((entry) => [entry.specifier, entry.default])
|
|
1039
996
|
);
|
|
1040
|
-
function renderImport(bindings, presetSpecifier) {
|
|
997
|
+
function renderImport(bindings, presetSpecifier, semi) {
|
|
1041
998
|
const render = (list) => list.map((b) => b.exported === b.local ? b.exported : `${b.exported} as ${b.local}`).sort((left, right) => left.localeCompare(right)).join(", ");
|
|
999
|
+
const end = semi ? ";" : "";
|
|
1042
1000
|
const values = bindings.filter((b) => !b.typeOnly);
|
|
1043
1001
|
const types = bindings.filter((b) => b.typeOnly);
|
|
1044
1002
|
const lines = [];
|
|
1045
|
-
if (values.length > 0) lines.push(`import { ${render(values)} } from '${presetSpecifier}'`);
|
|
1003
|
+
if (values.length > 0) lines.push(`import { ${render(values)} } from '${presetSpecifier}'${end}`);
|
|
1046
1004
|
if (types.length > 0) {
|
|
1047
|
-
lines.push(`import type { ${render(types)} } from '${presetSpecifier}'`);
|
|
1005
|
+
lines.push(`import type { ${render(types)} } from '${presetSpecifier}'${end}`);
|
|
1048
1006
|
}
|
|
1049
1007
|
return lines.join("\n");
|
|
1050
1008
|
}
|
|
@@ -1113,7 +1071,8 @@ function rewriteToolchainImports(toolchain, source, fileName) {
|
|
|
1113
1071
|
const after = out.slice(span.end).match(/^\r?\n/)?.[0].length ?? 0;
|
|
1114
1072
|
out = out.slice(0, span.start) + out.slice(span.end + after);
|
|
1115
1073
|
}
|
|
1116
|
-
|
|
1074
|
+
const semi = source.slice(first.start, first.end).trimEnd().endsWith(";");
|
|
1075
|
+
out = out.slice(0, first.start) + renderImport(bindings, toolchain.presetSpecifier, semi) + out.slice(first.end);
|
|
1117
1076
|
return {
|
|
1118
1077
|
kind: "rewritten",
|
|
1119
1078
|
source: out,
|
|
@@ -1299,9 +1258,9 @@ function planNestService(context, webpackTarget) {
|
|
|
1299
1258
|
};
|
|
1300
1259
|
}
|
|
1301
1260
|
function readConfigSource(cwd, configFile) {
|
|
1302
|
-
if (!
|
|
1261
|
+
if (!existsSync10(join10(cwd, configFile))) return "";
|
|
1303
1262
|
try {
|
|
1304
|
-
return
|
|
1263
|
+
return readFileSync8(join10(cwd, configFile), "utf8");
|
|
1305
1264
|
} catch {
|
|
1306
1265
|
return "";
|
|
1307
1266
|
}
|
|
@@ -1486,224 +1445,23 @@ var ViteDevAdapter = class extends ViteRoleAdapter {
|
|
|
1486
1445
|
};
|
|
1487
1446
|
|
|
1488
1447
|
// src/roles/build/declared-outputs.ts
|
|
1489
|
-
import { existsSync as
|
|
1490
|
-
import { join as
|
|
1491
|
-
|
|
1492
|
-
// src/core/workspace-prep.ts
|
|
1493
|
-
import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync } from "fs";
|
|
1494
|
-
import { dirname as dirname3, join as join12, relative as relative2 } from "path";
|
|
1495
|
-
|
|
1496
|
-
// src/core/settings.ts
|
|
1497
|
-
var WORKSPACE_ROOT_MARKER = "nx.json";
|
|
1498
|
-
var DEFAULT_MAX_DIAGNOSTICS = 100;
|
|
1499
|
-
|
|
1500
|
-
// src/core/workspace-prep.ts
|
|
1501
|
-
var OVERRIDE_KEY = "i18next>typescript";
|
|
1502
|
-
var NATIVE_TS_ALIAS = "@typescript/native";
|
|
1503
|
-
var WORKSPACE_YAML = "pnpm-workspace.yaml";
|
|
1504
|
-
var RELEASE_AGE_KEY = "minimumReleaseAgeExclude";
|
|
1505
|
-
var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
|
|
1506
|
-
var MAP_ENTRY = /^(\s+)(.*?):\s+(.*?)\s*$/;
|
|
1507
|
-
function findWorkspaceRoot(startDir) {
|
|
1508
|
-
let dir = startDir;
|
|
1509
|
-
for (; ; ) {
|
|
1510
|
-
if (existsSync12(join12(dir, WORKSPACE_ROOT_MARKER))) return dir;
|
|
1511
|
-
const parent = dirname3(dir);
|
|
1512
|
-
if (parent === dir) return void 0;
|
|
1513
|
-
dir = parent;
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
function declaredNativeTs(pkg) {
|
|
1517
|
-
const spec = pkg.dependencies?.[NATIVE_TS_ALIAS] ?? pkg.devDependencies?.[NATIVE_TS_ALIAS];
|
|
1518
|
-
if (!spec) return void 0;
|
|
1519
|
-
const version = spec.slice(spec.lastIndexOf("@") + 1).replace(/^[\^~>=<\s]+/, "");
|
|
1520
|
-
return version || void 0;
|
|
1521
|
-
}
|
|
1522
|
-
function ensureI18nextSingleton(root, dryRun) {
|
|
1523
|
-
const pkgPath = join12(root, "package.json");
|
|
1524
|
-
if (!existsSync12(pkgPath)) return void 0;
|
|
1525
|
-
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
1526
|
-
const want = declaredNativeTs(pkg);
|
|
1527
|
-
if (!want) return void 0;
|
|
1528
|
-
const yamlPath = join12(root, WORKSPACE_YAML);
|
|
1529
|
-
if (!existsSync12(yamlPath)) return void 0;
|
|
1530
|
-
const lines = readFileSync10(yamlPath, "utf8").split("\n");
|
|
1531
|
-
const entry = findOverrideEntry(lines);
|
|
1532
|
-
if (entry.valueIdx !== void 0 && entry.value === want) return void 0;
|
|
1533
|
-
if (dryRun) return `would pin ${OVERRIDE_KEY} to ${want} in ${WORKSPACE_YAML} (i18next singleton)`;
|
|
1534
|
-
applyOverride(lines, entry, want);
|
|
1535
|
-
writeFileSync(yamlPath, lines.join("\n"));
|
|
1536
|
-
return `pinned ${OVERRIDE_KEY} to ${want} in ${WORKSPACE_YAML} (i18next singleton)`;
|
|
1537
|
-
}
|
|
1538
|
-
function findOverrideEntry(lines) {
|
|
1539
|
-
const blockIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === "overrides:");
|
|
1540
|
-
if (blockIdx === -1) return { indent: " " };
|
|
1541
|
-
let lastEntryIdx = blockIdx;
|
|
1542
|
-
let indent = " ";
|
|
1543
|
-
for (let i = blockIdx + 1; i < lines.length; i++) {
|
|
1544
|
-
const match = lines[i]?.match(MAP_ENTRY);
|
|
1545
|
-
if (!match) break;
|
|
1546
|
-
indent = match[1] ?? indent;
|
|
1547
|
-
lastEntryIdx = i;
|
|
1548
|
-
if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === OVERRIDE_KEY) {
|
|
1549
|
-
return {
|
|
1550
|
-
valueIdx: i,
|
|
1551
|
-
value: (match[3] ?? "").replace(/^['"]|['"]$/g, ""),
|
|
1552
|
-
blockIdx,
|
|
1553
|
-
lastEntryIdx,
|
|
1554
|
-
indent
|
|
1555
|
-
};
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
return { blockIdx, lastEntryIdx, indent };
|
|
1559
|
-
}
|
|
1560
|
-
function applyOverride(lines, entry, want) {
|
|
1561
|
-
const line = `${entry.indent}'${OVERRIDE_KEY}': ${want}`;
|
|
1562
|
-
if (entry.valueIdx !== void 0) {
|
|
1563
|
-
lines[entry.valueIdx] = line;
|
|
1564
|
-
return;
|
|
1565
|
-
}
|
|
1566
|
-
if (entry.lastEntryIdx !== void 0) {
|
|
1567
|
-
lines.splice(entry.lastEntryIdx + 1, 0, line);
|
|
1568
|
-
return;
|
|
1569
|
-
}
|
|
1570
|
-
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
1571
|
-
lines.push("overrides:", line, "");
|
|
1572
|
-
}
|
|
1573
|
-
function ensureReleaseAgeAllowList(root, dryRun) {
|
|
1574
|
-
const yamlPath = join12(root, WORKSPACE_YAML);
|
|
1575
|
-
if (!existsSync12(yamlPath)) return void 0;
|
|
1576
|
-
const own = readOwnPackage().name;
|
|
1577
|
-
const lines = readFileSync10(yamlPath, "utf8").split("\n");
|
|
1578
|
-
const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
|
|
1579
|
-
if (keyIdx === -1) return void 0;
|
|
1580
|
-
let lastItemIdx = keyIdx;
|
|
1581
|
-
let indent = " ";
|
|
1582
|
-
for (let i = keyIdx + 1; i < lines.length; i++) {
|
|
1583
|
-
const match = lines[i]?.match(LIST_ITEM);
|
|
1584
|
-
if (!match) break;
|
|
1585
|
-
indent = match[1] ?? indent;
|
|
1586
|
-
lastItemIdx = i;
|
|
1587
|
-
if ((match[2] ?? "").replace(/^['"]|['"]$/g, "") === own) return void 0;
|
|
1588
|
-
}
|
|
1589
|
-
if (dryRun) return `would allow-list ${own} under ${RELEASE_AGE_KEY}`;
|
|
1590
|
-
lines.splice(lastItemIdx + 1, 0, `${indent}- '${own}'`);
|
|
1591
|
-
writeFileSync(yamlPath, lines.join("\n"));
|
|
1592
|
-
return `allow-listed ${own} under ${RELEASE_AGE_KEY} in ${WORKSPACE_YAML}`;
|
|
1593
|
-
}
|
|
1594
|
-
var PRETTIER_IGNORE = ".prettierignore";
|
|
1595
|
-
var EXCLUSION_HEADER = "# @hublo/sentinel: modules formatted by oxfmt, not by the root Prettier";
|
|
1596
|
-
var ROOT_PRETTIER_CONFIGS = [
|
|
1597
|
-
".prettierrc",
|
|
1598
|
-
".prettierrc.json",
|
|
1599
|
-
".prettierrc.json5",
|
|
1600
|
-
".prettierrc.yml",
|
|
1601
|
-
".prettierrc.yaml",
|
|
1602
|
-
".prettierrc.js",
|
|
1603
|
-
".prettierrc.cjs",
|
|
1604
|
-
".prettierrc.mjs",
|
|
1605
|
-
"prettier.config.js",
|
|
1606
|
-
"prettier.config.cjs",
|
|
1607
|
-
"prettier.config.mjs"
|
|
1608
|
-
];
|
|
1609
|
-
function ensureFormatterExclusion(root, moduleDir, dryRun) {
|
|
1610
|
-
const rel = relative2(root, moduleDir).replaceAll("\\", "/");
|
|
1611
|
-
if (rel === "" || rel.startsWith("..")) return void 0;
|
|
1612
|
-
const ignorePath = join12(root, PRETTIER_IGNORE);
|
|
1613
|
-
const hasPrettier = existsSync12(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync12(join12(root, name)));
|
|
1614
|
-
if (!hasPrettier) return void 0;
|
|
1615
|
-
const pattern = `/${rel}/`;
|
|
1616
|
-
const existing = existsSync12(ignorePath) ? readFileSync10(ignorePath, "utf8") : "";
|
|
1617
|
-
const lines = existing.split("\n");
|
|
1618
|
-
if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
|
|
1619
|
-
return void 0;
|
|
1620
|
-
}
|
|
1621
|
-
if (dryRun) return `would exclude ${rel} from the root Prettier (${PRETTIER_IGNORE})`;
|
|
1622
|
-
const headerIdx = lines.findIndex((line) => line.trim() === EXCLUSION_HEADER);
|
|
1623
|
-
if (headerIdx === -1) {
|
|
1624
|
-
const prefix = existing.trim() === "" ? "" : `${existing.replace(/\n*$/, "")}
|
|
1625
|
-
|
|
1626
|
-
`;
|
|
1627
|
-
writeFileSync(ignorePath, `${prefix}${EXCLUSION_HEADER}
|
|
1628
|
-
${pattern}
|
|
1629
|
-
`);
|
|
1630
|
-
} else {
|
|
1631
|
-
let end = headerIdx + 1;
|
|
1632
|
-
while (end < lines.length && lines[end]?.trim().startsWith("/")) end += 1;
|
|
1633
|
-
const managed = [...lines.slice(headerIdx + 1, end), pattern].sort();
|
|
1634
|
-
writeFileSync(
|
|
1635
|
-
ignorePath,
|
|
1636
|
-
[...lines.slice(0, headerIdx + 1), ...managed, ...lines.slice(end)].join("\n")
|
|
1637
|
-
);
|
|
1638
|
-
}
|
|
1639
|
-
return `excluded ${rel} from the root Prettier (${PRETTIER_IGNORE}): it formats itself now`;
|
|
1640
|
-
}
|
|
1641
|
-
function ensureWorkspacePrep(opts) {
|
|
1642
|
-
const dryRun = Boolean(opts.dryRun);
|
|
1643
|
-
return [
|
|
1644
|
-
ensureI18nextSingleton(opts.root, dryRun),
|
|
1645
|
-
ensureReleaseAgeAllowList(opts.root, dryRun),
|
|
1646
|
-
opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
|
|
1647
|
-
].filter((message) => message !== void 0);
|
|
1648
|
-
}
|
|
1649
|
-
function inspectWorkspacePrep(root) {
|
|
1650
|
-
const entries = [];
|
|
1651
|
-
let pkg = {};
|
|
1652
|
-
const pkgPath = join12(root, "package.json");
|
|
1653
|
-
if (existsSync12(pkgPath)) {
|
|
1654
|
-
try {
|
|
1655
|
-
pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
1656
|
-
} catch {
|
|
1657
|
-
pkg = {};
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
const yamlPath = join12(root, WORKSPACE_YAML);
|
|
1661
|
-
const yaml = existsSync12(yamlPath) ? readFileSync10(yamlPath, "utf8") : void 0;
|
|
1662
|
-
const pinned = yaml === void 0 ? void 0 : findOverrideEntry(yaml.split("\n")).value;
|
|
1663
|
-
if (pinned !== void 0) {
|
|
1664
|
-
const stillForks = declaredNativeTs(pkg);
|
|
1665
|
-
entries.push({
|
|
1666
|
-
rule: `${WORKSPACE_YAML} \u2192 overrides.${OVERRIDE_KEY}`,
|
|
1667
|
-
reason: stillForks === void 0 ? `pinned to ${pinned}, but the root no longer declares ${NATIVE_TS_ALIAS}, so nothing forks any more. This override is now dead config and can be removed.` : `pinned to ${pinned}, matching the second TypeScript the root declares (${NATIVE_TS_ALIAS}). It re-resolves i18next for EVERY module that depends on it, not only adopted ones. It retires with the second TypeScript.`
|
|
1668
|
-
});
|
|
1669
|
-
}
|
|
1670
|
-
const legacy = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
|
|
1671
|
-
if (legacy !== void 0) {
|
|
1672
|
-
entries.push({
|
|
1673
|
-
rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
|
|
1674
|
-
reason: `pinned to ${legacy}, but pnpm 11 no longer reads the \`pnpm\` field of package.json and says nothing about it, so this override is not applied. Move it to ${WORKSPACE_YAML} under \`overrides:\`, which pnpm 10 and 11 both read.`
|
|
1675
|
-
});
|
|
1676
|
-
}
|
|
1677
|
-
const own = readOwnPackage().name;
|
|
1678
|
-
if (yaml !== void 0) {
|
|
1679
|
-
const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
|
|
1680
|
-
if (listed) {
|
|
1681
|
-
const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
|
|
1682
|
-
entries.push({
|
|
1683
|
-
rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
|
|
1684
|
-
reason: gated ? `${own} is exempt from this workspace's release-age gate, so a freshly published version installs instead of being held. That is a standing exemption from a supply-chain control: the exposure is one bump wide, since the version is pinned and the lockfile carries its integrity, so a re-publish of the same version does not pass. Retire it once a bump can simply wait out the window.` : `${own} is allow-listed, but this workspace no longer sets minimumReleaseAge, so there is no gate to be exempt from. This entry is now dead config and can be removed.`
|
|
1685
|
-
});
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
return entries;
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
// src/roles/build/declared-outputs.ts
|
|
1448
|
+
import { existsSync as existsSync11, rmSync, statSync, writeFileSync } from "fs";
|
|
1449
|
+
import { join as join11 } from "path";
|
|
1692
1450
|
function declaredBuildTarget(cwd) {
|
|
1693
1451
|
return readProjectPackageJson(cwd).nx?.targets?.[BUILD_SCRIPT_NAME];
|
|
1694
1452
|
}
|
|
1695
1453
|
function resolveOutputToken(token, cwd) {
|
|
1696
|
-
if (token.startsWith("{projectRoot}/")) return
|
|
1454
|
+
if (token.startsWith("{projectRoot}/")) return join11(cwd, token.slice("{projectRoot}/".length));
|
|
1697
1455
|
if (token.startsWith("{workspaceRoot}/")) {
|
|
1698
1456
|
const root = findWorkspaceRoot(cwd);
|
|
1699
|
-
return root === void 0 ? void 0 :
|
|
1457
|
+
return root === void 0 ? void 0 : join11(root, token.slice("{workspaceRoot}/".length));
|
|
1700
1458
|
}
|
|
1701
|
-
return
|
|
1459
|
+
return join11(cwd, token);
|
|
1702
1460
|
}
|
|
1703
1461
|
function buildStamp(cwd) {
|
|
1704
|
-
const marker =
|
|
1462
|
+
const marker = join11(cwd, `.sentinel-build-stamp-${process.pid}`);
|
|
1705
1463
|
try {
|
|
1706
|
-
|
|
1464
|
+
writeFileSync(marker, "");
|
|
1707
1465
|
const stamp = statSync(marker).mtimeMs;
|
|
1708
1466
|
rmSync(marker, { force: true });
|
|
1709
1467
|
return stamp;
|
|
@@ -1712,7 +1470,7 @@ function buildStamp(cwd) {
|
|
|
1712
1470
|
}
|
|
1713
1471
|
}
|
|
1714
1472
|
function writtenSince(path, startedAt) {
|
|
1715
|
-
if (!
|
|
1473
|
+
if (!existsSync11(path)) return false;
|
|
1716
1474
|
try {
|
|
1717
1475
|
return statSync(path).mtimeMs >= startedAt;
|
|
1718
1476
|
} catch {
|
|
@@ -1861,12 +1619,12 @@ function registerBuild() {
|
|
|
1861
1619
|
|
|
1862
1620
|
// src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
|
|
1863
1621
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
1864
|
-
import { existsSync as
|
|
1865
|
-
import { join as
|
|
1622
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
|
|
1623
|
+
import { join as join16 } from "path";
|
|
1866
1624
|
|
|
1867
1625
|
// src/core/config/has-source.ts
|
|
1868
1626
|
import { readdirSync } from "fs";
|
|
1869
|
-
import { extname, join as
|
|
1627
|
+
import { extname, join as join12, relative as relative2 } from "path";
|
|
1870
1628
|
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
1871
1629
|
"node_modules",
|
|
1872
1630
|
"dist",
|
|
@@ -1917,7 +1675,7 @@ function hasSourceFiles(cwd, extensions) {
|
|
|
1917
1675
|
}
|
|
1918
1676
|
for (const entry of entries) {
|
|
1919
1677
|
if (entry.isDirectory()) {
|
|
1920
|
-
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(
|
|
1678
|
+
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join12(dir, entry.name));
|
|
1921
1679
|
continue;
|
|
1922
1680
|
}
|
|
1923
1681
|
if (wanted.has(extname(entry.name))) return true;
|
|
@@ -1938,12 +1696,12 @@ function listSourceFiles(cwd, extensions) {
|
|
|
1938
1696
|
continue;
|
|
1939
1697
|
}
|
|
1940
1698
|
for (const entry of entries) {
|
|
1941
|
-
const full =
|
|
1699
|
+
const full = join12(dir, entry.name);
|
|
1942
1700
|
if (entry.isDirectory()) {
|
|
1943
1701
|
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
|
|
1944
1702
|
continue;
|
|
1945
1703
|
}
|
|
1946
|
-
if (wanted.has(extname(entry.name))) found.push(
|
|
1704
|
+
if (wanted.has(extname(entry.name))) found.push(relative2(cwd, full));
|
|
1947
1705
|
}
|
|
1948
1706
|
}
|
|
1949
1707
|
return found;
|
|
@@ -1984,8 +1742,8 @@ function writesWhenRewritten(name, command) {
|
|
|
1984
1742
|
var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
|
|
1985
1743
|
|
|
1986
1744
|
// src/roles/format/inherited-ignores.ts
|
|
1987
|
-
import { existsSync as
|
|
1988
|
-
import { join as
|
|
1745
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
1746
|
+
import { join as join13 } from "path";
|
|
1989
1747
|
var ROOT_IGNORE_FILE = ".prettierignore";
|
|
1990
1748
|
function isPattern(line) {
|
|
1991
1749
|
const trimmed = line.trim();
|
|
@@ -2001,11 +1759,11 @@ function toModulePattern(pattern) {
|
|
|
2001
1759
|
}
|
|
2002
1760
|
function inheritedIgnorePatterns(workspaceRoot) {
|
|
2003
1761
|
if (!workspaceRoot) return [];
|
|
2004
|
-
const path =
|
|
2005
|
-
if (!
|
|
1762
|
+
const path = join13(workspaceRoot, ROOT_IGNORE_FILE);
|
|
1763
|
+
if (!existsSync12(path)) return [];
|
|
2006
1764
|
let contents;
|
|
2007
1765
|
try {
|
|
2008
|
-
contents =
|
|
1766
|
+
contents = readFileSync9(path, "utf8");
|
|
2009
1767
|
} catch {
|
|
2010
1768
|
return [];
|
|
2011
1769
|
}
|
|
@@ -2050,13 +1808,13 @@ function needsSvelteCompiler(output) {
|
|
|
2050
1808
|
}
|
|
2051
1809
|
|
|
2052
1810
|
// src/roles/format/prettier-config.ts
|
|
2053
|
-
import { existsSync as
|
|
2054
|
-
import { join as
|
|
1811
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
|
|
1812
|
+
import { join as join15 } from "path";
|
|
2055
1813
|
|
|
2056
1814
|
// src/roles/format/resolve-oxfmt.ts
|
|
2057
|
-
import { readFileSync as
|
|
1815
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2058
1816
|
import { createRequire as createRequire2 } from "module";
|
|
2059
|
-
import { dirname as
|
|
1817
|
+
import { dirname as dirname2, join as join14 } from "path";
|
|
2060
1818
|
function resolveOxfmt(cwd) {
|
|
2061
1819
|
return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
|
|
2062
1820
|
}
|
|
@@ -2085,8 +1843,8 @@ function configSchema() {
|
|
|
2085
1843
|
function readConfigSchema() {
|
|
2086
1844
|
try {
|
|
2087
1845
|
const manifest = createRequire2(import.meta.url).resolve("oxfmt/package.json");
|
|
2088
|
-
const schemaPath =
|
|
2089
|
-
return JSON.parse(
|
|
1846
|
+
const schemaPath = join14(dirname2(manifest), "configuration_schema.json");
|
|
1847
|
+
return JSON.parse(readFileSync10(schemaPath, "utf8"));
|
|
2090
1848
|
} catch {
|
|
2091
1849
|
return void 0;
|
|
2092
1850
|
}
|
|
@@ -2117,14 +1875,14 @@ function toOxfmtOverrides(value) {
|
|
|
2117
1875
|
return { overrides, unresolved };
|
|
2118
1876
|
}
|
|
2119
1877
|
function readPrettierSettings(cwd) {
|
|
2120
|
-
const file = PRETTIER_CONFIG_FILES.find((name) =>
|
|
1878
|
+
const file = PRETTIER_CONFIG_FILES.find((name) => existsSync13(join15(cwd, name)));
|
|
2121
1879
|
if (!file) return { options: {}, unresolved: [] };
|
|
2122
1880
|
if (/\.(js|cjs|mjs)$/.test(file)) {
|
|
2123
1881
|
return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
|
|
2124
1882
|
}
|
|
2125
1883
|
let parsed;
|
|
2126
1884
|
try {
|
|
2127
|
-
parsed = parseJsonc(
|
|
1885
|
+
parsed = parseJsonc(readFileSync11(join15(cwd, file), "utf8"), file);
|
|
2128
1886
|
} catch {
|
|
2129
1887
|
return { options: {}, file, unresolved: [`${file} could not be parsed`] };
|
|
2130
1888
|
}
|
|
@@ -2235,7 +1993,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
2235
1993
|
manifestOperation(context.cwd, this.formatScripts(context.cwd))
|
|
2236
1994
|
];
|
|
2237
1995
|
const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
|
|
2238
|
-
(name) =>
|
|
1996
|
+
(name) => existsSync14(join16(context.cwd, name))
|
|
2239
1997
|
);
|
|
2240
1998
|
for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
|
|
2241
1999
|
const removableDeps = this.modulePrettierDependencies(context.cwd);
|
|
@@ -2475,7 +2233,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
2475
2233
|
modulePrettierDependencies(cwd) {
|
|
2476
2234
|
const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
|
|
2477
2235
|
try {
|
|
2478
|
-
const manifest = JSON.parse(
|
|
2236
|
+
const manifest = JSON.parse(readFileSync12(join16(cwd, "package.json"), "utf8"));
|
|
2479
2237
|
return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
|
|
2480
2238
|
} catch {
|
|
2481
2239
|
return [];
|
|
@@ -2557,8 +2315,8 @@ function registerFormat() {
|
|
|
2557
2315
|
|
|
2558
2316
|
// src/roles/lint/adapters/oxlint/oxlint.adapter.ts
|
|
2559
2317
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
2560
|
-
import { existsSync as
|
|
2561
|
-
import { join as
|
|
2318
|
+
import { existsSync as existsSync22, readFileSync as readFileSync20, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2319
|
+
import { join as join26 } from "path";
|
|
2562
2320
|
|
|
2563
2321
|
// src/core/config/deferred-rules.ts
|
|
2564
2322
|
function deferredRuleNames(rules) {
|
|
@@ -2567,7 +2325,7 @@ function deferredRuleNames(rules) {
|
|
|
2567
2325
|
|
|
2568
2326
|
// src/core/fix-report.ts
|
|
2569
2327
|
import { statSync as statSync2 } from "fs";
|
|
2570
|
-
import { join as
|
|
2328
|
+
import { join as join17 } from "path";
|
|
2571
2329
|
var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
|
|
2572
2330
|
function readFailure(output) {
|
|
2573
2331
|
const lines = output.split("\n").map((line2) => line2.trim());
|
|
@@ -2587,7 +2345,7 @@ function fingerprint(path) {
|
|
|
2587
2345
|
function snapshotFiles(cwd, extensions) {
|
|
2588
2346
|
const snapshot = /* @__PURE__ */ new Map();
|
|
2589
2347
|
for (const file of listSourceFiles(cwd, extensions)) {
|
|
2590
|
-
const mark = fingerprint(
|
|
2348
|
+
const mark = fingerprint(join17(cwd, file));
|
|
2591
2349
|
if (mark !== void 0) snapshot.set(file, mark);
|
|
2592
2350
|
}
|
|
2593
2351
|
return snapshot;
|
|
@@ -2595,7 +2353,7 @@ function snapshotFiles(cwd, extensions) {
|
|
|
2595
2353
|
function changedSince(cwd, before, extensions) {
|
|
2596
2354
|
const changed = [];
|
|
2597
2355
|
for (const file of listSourceFiles(cwd, extensions)) {
|
|
2598
|
-
const now = fingerprint(
|
|
2356
|
+
const now = fingerprint(join17(cwd, file));
|
|
2599
2357
|
if (now === void 0) continue;
|
|
2600
2358
|
if (before.get(file) !== now) changed.push(file);
|
|
2601
2359
|
}
|
|
@@ -2614,8 +2372,8 @@ function describeFixOutcome(outcome, toolLabel) {
|
|
|
2614
2372
|
}
|
|
2615
2373
|
|
|
2616
2374
|
// src/roles/lint/module-baseline.ts
|
|
2617
|
-
import { existsSync as
|
|
2618
|
-
import { join as
|
|
2375
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
|
|
2376
|
+
import { join as join18 } from "path";
|
|
2619
2377
|
var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
|
|
2620
2378
|
var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
|
|
2621
2379
|
var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
|
|
@@ -2659,11 +2417,11 @@ function describeHolds(holds) {
|
|
|
2659
2417
|
return `held ${holds.length} rule(s) at \`warn\` for this module, covering ${total} violation(s) that were already there: ${listed}${rest}. They are written to ${LINT_BASELINE_FILE}, they still report, and they return to \`error\` once fixed and re-initialized`;
|
|
2660
2418
|
}
|
|
2661
2419
|
function heldRules(cwd) {
|
|
2662
|
-
const path =
|
|
2663
|
-
if (!
|
|
2420
|
+
const path = join18(cwd, LINT_BASELINE_FILE);
|
|
2421
|
+
if (!existsSync15(path)) return [];
|
|
2664
2422
|
try {
|
|
2665
2423
|
const parsed = parseJsonc(
|
|
2666
|
-
|
|
2424
|
+
readFileSync13(path, "utf8"),
|
|
2667
2425
|
LINT_BASELINE_FILE
|
|
2668
2426
|
);
|
|
2669
2427
|
return Object.keys(parsed.rules ?? {});
|
|
@@ -4489,14 +4247,14 @@ function downgradedRulesFor(preset) {
|
|
|
4489
4247
|
}
|
|
4490
4248
|
|
|
4491
4249
|
// src/roles/lint/extra-layers.ts
|
|
4492
|
-
import { existsSync as
|
|
4493
|
-
import { isAbsolute as isAbsolute2, join as
|
|
4250
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
|
|
4251
|
+
import { isAbsolute as isAbsolute2, join as join19, resolve as resolve3 } from "path";
|
|
4494
4252
|
function ruleCount(cwd, specifier) {
|
|
4495
4253
|
const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
|
|
4496
|
-
if (!
|
|
4254
|
+
if (!existsSync16(path)) return void 0;
|
|
4497
4255
|
try {
|
|
4498
4256
|
const parsed = parseJsonc(
|
|
4499
|
-
|
|
4257
|
+
readFileSync14(path, "utf8"),
|
|
4500
4258
|
specifier
|
|
4501
4259
|
);
|
|
4502
4260
|
return Object.keys(parsed.rules ?? {}).length;
|
|
@@ -4516,7 +4274,7 @@ function extraLayers(cwd, extendsList) {
|
|
|
4516
4274
|
function committedExtendsList(cwd, configFile) {
|
|
4517
4275
|
try {
|
|
4518
4276
|
const parsed = parseJsonc(
|
|
4519
|
-
|
|
4277
|
+
readFileSync14(join19(cwd, configFile), "utf8"),
|
|
4520
4278
|
configFile
|
|
4521
4279
|
);
|
|
4522
4280
|
return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
|
|
@@ -4575,8 +4333,8 @@ function errorCountsByConfigRule(stdout) {
|
|
|
4575
4333
|
}
|
|
4576
4334
|
|
|
4577
4335
|
// src/core/config/read-adoption.ts
|
|
4578
|
-
import { existsSync as
|
|
4579
|
-
import { join as
|
|
4336
|
+
import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
|
|
4337
|
+
import { join as join21 } from "path";
|
|
4580
4338
|
|
|
4581
4339
|
// src/core/config/owned-keys.ts
|
|
4582
4340
|
function presetOwnedKeys(config, permitted, presetSets) {
|
|
@@ -4591,12 +4349,12 @@ function localOnlyKeys(config, permitted, presetSets) {
|
|
|
4591
4349
|
}
|
|
4592
4350
|
|
|
4593
4351
|
// src/core/config/resolve-config-target.ts
|
|
4594
|
-
import { existsSync as
|
|
4595
|
-
import { join as
|
|
4352
|
+
import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
|
|
4353
|
+
import { join as join20 } from "path";
|
|
4596
4354
|
function readExtends(absolutePath) {
|
|
4597
4355
|
let parsed;
|
|
4598
4356
|
try {
|
|
4599
|
-
parsed = parseJsonc(
|
|
4357
|
+
parsed = parseJsonc(readFileSync15(absolutePath, "utf8"), absolutePath);
|
|
4600
4358
|
} catch {
|
|
4601
4359
|
return [];
|
|
4602
4360
|
}
|
|
@@ -4610,8 +4368,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
|
|
|
4610
4368
|
let existing;
|
|
4611
4369
|
let existingExtendsSomething = false;
|
|
4612
4370
|
for (const candidate of candidates) {
|
|
4613
|
-
const absolutePath =
|
|
4614
|
-
if (!
|
|
4371
|
+
const absolutePath = join20(moduleDir, candidate);
|
|
4372
|
+
if (!existsSync17(absolutePath)) continue;
|
|
4615
4373
|
const chain = readExtends(absolutePath);
|
|
4616
4374
|
if (existing === void 0) {
|
|
4617
4375
|
existing = candidate;
|
|
@@ -4644,12 +4402,12 @@ var NOT_ADOPTED3 = (configFile, unreadable = null) => ({
|
|
|
4644
4402
|
});
|
|
4645
4403
|
function readAdoption(cwd, options) {
|
|
4646
4404
|
const target = resolveConfigTarget(cwd, options);
|
|
4647
|
-
if (target.reason === "none" || !
|
|
4405
|
+
if (target.reason === "none" || !existsSync18(join21(cwd, target.path))) {
|
|
4648
4406
|
return NOT_ADOPTED3(target.reason === "none" ? null : target.path);
|
|
4649
4407
|
}
|
|
4650
4408
|
let parsed;
|
|
4651
4409
|
try {
|
|
4652
|
-
parsed = parseJsonc(
|
|
4410
|
+
parsed = parseJsonc(readFileSync16(join21(cwd, target.path), "utf8"), target.path);
|
|
4653
4411
|
} catch (error) {
|
|
4654
4412
|
const reason = error instanceof Error ? error.message : String(error);
|
|
4655
4413
|
return NOT_ADOPTED3(target.path, `${target.path} could not be parsed (${reason})`);
|
|
@@ -4683,10 +4441,10 @@ function readLintAdoption(cwd) {
|
|
|
4683
4441
|
}
|
|
4684
4442
|
|
|
4685
4443
|
// src/roles/lint/resolve-oxlint.ts
|
|
4686
|
-
import { existsSync as
|
|
4444
|
+
import { existsSync as existsSync19 } from "fs";
|
|
4687
4445
|
import { createRequire as createRequire3 } from "module";
|
|
4688
|
-
import { delimiter as delimiter2, dirname as
|
|
4689
|
-
import { fileURLToPath
|
|
4446
|
+
import { delimiter as delimiter2, dirname as dirname3, join as join22 } from "path";
|
|
4447
|
+
import { fileURLToPath } from "url";
|
|
4690
4448
|
var PACKAGE_OF = {
|
|
4691
4449
|
oxlint: "oxlint",
|
|
4692
4450
|
tsgolint: "oxlint-tsgolint"
|
|
@@ -4701,7 +4459,7 @@ function canRunTypeAware(cwd) {
|
|
|
4701
4459
|
function oxlintPath(cwd, env) {
|
|
4702
4460
|
const shim = tsgolintShim(cwd);
|
|
4703
4461
|
if (!shim) return env.PATH;
|
|
4704
|
-
return [
|
|
4462
|
+
return [dirname3(shim), env.PATH].filter(Boolean).join(delimiter2);
|
|
4705
4463
|
}
|
|
4706
4464
|
function tsgolintShim(cwd) {
|
|
4707
4465
|
const fromModule = resolveBin(cwd, "tsgolint");
|
|
@@ -4709,32 +4467,49 @@ function tsgolintShim(cwd) {
|
|
|
4709
4467
|
const candidates = [];
|
|
4710
4468
|
for (const owner of ["oxlint-tsgolint", "oxlint"]) {
|
|
4711
4469
|
try {
|
|
4712
|
-
const packageDir =
|
|
4713
|
-
candidates.push(
|
|
4714
|
-
candidates.push(
|
|
4470
|
+
const packageDir = dirname3(require3.resolve(`${owner}/package.json`));
|
|
4471
|
+
candidates.push(join22(packageDir, "node_modules", ".bin", "tsgolint"));
|
|
4472
|
+
candidates.push(join22(packageDir, "..", ".bin", "tsgolint"));
|
|
4715
4473
|
} catch {
|
|
4716
4474
|
}
|
|
4717
4475
|
}
|
|
4718
4476
|
try {
|
|
4719
|
-
const ownRoot =
|
|
4720
|
-
candidates.push(
|
|
4477
|
+
const ownRoot = dirname3(require3.resolve("@hublo/sentinel/package.json"));
|
|
4478
|
+
candidates.push(join22(ownRoot, "node_modules", ".bin", "tsgolint"));
|
|
4721
4479
|
} catch {
|
|
4722
|
-
candidates.push(resolveBin(
|
|
4480
|
+
candidates.push(resolveBin(dirname3(fileURLToPath(import.meta.url)), "tsgolint") ?? "");
|
|
4723
4481
|
}
|
|
4724
|
-
return candidates.find((candidate) => candidate !== "" &&
|
|
4482
|
+
return candidates.find((candidate) => candidate !== "" && existsSync19(candidate));
|
|
4725
4483
|
}
|
|
4726
4484
|
function oxlintSearchPath(cwd) {
|
|
4727
4485
|
return binSearchPath(cwd);
|
|
4728
4486
|
}
|
|
4729
4487
|
|
|
4730
4488
|
// src/roles/lint/adapters/oxlint/plan.ts
|
|
4731
|
-
import { existsSync as
|
|
4732
|
-
import { join as
|
|
4489
|
+
import { existsSync as existsSync21, readFileSync as readFileSync19 } from "fs";
|
|
4490
|
+
import { join as join25 } from "path";
|
|
4733
4491
|
|
|
4734
4492
|
// src/roles/lint/eslint-ignores.ts
|
|
4735
|
-
import { existsSync as
|
|
4736
|
-
import { join as
|
|
4493
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
|
|
4494
|
+
import { join as join23 } from "path";
|
|
4737
4495
|
import { parseSync as parseSync2 } from "oxc-parser";
|
|
4496
|
+
|
|
4497
|
+
// src/shared/ast.ts
|
|
4498
|
+
var SKIP = /* @__PURE__ */ Symbol("skip-subtree");
|
|
4499
|
+
function walk(node, visit) {
|
|
4500
|
+
if (node === null || typeof node !== "object") return;
|
|
4501
|
+
if (Array.isArray(node)) {
|
|
4502
|
+
for (const entry of node) walk(entry, visit);
|
|
4503
|
+
return;
|
|
4504
|
+
}
|
|
4505
|
+
const current = node;
|
|
4506
|
+
if (typeof current.type === "string" && visit(current) === SKIP) return;
|
|
4507
|
+
for (const value of Object.values(current)) {
|
|
4508
|
+
if (value !== null && typeof value === "object") walk(value, visit);
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
// src/roles/lint/eslint-ignores.ts
|
|
4738
4513
|
var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
|
|
4739
4514
|
function toOxlintPattern(pattern) {
|
|
4740
4515
|
return pattern.replace(/^\.\//, "");
|
|
@@ -4745,11 +4520,11 @@ function readRootEslintIgnores(root) {
|
|
|
4745
4520
|
return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
|
|
4746
4521
|
}
|
|
4747
4522
|
function readEslintIgnores(cwd) {
|
|
4748
|
-
const config = ESLINT_CONFIG_FILES.map((name) =>
|
|
4523
|
+
const config = ESLINT_CONFIG_FILES.map((name) => join23(cwd, name)).find((path) => existsSync20(path));
|
|
4749
4524
|
if (!config) return { patterns: [], unresolved: [] };
|
|
4750
4525
|
let source;
|
|
4751
4526
|
try {
|
|
4752
|
-
source =
|
|
4527
|
+
source = readFileSync17(config, "utf8");
|
|
4753
4528
|
} catch {
|
|
4754
4529
|
return { patterns: [], unresolved: [] };
|
|
4755
4530
|
}
|
|
@@ -4780,16 +4555,6 @@ function readEslintIgnores(cwd) {
|
|
|
4780
4555
|
});
|
|
4781
4556
|
return { patterns: [...patterns], unresolved: [...unresolved] };
|
|
4782
4557
|
}
|
|
4783
|
-
function walk(node, visit) {
|
|
4784
|
-
if (!node || typeof node !== "object") return;
|
|
4785
|
-
const current = node;
|
|
4786
|
-
if (typeof current.type === "string") visit(current);
|
|
4787
|
-
for (const key of Object.keys(current)) {
|
|
4788
|
-
const value = current[key];
|
|
4789
|
-
if (Array.isArray(value)) for (const entry of value) walk(entry, visit);
|
|
4790
|
-
else if (value && typeof value === "object") walk(value, visit);
|
|
4791
|
-
}
|
|
4792
|
-
}
|
|
4793
4558
|
function calleeName(node) {
|
|
4794
4559
|
const callee = node.callee;
|
|
4795
4560
|
return callee?.type === "Identifier" ? callee.name : void 0;
|
|
@@ -4850,8 +4615,8 @@ function lintPresetFor(preset) {
|
|
|
4850
4615
|
}
|
|
4851
4616
|
|
|
4852
4617
|
// src/roles/lint/rename-suppressions.ts
|
|
4853
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
4854
|
-
import { join as
|
|
4618
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync18, statSync as statSync3 } from "fs";
|
|
4619
|
+
import { join as join24, relative as relative3 } from "path";
|
|
4855
4620
|
var SOURCE_EXTENSIONS = [
|
|
4856
4621
|
".ts",
|
|
4857
4622
|
".tsx",
|
|
@@ -4898,7 +4663,7 @@ function* sourceFiles(dir) {
|
|
|
4898
4663
|
return;
|
|
4899
4664
|
}
|
|
4900
4665
|
for (const entry of entries) {
|
|
4901
|
-
const full =
|
|
4666
|
+
const full = join24(dir, entry);
|
|
4902
4667
|
let isDirectory;
|
|
4903
4668
|
try {
|
|
4904
4669
|
isDirectory = statSync3(full).isDirectory();
|
|
@@ -4918,7 +4683,7 @@ function findSuppressionRenames(cwd, renames) {
|
|
|
4918
4683
|
for (const file of sourceFiles(cwd)) {
|
|
4919
4684
|
let content;
|
|
4920
4685
|
try {
|
|
4921
|
-
content =
|
|
4686
|
+
content = readFileSync18(file, "utf8");
|
|
4922
4687
|
} catch {
|
|
4923
4688
|
continue;
|
|
4924
4689
|
}
|
|
@@ -4942,7 +4707,7 @@ function findSuppressionRenames(cwd, renames) {
|
|
|
4942
4707
|
}
|
|
4943
4708
|
if (hits.length === 0) return;
|
|
4944
4709
|
found.push({
|
|
4945
|
-
file:
|
|
4710
|
+
file: relative3(cwd, file),
|
|
4946
4711
|
line: index + 1,
|
|
4947
4712
|
from: line,
|
|
4948
4713
|
to: line.replace(rules, renamed.join(", ")),
|
|
@@ -5046,7 +4811,7 @@ function plan2(context) {
|
|
|
5046
4811
|
keys: removableDeps
|
|
5047
4812
|
});
|
|
5048
4813
|
}
|
|
5049
|
-
const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) =>
|
|
4814
|
+
const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync21(join25(context.cwd, name)));
|
|
5050
4815
|
for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
|
|
5051
4816
|
operations.push(
|
|
5052
4817
|
...nxTargetOperations({
|
|
@@ -5144,7 +4909,7 @@ function lintScripts(cwd) {
|
|
|
5144
4909
|
function committedExtends(cwd) {
|
|
5145
4910
|
try {
|
|
5146
4911
|
const parsed = parseJsonc(
|
|
5147
|
-
|
|
4912
|
+
readFileSync19(join25(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5148
4913
|
LINT_CONFIG_FILE
|
|
5149
4914
|
);
|
|
5150
4915
|
return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
|
|
@@ -5155,7 +4920,7 @@ function committedExtends(cwd) {
|
|
|
5155
4920
|
function committedIgnorePatterns(cwd) {
|
|
5156
4921
|
try {
|
|
5157
4922
|
const parsed = parseJsonc(
|
|
5158
|
-
|
|
4923
|
+
readFileSync19(join25(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5159
4924
|
LINT_CONFIG_FILE
|
|
5160
4925
|
);
|
|
5161
4926
|
return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
|
|
@@ -5167,7 +4932,7 @@ function moduleEslintDependencies(cwd) {
|
|
|
5167
4932
|
const isEslintPackage = (name) => name === "eslint" || name === "@types/eslint" || name === "typescript-eslint" || name.startsWith("@typescript-eslint/") || name.startsWith("eslint-plugin-") || name.startsWith("eslint-config-") || name.startsWith("@eslint/");
|
|
5168
4933
|
let manifest;
|
|
5169
4934
|
try {
|
|
5170
|
-
manifest = JSON.parse(
|
|
4935
|
+
manifest = JSON.parse(readFileSync19(join25(cwd, "package.json"), "utf8"));
|
|
5171
4936
|
} catch {
|
|
5172
4937
|
return [];
|
|
5173
4938
|
}
|
|
@@ -5301,23 +5066,23 @@ ${result.stderr ?? ""}`);
|
|
|
5301
5066
|
* build the developer can see, rather than a silent half-adoption they cannot.
|
|
5302
5067
|
*/
|
|
5303
5068
|
writeModuleBaseline(ctx, oxlint, env) {
|
|
5304
|
-
const configPath =
|
|
5305
|
-
const baselinePath =
|
|
5306
|
-
if (!
|
|
5069
|
+
const configPath = join26(ctx.cwd, LINT_CONFIG_FILE);
|
|
5070
|
+
const baselinePath = join26(ctx.cwd, LINT_BASELINE_FILE);
|
|
5071
|
+
if (!existsSync22(configPath)) return;
|
|
5307
5072
|
let config;
|
|
5308
5073
|
try {
|
|
5309
5074
|
config = parseJsonc(
|
|
5310
|
-
|
|
5075
|
+
readFileSync20(configPath, "utf8"),
|
|
5311
5076
|
LINT_CONFIG_FILE
|
|
5312
5077
|
);
|
|
5313
5078
|
} catch {
|
|
5314
5079
|
return;
|
|
5315
5080
|
}
|
|
5316
5081
|
const current = Array.isArray(config.extends) ? config.extends : [];
|
|
5317
|
-
const measurePath =
|
|
5082
|
+
const measurePath = join26(ctx.cwd, LINT_MEASURE_FILE);
|
|
5318
5083
|
let measured;
|
|
5319
5084
|
try {
|
|
5320
|
-
|
|
5085
|
+
writeFileSync2(
|
|
5321
5086
|
measurePath,
|
|
5322
5087
|
`${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
|
|
5323
5088
|
`
|
|
@@ -5349,7 +5114,7 @@ ${result.stderr ?? ""}`);
|
|
|
5349
5114
|
const holds = holdsFrom(errorCountsByConfigRule(measured.stdout ?? ""));
|
|
5350
5115
|
const next = extendsWithBaseline(current, holds.length > 0);
|
|
5351
5116
|
if (holds.length > 0) {
|
|
5352
|
-
|
|
5117
|
+
writeFileSync2(baselinePath, renderBaseline(holds, readOwnPackage().version));
|
|
5353
5118
|
process.stderr.write(` ${palette(process.stderr).warn(describeHolds(holds))}
|
|
5354
5119
|
`);
|
|
5355
5120
|
} else {
|
|
@@ -5357,12 +5122,12 @@ ${result.stderr ?? ""}`);
|
|
|
5357
5122
|
}
|
|
5358
5123
|
if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
|
|
5359
5124
|
config.extends = next;
|
|
5360
|
-
|
|
5125
|
+
writeFileSync2(configPath, `${JSON.stringify(config, null, 2)}
|
|
5361
5126
|
`);
|
|
5362
5127
|
}
|
|
5363
5128
|
}
|
|
5364
5129
|
async run(ctx) {
|
|
5365
|
-
if (!
|
|
5130
|
+
if (!existsSync22(join26(ctx.cwd, LINT_CONFIG_FILE))) {
|
|
5366
5131
|
process.stderr.write(
|
|
5367
5132
|
`sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
|
|
5368
5133
|
`
|
|
@@ -5510,7 +5275,7 @@ ${result.stderr ?? ""}`);
|
|
|
5510
5275
|
let stub;
|
|
5511
5276
|
try {
|
|
5512
5277
|
stub = parseJsonc(
|
|
5513
|
-
|
|
5278
|
+
readFileSync20(join26(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5514
5279
|
LINT_CONFIG_FILE
|
|
5515
5280
|
);
|
|
5516
5281
|
} catch {
|
|
@@ -5520,7 +5285,7 @@ ${result.stderr ?? ""}`);
|
|
|
5520
5285
|
for (const entry of stub.extends ?? []) {
|
|
5521
5286
|
try {
|
|
5522
5287
|
const preset = parseJsonc(
|
|
5523
|
-
|
|
5288
|
+
readFileSync20(join26(cwd, entry), "utf8"),
|
|
5524
5289
|
entry
|
|
5525
5290
|
);
|
|
5526
5291
|
for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
|
|
@@ -5590,14 +5355,14 @@ ${result.stderr ?? ""}`);
|
|
|
5590
5355
|
let parsed;
|
|
5591
5356
|
try {
|
|
5592
5357
|
parsed = parseJsonc(
|
|
5593
|
-
|
|
5358
|
+
readFileSync20(join26(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5594
5359
|
LINT_CONFIG_FILE
|
|
5595
5360
|
);
|
|
5596
5361
|
} catch {
|
|
5597
5362
|
return void 0;
|
|
5598
5363
|
}
|
|
5599
5364
|
const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
|
|
5600
|
-
return targets.find((target) => !
|
|
5365
|
+
return targets.find((target) => !existsSync22(join26(cwd, target)));
|
|
5601
5366
|
}
|
|
5602
5367
|
/** Announce what is not enforced, so reduced coverage is never silent. */
|
|
5603
5368
|
announceDisabled(preset) {
|
|
@@ -5627,89 +5392,3140 @@ function registerLint() {
|
|
|
5627
5392
|
setDefaultRunner("lint", "oxlint");
|
|
5628
5393
|
}
|
|
5629
5394
|
|
|
5630
|
-
// src/roles/
|
|
5395
|
+
// src/roles/test/adapters/vitest/vitest.adapter.ts
|
|
5631
5396
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
5632
|
-
import { existsSync as existsSync25, readFileSync as readFileSync24 } from "fs";
|
|
5633
|
-
import { createRequire as createRequire4 } from "module";
|
|
5634
|
-
import { join as join30 } from "path";
|
|
5635
5397
|
|
|
5636
|
-
// src/roles/
|
|
5637
|
-
var
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5398
|
+
// src/roles/test/presets/toolchain.json
|
|
5399
|
+
var toolchain_default2 = {
|
|
5400
|
+
$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.",
|
|
5401
|
+
owned: [
|
|
5402
|
+
{
|
|
5403
|
+
package: "vitest",
|
|
5404
|
+
specifier: "vitest/config",
|
|
5405
|
+
named: ["defineConfig", "mergeConfig"],
|
|
5406
|
+
types: ["ConfigEnv", "TestUserConfig", "ViteUserConfig"],
|
|
5407
|
+
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."
|
|
5408
|
+
},
|
|
5409
|
+
{
|
|
5410
|
+
package: "vite",
|
|
5411
|
+
specifier: "vite",
|
|
5412
|
+
named: ["loadEnv"],
|
|
5413
|
+
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."
|
|
5414
|
+
}
|
|
5415
|
+
]
|
|
5651
5416
|
};
|
|
5652
5417
|
|
|
5653
|
-
// src/roles/
|
|
5654
|
-
var
|
|
5655
|
-
compilerOptions: {
|
|
5656
|
-
module: "esnext",
|
|
5657
|
-
moduleResolution: "bundler",
|
|
5658
|
-
types: ["node"],
|
|
5659
|
-
declaration: true
|
|
5660
|
-
},
|
|
5661
|
-
notes: {
|
|
5662
|
-
module: "esnext/bundler like the react and nest presets, matching a monorepo whose libs are bundler-resolved in practice. NOT NodeNext: that would force its stricter ESM rules onto libs the repo resolves through a bundler"
|
|
5663
|
-
}
|
|
5664
|
-
};
|
|
5418
|
+
// src/roles/test/preset-data.ts
|
|
5419
|
+
var OWNED_TEST_PACKAGES = toolchain_default2.owned;
|
|
5665
5420
|
|
|
5666
|
-
// src/roles/
|
|
5667
|
-
var
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
lib: ["ES2023", "DOM", "DOM.Iterable"],
|
|
5671
|
-
module: "ESNext",
|
|
5672
|
-
moduleResolution: "bundler",
|
|
5673
|
-
jsx: "react-jsx",
|
|
5674
|
-
useDefineForClassFields: true,
|
|
5675
|
-
isolatedModules: true,
|
|
5676
|
-
moduleDetection: "force",
|
|
5677
|
-
noEmit: true,
|
|
5678
|
-
noUncheckedSideEffectImports: true
|
|
5679
|
-
},
|
|
5680
|
-
notes: {
|
|
5681
|
-
noEmit: "vite owns the build; tsc only typechecks here"
|
|
5682
|
-
}
|
|
5421
|
+
// src/roles/test/config-policy.ts
|
|
5422
|
+
var TEST_PRESET_SPECIFIERS = {
|
|
5423
|
+
react: "@hublo/sentinel/test/react",
|
|
5424
|
+
nest: "@hublo/sentinel/test/nest"
|
|
5683
5425
|
};
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5426
|
+
function testPresetSpecifier(preset) {
|
|
5427
|
+
return TEST_PRESET_SPECIFIERS[preset] ?? TEST_PRESET_SPECIFIERS.react;
|
|
5428
|
+
}
|
|
5429
|
+
var ALL_TEST_PRESET_SPECIFIERS = Object.values(TEST_PRESET_SPECIFIERS);
|
|
5430
|
+
var TEST_PRESET_SPECIFIER = TEST_PRESET_SPECIFIERS.react;
|
|
5431
|
+
var TEST_CONFIG_FILES = [
|
|
5432
|
+
"vitest.config.ts",
|
|
5433
|
+
"vitest.config.mts",
|
|
5434
|
+
"vitest.config.js",
|
|
5435
|
+
"vitest.config.mjs"
|
|
5436
|
+
];
|
|
5437
|
+
var TEST_OWNED_PACKAGES = OWNED_TEST_PACKAGES;
|
|
5438
|
+
var SENTINEL_OWNED_TEST_PACKAGES = TEST_OWNED_PACKAGES.filter(
|
|
5439
|
+
(entry) => entry.package !== "vite"
|
|
5440
|
+
).map((entry) => entry.package);
|
|
5441
|
+
function testToolchain(preset) {
|
|
5442
|
+
return { presetSpecifier: testPresetSpecifier(preset), owned: TEST_OWNED_PACKAGES };
|
|
5443
|
+
}
|
|
5444
|
+
var TEST_SCRIPT_NAME = "test";
|
|
5445
|
+
var SENTINEL_TEST_COMMAND = "sentinel --run --test --";
|
|
5446
|
+
var VITEST_INVOCATION = /(^|&&\s*|\|\|\s*|;\s*)(?:npx\s+|pnpm\s+(?:exec\s+|dlx\s+)?)?vitest\b([^&|;]*)/g;
|
|
5447
|
+
function rewriteVitestScript(command) {
|
|
5448
|
+
return command.replace(VITEST_INVOCATION, (_match, lead, tail) => {
|
|
5449
|
+
const args = tail.trim().split(/\s+/).filter(Boolean);
|
|
5450
|
+
const oneShot = args[0] === "run" || args.includes("--run");
|
|
5451
|
+
const rest = args.filter((arg, index) => !(index === 0 && arg === "run") && arg !== "--run");
|
|
5452
|
+
const passthrough = oneShot ? rest : ["--watch", ...rest];
|
|
5453
|
+
const suffix = passthrough.length > 0 ? ` -- ${passthrough.join(" ")}` : "";
|
|
5454
|
+
const trailing = /\s$/.test(tail) ? " " : "";
|
|
5455
|
+
return `${lead}sentinel --run --test${suffix}${trailing}`;
|
|
5456
|
+
});
|
|
5457
|
+
}
|
|
5458
|
+
function testTarget(options) {
|
|
5459
|
+
const { name, configFiles, outputs, dependsOn, configurations } = options;
|
|
5460
|
+
const cached = outputs !== void 0 && outputs.length > 0;
|
|
5461
|
+
return {
|
|
5462
|
+
[name ?? TEST_SCRIPT_NAME]: {
|
|
5463
|
+
cache: cached,
|
|
5464
|
+
inputs: ["default", "^default", ...configFiles.map((name2) => `{projectRoot}/${name2}`)],
|
|
5465
|
+
...cached ? { outputs: [...outputs] } : {},
|
|
5466
|
+
...dependsOn !== void 0 && dependsOn.length > 0 ? { dependsOn: [...dependsOn] } : {},
|
|
5467
|
+
...configurations !== void 0 && Object.keys(configurations).length > 0 ? { configurations: { ...configurations } } : {}
|
|
5707
5468
|
}
|
|
5708
|
-
}
|
|
5709
|
-
}
|
|
5469
|
+
};
|
|
5470
|
+
}
|
|
5710
5471
|
|
|
5711
|
-
// src/roles/
|
|
5712
|
-
|
|
5472
|
+
// src/roles/test/jest-migration.ts
|
|
5473
|
+
import { existsSync as existsSync28, readFileSync as readFileSync28 } from "fs";
|
|
5474
|
+
import { join as join35 } from "path";
|
|
5475
|
+
|
|
5476
|
+
// src/roles/test/codemod/migrate-module.ts
|
|
5477
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync25, statSync as statSync6 } from "fs";
|
|
5478
|
+
import { join as join31 } from "path";
|
|
5479
|
+
import { parseSync as parseSync8 } from "oxc-parser";
|
|
5480
|
+
|
|
5481
|
+
// src/roles/test/read-adoption.ts
|
|
5482
|
+
import { existsSync as existsSync23, readFileSync as readFileSync21, readdirSync as readdirSync3 } from "fs";
|
|
5483
|
+
import { join as join27, relative as relative4 } from "path";
|
|
5484
|
+
var JEST_CONFIG_FILES = [
|
|
5485
|
+
"jest.config.ts",
|
|
5486
|
+
"jest.config.js",
|
|
5487
|
+
"jest.prisma.config.ts",
|
|
5488
|
+
"jest.functional.config.ts",
|
|
5489
|
+
"jest.integration-config.ts"
|
|
5490
|
+
];
|
|
5491
|
+
function testConfigFile(cwd) {
|
|
5492
|
+
return TEST_CONFIG_FILES.find((name) => existsSync23(join27(cwd, name)));
|
|
5493
|
+
}
|
|
5494
|
+
function jestConfigFiles(cwd) {
|
|
5495
|
+
return JEST_CONFIG_FILES.filter((name) => existsSync23(join27(cwd, name)));
|
|
5496
|
+
}
|
|
5497
|
+
function rootJestConfigFiles(cwd) {
|
|
5498
|
+
const root = findWorkspaceRoot(cwd);
|
|
5499
|
+
if (root === void 0 || root === cwd) return [];
|
|
5500
|
+
return JEST_CONFIG_FILES.filter((name) => existsSync23(join27(root, name)));
|
|
5501
|
+
}
|
|
5502
|
+
var TEST_FILE = /[.-](spec|test)\.[cm]?[jt]sx?$/;
|
|
5503
|
+
var NOT_WALKED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo"]);
|
|
5504
|
+
function hasTestFiles(cwd) {
|
|
5505
|
+
return walkTestFiles(cwd, true).length > 0;
|
|
5506
|
+
}
|
|
5507
|
+
function listTestFiles(cwd) {
|
|
5508
|
+
return walkTestFiles(cwd, false).sort();
|
|
5509
|
+
}
|
|
5510
|
+
function walkTestFiles(cwd, stopAtFirst) {
|
|
5511
|
+
const found = [];
|
|
5512
|
+
const stack = [cwd];
|
|
5513
|
+
while (stack.length > 0) {
|
|
5514
|
+
const dir = stack.pop();
|
|
5515
|
+
let entries;
|
|
5516
|
+
try {
|
|
5517
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
5518
|
+
} catch {
|
|
5519
|
+
continue;
|
|
5520
|
+
}
|
|
5521
|
+
for (const entry of entries) {
|
|
5522
|
+
if (entry.isDirectory()) {
|
|
5523
|
+
if (!NOT_WALKED.has(entry.name)) stack.push(join27(dir, entry.name));
|
|
5524
|
+
} else if (TEST_FILE.test(entry.name)) {
|
|
5525
|
+
found.push(relative4(cwd, join27(dir, entry.name)).replaceAll("\\", "/"));
|
|
5526
|
+
if (stopAtFirst) return found;
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
return found;
|
|
5531
|
+
}
|
|
5532
|
+
function readTestConfigSource(cwd, fileName) {
|
|
5533
|
+
try {
|
|
5534
|
+
return readFileSync21(join27(cwd, fileName), "utf8");
|
|
5535
|
+
} catch {
|
|
5536
|
+
return "";
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
function importsPreset2(source) {
|
|
5540
|
+
return ALL_TEST_PRESET_SPECIFIERS.some((specifier) => {
|
|
5541
|
+
const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5542
|
+
return new RegExp(String.raw`(?:from|import)\s*\(?\s*['"]${escaped}['"]`).test(source);
|
|
5543
|
+
});
|
|
5544
|
+
}
|
|
5545
|
+
function readTestAdoption(cwd) {
|
|
5546
|
+
const manifest = readProjectPackageJson(cwd);
|
|
5547
|
+
const jestConfigs = jestConfigFiles(cwd);
|
|
5548
|
+
const configFile = testConfigFile(cwd);
|
|
5549
|
+
const declared = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
5550
|
+
const ownDeclarations = ["vitest", "jest", "@nx/jest", "jest-mock-extended", "@swc/jest"].filter((name) => declared[name] !== void 0).map((name) => ({ name, version: declared[name] }));
|
|
5551
|
+
if (configFile === void 0) {
|
|
5552
|
+
const hasTests = hasTestFiles(cwd);
|
|
5553
|
+
const inheritsRootConfig = hasTests && jestConfigs.length === 0 && rootJestConfigFiles(cwd).length > 0;
|
|
5554
|
+
return {
|
|
5555
|
+
configFile: null,
|
|
5556
|
+
preset: null,
|
|
5557
|
+
adopted: false,
|
|
5558
|
+
conformant: false,
|
|
5559
|
+
drift: [],
|
|
5560
|
+
unreadable: null,
|
|
5561
|
+
state: hasTests ? "jest" : "no-tests",
|
|
5562
|
+
runner: hasTests ? "jest" : "none",
|
|
5563
|
+
jestConfigs,
|
|
5564
|
+
inheritsRootConfig,
|
|
5565
|
+
ownDeclarations
|
|
5566
|
+
};
|
|
5567
|
+
}
|
|
5568
|
+
const source = readTestConfigSource(cwd, configFile);
|
|
5569
|
+
if (source === "") {
|
|
5570
|
+
return {
|
|
5571
|
+
configFile,
|
|
5572
|
+
preset: null,
|
|
5573
|
+
adopted: false,
|
|
5574
|
+
conformant: false,
|
|
5575
|
+
drift: [],
|
|
5576
|
+
unreadable: `${configFile} could not be read`,
|
|
5577
|
+
state: "vitest-own-toolchain",
|
|
5578
|
+
runner: "vitest",
|
|
5579
|
+
jestConfigs,
|
|
5580
|
+
inheritsRootConfig: false,
|
|
5581
|
+
// it has a config of its own, readable or not
|
|
5582
|
+
ownDeclarations
|
|
5583
|
+
};
|
|
5584
|
+
}
|
|
5585
|
+
const adopted = importsPreset2(source);
|
|
5586
|
+
const script = moduleScripts(cwd)["test"];
|
|
5587
|
+
return {
|
|
5588
|
+
configFile,
|
|
5589
|
+
preset: adopted ? "react" : null,
|
|
5590
|
+
adopted,
|
|
5591
|
+
conformant: adopted && script !== void 0 && jestConfigs.length === 0,
|
|
5592
|
+
drift: [],
|
|
5593
|
+
unreadable: null,
|
|
5594
|
+
state: adopted ? "adopted" : "vitest-own-toolchain",
|
|
5595
|
+
runner: "vitest",
|
|
5596
|
+
jestConfigs,
|
|
5597
|
+
inheritsRootConfig: false,
|
|
5598
|
+
// local always wins: this module is out of the root's reach
|
|
5599
|
+
ownDeclarations
|
|
5600
|
+
};
|
|
5601
|
+
}
|
|
5602
|
+
|
|
5603
|
+
// src/roles/test/codemod/migrate-file.ts
|
|
5604
|
+
import { parseSync as parseSync7 } from "oxc-parser";
|
|
5605
|
+
|
|
5606
|
+
// src/roles/test/codemod/assertion-return-value.ts
|
|
5607
|
+
var MEMBER_TYPES = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
5608
|
+
function rootedAtExpect(node) {
|
|
5609
|
+
let current = node;
|
|
5610
|
+
for (let depth = 0; current !== void 0 && depth < 12; depth += 1) {
|
|
5611
|
+
if (current.type === "CallExpression") {
|
|
5612
|
+
const callee = current.callee;
|
|
5613
|
+
if (callee?.type === "Identifier") return callee.name === "expect";
|
|
5614
|
+
current = callee;
|
|
5615
|
+
continue;
|
|
5616
|
+
}
|
|
5617
|
+
if (MEMBER_TYPES.has(current.type)) {
|
|
5618
|
+
current = current.object;
|
|
5619
|
+
continue;
|
|
5620
|
+
}
|
|
5621
|
+
return false;
|
|
5622
|
+
}
|
|
5623
|
+
return false;
|
|
5624
|
+
}
|
|
5625
|
+
function capture(node) {
|
|
5626
|
+
if (node.type !== "VariableDeclaration") return void 0;
|
|
5627
|
+
const declarations = node.declarations ?? [];
|
|
5628
|
+
if (declarations.length !== 1) return void 0;
|
|
5629
|
+
const only = declarations[0];
|
|
5630
|
+
const id = only.id;
|
|
5631
|
+
const init = only.init;
|
|
5632
|
+
if (id?.type !== "Identifier" || init?.type !== "AwaitExpression") return void 0;
|
|
5633
|
+
if (!rootedAtExpect(init.argument)) return void 0;
|
|
5634
|
+
return { name: id.name, init };
|
|
5635
|
+
}
|
|
5636
|
+
function undefinedAssertionOf(node, name) {
|
|
5637
|
+
if (node.type !== "CallExpression") return false;
|
|
5638
|
+
const callee = node.callee;
|
|
5639
|
+
if (callee === void 0 || !MEMBER_TYPES.has(callee.type)) return false;
|
|
5640
|
+
const property = callee.property;
|
|
5641
|
+
if (property?.type !== "Identifier" || property.name !== "toBeUndefined") return false;
|
|
5642
|
+
const object = callee.object;
|
|
5643
|
+
if (object?.type !== "CallExpression") return false;
|
|
5644
|
+
if (object.callee?.type !== "Identifier") return false;
|
|
5645
|
+
if (object.callee.name !== "expect") return false;
|
|
5646
|
+
const [argument] = object.arguments ?? [];
|
|
5647
|
+
return argument?.type === "Identifier" && argument.name === name;
|
|
5648
|
+
}
|
|
5649
|
+
function rewriteAssertionReturnValue(source, program) {
|
|
5650
|
+
const captures = [];
|
|
5651
|
+
walk(program, (node) => {
|
|
5652
|
+
const found = capture(node);
|
|
5653
|
+
if (found === void 0) return;
|
|
5654
|
+
captures.push({
|
|
5655
|
+
name: found.name,
|
|
5656
|
+
declarationStart: node.start,
|
|
5657
|
+
initStart: found.init.start
|
|
5658
|
+
});
|
|
5659
|
+
});
|
|
5660
|
+
const notReferences = [];
|
|
5661
|
+
walk(program, (node) => {
|
|
5662
|
+
if (node.type === "Property" || node.type === "ObjectProperty" || node.type === "PropertyDefinition") {
|
|
5663
|
+
if (node.computed === true) return;
|
|
5664
|
+
const key = node.key;
|
|
5665
|
+
if (key !== void 0)
|
|
5666
|
+
notReferences.push({ start: key.start, end: key.end });
|
|
5667
|
+
return;
|
|
5668
|
+
}
|
|
5669
|
+
if (MEMBER_TYPES.has(node.type) && node.computed !== true) {
|
|
5670
|
+
const property = node.property;
|
|
5671
|
+
if (property !== void 0) {
|
|
5672
|
+
notReferences.push({ start: property.start, end: property.end });
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
});
|
|
5676
|
+
const functions = [];
|
|
5677
|
+
walk(program, (node) => {
|
|
5678
|
+
if (/Function|ArrowFunctionExpression/.test(node.type)) functions.push(node);
|
|
5679
|
+
});
|
|
5680
|
+
const scopeOf = (position) => {
|
|
5681
|
+
const containing = functions.filter((fn) => position >= fn.start && position <= fn.end).sort(
|
|
5682
|
+
(a, b) => a.end - a.start - (b.end - b.start)
|
|
5683
|
+
);
|
|
5684
|
+
const nearest = containing[0];
|
|
5685
|
+
return nearest === void 0 ? { start: 0, end: source.length } : { start: nearest.start, end: nearest.end };
|
|
5686
|
+
};
|
|
5687
|
+
const edits = [];
|
|
5688
|
+
const refusals = [];
|
|
5689
|
+
for (const found of captures) {
|
|
5690
|
+
const readings = [];
|
|
5691
|
+
let readElsewhere = false;
|
|
5692
|
+
const scope = scopeOf(found.declarationStart);
|
|
5693
|
+
walk(program, (node) => {
|
|
5694
|
+
const start = node.start;
|
|
5695
|
+
if (start < scope.start || node.end > scope.end) return;
|
|
5696
|
+
if (undefinedAssertionOf(node, found.name)) {
|
|
5697
|
+
readings.push(node);
|
|
5698
|
+
return;
|
|
5699
|
+
}
|
|
5700
|
+
if (node.type !== "Identifier" || node.name !== found.name) return;
|
|
5701
|
+
if (notReferences.some((span) => start >= span.start && node.end <= span.end)) {
|
|
5702
|
+
return;
|
|
5703
|
+
}
|
|
5704
|
+
const inside = readings.some(
|
|
5705
|
+
(reading) => start >= reading.start && node.end <= reading.end
|
|
5706
|
+
);
|
|
5707
|
+
const isBinding = start < found.initStart;
|
|
5708
|
+
if (!inside && !isBinding) readElsewhere = true;
|
|
5709
|
+
});
|
|
5710
|
+
if (readings.length === 0 || readElsewhere) {
|
|
5711
|
+
if (readings.length > 0) refusals.push(found.name);
|
|
5712
|
+
continue;
|
|
5713
|
+
}
|
|
5714
|
+
edits.push({ start: found.declarationStart, end: found.initStart, text: "" });
|
|
5715
|
+
for (const reading of readings) {
|
|
5716
|
+
const from = reading.start;
|
|
5717
|
+
let to = reading.end;
|
|
5718
|
+
while (to < source.length && /[\s;]/.test(source[to])) to += 1;
|
|
5719
|
+
let head = from;
|
|
5720
|
+
while (head > 0 && /[ \t]/.test(source[head - 1])) head -= 1;
|
|
5721
|
+
if (head > 0 && source[head - 1] === "\n") head -= 1;
|
|
5722
|
+
edits.push({ start: head, end: to, text: "" });
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
let result = source;
|
|
5726
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
5727
|
+
result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
|
|
5728
|
+
}
|
|
5729
|
+
return { source: result, rewritten: captures.length - refusals.length, refusals };
|
|
5730
|
+
}
|
|
5731
|
+
|
|
5732
|
+
// src/roles/test/codemod/constructible-mocks.ts
|
|
5733
|
+
var MEMBER_TYPES2 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
5734
|
+
function mockImplementationArrow(node) {
|
|
5735
|
+
if (node.type !== "CallExpression") return void 0;
|
|
5736
|
+
const callee = node.callee;
|
|
5737
|
+
if (callee === void 0 || !MEMBER_TYPES2.has(callee.type)) return void 0;
|
|
5738
|
+
const property = callee.property;
|
|
5739
|
+
if (property?.type !== "Identifier" || property.name !== "mockImplementation") return void 0;
|
|
5740
|
+
const [argument] = node.arguments ?? [];
|
|
5741
|
+
return argument?.type === "ArrowFunctionExpression" ? argument : void 0;
|
|
5742
|
+
}
|
|
5743
|
+
function assertedMockedClass(source, node) {
|
|
5744
|
+
if (node.type !== "CallExpression") return false;
|
|
5745
|
+
const callee = node.callee;
|
|
5746
|
+
if (callee === void 0 || !MEMBER_TYPES2.has(callee.type)) return false;
|
|
5747
|
+
const object = callee.object;
|
|
5748
|
+
if (object === void 0) return false;
|
|
5749
|
+
return /\bMockedClass\b/.test(source.slice(object.start, object.end));
|
|
5750
|
+
}
|
|
5751
|
+
function readsThis(node) {
|
|
5752
|
+
let found = false;
|
|
5753
|
+
walk(node, (inner) => {
|
|
5754
|
+
if (inner.type === "ThisExpression") found = true;
|
|
5755
|
+
});
|
|
5756
|
+
return found;
|
|
5757
|
+
}
|
|
5758
|
+
function rewriteConstructibleMocks(source, program, factories) {
|
|
5759
|
+
const edits = [];
|
|
5760
|
+
const refusals = [];
|
|
5761
|
+
let rewritten = 0;
|
|
5762
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5763
|
+
const subtrees = [...factories];
|
|
5764
|
+
walk(program, (node) => {
|
|
5765
|
+
if (assertedMockedClass(source, node)) subtrees.push(node);
|
|
5766
|
+
});
|
|
5767
|
+
for (const subtree of subtrees) {
|
|
5768
|
+
walk(subtree, (node) => {
|
|
5769
|
+
const arrow = mockImplementationArrow(node);
|
|
5770
|
+
if (arrow === void 0 || seen.has(arrow)) return;
|
|
5771
|
+
seen.add(arrow);
|
|
5772
|
+
if (readsThis(arrow)) {
|
|
5773
|
+
refusals.push({
|
|
5774
|
+
binding: "mockImplementation",
|
|
5775
|
+
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.`
|
|
5776
|
+
});
|
|
5777
|
+
return;
|
|
5778
|
+
}
|
|
5779
|
+
const params = arrow.params ?? [];
|
|
5780
|
+
const paramsText = params.length === 0 ? "" : source.slice(params[0]?.start, params[params.length - 1]?.end);
|
|
5781
|
+
const body = arrow.body;
|
|
5782
|
+
const bodyText = source.slice(body.start, body.end);
|
|
5783
|
+
const text = body.type === "BlockStatement" ? `function (${paramsText}) ${bodyText}` : `function (${paramsText}) {
|
|
5784
|
+
return ${bodyText}
|
|
5785
|
+
}`;
|
|
5786
|
+
edits.push({ start: arrow.start, end: arrow.end, text });
|
|
5787
|
+
rewritten += 1;
|
|
5788
|
+
});
|
|
5789
|
+
}
|
|
5790
|
+
let result = source;
|
|
5791
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
5792
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
5793
|
+
}
|
|
5794
|
+
return { source: result, rewritten, refusals };
|
|
5795
|
+
}
|
|
5796
|
+
|
|
5797
|
+
// src/roles/test/codemod/deep-imports.ts
|
|
5798
|
+
var DEEP_ENTRIES = {
|
|
5799
|
+
"jest-mock-extended/lib/Mock": { root: "jest-mock-extended", defaultExport: "mock" }
|
|
5800
|
+
};
|
|
5801
|
+
function normaliseDeepImports(source) {
|
|
5802
|
+
let result = source;
|
|
5803
|
+
let rewritten = 0;
|
|
5804
|
+
for (const [deep, { root, defaultExport }] of Object.entries(DEEP_ENTRIES)) {
|
|
5805
|
+
const quoted = deep.replaceAll("/", "\\/");
|
|
5806
|
+
const defaultImport = new RegExp(`import\\s+(\\w+)\\s+from\\s+(['"])${quoted}\\2`, "g");
|
|
5807
|
+
result = result.replace(defaultImport, (_whole, binding, quote) => {
|
|
5808
|
+
rewritten += 1;
|
|
5809
|
+
const named = binding === defaultExport ? defaultExport : `${defaultExport} as ${binding}`;
|
|
5810
|
+
return `import { ${named} } from ${quote}${root}${quote}`;
|
|
5811
|
+
});
|
|
5812
|
+
const anyImport = new RegExp(`(from\\s+)(['"])${quoted}\\2`, "g");
|
|
5813
|
+
result = result.replace(anyImport, (_whole, from, quote) => {
|
|
5814
|
+
rewritten += 1;
|
|
5815
|
+
return `${from}${quote}${root}${quote}`;
|
|
5816
|
+
});
|
|
5817
|
+
}
|
|
5818
|
+
return { source: result, rewritten };
|
|
5819
|
+
}
|
|
5820
|
+
|
|
5821
|
+
// src/roles/test/codemod/each-table-rows.ts
|
|
5822
|
+
var MEMBER_TYPES3 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
5823
|
+
var RUNNERS = /* @__PURE__ */ new Set(["it", "test", "describe"]);
|
|
5824
|
+
function eachTable(node) {
|
|
5825
|
+
if (node.type !== "CallExpression") return void 0;
|
|
5826
|
+
const callee = node.callee;
|
|
5827
|
+
if (callee === void 0 || !MEMBER_TYPES3.has(callee.type)) return void 0;
|
|
5828
|
+
const property = callee.property;
|
|
5829
|
+
if (property?.type !== "Identifier" || property.name !== "each") return void 0;
|
|
5830
|
+
let object = callee.object;
|
|
5831
|
+
while (object !== void 0 && MEMBER_TYPES3.has(object.type)) {
|
|
5832
|
+
object = object.object;
|
|
5833
|
+
}
|
|
5834
|
+
if (object?.type !== "Identifier" || !RUNNERS.has(object.name)) return void 0;
|
|
5835
|
+
const [table] = node.arguments ?? [];
|
|
5836
|
+
return table?.type === "ArrayExpression" ? table : void 0;
|
|
5837
|
+
}
|
|
5838
|
+
function rewriteEachTableRows(source, program) {
|
|
5839
|
+
const edits = [];
|
|
5840
|
+
walk(program, (node) => {
|
|
5841
|
+
const table = eachTable(node);
|
|
5842
|
+
if (table === void 0) return;
|
|
5843
|
+
const rows = table.elements ?? [];
|
|
5844
|
+
if (rows.length === 0 || rows.some((row) => row === null)) return;
|
|
5845
|
+
const arrays = rows.filter((row) => row.type === "ArrayExpression").length;
|
|
5846
|
+
if (arrays === 0 || arrays === rows.length) return;
|
|
5847
|
+
const wrapped = rows.map((row) => source.slice(row.start, row.end)).map((text) => `[${text}]`).join(", ");
|
|
5848
|
+
edits.push({ start: table.start, end: table.end, text: `[${wrapped}]` });
|
|
5849
|
+
});
|
|
5850
|
+
let result = source;
|
|
5851
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
5852
|
+
result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
|
|
5853
|
+
}
|
|
5854
|
+
return { source: result, rewritten: edits.length };
|
|
5855
|
+
}
|
|
5856
|
+
|
|
5857
|
+
// src/roles/test/codemod/each-title.ts
|
|
5858
|
+
var MEMBER_TYPES4 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
5859
|
+
function eachTitle(node) {
|
|
5860
|
+
if (node.type !== "CallExpression") return void 0;
|
|
5861
|
+
const inner = node.callee;
|
|
5862
|
+
if (inner?.type !== "CallExpression") return void 0;
|
|
5863
|
+
const member = inner.callee;
|
|
5864
|
+
if (member === void 0 || !MEMBER_TYPES4.has(member.type)) return void 0;
|
|
5865
|
+
const property = member.property;
|
|
5866
|
+
if (property?.type !== "Identifier" || property.name !== "each") return void 0;
|
|
5867
|
+
const [title] = node.arguments ?? [];
|
|
5868
|
+
if (title === void 0) return void 0;
|
|
5869
|
+
return title.type === "StringLiteral" || title.type === "Literal" || title.type === "TemplateLiteral" ? title : void 0;
|
|
5870
|
+
}
|
|
5871
|
+
function rewriteSpecifiers(text) {
|
|
5872
|
+
let result = "";
|
|
5873
|
+
let rewritten = 0;
|
|
5874
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
5875
|
+
if (text[index] !== "%") {
|
|
5876
|
+
result += text[index];
|
|
5877
|
+
continue;
|
|
5878
|
+
}
|
|
5879
|
+
const next = text[index + 1];
|
|
5880
|
+
if (next === "%") {
|
|
5881
|
+
result += "%%";
|
|
5882
|
+
index += 1;
|
|
5883
|
+
continue;
|
|
5884
|
+
}
|
|
5885
|
+
if (next === "p") {
|
|
5886
|
+
result += "%j";
|
|
5887
|
+
rewritten += 1;
|
|
5888
|
+
index += 1;
|
|
5889
|
+
continue;
|
|
5890
|
+
}
|
|
5891
|
+
result += "%";
|
|
5892
|
+
}
|
|
5893
|
+
return { text: result, rewritten };
|
|
5894
|
+
}
|
|
5895
|
+
function rewriteEachTitles(source, program) {
|
|
5896
|
+
const edits = [];
|
|
5897
|
+
let rewritten = 0;
|
|
5898
|
+
walk(program, (node) => {
|
|
5899
|
+
const title = eachTitle(node);
|
|
5900
|
+
if (title === void 0) return;
|
|
5901
|
+
const start = title.start;
|
|
5902
|
+
const end = title.end;
|
|
5903
|
+
const replacement = rewriteSpecifiers(source.slice(start, end));
|
|
5904
|
+
if (replacement.rewritten === 0) return;
|
|
5905
|
+
edits.push({ start, end, text: replacement.text });
|
|
5906
|
+
rewritten += replacement.rewritten;
|
|
5907
|
+
});
|
|
5908
|
+
let result = source;
|
|
5909
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
5910
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
5911
|
+
}
|
|
5912
|
+
return { source: result, rewritten };
|
|
5913
|
+
}
|
|
5914
|
+
|
|
5915
|
+
// src/roles/test/codemod/empty-mock-implementation.ts
|
|
5916
|
+
var METHODS = /* @__PURE__ */ new Set(["mockImplementation", "mockImplementationOnce"]);
|
|
5917
|
+
var MEMBER_TYPES5 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
5918
|
+
function rewriteEmptyMockImplementation(source, program) {
|
|
5919
|
+
const insertAt = [];
|
|
5920
|
+
walk(program, (node) => {
|
|
5921
|
+
if (node.type !== "CallExpression") return;
|
|
5922
|
+
if ((node.arguments ?? []).length > 0) return;
|
|
5923
|
+
const callee = node.callee;
|
|
5924
|
+
if (callee === void 0 || !MEMBER_TYPES5.has(callee.type)) return;
|
|
5925
|
+
const property = callee.property;
|
|
5926
|
+
if (property?.type !== "Identifier" || !METHODS.has(property.name)) return;
|
|
5927
|
+
insertAt.push(node.end - 1);
|
|
5928
|
+
});
|
|
5929
|
+
let result = source;
|
|
5930
|
+
for (const position of [...insertAt].sort((left, right) => right - left)) {
|
|
5931
|
+
result = `${result.slice(0, position)}() => undefined${result.slice(position)}`;
|
|
5932
|
+
}
|
|
5933
|
+
return { source: result, rewritten: insertAt.length };
|
|
5934
|
+
}
|
|
5935
|
+
|
|
5936
|
+
// src/roles/test/codemod/fake-timer-options.ts
|
|
5937
|
+
var MEMBER_TYPES6 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
5938
|
+
var TIMER_CALLS = /* @__PURE__ */ new Set(["useFakeTimers"]);
|
|
5939
|
+
function timerOptions(node) {
|
|
5940
|
+
if (node.type !== "CallExpression") return void 0;
|
|
5941
|
+
const callee = node.callee;
|
|
5942
|
+
if (callee === void 0 || !MEMBER_TYPES6.has(callee.type)) return void 0;
|
|
5943
|
+
const object = callee.object;
|
|
5944
|
+
const property = callee.property;
|
|
5945
|
+
if (object?.type !== "Identifier") return void 0;
|
|
5946
|
+
if (object.name !== "jest" && object.name !== "vi") return void 0;
|
|
5947
|
+
if (property?.type !== "Identifier" || !TIMER_CALLS.has(property.name)) return void 0;
|
|
5948
|
+
const [options] = node.arguments ?? [];
|
|
5949
|
+
return options?.type === "ObjectExpression" ? options : void 0;
|
|
5950
|
+
}
|
|
5951
|
+
var FAKEABLE = [
|
|
5952
|
+
"setTimeout",
|
|
5953
|
+
"clearTimeout",
|
|
5954
|
+
"setImmediate",
|
|
5955
|
+
"clearImmediate",
|
|
5956
|
+
"setInterval",
|
|
5957
|
+
"clearInterval",
|
|
5958
|
+
"Date",
|
|
5959
|
+
"hrtime",
|
|
5960
|
+
"requestAnimationFrame",
|
|
5961
|
+
"cancelAnimationFrame",
|
|
5962
|
+
"requestIdleCallback",
|
|
5963
|
+
"cancelIdleCallback",
|
|
5964
|
+
"performance"
|
|
5965
|
+
];
|
|
5966
|
+
function stringLiterals2(node) {
|
|
5967
|
+
if (node.type !== "ArrayExpression") return void 0;
|
|
5968
|
+
const values = [];
|
|
5969
|
+
for (const element of node.elements ?? []) {
|
|
5970
|
+
if (element?.type !== "Literal" || typeof element.value !== "string") return void 0;
|
|
5971
|
+
values.push(element.value);
|
|
5972
|
+
}
|
|
5973
|
+
return values;
|
|
5974
|
+
}
|
|
5975
|
+
function rewriteFakeTimerOptions(source, program) {
|
|
5976
|
+
const edits = [];
|
|
5977
|
+
const translated = [];
|
|
5978
|
+
const refusals = [];
|
|
5979
|
+
walk(program, (node) => {
|
|
5980
|
+
const options = timerOptions(node);
|
|
5981
|
+
if (options === void 0) return;
|
|
5982
|
+
for (const property of options.properties ?? []) {
|
|
5983
|
+
const key = property.key;
|
|
5984
|
+
if (key?.type !== "Identifier") continue;
|
|
5985
|
+
const value = property.value;
|
|
5986
|
+
if (value === void 0) continue;
|
|
5987
|
+
const span = { start: property.start, end: property.end };
|
|
5988
|
+
if (key.name === "advanceTimers") {
|
|
5989
|
+
if (value.type === "Literal" && value.value === true) {
|
|
5990
|
+
edits.push({ ...span, text: "shouldAdvanceTime: true" });
|
|
5991
|
+
translated.push("advanceTimers -> shouldAdvanceTime");
|
|
5992
|
+
continue;
|
|
5993
|
+
}
|
|
5994
|
+
if (value.type === "Literal" && typeof value.value === "number") {
|
|
5995
|
+
edits.push({
|
|
5996
|
+
...span,
|
|
5997
|
+
text: `shouldAdvanceTime: true, advanceTimeDelta: ${value.value}`
|
|
5998
|
+
});
|
|
5999
|
+
translated.push("advanceTimers -> shouldAdvanceTime + advanceTimeDelta");
|
|
6000
|
+
continue;
|
|
6001
|
+
}
|
|
6002
|
+
refusals.push({
|
|
6003
|
+
binding: "advanceTimers",
|
|
6004
|
+
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."
|
|
6005
|
+
});
|
|
6006
|
+
continue;
|
|
6007
|
+
}
|
|
6008
|
+
if (key.name === "doNotFake") {
|
|
6009
|
+
const excluded = stringLiterals2(value);
|
|
6010
|
+
if (excluded === void 0) {
|
|
6011
|
+
refusals.push({
|
|
6012
|
+
binding: "doNotFake",
|
|
6013
|
+
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."
|
|
6014
|
+
});
|
|
6015
|
+
continue;
|
|
6016
|
+
}
|
|
6017
|
+
const kept = FAKEABLE.filter((name) => !excluded.includes(name));
|
|
6018
|
+
edits.push({
|
|
6019
|
+
...span,
|
|
6020
|
+
text: `toFake: [${kept.map((name) => `'${name}'`).join(", ")}]`
|
|
6021
|
+
});
|
|
6022
|
+
translated.push("doNotFake -> toFake");
|
|
6023
|
+
}
|
|
6024
|
+
}
|
|
6025
|
+
});
|
|
6026
|
+
if (edits.length === 0) return { source, translated, refusals };
|
|
6027
|
+
let result = source;
|
|
6028
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
6029
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
6030
|
+
}
|
|
6031
|
+
return { source: result, translated, refusals };
|
|
6032
|
+
}
|
|
6033
|
+
|
|
6034
|
+
// src/roles/test/codemod/scope.ts
|
|
6035
|
+
function readIdentifiers(node, runsLater) {
|
|
6036
|
+
const names = [];
|
|
6037
|
+
walk(node, (current) => {
|
|
6038
|
+
if (current !== node && runsLater?.(current) === true) return SKIP;
|
|
6039
|
+
if (current.type === "Property" || current.type === "ObjectProperty") {
|
|
6040
|
+
if (current.shorthand === true) {
|
|
6041
|
+
names.push(...readIdentifiers(current.value, runsLater));
|
|
6042
|
+
return SKIP;
|
|
6043
|
+
}
|
|
6044
|
+
if (current.computed === true) return;
|
|
6045
|
+
const key = current.key;
|
|
6046
|
+
if (key?.type !== "Identifier") return;
|
|
6047
|
+
names.push(...readIdentifiers(current.value, runsLater));
|
|
6048
|
+
return SKIP;
|
|
6049
|
+
}
|
|
6050
|
+
if (current.type === "MemberExpression" || current.type === "StaticMemberExpression") {
|
|
6051
|
+
if (current.computed === true) return;
|
|
6052
|
+
names.push(...readIdentifiers(current.object, runsLater));
|
|
6053
|
+
return SKIP;
|
|
6054
|
+
}
|
|
6055
|
+
if (current.type === "Identifier") {
|
|
6056
|
+
names.push(current.name);
|
|
6057
|
+
return SKIP;
|
|
6058
|
+
}
|
|
6059
|
+
return void 0;
|
|
6060
|
+
});
|
|
6061
|
+
return names;
|
|
6062
|
+
}
|
|
6063
|
+
function patternNames(pattern) {
|
|
6064
|
+
const names = [];
|
|
6065
|
+
const visit = (node) => {
|
|
6066
|
+
if (node === null || typeof node !== "object") return;
|
|
6067
|
+
const current = node;
|
|
6068
|
+
switch (current.type) {
|
|
6069
|
+
case "Identifier":
|
|
6070
|
+
names.push(current.name);
|
|
6071
|
+
return;
|
|
6072
|
+
case "ObjectPattern":
|
|
6073
|
+
for (const property of current.properties ?? []) {
|
|
6074
|
+
visit(property.type === "RestElement" ? property.argument : property.value);
|
|
6075
|
+
}
|
|
6076
|
+
return;
|
|
6077
|
+
case "ArrayPattern":
|
|
6078
|
+
for (const element of current.elements ?? []) visit(element);
|
|
6079
|
+
return;
|
|
6080
|
+
case "AssignmentPattern":
|
|
6081
|
+
visit(current.left);
|
|
6082
|
+
return;
|
|
6083
|
+
case "RestElement":
|
|
6084
|
+
visit(current.argument);
|
|
6085
|
+
return;
|
|
6086
|
+
default:
|
|
6087
|
+
return;
|
|
6088
|
+
}
|
|
6089
|
+
};
|
|
6090
|
+
visit(pattern);
|
|
6091
|
+
return names;
|
|
6092
|
+
}
|
|
6093
|
+
function moduleBindings(program) {
|
|
6094
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
6095
|
+
for (const statement of program.body ?? []) {
|
|
6096
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
6097
|
+
for (const declarator of statement.declarations ?? []) {
|
|
6098
|
+
const init = declarator.init ?? void 0;
|
|
6099
|
+
for (const name of patternNames(declarator.id)) bindings.set(name, init);
|
|
6100
|
+
}
|
|
6101
|
+
}
|
|
6102
|
+
return bindings;
|
|
6103
|
+
}
|
|
6104
|
+
function importedNames(program) {
|
|
6105
|
+
const names = /* @__PURE__ */ new Set();
|
|
6106
|
+
for (const statement of program.body ?? []) {
|
|
6107
|
+
if (statement.type !== "ImportDeclaration") continue;
|
|
6108
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
6109
|
+
const local = specifier.local;
|
|
6110
|
+
if (local?.type === "Identifier") names.add(local.name);
|
|
6111
|
+
}
|
|
6112
|
+
}
|
|
6113
|
+
return names;
|
|
6114
|
+
}
|
|
6115
|
+
function declaredInside(factory) {
|
|
6116
|
+
const names = /* @__PURE__ */ new Set();
|
|
6117
|
+
walk(factory, (node) => {
|
|
6118
|
+
if (node.type === "VariableDeclarator" || node.type === "FunctionDeclaration") {
|
|
6119
|
+
for (const name of patternNames(node.id)) names.add(name);
|
|
6120
|
+
}
|
|
6121
|
+
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") {
|
|
6122
|
+
for (const parameter of node.params ?? []) {
|
|
6123
|
+
for (const name of patternNames(parameter)) names.add(name);
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
});
|
|
6127
|
+
return names;
|
|
6128
|
+
}
|
|
6129
|
+
var FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
6130
|
+
"ArrowFunctionExpression",
|
|
6131
|
+
"FunctionExpression",
|
|
6132
|
+
"FunctionDeclaration"
|
|
6133
|
+
]);
|
|
6134
|
+
function capturedBindings(factory, bindings) {
|
|
6135
|
+
const own = declaredInside(factory);
|
|
6136
|
+
const invoked = /* @__PURE__ */ new Set();
|
|
6137
|
+
walk(factory, (node) => {
|
|
6138
|
+
if (node.type !== "CallExpression") return;
|
|
6139
|
+
let callee = node.callee;
|
|
6140
|
+
while (callee?.type === "ParenthesizedExpression") callee = callee.expression;
|
|
6141
|
+
if (callee !== void 0 && FUNCTION_TYPES.has(callee.type)) invoked.add(callee);
|
|
6142
|
+
});
|
|
6143
|
+
const runsLater = (candidate) => FUNCTION_TYPES.has(candidate.type) && !invoked.has(candidate);
|
|
6144
|
+
const captured = /* @__PURE__ */ new Set();
|
|
6145
|
+
for (const name of readIdentifiers(factory, runsLater)) {
|
|
6146
|
+
if (bindings.has(name) && !own.has(name)) captured.add(name);
|
|
6147
|
+
}
|
|
6148
|
+
return [...captured];
|
|
6149
|
+
}
|
|
6150
|
+
function hoistable(name, bindings, imported, seen = /* @__PURE__ */ new Set()) {
|
|
6151
|
+
if (seen.has(name)) return true;
|
|
6152
|
+
seen.add(name);
|
|
6153
|
+
const init = bindings.get(name);
|
|
6154
|
+
if (init === void 0) return false;
|
|
6155
|
+
for (const read3 of readIdentifiers(init)) {
|
|
6156
|
+
if (imported.has(read3)) return false;
|
|
6157
|
+
if (bindings.has(read3) && !hoistable(read3, bindings, imported, seen)) return false;
|
|
6158
|
+
}
|
|
6159
|
+
return true;
|
|
6160
|
+
}
|
|
6161
|
+
|
|
6162
|
+
// src/roles/test/codemod/hoist.ts
|
|
6163
|
+
function simpleDeclarations(program) {
|
|
6164
|
+
const found = /* @__PURE__ */ new Map();
|
|
6165
|
+
for (const statement of program.body ?? []) {
|
|
6166
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
6167
|
+
const declarations = statement.declarations ?? [];
|
|
6168
|
+
if (declarations.length !== 1) continue;
|
|
6169
|
+
const declarator = declarations[0];
|
|
6170
|
+
const id = declarator.id;
|
|
6171
|
+
const init = declarator.init;
|
|
6172
|
+
if (id?.type !== "Identifier" || init === void 0 || init === null) continue;
|
|
6173
|
+
found.set(id.name, { statement, init, kind: statement.kind ?? "const" });
|
|
6174
|
+
}
|
|
6175
|
+
return found;
|
|
6176
|
+
}
|
|
6177
|
+
function interdependent(names, bindings) {
|
|
6178
|
+
for (const name of names) {
|
|
6179
|
+
for (const read3 of readIdentifiers(bindings.get(name))) {
|
|
6180
|
+
if (read3 !== name && names.includes(read3)) return `${name} reads ${read3}`;
|
|
6181
|
+
}
|
|
6182
|
+
}
|
|
6183
|
+
return void 0;
|
|
6184
|
+
}
|
|
6185
|
+
function statementSpan(source, statement) {
|
|
6186
|
+
let end = statement.end;
|
|
6187
|
+
if (source[end] === "\r") end++;
|
|
6188
|
+
if (source[end] === "\n") end++;
|
|
6189
|
+
return { start: statement.start, end };
|
|
6190
|
+
}
|
|
6191
|
+
function hoistCapturedBindings(source, program, factories, bindings, imported) {
|
|
6192
|
+
const captured = [...new Set(factories.flatMap((factory) => capturedBindings(factory, bindings)))];
|
|
6193
|
+
if (captured.length === 0) return { source, hoisted: [], refusals: [] };
|
|
6194
|
+
const refusals = [];
|
|
6195
|
+
for (const name of captured) {
|
|
6196
|
+
if (hoistable(name, bindings, imported)) continue;
|
|
6197
|
+
refusals.push({
|
|
6198
|
+
binding: name,
|
|
6199
|
+
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`
|
|
6200
|
+
});
|
|
6201
|
+
}
|
|
6202
|
+
const declarations = simpleDeclarations(program);
|
|
6203
|
+
for (const name of captured) {
|
|
6204
|
+
if (!declarations.has(name) && !refusals.some((refusal) => refusal.binding === name)) {
|
|
6205
|
+
refusals.push({
|
|
6206
|
+
binding: name,
|
|
6207
|
+
reason: `it is not declared as a plain \`${name} = ...\` at module scope`
|
|
6208
|
+
});
|
|
6209
|
+
}
|
|
6210
|
+
}
|
|
6211
|
+
const cycle = interdependent(captured, bindings);
|
|
6212
|
+
if (cycle !== void 0) {
|
|
6213
|
+
refusals.push({
|
|
6214
|
+
binding: captured.join(", "),
|
|
6215
|
+
reason: `${cycle}, and an object literal cannot refer to its own keys`
|
|
6216
|
+
});
|
|
6217
|
+
}
|
|
6218
|
+
if (refusals.length > 0) return { source, hoisted: [], refusals };
|
|
6219
|
+
const ordered = captured.map((name) => ({
|
|
6220
|
+
name,
|
|
6221
|
+
...declarations.get(name)
|
|
6222
|
+
})).sort((left, right) => left.statement.start - right.statement.start);
|
|
6223
|
+
const first = ordered[0];
|
|
6224
|
+
const semi = source.slice(first.statement.start, first.statement.end).trimEnd().endsWith(";");
|
|
6225
|
+
const end = semi ? ";" : "";
|
|
6226
|
+
const initOf = ({ init }) => source.slice(init.start, init.end);
|
|
6227
|
+
const constants = ordered.filter(({ kind }) => kind === "const");
|
|
6228
|
+
const objectLine = constants.length === 0 ? void 0 : `const { ${constants.map(({ name }) => name).join(", ")} } = vi.hoisted(() => ({ ${constants.map((entry) => `${entry.name}: ${initOf(entry)}`).join(", ")} }))${end}
|
|
6229
|
+
`;
|
|
6230
|
+
const lines = [];
|
|
6231
|
+
let objectEmitted = false;
|
|
6232
|
+
for (const entry of ordered) {
|
|
6233
|
+
if (entry.kind === "const") {
|
|
6234
|
+
if (!objectEmitted && objectLine !== void 0) {
|
|
6235
|
+
lines.push(objectLine);
|
|
6236
|
+
objectEmitted = true;
|
|
6237
|
+
}
|
|
6238
|
+
continue;
|
|
6239
|
+
}
|
|
6240
|
+
lines.push(`${entry.kind} ${entry.name} = vi.hoisted(() => ${initOf(entry)})${end}
|
|
6241
|
+
`);
|
|
6242
|
+
}
|
|
6243
|
+
const hoistedLine = lines.join("");
|
|
6244
|
+
const edits = ordered.map(({ statement }, index) => {
|
|
6245
|
+
const span = statementSpan(source, statement);
|
|
6246
|
+
return { ...span, text: index === 0 ? hoistedLine : "" };
|
|
6247
|
+
});
|
|
6248
|
+
let rewritten = source;
|
|
6249
|
+
for (const edit of edits.sort((left, right) => right.start - left.start)) {
|
|
6250
|
+
rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
|
|
6251
|
+
}
|
|
6252
|
+
return { source: rewritten, hoisted: ordered.map(({ name }) => name), refusals: [] };
|
|
6253
|
+
}
|
|
6254
|
+
|
|
6255
|
+
// src/roles/test/codemod/hook-return-value.ts
|
|
6256
|
+
var HOOKS = /* @__PURE__ */ new Set(["beforeEach", "afterEach", "beforeAll", "afterAll"]);
|
|
6257
|
+
var MEMBER_TYPES7 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
6258
|
+
function hookCallback(node) {
|
|
6259
|
+
if (node.type !== "CallExpression") return void 0;
|
|
6260
|
+
const callee = node.callee;
|
|
6261
|
+
if (callee?.type !== "Identifier" || !HOOKS.has(callee.name)) return void 0;
|
|
6262
|
+
const [callback] = node.arguments ?? [];
|
|
6263
|
+
return callback?.type === "ArrowFunctionExpression" ? callback : void 0;
|
|
6264
|
+
}
|
|
6265
|
+
function configuresAMock(body) {
|
|
6266
|
+
if (body.type !== "CallExpression") return false;
|
|
6267
|
+
const callee = body.callee;
|
|
6268
|
+
if (callee === void 0 || !MEMBER_TYPES7.has(callee.type)) return false;
|
|
6269
|
+
const property = callee.property;
|
|
6270
|
+
return property?.type === "Identifier" && /^mock[A-Z]/.test(property.name);
|
|
6271
|
+
}
|
|
6272
|
+
function rewriteHookReturnValue(source, program) {
|
|
6273
|
+
const edits = [];
|
|
6274
|
+
walk(program, (node) => {
|
|
6275
|
+
const callback = hookCallback(node);
|
|
6276
|
+
if (callback === void 0 || callback.async === true) return;
|
|
6277
|
+
const body = callback.body;
|
|
6278
|
+
if (body === void 0 || body.type === "BlockStatement") return;
|
|
6279
|
+
if (!configuresAMock(body)) return;
|
|
6280
|
+
const start = body.start;
|
|
6281
|
+
const end = body.end;
|
|
6282
|
+
edits.push({ start, end, text: `{
|
|
6283
|
+
${source.slice(start, end)}
|
|
6284
|
+
}` });
|
|
6285
|
+
});
|
|
6286
|
+
let result = source;
|
|
6287
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
6288
|
+
result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
|
|
6289
|
+
}
|
|
6290
|
+
return { source: result, rewritten: edits.length };
|
|
6291
|
+
}
|
|
6292
|
+
|
|
6293
|
+
// src/roles/test/codemod/inject-type-import.ts
|
|
6294
|
+
import { parseSync as parseSync4 } from "oxc-parser";
|
|
6295
|
+
|
|
6296
|
+
// src/roles/test/codemod/rename.ts
|
|
6297
|
+
import { parseSync as parseSync3 } from "oxc-parser";
|
|
6298
|
+
var VITEST_TYPE_SOURCE = "vitest";
|
|
6299
|
+
var RUNTIME_RENAMES = /* @__PURE__ */ new Set([
|
|
6300
|
+
// Mocks
|
|
6301
|
+
"clearAllMocks",
|
|
6302
|
+
"doMock",
|
|
6303
|
+
"fn",
|
|
6304
|
+
"mock",
|
|
6305
|
+
"mocked",
|
|
6306
|
+
"resetAllMocks",
|
|
6307
|
+
"resetModules",
|
|
6308
|
+
"restoreAllMocks",
|
|
6309
|
+
"spyOn",
|
|
6310
|
+
"unmock",
|
|
6311
|
+
/*
|
|
6312
|
+
* Timers, as a family rather than as the two the corpus happens to use today. Running this over
|
|
6313
|
+
* the repo reported `jest.runAllTimers` as unknown, which was a hole in the table rather than a
|
|
6314
|
+
* finding about the code. Each name here was then checked against a running Vitest, so adding the
|
|
6315
|
+
* siblings is not speculation: a file gaining one tomorrow gets the same answer as one using it
|
|
6316
|
+
* today, instead of a report about a member that was always going to be a rename.
|
|
6317
|
+
*/
|
|
6318
|
+
"advanceTimersByTime",
|
|
6319
|
+
"advanceTimersByTimeAsync",
|
|
6320
|
+
"advanceTimersToNextTimer",
|
|
6321
|
+
"clearAllTimers",
|
|
6322
|
+
"getRealSystemTime",
|
|
6323
|
+
"getTimerCount",
|
|
6324
|
+
"runAllTimers",
|
|
6325
|
+
"runAllTimersAsync",
|
|
6326
|
+
"runOnlyPendingTimers",
|
|
6327
|
+
"setSystemTime",
|
|
6328
|
+
"useFakeTimers",
|
|
6329
|
+
"useRealTimers"
|
|
6330
|
+
]);
|
|
6331
|
+
var TYPE_RENAMES = {
|
|
6332
|
+
Mock: "Mock",
|
|
6333
|
+
MockedClass: "MockedClass",
|
|
6334
|
+
MockedFunction: "MockedFunction",
|
|
6335
|
+
MockedObject: "MockedObject",
|
|
6336
|
+
Mocked: "Mocked",
|
|
6337
|
+
SpiedFunction: "MockInstance",
|
|
6338
|
+
SpyInstance: "MockInstance"
|
|
6339
|
+
};
|
|
6340
|
+
var NAMED_RULE = {
|
|
6341
|
+
requireActual: `await vi.importActual(...), which makes the surrounding factory async`,
|
|
6342
|
+
requireMock: `await vi.importMock(...), which makes the surrounding factory async`,
|
|
6343
|
+
setTimeout: `vi.setConfig({ testTimeout: ... }), which is configuration rather than a call`
|
|
6344
|
+
};
|
|
6345
|
+
var OWNED_BY_A_LATER_PASS = /* @__PURE__ */ new Set(["isolateModules", "isolateModulesAsync"]);
|
|
6346
|
+
function lineOf(source, offset) {
|
|
6347
|
+
let line = 1;
|
|
6348
|
+
for (let index = 0; index < offset && index < source.length; index++) {
|
|
6349
|
+
if (source[index] === "\n") line++;
|
|
6350
|
+
}
|
|
6351
|
+
return line;
|
|
6352
|
+
}
|
|
6353
|
+
function valueEdit(node, source, outcome) {
|
|
6354
|
+
const object = node.object;
|
|
6355
|
+
const property = node.property;
|
|
6356
|
+
if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
|
|
6357
|
+
if (property?.type !== "Identifier") return void 0;
|
|
6358
|
+
const member = property.name;
|
|
6359
|
+
if (RUNTIME_RENAMES.has(member)) {
|
|
6360
|
+
outcome.rewritten++;
|
|
6361
|
+
return { start: object.start, end: object.end, text: "vi" };
|
|
6362
|
+
}
|
|
6363
|
+
if (OWNED_BY_A_LATER_PASS.has(member)) return void 0;
|
|
6364
|
+
outcome.unhandled.push({
|
|
6365
|
+
member: `jest.${member}`,
|
|
6366
|
+
line: lineOf(source, object.start),
|
|
6367
|
+
...NAMED_RULE[member] === void 0 ? {} : { rule: NAMED_RULE[member] }
|
|
6368
|
+
});
|
|
6369
|
+
return void 0;
|
|
6370
|
+
}
|
|
6371
|
+
function typeEdit(node, source, outcome, needed) {
|
|
6372
|
+
const left = node.left;
|
|
6373
|
+
const right = node.right;
|
|
6374
|
+
if (left?.type !== "Identifier" || left.name !== "jest") return void 0;
|
|
6375
|
+
if (right?.type !== "Identifier") return void 0;
|
|
6376
|
+
const member = right.name;
|
|
6377
|
+
const replacement = TYPE_RENAMES[member];
|
|
6378
|
+
if (replacement === void 0) {
|
|
6379
|
+
outcome.unhandled.push({ member: `jest.${member}`, line: lineOf(source, node.start) });
|
|
6380
|
+
return void 0;
|
|
6381
|
+
}
|
|
6382
|
+
needed.add(replacement);
|
|
6383
|
+
outcome.rewritten++;
|
|
6384
|
+
return { start: node.start, end: node.end, text: replacement };
|
|
6385
|
+
}
|
|
6386
|
+
function renameJestToVitest(fileName, source) {
|
|
6387
|
+
const outcome = { source, typeImports: [], rewritten: 0, unhandled: [] };
|
|
6388
|
+
const { program, errors } = parseSync3(fileName, source, { sourceType: "module" });
|
|
6389
|
+
if (errors.length > 0) return outcome;
|
|
6390
|
+
const edits = [];
|
|
6391
|
+
const needed = /* @__PURE__ */ new Set();
|
|
6392
|
+
walk(program, (node) => {
|
|
6393
|
+
if (node.type === "MemberExpression" || node.type === "StaticMemberExpression") {
|
|
6394
|
+
const edit = valueEdit(node, source, outcome);
|
|
6395
|
+
if (edit !== void 0) edits.push(edit);
|
|
6396
|
+
return;
|
|
6397
|
+
}
|
|
6398
|
+
if (node.type === "TSQualifiedName") {
|
|
6399
|
+
const edit = typeEdit(node, source, outcome, needed);
|
|
6400
|
+
if (edit !== void 0) edits.push(edit);
|
|
6401
|
+
}
|
|
6402
|
+
});
|
|
6403
|
+
outcome.typeImports = [...needed].sort();
|
|
6404
|
+
let rewritten = source;
|
|
6405
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
6406
|
+
rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
|
|
6407
|
+
}
|
|
6408
|
+
outcome.source = rewritten;
|
|
6409
|
+
return outcome;
|
|
6410
|
+
}
|
|
6411
|
+
|
|
6412
|
+
// src/roles/test/codemod/inject-type-import.ts
|
|
6413
|
+
function renderTypeImport(names, semi) {
|
|
6414
|
+
return `import type { ${names.join(", ")} } from '${VITEST_TYPE_SOURCE}'${semi ? ";" : ""}`;
|
|
6415
|
+
}
|
|
6416
|
+
function importDeclarations(program) {
|
|
6417
|
+
return (program.body ?? []).filter((node) => node.type === "ImportDeclaration");
|
|
6418
|
+
}
|
|
6419
|
+
function usesSemicolons(source, imports) {
|
|
6420
|
+
const first = imports[0];
|
|
6421
|
+
if (first === void 0) return false;
|
|
6422
|
+
return source.slice(first.start, first.end).trimEnd().endsWith(";");
|
|
6423
|
+
}
|
|
6424
|
+
function existingVitestImport(imports) {
|
|
6425
|
+
return imports.find(
|
|
6426
|
+
(node) => node.source?.value === VITEST_TYPE_SOURCE
|
|
6427
|
+
);
|
|
6428
|
+
}
|
|
6429
|
+
function alreadyImported(declaration) {
|
|
6430
|
+
return (declaration.specifiers ?? []).filter((specifier) => specifier.type === "ImportSpecifier").map((specifier) => specifier.imported?.name).filter((name) => name !== void 0);
|
|
6431
|
+
}
|
|
6432
|
+
function injectTypeImport(fileName, source, typeNames) {
|
|
6433
|
+
if (typeNames.length === 0) return source;
|
|
6434
|
+
const { program, errors } = parseSync4(fileName, source, { sourceType: "module" });
|
|
6435
|
+
if (errors.length > 0) return source;
|
|
6436
|
+
const imports = importDeclarations(program);
|
|
6437
|
+
const semi = usesSemicolons(source, imports);
|
|
6438
|
+
const existing = existingVitestImport(imports);
|
|
6439
|
+
if (existing !== void 0) {
|
|
6440
|
+
const merged = [.../* @__PURE__ */ new Set([...alreadyImported(existing), ...typeNames])].sort();
|
|
6441
|
+
if (merged.length === alreadyImported(existing).length) return source;
|
|
6442
|
+
return source.slice(0, existing.start) + renderTypeImport(merged, semi) + source.slice(existing.end);
|
|
6443
|
+
}
|
|
6444
|
+
const line = renderTypeImport([...typeNames].sort(), semi);
|
|
6445
|
+
const first = imports[0];
|
|
6446
|
+
if (first === void 0) return `${line}
|
|
6447
|
+
|
|
6448
|
+
${source}`;
|
|
6449
|
+
return source.slice(0, first.start) + line + "\n" + source.slice(first.start);
|
|
6450
|
+
}
|
|
6451
|
+
|
|
6452
|
+
// src/roles/test/codemod/named-rules.ts
|
|
6453
|
+
import { existsSync as existsSync24, readFileSync as readFileSync22 } from "fs";
|
|
6454
|
+
import { createRequire as createRequire4 } from "module";
|
|
6455
|
+
import { dirname as dirname4, join as join28 } from "path";
|
|
6456
|
+
function isCommonJsPackage(fromFile, specifier) {
|
|
6457
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) return false;
|
|
6458
|
+
let resolved;
|
|
6459
|
+
try {
|
|
6460
|
+
resolved = createRequire4(fromFile).resolve(specifier);
|
|
6461
|
+
} catch {
|
|
6462
|
+
return false;
|
|
6463
|
+
}
|
|
6464
|
+
if (!resolved.includes(`${"/"}node_modules${"/"}`)) return false;
|
|
6465
|
+
if (resolved.endsWith(".mjs")) return false;
|
|
6466
|
+
if (resolved.endsWith(".cjs")) return true;
|
|
6467
|
+
let dir = dirname4(resolved);
|
|
6468
|
+
for (let depth = 0; depth < 12; depth += 1) {
|
|
6469
|
+
const manifest = join28(dir, "package.json");
|
|
6470
|
+
if (existsSync24(manifest)) {
|
|
6471
|
+
try {
|
|
6472
|
+
return JSON.parse(readFileSync22(manifest, "utf8")).type !== "module";
|
|
6473
|
+
} catch {
|
|
6474
|
+
return false;
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6477
|
+
const parent = dirname4(dir);
|
|
6478
|
+
if (parent === dir) break;
|
|
6479
|
+
dir = parent;
|
|
6480
|
+
}
|
|
6481
|
+
return false;
|
|
6482
|
+
}
|
|
6483
|
+
var MEMBER_TYPES8 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
6484
|
+
var ASYNC_REPLACEMENTS = {
|
|
6485
|
+
requireActual: "importActual",
|
|
6486
|
+
requireMock: "importMock"
|
|
6487
|
+
};
|
|
6488
|
+
function mockedSpecifiers(program) {
|
|
6489
|
+
const found = /* @__PURE__ */ new Set();
|
|
6490
|
+
walk(program, (node) => {
|
|
6491
|
+
if (node.type !== "CallExpression") return;
|
|
6492
|
+
const callee = node.callee;
|
|
6493
|
+
if (callee === void 0 || !MEMBER_TYPES8.has(callee.type)) return;
|
|
6494
|
+
const object = callee.object;
|
|
6495
|
+
const property = callee.property;
|
|
6496
|
+
if (object?.type !== "Identifier") return;
|
|
6497
|
+
if (object.name !== "jest" && object.name !== "vi") return;
|
|
6498
|
+
if (property?.type !== "Identifier" || property.name !== "mock") return;
|
|
6499
|
+
const [specifier] = node.arguments ?? [];
|
|
6500
|
+
if (specifier?.type === "StringLiteral" || specifier?.type === "Literal") {
|
|
6501
|
+
found.add(String(specifier.value));
|
|
6502
|
+
}
|
|
6503
|
+
});
|
|
6504
|
+
return found;
|
|
6505
|
+
}
|
|
6506
|
+
var FUNCTIONS = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
|
|
6507
|
+
function walkWithEnclosingFunction(node, enclosing, visit) {
|
|
6508
|
+
if (node === null || typeof node !== "object") return;
|
|
6509
|
+
if (Array.isArray(node)) {
|
|
6510
|
+
for (const entry of node) walkWithEnclosingFunction(entry, enclosing, visit);
|
|
6511
|
+
return;
|
|
6512
|
+
}
|
|
6513
|
+
const current = node;
|
|
6514
|
+
const next = FUNCTIONS.has(current.type) ? current : enclosing;
|
|
6515
|
+
if (typeof current.type === "string") visit(current, enclosing);
|
|
6516
|
+
for (const value of Object.values(current)) {
|
|
6517
|
+
if (value !== null && typeof value === "object") walkWithEnclosingFunction(value, next, visit);
|
|
6518
|
+
}
|
|
6519
|
+
}
|
|
6520
|
+
function bareRequire(node) {
|
|
6521
|
+
if (node.type !== "CallExpression") return false;
|
|
6522
|
+
const callee = node.callee;
|
|
6523
|
+
if (callee?.type !== "Identifier" || callee.name !== "require") return false;
|
|
6524
|
+
const args = node.arguments ?? [];
|
|
6525
|
+
return args.length === 1 && args[0] !== void 0;
|
|
6526
|
+
}
|
|
6527
|
+
function mockFactoryReturn(node) {
|
|
6528
|
+
if (node.type !== "CallExpression") return void 0;
|
|
6529
|
+
const callee = node.callee;
|
|
6530
|
+
if (callee === void 0 || !MEMBER_TYPES8.has(callee.type)) return void 0;
|
|
6531
|
+
const object = callee.object;
|
|
6532
|
+
const property = callee.property;
|
|
6533
|
+
if (object?.type !== "Identifier") return void 0;
|
|
6534
|
+
if (!["jest", "vi", "vitest"].includes(object.name)) return void 0;
|
|
6535
|
+
if (property?.name !== "mock") return void 0;
|
|
6536
|
+
const factory = (node.arguments ?? [])[1];
|
|
6537
|
+
if (factory?.type !== "ArrowFunctionExpression" && factory?.type !== "FunctionExpression") {
|
|
6538
|
+
return void 0;
|
|
6539
|
+
}
|
|
6540
|
+
let body = factory.body;
|
|
6541
|
+
while (body?.type === "ParenthesizedExpression") body = body.expression;
|
|
6542
|
+
if (body === void 0) return void 0;
|
|
6543
|
+
if (body.type !== "BlockStatement") return body;
|
|
6544
|
+
const statements = body.body ?? [];
|
|
6545
|
+
if (statements.length !== 1) return void 0;
|
|
6546
|
+
const only = statements[0];
|
|
6547
|
+
if (only?.type !== "ReturnStatement") return void 0;
|
|
6548
|
+
let argument = only.argument;
|
|
6549
|
+
while (argument?.type === "ParenthesizedExpression") {
|
|
6550
|
+
argument = argument.expression;
|
|
6551
|
+
}
|
|
6552
|
+
return argument;
|
|
6553
|
+
}
|
|
6554
|
+
function ownedCall(node) {
|
|
6555
|
+
if (node.type !== "CallExpression") return void 0;
|
|
6556
|
+
const callee = node.callee;
|
|
6557
|
+
if (callee?.type !== "MemberExpression" && callee?.type !== "StaticMemberExpression") {
|
|
6558
|
+
return void 0;
|
|
6559
|
+
}
|
|
6560
|
+
const object = callee.object;
|
|
6561
|
+
const property = callee.property;
|
|
6562
|
+
if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
|
|
6563
|
+
return property?.type === "Identifier" ? property.name : void 0;
|
|
6564
|
+
}
|
|
6565
|
+
function argumentText(source, call) {
|
|
6566
|
+
const args = call.arguments ?? [];
|
|
6567
|
+
if (args.length === 0) return "";
|
|
6568
|
+
const first = args[0];
|
|
6569
|
+
const last = args[args.length - 1];
|
|
6570
|
+
return source.slice(first.start, last.end);
|
|
6571
|
+
}
|
|
6572
|
+
function applyEdits(source, edits) {
|
|
6573
|
+
let result = source;
|
|
6574
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
6575
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
6576
|
+
}
|
|
6577
|
+
return result;
|
|
6578
|
+
}
|
|
6579
|
+
function applyNamedRules(source, program, fileName = "") {
|
|
6580
|
+
const edits = [];
|
|
6581
|
+
const applied = /* @__PURE__ */ new Set();
|
|
6582
|
+
const asyncify = /* @__PURE__ */ new Set();
|
|
6583
|
+
const mocked = mockedSpecifiers(program);
|
|
6584
|
+
const wholeStatement = /* @__PURE__ */ new Set();
|
|
6585
|
+
walk(program, (node) => {
|
|
6586
|
+
if (node.type !== "ExpressionStatement") return;
|
|
6587
|
+
const expression = node.expression;
|
|
6588
|
+
if (expression !== void 0) wholeStatement.add(expression);
|
|
6589
|
+
});
|
|
6590
|
+
const handedToTheRunnerAsAModule = /* @__PURE__ */ new Set();
|
|
6591
|
+
walk(program, (node) => {
|
|
6592
|
+
const returned = mockFactoryReturn(node);
|
|
6593
|
+
if (returned !== void 0) handedToTheRunnerAsAModule.add(returned);
|
|
6594
|
+
});
|
|
6595
|
+
const readsANameOffIt = /* @__PURE__ */ new Set();
|
|
6596
|
+
walk(program, (node) => {
|
|
6597
|
+
if (node.type === "VariableDeclarator") {
|
|
6598
|
+
const id = node.id;
|
|
6599
|
+
const init = node.init ?? void 0;
|
|
6600
|
+
if (id !== void 0 && id.type !== "Identifier" && init !== void 0) {
|
|
6601
|
+
readsANameOffIt.add(init);
|
|
6602
|
+
}
|
|
6603
|
+
return;
|
|
6604
|
+
}
|
|
6605
|
+
if (!MEMBER_TYPES8.has(node.type)) return;
|
|
6606
|
+
const object = node.object;
|
|
6607
|
+
if (object !== void 0) readsANameOffIt.add(object);
|
|
6608
|
+
});
|
|
6609
|
+
walkWithEnclosingFunction(program, void 0, (node, enclosing) => {
|
|
6610
|
+
const member = ownedCall(node);
|
|
6611
|
+
if (member === void 0 && !bareRequire(node)) return;
|
|
6612
|
+
if (member === "requireMock") {
|
|
6613
|
+
const [specifier] = node.arguments ?? [];
|
|
6614
|
+
const literal = specifier?.type === "StringLiteral" || specifier?.type === "Literal" ? String(specifier.value) : void 0;
|
|
6615
|
+
if (literal !== void 0 && mocked.has(literal)) {
|
|
6616
|
+
edits.push({
|
|
6617
|
+
start: node.start,
|
|
6618
|
+
end: node.end,
|
|
6619
|
+
text: `(await import(${argumentText(source, node)}))`
|
|
6620
|
+
});
|
|
6621
|
+
applied.add("jest.requireMock");
|
|
6622
|
+
if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
|
|
6623
|
+
return;
|
|
6624
|
+
}
|
|
6625
|
+
}
|
|
6626
|
+
const asyncReplacement = member === void 0 ? void 0 : ASYNC_REPLACEMENTS[member];
|
|
6627
|
+
if (asyncReplacement !== void 0) {
|
|
6628
|
+
edits.push({
|
|
6629
|
+
start: node.start,
|
|
6630
|
+
end: node.end,
|
|
6631
|
+
text: `(await vi.${asyncReplacement}(${argumentText(source, node)}))`
|
|
6632
|
+
});
|
|
6633
|
+
applied.add(`jest.${member}`);
|
|
6634
|
+
if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
|
|
6635
|
+
return;
|
|
6636
|
+
}
|
|
6637
|
+
if (bareRequire(node)) {
|
|
6638
|
+
const [specifier] = node.arguments ?? [];
|
|
6639
|
+
const literal = specifier?.type === "StringLiteral" || specifier?.type === "Literal" ? String(specifier.value) : void 0;
|
|
6640
|
+
const interop = literal !== void 0 && !readsANameOffIt.has(node) && !handedToTheRunnerAsAModule.has(node) && !wholeStatement.has(node) && isCommonJsPackage(fileName, literal) ? ".default" : "";
|
|
6641
|
+
edits.push({
|
|
6642
|
+
start: node.start,
|
|
6643
|
+
end: node.end,
|
|
6644
|
+
text: wholeStatement.has(node) ? `await import(${argumentText(source, node)});` : `(await import(${argumentText(source, node)}))${interop}`
|
|
6645
|
+
});
|
|
6646
|
+
applied.add("require");
|
|
6647
|
+
if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
|
|
6648
|
+
return;
|
|
6649
|
+
}
|
|
6650
|
+
if (member === "mock" && enclosing !== void 0) {
|
|
6651
|
+
const callee = node.callee;
|
|
6652
|
+
edits.push({
|
|
6653
|
+
start: callee.start,
|
|
6654
|
+
end: callee.end,
|
|
6655
|
+
text: "vi.doMock"
|
|
6656
|
+
});
|
|
6657
|
+
applied.add("jest.mock inside a function");
|
|
6658
|
+
return;
|
|
6659
|
+
}
|
|
6660
|
+
if (member === "setTimeout") {
|
|
6661
|
+
edits.push({
|
|
6662
|
+
start: node.start,
|
|
6663
|
+
end: node.end,
|
|
6664
|
+
text: `vi.setConfig({ testTimeout: ${argumentText(source, node)} })`
|
|
6665
|
+
});
|
|
6666
|
+
applied.add("jest.setTimeout");
|
|
6667
|
+
}
|
|
6668
|
+
});
|
|
6669
|
+
for (const fn of asyncify) {
|
|
6670
|
+
edits.push({ start: fn.start, end: fn.start, text: "async " });
|
|
6671
|
+
}
|
|
6672
|
+
return { source: applyEdits(source, edits), applied: [...applied].sort() };
|
|
6673
|
+
}
|
|
6674
|
+
|
|
6675
|
+
// src/roles/test/codemod/isolate-modules.ts
|
|
6676
|
+
var MEMBER_TYPES9 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
6677
|
+
var ISOLATORS = /* @__PURE__ */ new Set(["isolateModules", "isolateModulesAsync"]);
|
|
6678
|
+
var FUNCTIONS2 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression"]);
|
|
6679
|
+
function isolatingCall(node) {
|
|
6680
|
+
if (node.type !== "CallExpression") return void 0;
|
|
6681
|
+
const callee = node.callee;
|
|
6682
|
+
if (callee === void 0 || !MEMBER_TYPES9.has(callee.type)) return void 0;
|
|
6683
|
+
const object = callee.object;
|
|
6684
|
+
const property = callee.property;
|
|
6685
|
+
if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
|
|
6686
|
+
if (property?.type !== "Identifier" || !ISOLATORS.has(property.name)) return void 0;
|
|
6687
|
+
const [callback] = node.arguments ?? [];
|
|
6688
|
+
return callback !== void 0 && FUNCTIONS2.has(callback.type) ? { call: node, callback } : void 0;
|
|
6689
|
+
}
|
|
6690
|
+
function unusableIsolatingCall(node) {
|
|
6691
|
+
if (node.type !== "CallExpression") return void 0;
|
|
6692
|
+
const callee = node.callee;
|
|
6693
|
+
if (callee === void 0 || !MEMBER_TYPES9.has(callee.type)) return void 0;
|
|
6694
|
+
const object = callee.object;
|
|
6695
|
+
const property = callee.property;
|
|
6696
|
+
if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
|
|
6697
|
+
if (property?.type !== "Identifier" || !ISOLATORS.has(property.name)) return void 0;
|
|
6698
|
+
const [callback] = node.arguments ?? [];
|
|
6699
|
+
return callback === void 0 || FUNCTIONS2.has(callback.type) ? void 0 : `jest.${property.name}`;
|
|
6700
|
+
}
|
|
6701
|
+
function indentationAt(source, offset) {
|
|
6702
|
+
const lineStart = source.lastIndexOf("\n", offset - 1) + 1;
|
|
6703
|
+
return /^[ \t]*/.exec(source.slice(lineStart, offset))?.[0] ?? "";
|
|
6704
|
+
}
|
|
6705
|
+
function rewriteIsolateModules(source, program) {
|
|
6706
|
+
const edits = [];
|
|
6707
|
+
const refusals = [];
|
|
6708
|
+
const asyncify = /* @__PURE__ */ new Set();
|
|
6709
|
+
let rewritten = 0;
|
|
6710
|
+
walkWithEnclosingFunction(program, void 0, (node, enclosing) => {
|
|
6711
|
+
const unusable = unusableIsolatingCall(node);
|
|
6712
|
+
if (unusable !== void 0) {
|
|
6713
|
+
refusals.push({
|
|
6714
|
+
binding: unusable,
|
|
6715
|
+
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.`
|
|
6716
|
+
});
|
|
6717
|
+
return;
|
|
6718
|
+
}
|
|
6719
|
+
const found = isolatingCall(node);
|
|
6720
|
+
if (found === void 0) return;
|
|
6721
|
+
const callbackText = source.slice(found.callback.start, found.callback.end);
|
|
6722
|
+
const callback = found.callback.async === true ? callbackText : `async ${callbackText}`;
|
|
6723
|
+
const indent = indentationAt(source, found.call.start);
|
|
6724
|
+
edits.push({
|
|
6725
|
+
start: found.call.start,
|
|
6726
|
+
end: found.call.end,
|
|
6727
|
+
text: `vi.resetModules()
|
|
6728
|
+
${indent}await (${callback})()`
|
|
6729
|
+
});
|
|
6730
|
+
rewritten += 1;
|
|
6731
|
+
if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
|
|
6732
|
+
});
|
|
6733
|
+
for (const fn of asyncify) {
|
|
6734
|
+
edits.push({ start: fn.start, end: fn.start, text: "async " });
|
|
6735
|
+
}
|
|
6736
|
+
let result = source;
|
|
6737
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
6738
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
6739
|
+
}
|
|
6740
|
+
return { source: result, rewritten, refusals };
|
|
6741
|
+
}
|
|
6742
|
+
|
|
6743
|
+
// src/roles/test/codemod/mock-as-value.ts
|
|
6744
|
+
var MEMBER_TYPES10 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
6745
|
+
function isViMockCall(node) {
|
|
6746
|
+
if (node?.type !== "CallExpression") return false;
|
|
6747
|
+
const callee = node.callee;
|
|
6748
|
+
if (callee === void 0 || !MEMBER_TYPES10.has(callee.type)) return false;
|
|
6749
|
+
const object = callee.object;
|
|
6750
|
+
const property = callee.property;
|
|
6751
|
+
return object?.type === "Identifier" && object.name === "vi" && property?.type === "Identifier" && (property.name === "mock" || property.name === "doMock");
|
|
6752
|
+
}
|
|
6753
|
+
function mockBinding(node) {
|
|
6754
|
+
if (node.type !== "VariableDeclaration") return void 0;
|
|
6755
|
+
const declarations = node.declarations ?? [];
|
|
6756
|
+
if (declarations.length !== 1) return void 0;
|
|
6757
|
+
const [declarator] = declarations;
|
|
6758
|
+
const id = declarator?.id;
|
|
6759
|
+
const init = declarator?.init;
|
|
6760
|
+
if (id?.type !== "Identifier" || !isViMockCall(init)) return void 0;
|
|
6761
|
+
return { name: id.name, call: init };
|
|
6762
|
+
}
|
|
6763
|
+
function rewriteMockAsValue(source, program) {
|
|
6764
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
6765
|
+
walk(program, (node) => {
|
|
6766
|
+
const found = mockBinding(node);
|
|
6767
|
+
if (found !== void 0) bindings.set(found.name, { declaration: node, call: found.call });
|
|
6768
|
+
});
|
|
6769
|
+
if (bindings.size === 0) return { source, rewritten: 0, refusals: [] };
|
|
6770
|
+
const memberUses = /* @__PURE__ */ new Map();
|
|
6771
|
+
const otherUses = /* @__PURE__ */ new Map();
|
|
6772
|
+
walk(program, (node) => {
|
|
6773
|
+
if (MEMBER_TYPES10.has(node.type)) {
|
|
6774
|
+
const object = node.object;
|
|
6775
|
+
if (object?.type === "Identifier" && bindings.has(object.name)) {
|
|
6776
|
+
const list = memberUses.get(object.name) ?? [];
|
|
6777
|
+
list.push(object);
|
|
6778
|
+
memberUses.set(object.name, list);
|
|
6779
|
+
return;
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6782
|
+
if (node.type !== "Identifier" || !bindings.has(node.name)) return;
|
|
6783
|
+
const declared = bindings.get(node.name);
|
|
6784
|
+
const insideDeclaration = node.start >= declared?.declaration.start && node.end <= declared?.declaration.end;
|
|
6785
|
+
if (insideDeclaration) return;
|
|
6786
|
+
const seen = memberUses.get(node.name) ?? [];
|
|
6787
|
+
if (seen.some((use) => use.start === node.start)) return;
|
|
6788
|
+
otherUses.set(node.name, (otherUses.get(node.name) ?? 0) + 1);
|
|
6789
|
+
});
|
|
6790
|
+
const edits = [];
|
|
6791
|
+
const refusals = [];
|
|
6792
|
+
let rewritten = 0;
|
|
6793
|
+
for (const [name, { declaration, call }] of bindings) {
|
|
6794
|
+
if ((otherUses.get(name) ?? 0) > 0) {
|
|
6795
|
+
refusals.push({
|
|
6796
|
+
binding: name,
|
|
6797
|
+
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.`
|
|
6798
|
+
});
|
|
6799
|
+
continue;
|
|
6800
|
+
}
|
|
6801
|
+
edits.push({
|
|
6802
|
+
start: declaration.start,
|
|
6803
|
+
end: declaration.end,
|
|
6804
|
+
text: source.slice(call.start, call.end)
|
|
6805
|
+
});
|
|
6806
|
+
for (const use of memberUses.get(name) ?? []) {
|
|
6807
|
+
edits.push({ start: use.start, end: use.end, text: "vi" });
|
|
6808
|
+
}
|
|
6809
|
+
rewritten += 1;
|
|
6810
|
+
}
|
|
6811
|
+
let result = source;
|
|
6812
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
6813
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
6814
|
+
}
|
|
6815
|
+
return { source: result, rewritten, refusals };
|
|
6816
|
+
}
|
|
6817
|
+
|
|
6818
|
+
// src/roles/test/codemod/mock-factory-default.ts
|
|
6819
|
+
var MEMBER_TYPES11 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
6820
|
+
var BINDING = "sentinelMockedModule";
|
|
6821
|
+
function mockCall(node) {
|
|
6822
|
+
if (node.type !== "CallExpression") return void 0;
|
|
6823
|
+
const callee = node.callee;
|
|
6824
|
+
if (callee === void 0 || !MEMBER_TYPES11.has(callee.type)) return void 0;
|
|
6825
|
+
const property = callee.property;
|
|
6826
|
+
if (property?.type !== "Identifier" || property.name !== "mock") return void 0;
|
|
6827
|
+
const object = callee.object;
|
|
6828
|
+
if (object?.type !== "Identifier" || !["vi", "vitest"].includes(object.name)) {
|
|
6829
|
+
return void 0;
|
|
6830
|
+
}
|
|
6831
|
+
const [specifier, factory] = node.arguments ?? [];
|
|
6832
|
+
if (specifier?.type !== "Literal" || typeof specifier.value !== "string") return void 0;
|
|
6833
|
+
if (factory === void 0) return void 0;
|
|
6834
|
+
return { specifier: specifier.value, factory };
|
|
6835
|
+
}
|
|
6836
|
+
function returnedExpression(factory) {
|
|
6837
|
+
const object = returnedObject(factory);
|
|
6838
|
+
if (object !== void 0) return { node: object, isObject: true };
|
|
6839
|
+
if (factory.type !== "ArrowFunctionExpression") return void 0;
|
|
6840
|
+
let body = factory.body;
|
|
6841
|
+
while (body?.type === "ParenthesizedExpression") body = body.expression;
|
|
6842
|
+
if (body === void 0) return void 0;
|
|
6843
|
+
if (body.type !== "BlockStatement") return { node: body, isObject: false, concise: true };
|
|
6844
|
+
const statements = body.body ?? [];
|
|
6845
|
+
if (statements.length !== 1) return void 0;
|
|
6846
|
+
const only = statements[0];
|
|
6847
|
+
if (only?.type !== "ReturnStatement") return void 0;
|
|
6848
|
+
const argument = only.argument;
|
|
6849
|
+
return argument === void 0 ? void 0 : { node: argument, isObject: false };
|
|
6850
|
+
}
|
|
6851
|
+
function returnedObject(factory) {
|
|
6852
|
+
if (factory.type !== "ArrowFunctionExpression") return void 0;
|
|
6853
|
+
let body = factory.body;
|
|
6854
|
+
while (body?.type === "ParenthesizedExpression") body = body.expression;
|
|
6855
|
+
if (body?.type === "ObjectExpression") return body;
|
|
6856
|
+
const statements = body?.body ?? [];
|
|
6857
|
+
if (statements.length !== 1) return void 0;
|
|
6858
|
+
const only = statements[0];
|
|
6859
|
+
if (only?.type === "ExpressionStatement" && only.expression?.type === "ObjectExpression") {
|
|
6860
|
+
return only.expression;
|
|
6861
|
+
}
|
|
6862
|
+
if (only?.type === "ReturnStatement" && only.argument?.type === "ObjectExpression") {
|
|
6863
|
+
return only.argument;
|
|
6864
|
+
}
|
|
6865
|
+
return void 0;
|
|
6866
|
+
}
|
|
6867
|
+
function isModuleNamespace(node) {
|
|
6868
|
+
let current = node;
|
|
6869
|
+
while (current?.type === "ParenthesizedExpression") {
|
|
6870
|
+
current = current.expression;
|
|
6871
|
+
}
|
|
6872
|
+
if (current?.type === "AwaitExpression") {
|
|
6873
|
+
let argument = current.argument;
|
|
6874
|
+
while (argument?.type === "ParenthesizedExpression") {
|
|
6875
|
+
argument = argument.expression;
|
|
6876
|
+
}
|
|
6877
|
+
current = argument;
|
|
6878
|
+
}
|
|
6879
|
+
return current?.type === "ImportExpression";
|
|
6880
|
+
}
|
|
6881
|
+
function namesDefault(object) {
|
|
6882
|
+
return (object.properties ?? []).some((property) => {
|
|
6883
|
+
const key = property.key;
|
|
6884
|
+
if (key?.type === "Identifier") return key.name === "default";
|
|
6885
|
+
if (key?.type === "Literal") return key.value === "default";
|
|
6886
|
+
return false;
|
|
6887
|
+
});
|
|
6888
|
+
}
|
|
6889
|
+
function rewriteMockFactoryDefault(source, program, defaultImported) {
|
|
6890
|
+
const edits = [];
|
|
6891
|
+
walk(program, (node) => {
|
|
6892
|
+
const call = mockCall(node);
|
|
6893
|
+
if (call === void 0) return;
|
|
6894
|
+
const returned = returnedExpression(call.factory);
|
|
6895
|
+
if (returned === void 0) return;
|
|
6896
|
+
if (returned.isObject && namesDefault(returned.node)) return;
|
|
6897
|
+
if (returned.isObject && !defaultImported.has(call.specifier)) return;
|
|
6898
|
+
if (!returned.isObject && isModuleNamespace(returned.node)) return;
|
|
6899
|
+
if (!returned.isObject) {
|
|
6900
|
+
const expression = source.slice(returned.node.start, returned.node.end);
|
|
6901
|
+
edits.push({
|
|
6902
|
+
start: returned.node.start,
|
|
6903
|
+
end: returned.node.end,
|
|
6904
|
+
// Parenthesised when it is the arrow's own body, or it is read as a block, not an object.
|
|
6905
|
+
literal: returned.concise === true ? `({ default: ${expression} })` : `{ default: ${expression} }`,
|
|
6906
|
+
specifier: call.specifier,
|
|
6907
|
+
wrap: false
|
|
6908
|
+
});
|
|
6909
|
+
return;
|
|
6910
|
+
}
|
|
6911
|
+
const object = returned.node;
|
|
6912
|
+
const literal = source.slice(object.start, object.end);
|
|
6913
|
+
if (literal.includes(BINDING)) return;
|
|
6914
|
+
const body = call.factory.body;
|
|
6915
|
+
edits.push({
|
|
6916
|
+
start: body.start,
|
|
6917
|
+
end: body.end,
|
|
6918
|
+
literal,
|
|
6919
|
+
specifier: call.specifier,
|
|
6920
|
+
wrap: true
|
|
6921
|
+
});
|
|
6922
|
+
});
|
|
6923
|
+
let result = source;
|
|
6924
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
6925
|
+
const text = edit.wrap ? `{ const ${BINDING} = ${edit.literal}; return { ...${BINDING}, default: ${BINDING} } }` : edit.literal;
|
|
6926
|
+
result = `${result.slice(0, edit.start)}${text}${result.slice(edit.end)}`;
|
|
6927
|
+
}
|
|
6928
|
+
return {
|
|
6929
|
+
source: result,
|
|
6930
|
+
rewritten: edits.length,
|
|
6931
|
+
specifiers: [...new Set(edits.map((edit) => edit.specifier))]
|
|
6932
|
+
};
|
|
6933
|
+
}
|
|
6934
|
+
|
|
6935
|
+
// src/roles/test/codemod/propagate-await.ts
|
|
6936
|
+
import { parseSync as parseSync5 } from "oxc-parser";
|
|
6937
|
+
var FUNCTIONS3 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
|
|
6938
|
+
var MEMBER_TYPES12 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
6939
|
+
var ALREADY_A_PROMISE = /* @__PURE__ */ new Set(["then", "catch", "finally"]);
|
|
6940
|
+
function asyncFunctionNames(program) {
|
|
6941
|
+
const names = /* @__PURE__ */ new Set();
|
|
6942
|
+
walk(program, (node) => {
|
|
6943
|
+
if (node.type === "FunctionDeclaration" && node.async === true) {
|
|
6944
|
+
const id2 = node.id;
|
|
6945
|
+
if (id2?.type === "Identifier") names.add(id2.name);
|
|
6946
|
+
return;
|
|
6947
|
+
}
|
|
6948
|
+
if (node.type !== "VariableDeclarator") return;
|
|
6949
|
+
const init = node.init ?? void 0;
|
|
6950
|
+
const id = node.id;
|
|
6951
|
+
if (init === void 0 || !FUNCTIONS3.has(init.type) || init.async !== true) return;
|
|
6952
|
+
if (id?.type === "Identifier") names.add(id.name);
|
|
6953
|
+
});
|
|
6954
|
+
return names;
|
|
6955
|
+
}
|
|
6956
|
+
function returningFunctionNames(program, changed) {
|
|
6957
|
+
const names = /* @__PURE__ */ new Set();
|
|
6958
|
+
const returnsChanged = (fn) => {
|
|
6959
|
+
const body = fn.body;
|
|
6960
|
+
if (body === void 0) return false;
|
|
6961
|
+
if (callTo(body, changed) !== void 0) return true;
|
|
6962
|
+
let found = false;
|
|
6963
|
+
walk(body, (node) => {
|
|
6964
|
+
if (node.type !== "ReturnStatement") return;
|
|
6965
|
+
const argument = node.argument;
|
|
6966
|
+
if (argument !== void 0 && callTo(argument, changed) !== void 0) found = true;
|
|
6967
|
+
});
|
|
6968
|
+
return found;
|
|
6969
|
+
};
|
|
6970
|
+
walk(program, (node) => {
|
|
6971
|
+
if (node.type === "FunctionDeclaration" && returnsChanged(node)) {
|
|
6972
|
+
const id2 = node.id;
|
|
6973
|
+
if (id2?.type === "Identifier") names.add(id2.name);
|
|
6974
|
+
return;
|
|
6975
|
+
}
|
|
6976
|
+
if (node.type !== "VariableDeclarator") return;
|
|
6977
|
+
const init = node.init ?? void 0;
|
|
6978
|
+
const id = node.id;
|
|
6979
|
+
if (init === void 0 || !FUNCTIONS3.has(init.type)) return;
|
|
6980
|
+
if (id?.type === "Identifier" && returnsChanged(init)) names.add(id.name);
|
|
6981
|
+
});
|
|
6982
|
+
return names;
|
|
6983
|
+
}
|
|
6984
|
+
function callTo(node, names) {
|
|
6985
|
+
if (node.type !== "CallExpression") return void 0;
|
|
6986
|
+
const callee = node.callee;
|
|
6987
|
+
if (callee?.type !== "Identifier") return void 0;
|
|
6988
|
+
const name = callee.name;
|
|
6989
|
+
return names.has(name) ? name : void 0;
|
|
6990
|
+
}
|
|
6991
|
+
function walkWithContext(node, enclosing, parent, visit) {
|
|
6992
|
+
if (node === null || typeof node !== "object") return;
|
|
6993
|
+
if (Array.isArray(node)) {
|
|
6994
|
+
for (const item of node) walkWithContext(item, enclosing, parent, visit);
|
|
6995
|
+
return;
|
|
6996
|
+
}
|
|
6997
|
+
const current = node;
|
|
6998
|
+
const isFunction = typeof current.type === "string" && FUNCTIONS3.has(current.type);
|
|
6999
|
+
if (typeof current.type === "string") visit(current, enclosing, parent);
|
|
7000
|
+
const nextEnclosing = isFunction ? current : enclosing;
|
|
7001
|
+
const nextParent = typeof current.type === "string" ? current : parent;
|
|
7002
|
+
for (const value of Object.values(current)) {
|
|
7003
|
+
if (value !== null && typeof value === "object") {
|
|
7004
|
+
walkWithContext(value, nextEnclosing, nextParent, visit);
|
|
7005
|
+
}
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
7008
|
+
function alreadyHandled(call, parent) {
|
|
7009
|
+
if (parent === void 0) return false;
|
|
7010
|
+
if (parent.type === "AwaitExpression") return true;
|
|
7011
|
+
if (parent.type === "ReturnStatement" || parent.type === "ArrowFunctionExpression") return true;
|
|
7012
|
+
if (MEMBER_TYPES12.has(parent.type) && parent.object === call) {
|
|
7013
|
+
const property = parent.property;
|
|
7014
|
+
return property?.type === "Identifier" && ALREADY_A_PROMISE.has(property.name);
|
|
7015
|
+
}
|
|
7016
|
+
return false;
|
|
7017
|
+
}
|
|
7018
|
+
function applyEdits2(source, edits) {
|
|
7019
|
+
let result = source;
|
|
7020
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
7021
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
7022
|
+
}
|
|
7023
|
+
return result;
|
|
7024
|
+
}
|
|
7025
|
+
function propagateAwait(fileName, source, original, elsewhere = /* @__PURE__ */ new Set()) {
|
|
7026
|
+
const wasAsync = asyncFunctionNames(original);
|
|
7027
|
+
let current = source;
|
|
7028
|
+
let rewritten = 0;
|
|
7029
|
+
for (let round = 0; round < 12; round += 1) {
|
|
7030
|
+
let parsed;
|
|
7031
|
+
try {
|
|
7032
|
+
parsed = parseSync5(fileName, current);
|
|
7033
|
+
} catch {
|
|
7034
|
+
return { source: current, rewritten };
|
|
7035
|
+
}
|
|
7036
|
+
const program = parsed.program;
|
|
7037
|
+
const changed = new Set(elsewhere);
|
|
7038
|
+
for (const name of asyncFunctionNames(program)) {
|
|
7039
|
+
if (!wasAsync.has(name)) changed.add(name);
|
|
7040
|
+
}
|
|
7041
|
+
if (changed.size === 0) return { source: current, rewritten };
|
|
7042
|
+
for (const name of returningFunctionNames(program, changed)) changed.add(name);
|
|
7043
|
+
const edits = [];
|
|
7044
|
+
const asyncify = /* @__PURE__ */ new Set();
|
|
7045
|
+
walkWithContext(program, void 0, void 0, (node, enclosing, parent) => {
|
|
7046
|
+
if (callTo(node, changed) === void 0) return;
|
|
7047
|
+
if (alreadyHandled(node, parent)) return;
|
|
7048
|
+
edits.push({ start: node.start, end: node.start, text: "await " });
|
|
7049
|
+
if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
|
|
7050
|
+
});
|
|
7051
|
+
if (edits.length === 0) return { source: current, rewritten };
|
|
7052
|
+
for (const fn of asyncify) {
|
|
7053
|
+
edits.push({ start: fn.start, end: fn.start, text: "async " });
|
|
7054
|
+
}
|
|
7055
|
+
rewritten += edits.length - asyncify.size;
|
|
7056
|
+
current = applyEdits2(current, edits);
|
|
7057
|
+
}
|
|
7058
|
+
return { source: current, rewritten };
|
|
7059
|
+
}
|
|
7060
|
+
|
|
7061
|
+
// src/roles/test/codemod/reexport-barrel-mock.ts
|
|
7062
|
+
import { readFileSync as readFileSync23, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
7063
|
+
import { createRequire as createRequire5 } from "module";
|
|
7064
|
+
import { dirname as dirname5, join as join29 } from "path";
|
|
7065
|
+
var MEMBER_TYPES13 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
7066
|
+
var PACKAGE_SUBPATH = /^(@[^/]+\/[^/]+|[^@.][^/]*)\/(.+)$/;
|
|
7067
|
+
var PURE_REEXPORT = /^module\.exports\s*=\s*require\(\s*(['"])([^'"]+)\1\s*\)\s*;?$/;
|
|
7068
|
+
function mockCalls(program) {
|
|
7069
|
+
const found = [];
|
|
7070
|
+
walk(program, (node) => {
|
|
7071
|
+
if (node.type !== "CallExpression") return;
|
|
7072
|
+
const callee = node.callee;
|
|
7073
|
+
if (callee === void 0 || !MEMBER_TYPES13.has(callee.type)) return;
|
|
7074
|
+
const object = callee.object;
|
|
7075
|
+
const property = callee.property;
|
|
7076
|
+
if (object?.type !== "Identifier") return;
|
|
7077
|
+
if (object.name !== "vi" && object.name !== "jest") return;
|
|
7078
|
+
if (property?.name !== "mock") return;
|
|
7079
|
+
const first = (node.arguments ?? [])[0];
|
|
7080
|
+
if (first?.type !== "Literal" || typeof first.value !== "string") return;
|
|
7081
|
+
found.push({
|
|
7082
|
+
specifier: first.value,
|
|
7083
|
+
start: node.start,
|
|
7084
|
+
end: node.end,
|
|
7085
|
+
literalStart: first.start,
|
|
7086
|
+
literalEnd: first.end
|
|
7087
|
+
});
|
|
7088
|
+
});
|
|
7089
|
+
return found;
|
|
7090
|
+
}
|
|
7091
|
+
function packageRoot(fromFile, name) {
|
|
7092
|
+
try {
|
|
7093
|
+
return dirname5(createRequire5(fromFile).resolve(`${name}/package.json`));
|
|
7094
|
+
} catch {
|
|
7095
|
+
return void 0;
|
|
7096
|
+
}
|
|
7097
|
+
}
|
|
7098
|
+
function resolveFrom(fromFile, specifier) {
|
|
7099
|
+
try {
|
|
7100
|
+
return createRequire5(fromFile).resolve(specifier);
|
|
7101
|
+
} catch {
|
|
7102
|
+
return void 0;
|
|
7103
|
+
}
|
|
7104
|
+
}
|
|
7105
|
+
function barrelsFor(fromFile, name, target) {
|
|
7106
|
+
const root = packageRoot(fromFile, name);
|
|
7107
|
+
if (root === void 0) return [];
|
|
7108
|
+
let entries;
|
|
7109
|
+
try {
|
|
7110
|
+
entries = readdirSync4(root);
|
|
7111
|
+
} catch {
|
|
7112
|
+
return [];
|
|
7113
|
+
}
|
|
7114
|
+
const aliases = [];
|
|
7115
|
+
for (const entry of entries) {
|
|
7116
|
+
if (!entry.endsWith(".js")) continue;
|
|
7117
|
+
const path = join29(root, entry);
|
|
7118
|
+
let body;
|
|
7119
|
+
try {
|
|
7120
|
+
if (!statSync4(path).isFile()) continue;
|
|
7121
|
+
body = readFileSync23(path, "utf8");
|
|
7122
|
+
} catch {
|
|
7123
|
+
continue;
|
|
7124
|
+
}
|
|
7125
|
+
const match = PURE_REEXPORT.exec(body.trim());
|
|
7126
|
+
if (match === null) continue;
|
|
7127
|
+
if (resolveFrom(path, match[2]) !== target) continue;
|
|
7128
|
+
aliases.push(`${name}/${entry.slice(0, -".js".length)}`);
|
|
7129
|
+
}
|
|
7130
|
+
return aliases.sort();
|
|
7131
|
+
}
|
|
7132
|
+
function rewriteReexportBarrelMocks(fromFile, source, program) {
|
|
7133
|
+
const calls = mockCalls(program);
|
|
7134
|
+
if (calls.length === 0) return { source, added: [] };
|
|
7135
|
+
const alreadyMocked = new Set(calls.map((call) => call.specifier));
|
|
7136
|
+
const added = [];
|
|
7137
|
+
const insertions = [];
|
|
7138
|
+
for (const call of calls) {
|
|
7139
|
+
const parts = PACKAGE_SUBPATH.exec(call.specifier);
|
|
7140
|
+
if (parts === null) continue;
|
|
7141
|
+
const target = resolveFrom(fromFile, call.specifier);
|
|
7142
|
+
if (target === void 0) continue;
|
|
7143
|
+
for (const alias of barrelsFor(fromFile, parts[1], target)) {
|
|
7144
|
+
if (alreadyMocked.has(alias)) continue;
|
|
7145
|
+
alreadyMocked.add(alias);
|
|
7146
|
+
added.push(alias);
|
|
7147
|
+
const text = source.slice(call.start, call.literalStart) + `'${alias}'` + source.slice(call.literalEnd, call.end);
|
|
7148
|
+
insertions.push({ at: call.end, text: `
|
|
7149
|
+
${text}` });
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7152
|
+
if (insertions.length === 0) return { source, added: [] };
|
|
7153
|
+
let rewritten = source;
|
|
7154
|
+
for (const insertion of insertions.sort((a, b) => b.at - a.at)) {
|
|
7155
|
+
rewritten = rewritten.slice(0, insertion.at) + insertion.text + rewritten.slice(insertion.at);
|
|
7156
|
+
}
|
|
7157
|
+
return { source: rewritten, added };
|
|
7158
|
+
}
|
|
7159
|
+
|
|
7160
|
+
// src/roles/test/codemod/repoint-msw.ts
|
|
7161
|
+
import { parseSync as parseSync6 } from "oxc-parser";
|
|
7162
|
+
var SENTINEL_MSW_SERVER = "@hublo/sentinel/test/msw";
|
|
7163
|
+
var MSW_SERVER_MODULE = /(^|\/)msw\/server$/;
|
|
7164
|
+
var MSW_PACKAGE = /^msw$/;
|
|
7165
|
+
function importsMswPackage(declaration) {
|
|
7166
|
+
const source = declaration.source?.value;
|
|
7167
|
+
return source !== void 0 && MSW_PACKAGE.test(source);
|
|
7168
|
+
}
|
|
7169
|
+
function importsServer(declaration) {
|
|
7170
|
+
return (declaration.specifiers ?? []).some((specifier) => {
|
|
7171
|
+
const imported = specifier.imported;
|
|
7172
|
+
const local = specifier.local;
|
|
7173
|
+
return imported?.name === "server" || imported === void 0 && local?.name === "server";
|
|
7174
|
+
});
|
|
7175
|
+
}
|
|
7176
|
+
function repointMswServer(fileName, source) {
|
|
7177
|
+
const { program, errors } = parseSync6(fileName, source, { sourceType: "module" });
|
|
7178
|
+
if (errors.length > 0) return { source, repointed: false };
|
|
7179
|
+
const edits = [];
|
|
7180
|
+
walk(program, (node) => {
|
|
7181
|
+
if (node.type !== "ImportDeclaration") return;
|
|
7182
|
+
const specifier = node.source;
|
|
7183
|
+
if (specifier?.value === void 0) return;
|
|
7184
|
+
const isServerModule = MSW_SERVER_MODULE.test(specifier.value) && importsServer(node);
|
|
7185
|
+
if (!isServerModule && !importsMswPackage(node)) return;
|
|
7186
|
+
edits.push({ start: specifier.start, end: specifier.end });
|
|
7187
|
+
});
|
|
7188
|
+
if (edits.length === 0) return { source, repointed: false };
|
|
7189
|
+
let rewritten = source;
|
|
7190
|
+
for (const edit of edits.sort((left, right) => right.start - left.start)) {
|
|
7191
|
+
const quote = source[edit.start] === '"' ? '"' : "'";
|
|
7192
|
+
rewritten = rewritten.slice(0, edit.start) + `${quote}${SENTINEL_MSW_SERVER}${quote}` + rewritten.slice(edit.end);
|
|
7193
|
+
}
|
|
7194
|
+
return { source: rewritten, repointed: true };
|
|
7195
|
+
}
|
|
7196
|
+
|
|
7197
|
+
// src/roles/test/codemod/spy-instance-type.ts
|
|
7198
|
+
var SPY_TYPES = /* @__PURE__ */ new Set(["SpyInstance"]);
|
|
7199
|
+
function typeNameOf(source, node) {
|
|
7200
|
+
const name = node.typeName;
|
|
7201
|
+
if (name === void 0) return void 0;
|
|
7202
|
+
return source.slice(name.start, name.end);
|
|
7203
|
+
}
|
|
7204
|
+
function rewriteSpyInstanceType(source, program) {
|
|
7205
|
+
const edits = [];
|
|
7206
|
+
walk(program, (node) => {
|
|
7207
|
+
if (node.type !== "TSTypeReference") return;
|
|
7208
|
+
const written = typeNameOf(source, node);
|
|
7209
|
+
if (written === void 0) return;
|
|
7210
|
+
const bare = written.startsWith("jest.") ? written.slice("jest.".length) : written;
|
|
7211
|
+
if (!SPY_TYPES.has(bare)) return;
|
|
7212
|
+
const args = (node.typeArguments ?? node.typeParameters)?.params;
|
|
7213
|
+
if (args === void 0 || args.length === 0) return;
|
|
7214
|
+
const text = (argument) => source.slice(argument.start, argument.end);
|
|
7215
|
+
const returns = text(args[0]);
|
|
7216
|
+
const parameters = args.length > 1 ? `...args: ${text(args[1])}` : "...args: any[]";
|
|
7217
|
+
edits.push({
|
|
7218
|
+
start: node.start,
|
|
7219
|
+
end: node.end,
|
|
7220
|
+
text: `MockInstance<(${parameters}) => ${returns}>`
|
|
7221
|
+
});
|
|
7222
|
+
});
|
|
7223
|
+
let result = source;
|
|
7224
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
7225
|
+
result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
|
|
7226
|
+
}
|
|
7227
|
+
return {
|
|
7228
|
+
source: result,
|
|
7229
|
+
rewritten: edits.length,
|
|
7230
|
+
typeImports: edits.length > 0 ? ["MockInstance"] : []
|
|
7231
|
+
};
|
|
7232
|
+
}
|
|
7233
|
+
|
|
7234
|
+
// src/roles/test/codemod/migrate-file.ts
|
|
7235
|
+
function mockFactories(program) {
|
|
7236
|
+
const factories = [];
|
|
7237
|
+
walk(program, (node) => {
|
|
7238
|
+
if (node.type !== "CallExpression") return;
|
|
7239
|
+
const callee = node.callee;
|
|
7240
|
+
if (callee?.type !== "MemberExpression" && callee?.type !== "StaticMemberExpression") return;
|
|
7241
|
+
const object = callee.object;
|
|
7242
|
+
const property = callee.property;
|
|
7243
|
+
if (object?.type !== "Identifier") return;
|
|
7244
|
+
if (object.name !== "jest" && object.name !== "vi") return;
|
|
7245
|
+
if (property?.name !== "mock") return;
|
|
7246
|
+
const args = node.arguments ?? [];
|
|
7247
|
+
if (args.length > 1) factories.push(args[1]);
|
|
7248
|
+
});
|
|
7249
|
+
return factories;
|
|
7250
|
+
}
|
|
7251
|
+
function parse2(fileName, source) {
|
|
7252
|
+
const { program, errors } = parseSync7(fileName, source, { sourceType: "module" });
|
|
7253
|
+
return errors.length > 0 ? void 0 : program;
|
|
7254
|
+
}
|
|
7255
|
+
function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new Set(), asyncHelpers = /* @__PURE__ */ new Set()) {
|
|
7256
|
+
const untouched = {
|
|
7257
|
+
source,
|
|
7258
|
+
changed: false,
|
|
7259
|
+
renamed: 0,
|
|
7260
|
+
hoisted: [],
|
|
7261
|
+
unhandled: [],
|
|
7262
|
+
refusals: [],
|
|
7263
|
+
rulesApplied: [],
|
|
7264
|
+
mswRepointed: false
|
|
7265
|
+
};
|
|
7266
|
+
const unparsable = (step) => ({
|
|
7267
|
+
...untouched,
|
|
7268
|
+
refusals: [
|
|
7269
|
+
{
|
|
7270
|
+
binding: step,
|
|
7271
|
+
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.`
|
|
7272
|
+
}
|
|
7273
|
+
]
|
|
7274
|
+
});
|
|
7275
|
+
const original = parse2(fileName, source);
|
|
7276
|
+
if (original === void 0) return unparsable("reading the file");
|
|
7277
|
+
const titles = rewriteEachTitles(source, original);
|
|
7278
|
+
const tables = rewriteEachTableRows(titles.source, original);
|
|
7279
|
+
const afterTables = parse2(fileName, tables.source);
|
|
7280
|
+
if (afterTables === void 0) return unparsable("each table rows");
|
|
7281
|
+
const spyTypes = rewriteSpyInstanceType(tables.source, afterTables);
|
|
7282
|
+
const afterSpyTypes = parse2(fileName, spyTypes.source);
|
|
7283
|
+
if (afterSpyTypes === void 0) return unparsable("SpyInstance types");
|
|
7284
|
+
const ruled = applyNamedRules(spyTypes.source, afterSpyTypes, fileName);
|
|
7285
|
+
const renamed = renameJestToVitest(fileName, ruled.source);
|
|
7286
|
+
const afterRename = parse2(fileName, renamed.source);
|
|
7287
|
+
if (afterRename === void 0) return unparsable("jest to vi rename");
|
|
7288
|
+
const fakeTimers = rewriteFakeTimerOptions(renamed.source, afterRename);
|
|
7289
|
+
const afterFakeTimers = parse2(fileName, fakeTimers.source);
|
|
7290
|
+
if (afterFakeTimers === void 0) return unparsable("fake timer options");
|
|
7291
|
+
const mocks = rewriteMockAsValue(fakeTimers.source, afterFakeTimers);
|
|
7292
|
+
const afterMocks = parse2(fileName, mocks.source);
|
|
7293
|
+
if (afterMocks === void 0) return unparsable("mock as a value");
|
|
7294
|
+
const constructible = rewriteConstructibleMocks(
|
|
7295
|
+
mocks.source,
|
|
7296
|
+
afterMocks,
|
|
7297
|
+
mockFactories(afterMocks)
|
|
7298
|
+
);
|
|
7299
|
+
const afterConstructible = parse2(fileName, constructible.source);
|
|
7300
|
+
if (afterConstructible === void 0) return unparsable("constructible mocks");
|
|
7301
|
+
const factoryDefault = rewriteMockFactoryDefault(
|
|
7302
|
+
constructible.source,
|
|
7303
|
+
afterConstructible,
|
|
7304
|
+
defaultImported
|
|
7305
|
+
);
|
|
7306
|
+
const beforeReturnValue = parse2(fileName, factoryDefault.source);
|
|
7307
|
+
if (beforeReturnValue === void 0) return unparsable("mock factory default");
|
|
7308
|
+
const returnValue = rewriteAssertionReturnValue(factoryDefault.source, beforeReturnValue);
|
|
7309
|
+
const afterReturnValue = parse2(fileName, returnValue.source);
|
|
7310
|
+
if (afterReturnValue === void 0) return unparsable("assertion return value");
|
|
7311
|
+
const isolated = rewriteIsolateModules(returnValue.source, afterReturnValue);
|
|
7312
|
+
const afterIsolate = parse2(fileName, isolated.source);
|
|
7313
|
+
if (afterIsolate === void 0) return unparsable("isolate modules");
|
|
7314
|
+
const emptyImplementations = rewriteEmptyMockImplementation(isolated.source, afterIsolate);
|
|
7315
|
+
const afterEmpty = parse2(fileName, emptyImplementations.source);
|
|
7316
|
+
if (afterEmpty === void 0) return unparsable("empty mock implementation");
|
|
7317
|
+
const hooks = rewriteHookReturnValue(emptyImplementations.source, afterEmpty);
|
|
7318
|
+
const awaited = propagateAwait(fileName, hooks.source, original, asyncHelpers);
|
|
7319
|
+
const program = parse2(fileName, awaited.source);
|
|
7320
|
+
if (program === void 0) return unparsable("hook body");
|
|
7321
|
+
const hoist = hoistCapturedBindings(
|
|
7322
|
+
awaited.source,
|
|
7323
|
+
program,
|
|
7324
|
+
mockFactories(program),
|
|
7325
|
+
moduleBindings(program),
|
|
7326
|
+
importedNames(program)
|
|
7327
|
+
);
|
|
7328
|
+
const withImport = injectTypeImport(fileName, hoist.source, [
|
|
7329
|
+
...renamed.typeImports,
|
|
7330
|
+
...spyTypes.typeImports
|
|
7331
|
+
]);
|
|
7332
|
+
const deep = normaliseDeepImports(withImport);
|
|
7333
|
+
const repointed = repointMswServer(fileName, deep.source);
|
|
7334
|
+
const finalProgram = parse2(fileName, repointed.source);
|
|
7335
|
+
if (finalProgram === void 0) return unparsable("msw server import");
|
|
7336
|
+
const barrels = rewriteReexportBarrelMocks(fileName, repointed.source, finalProgram);
|
|
7337
|
+
return {
|
|
7338
|
+
source: barrels.source,
|
|
7339
|
+
changed: barrels.source !== source,
|
|
7340
|
+
renamed: renamed.rewritten,
|
|
7341
|
+
hoisted: hoist.hoisted,
|
|
7342
|
+
unhandled: renamed.unhandled,
|
|
7343
|
+
rulesApplied: [
|
|
7344
|
+
...ruled.applied,
|
|
7345
|
+
...titles.rewritten > 0 ? ["each title %p"] : [],
|
|
7346
|
+
...mocks.rewritten > 0 ? ["vi.mock as a value"] : [],
|
|
7347
|
+
...deep.rewritten > 0 ? ["deep import normalised"] : [],
|
|
7348
|
+
...constructible.rewritten > 0 ? ["constructible mock implementation"] : [],
|
|
7349
|
+
...isolated.rewritten > 0 ? ["jest.isolateModules"] : [],
|
|
7350
|
+
...emptyImplementations.rewritten > 0 ? ["empty mockImplementation"] : [],
|
|
7351
|
+
...hooks.rewritten > 0 ? ["hook body, so it does not return its mock"] : [],
|
|
7352
|
+
...spyTypes.rewritten > 0 ? ["SpyInstance<T> as a signature, not a return type"] : [],
|
|
7353
|
+
...awaited.rewritten > 0 ? ["await propagated to a helper that became async"] : [],
|
|
7354
|
+
...barrels.added.map((alias) => `mock carried to ${alias}, which re-exports the mocked file`),
|
|
7355
|
+
...fakeTimers.translated
|
|
7356
|
+
].sort(),
|
|
7357
|
+
refusals: [
|
|
7358
|
+
...fakeTimers.refusals,
|
|
7359
|
+
...hoist.refusals,
|
|
7360
|
+
...mocks.refusals.map((refusal) => ({ binding: refusal.binding, reason: refusal.reason })),
|
|
7361
|
+
...constructible.refusals,
|
|
7362
|
+
...isolated.refusals
|
|
7363
|
+
],
|
|
7364
|
+
mswRepointed: repointed.repointed
|
|
7365
|
+
};
|
|
7366
|
+
}
|
|
7367
|
+
|
|
7368
|
+
// src/roles/test/codemod/test-support-files.ts
|
|
7369
|
+
import { existsSync as existsSync25, readFileSync as readFileSync24, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
7370
|
+
import { dirname as dirname6, join as join30, relative as relative5, resolve as resolve4 } from "path";
|
|
7371
|
+
var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
|
|
7372
|
+
var TEST_FILE2 = /\.(spec|test)\.[cm]?[jt]sx?$/;
|
|
7373
|
+
var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
|
|
7374
|
+
var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"];
|
|
7375
|
+
function relativeSpecifiers(source) {
|
|
7376
|
+
const found = [];
|
|
7377
|
+
const patterns = [
|
|
7378
|
+
/\bfrom\s+['"](\.[^'"]*)['"]/g,
|
|
7379
|
+
/\bimport\s+['"](\.[^'"]*)['"]/g,
|
|
7380
|
+
/\bimport\s*\(\s*['"](\.[^'"]*)['"]\s*\)/g,
|
|
7381
|
+
/\brequire\s*\(\s*['"](\.[^'"]*)['"]\s*\)/g
|
|
7382
|
+
];
|
|
7383
|
+
for (const pattern of patterns) {
|
|
7384
|
+
for (const match of source.matchAll(pattern)) found.push(match[1]);
|
|
7385
|
+
}
|
|
7386
|
+
return found;
|
|
7387
|
+
}
|
|
7388
|
+
function resolveSpecifier(fromFile, specifier) {
|
|
7389
|
+
const base = resolve4(dirname6(fromFile), specifier);
|
|
7390
|
+
if (existsSync25(base) && statSync5(base).isFile()) return base;
|
|
7391
|
+
for (const extension of EXTENSIONS) {
|
|
7392
|
+
if (existsSync25(`${base}${extension}`)) return `${base}${extension}`;
|
|
7393
|
+
}
|
|
7394
|
+
for (const extension of EXTENSIONS) {
|
|
7395
|
+
const index = join30(base, `index${extension}`);
|
|
7396
|
+
if (existsSync25(index)) return index;
|
|
7397
|
+
}
|
|
7398
|
+
return void 0;
|
|
7399
|
+
}
|
|
7400
|
+
function moduleSources(cwd) {
|
|
7401
|
+
const found = [];
|
|
7402
|
+
const stack = [cwd];
|
|
7403
|
+
while (stack.length > 0) {
|
|
7404
|
+
const dir = stack.pop();
|
|
7405
|
+
let entries;
|
|
7406
|
+
try {
|
|
7407
|
+
entries = readdirSync5(dir, { withFileTypes: true });
|
|
7408
|
+
} catch {
|
|
7409
|
+
continue;
|
|
7410
|
+
}
|
|
7411
|
+
for (const entry of entries) {
|
|
7412
|
+
if (entry.isDirectory()) {
|
|
7413
|
+
if (!SKIPPED.has(entry.name)) stack.push(join30(dir, entry.name));
|
|
7414
|
+
} else if (SOURCE_FILE.test(entry.name)) {
|
|
7415
|
+
found.push(join30(dir, entry.name));
|
|
7416
|
+
}
|
|
7417
|
+
}
|
|
7418
|
+
}
|
|
7419
|
+
return found;
|
|
7420
|
+
}
|
|
7421
|
+
function read(path) {
|
|
7422
|
+
try {
|
|
7423
|
+
return readFileSync24(path, "utf8");
|
|
7424
|
+
} catch {
|
|
7425
|
+
return void 0;
|
|
7426
|
+
}
|
|
7427
|
+
}
|
|
7428
|
+
function setupFilesInside(cwd, named) {
|
|
7429
|
+
const kept = [];
|
|
7430
|
+
for (const raw of named) {
|
|
7431
|
+
const withoutToken = raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", ".");
|
|
7432
|
+
const full = resolve4(cwd, withoutToken);
|
|
7433
|
+
const inside = relative5(cwd, full);
|
|
7434
|
+
if (inside.startsWith("..") || inside === "") continue;
|
|
7435
|
+
if (!existsSync25(full)) continue;
|
|
7436
|
+
kept.push(inside.replaceAll("\\", "/"));
|
|
7437
|
+
}
|
|
7438
|
+
return kept;
|
|
7439
|
+
}
|
|
7440
|
+
function listTestSupportFiles(cwd, setupFiles = []) {
|
|
7441
|
+
const sources = moduleSources(cwd);
|
|
7442
|
+
const contents = /* @__PURE__ */ new Map();
|
|
7443
|
+
for (const file of sources) {
|
|
7444
|
+
const source = read(file);
|
|
7445
|
+
if (source !== void 0) contents.set(file, source);
|
|
7446
|
+
}
|
|
7447
|
+
const importedBy = /* @__PURE__ */ new Map();
|
|
7448
|
+
for (const [file, source] of contents) {
|
|
7449
|
+
for (const specifier of relativeSpecifiers(source)) {
|
|
7450
|
+
const target = resolveSpecifier(file, specifier);
|
|
7451
|
+
if (target === void 0 || !contents.has(target)) continue;
|
|
7452
|
+
importedBy.set(target, [...importedBy.get(target) ?? [], file]);
|
|
7453
|
+
}
|
|
7454
|
+
}
|
|
7455
|
+
const testOnly = /* @__PURE__ */ new Set();
|
|
7456
|
+
const queue = sources.filter((file) => TEST_FILE2.test(file));
|
|
7457
|
+
const isTestOnly = (file) => (importedBy.get(file) ?? []).every(
|
|
7458
|
+
(importer) => TEST_FILE2.test(importer) || testOnly.has(importer)
|
|
7459
|
+
);
|
|
7460
|
+
while (queue.length > 0) {
|
|
7461
|
+
const file = queue.shift();
|
|
7462
|
+
for (const specifier of relativeSpecifiers(contents.get(file) ?? "")) {
|
|
7463
|
+
const target = resolveSpecifier(file, specifier);
|
|
7464
|
+
if (target === void 0 || TEST_FILE2.test(target) || testOnly.has(target)) continue;
|
|
7465
|
+
if (!isTestOnly(target)) continue;
|
|
7466
|
+
testOnly.add(target);
|
|
7467
|
+
queue.push(target);
|
|
7468
|
+
}
|
|
7469
|
+
}
|
|
7470
|
+
const reachable = [...testOnly].filter((file) => /\brequire\s*\(/.test(contents.get(file) ?? "")).map((file) => relative5(cwd, file).replaceAll("\\", "/"));
|
|
7471
|
+
return [.../* @__PURE__ */ new Set([...reachable, ...setupFilesInside(cwd, setupFiles)])].sort();
|
|
7472
|
+
}
|
|
7473
|
+
|
|
7474
|
+
// src/roles/test/codemod/migrate-module.ts
|
|
7475
|
+
function parse3(source) {
|
|
7476
|
+
const { program, errors } = parseSync8("file.ts", source, { sourceType: "module" });
|
|
7477
|
+
return errors.length > 0 ? void 0 : program;
|
|
7478
|
+
}
|
|
7479
|
+
function read2(path) {
|
|
7480
|
+
try {
|
|
7481
|
+
return readFileSync25(path, "utf8");
|
|
7482
|
+
} catch {
|
|
7483
|
+
return void 0;
|
|
7484
|
+
}
|
|
7485
|
+
}
|
|
7486
|
+
var SKIPPED2 = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
|
|
7487
|
+
function defaultImportedSpecifiers(cwd) {
|
|
7488
|
+
const found = /* @__PURE__ */ new Set();
|
|
7489
|
+
const pattern = /^\s*import\s+[A-Za-z_$][\w$]*\s*(?:,\s*\{[^}]*\}\s*)?from\s+['"]([^'"]+)['"]/gm;
|
|
7490
|
+
const walk2 = (dir) => {
|
|
7491
|
+
let entries;
|
|
7492
|
+
try {
|
|
7493
|
+
entries = readdirSync6(dir);
|
|
7494
|
+
} catch {
|
|
7495
|
+
return;
|
|
7496
|
+
}
|
|
7497
|
+
for (const entry of entries) {
|
|
7498
|
+
if (SKIPPED2.has(entry)) continue;
|
|
7499
|
+
const path = join31(dir, entry);
|
|
7500
|
+
let directory;
|
|
7501
|
+
try {
|
|
7502
|
+
directory = statSync6(path).isDirectory();
|
|
7503
|
+
} catch {
|
|
7504
|
+
continue;
|
|
7505
|
+
}
|
|
7506
|
+
if (directory) {
|
|
7507
|
+
walk2(path);
|
|
7508
|
+
continue;
|
|
7509
|
+
}
|
|
7510
|
+
if (!/\.tsx?$/.test(entry)) continue;
|
|
7511
|
+
const source = read2(path);
|
|
7512
|
+
if (source === void 0) continue;
|
|
7513
|
+
for (const match of source.matchAll(pattern)) found.add(match[1]);
|
|
7514
|
+
}
|
|
7515
|
+
};
|
|
7516
|
+
walk2(cwd);
|
|
7517
|
+
return found;
|
|
7518
|
+
}
|
|
7519
|
+
function migrateModuleTests(cwd, setupFiles = []) {
|
|
7520
|
+
const operations = [];
|
|
7521
|
+
const blockers = [];
|
|
7522
|
+
let files = 0;
|
|
7523
|
+
let renamed = 0;
|
|
7524
|
+
let hoisted = 0;
|
|
7525
|
+
const defaultImported = defaultImportedSpecifiers(cwd);
|
|
7526
|
+
const asyncHelpers = /* @__PURE__ */ new Set();
|
|
7527
|
+
for (const relative7 of listTestSupportFiles(cwd, setupFiles)) {
|
|
7528
|
+
const source = read2(join31(cwd, relative7));
|
|
7529
|
+
if (source === void 0) {
|
|
7530
|
+
blockers.push({ file: relative7, reason: "this file could not be read" });
|
|
7531
|
+
continue;
|
|
7532
|
+
}
|
|
7533
|
+
const migrated = migrateTestFile(join31(cwd, relative7), source, defaultImported);
|
|
7534
|
+
if (!migrated.changed) continue;
|
|
7535
|
+
const before = parse3(source);
|
|
7536
|
+
const after = parse3(migrated.source);
|
|
7537
|
+
if (before !== void 0 && after !== void 0) {
|
|
7538
|
+
const wasAsync = asyncFunctionNames(before);
|
|
7539
|
+
for (const name of asyncFunctionNames(after)) {
|
|
7540
|
+
if (!wasAsync.has(name)) asyncHelpers.add(name);
|
|
7541
|
+
}
|
|
7542
|
+
}
|
|
7543
|
+
files++;
|
|
7544
|
+
renamed += migrated.renamed;
|
|
7545
|
+
operations.push({ kind: "write", path: relative7, contents: migrated.source });
|
|
7546
|
+
}
|
|
7547
|
+
for (const relative7 of listTestFiles(cwd)) {
|
|
7548
|
+
const source = read2(join31(cwd, relative7));
|
|
7549
|
+
if (source === void 0) {
|
|
7550
|
+
blockers.push({ file: relative7, reason: "this file could not be read" });
|
|
7551
|
+
continue;
|
|
7552
|
+
}
|
|
7553
|
+
const migrated = migrateTestFile(join31(cwd, relative7), source, defaultImported, asyncHelpers);
|
|
7554
|
+
for (const member of migrated.unhandled) {
|
|
7555
|
+
blockers.push({
|
|
7556
|
+
file: `${relative7}:${member.line}`,
|
|
7557
|
+
reason: member.rule === void 0 ? `${member.member} has no rule, and guessing one is how a suite goes green for the wrong reason` : `${member.member} needs ${member.rule}`
|
|
7558
|
+
});
|
|
7559
|
+
}
|
|
7560
|
+
for (const refusal of migrated.refusals) {
|
|
7561
|
+
blockers.push({ file: relative7, reason: refusal.reason });
|
|
7562
|
+
}
|
|
7563
|
+
if (!migrated.changed) continue;
|
|
7564
|
+
files++;
|
|
7565
|
+
renamed += migrated.renamed;
|
|
7566
|
+
if (migrated.hoisted.length > 0) hoisted++;
|
|
7567
|
+
operations.push({ kind: "write", path: relative7, contents: migrated.source });
|
|
7568
|
+
}
|
|
7569
|
+
return blockers.length > 0 ? { operations: [], files, renamed, hoisted, blockers } : { operations, files, renamed, hoisted, blockers: [] };
|
|
7570
|
+
}
|
|
7571
|
+
|
|
7572
|
+
// src/roles/test/nest/generate-config.ts
|
|
7573
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync26, statSync as statSync7 } from "fs";
|
|
7574
|
+
import { join as join32, relative as relative6 } from "path";
|
|
7575
|
+
var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
|
|
7576
|
+
var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/nest";
|
|
7577
|
+
var SENTINEL_MSW_SETUP = "@hublo/sentinel/test/setup/msw-lifecycle";
|
|
7578
|
+
var SENTINEL_WORKSPACE_SETUP = "@hublo/sentinel/test/setup/workspace";
|
|
7579
|
+
var NEST_TEST_CONFIG_FILE = "vitest.config.mts";
|
|
7580
|
+
function hopsToRoot(cwd, workspaceRoot) {
|
|
7581
|
+
const up = relative6(cwd, workspaceRoot).replaceAll("\\", "/");
|
|
7582
|
+
return up === "" ? "." : up;
|
|
7583
|
+
}
|
|
7584
|
+
function inlineBlock(packages) {
|
|
7585
|
+
if (packages.length === 0) return "";
|
|
7586
|
+
const list = packages.map((name) => `'${name}'`).join(", ");
|
|
7587
|
+
return ` server: { deps: { inline: [${list}] } },`;
|
|
7588
|
+
}
|
|
7589
|
+
function coverageBlock(reading) {
|
|
7590
|
+
return reading.coverageDirectory === void 0 ? "" : ` coverage: { reportsDirectory: '${reading.coverageDirectory}' },`;
|
|
7591
|
+
}
|
|
7592
|
+
function environmentBlock(reading) {
|
|
7593
|
+
return reading.testEnvironment === void 0 ? "" : ` environment: '${reading.testEnvironment}',`;
|
|
7594
|
+
}
|
|
7595
|
+
var SKIPPED3 = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
|
|
7596
|
+
var BARE_AUTOMOCK = /(?:vi|vitest|jest)\.mock\(\s*['"]([^'".][^'"]*)['"]\s*\)/g;
|
|
7597
|
+
var MENTIONS_MSW = /from\s+['"](msw(\/[^'"]*)?|@hublo\/test\/msw\/server|@hublo\/sentinel\/test\/msw)['"]/;
|
|
7598
|
+
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*[:(]/;
|
|
7599
|
+
function readModuleTraits(cwd) {
|
|
7600
|
+
const traits = {
|
|
7601
|
+
msw: false,
|
|
7602
|
+
decoratedAbstractMember: false,
|
|
7603
|
+
automockedPackages: []
|
|
7604
|
+
};
|
|
7605
|
+
const automocked = /* @__PURE__ */ new Set();
|
|
7606
|
+
const walk2 = (dir) => {
|
|
7607
|
+
let entries;
|
|
7608
|
+
try {
|
|
7609
|
+
entries = readdirSync7(dir);
|
|
7610
|
+
} catch {
|
|
7611
|
+
return;
|
|
7612
|
+
}
|
|
7613
|
+
for (const entry of entries) {
|
|
7614
|
+
if (SKIPPED3.has(entry)) continue;
|
|
7615
|
+
const path = join32(dir, entry);
|
|
7616
|
+
let directory;
|
|
7617
|
+
try {
|
|
7618
|
+
directory = statSync7(path).isDirectory();
|
|
7619
|
+
} catch {
|
|
7620
|
+
continue;
|
|
7621
|
+
}
|
|
7622
|
+
if (directory) {
|
|
7623
|
+
walk2(path);
|
|
7624
|
+
continue;
|
|
7625
|
+
}
|
|
7626
|
+
if (!/\.tsx?$/.test(entry)) continue;
|
|
7627
|
+
let source;
|
|
7628
|
+
try {
|
|
7629
|
+
source = readFileSync26(path, "utf8");
|
|
7630
|
+
} catch {
|
|
7631
|
+
continue;
|
|
7632
|
+
}
|
|
7633
|
+
if (!traits.msw && MENTIONS_MSW.test(source)) traits.msw = true;
|
|
7634
|
+
if (!traits.decoratedAbstractMember && DECORATED_ABSTRACT_MEMBER.test(source)) {
|
|
7635
|
+
traits.decoratedAbstractMember = true;
|
|
7636
|
+
}
|
|
7637
|
+
BARE_AUTOMOCK.lastIndex = 0;
|
|
7638
|
+
for (const match of source.matchAll(BARE_AUTOMOCK)) automocked.add(match[1]);
|
|
7639
|
+
}
|
|
7640
|
+
};
|
|
7641
|
+
walk2(cwd);
|
|
7642
|
+
traits.automockedPackages = [...automocked].sort();
|
|
7643
|
+
return traits;
|
|
7644
|
+
}
|
|
7645
|
+
var withoutRootDir = (value) => value.replaceAll("<rootDir>/", "./").replaceAll("<rootDir>", ".");
|
|
7646
|
+
function setupBlock(reading, msw) {
|
|
7647
|
+
const declared = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []];
|
|
7648
|
+
const namedTheRootSetup = declared.some((file) => file.endsWith("jest.setup.after.env.js"));
|
|
7649
|
+
const own = declared.filter((file) => !file.endsWith("jest.setup.after.env.js")).map((file) => `'${withoutRootDir(file)}'`);
|
|
7650
|
+
const sentinel = [
|
|
7651
|
+
`'${SENTINEL_TEST_SETUP}'`,
|
|
7652
|
+
...namedTheRootSetup ? [`'${SENTINEL_WORKSPACE_SETUP}'`] : [],
|
|
7653
|
+
...msw ? [`'${SENTINEL_MSW_SETUP}'`] : []
|
|
7654
|
+
];
|
|
7655
|
+
return ` setupFiles: [${[...sentinel, ...own].join(", ")}],`;
|
|
7656
|
+
}
|
|
7657
|
+
function envBlock(reading) {
|
|
7658
|
+
if (reading.env === void 0 || reading.env.length === 0) return "";
|
|
7659
|
+
const entries = reading.env.map((entry) => ` ${entry.name}: ${entry.value},`);
|
|
7660
|
+
return [" env: {", ...entries, " },"].join("\n");
|
|
7661
|
+
}
|
|
7662
|
+
function concurrencyBlock(cwd, reading, target) {
|
|
7663
|
+
const declared = reading.maxWorkers;
|
|
7664
|
+
if (declared !== void 0) {
|
|
7665
|
+
return declared === "1" ? ` fileParallelism: false,` : ` maxWorkers: ${declared},`;
|
|
7666
|
+
}
|
|
7667
|
+
const ci = projectTargets(cwd)?.[target]?.configurations?.ci;
|
|
7668
|
+
if (ci === void 0) return "";
|
|
7669
|
+
if (ci.runInBand === true) return ` fileParallelism: false,`;
|
|
7670
|
+
if (typeof ci.maxWorkers === "number") {
|
|
7671
|
+
return ci.maxWorkers === 1 ? ` fileParallelism: false,` : ` maxWorkers: ${ci.maxWorkers},`;
|
|
7672
|
+
}
|
|
7673
|
+
return "";
|
|
7674
|
+
}
|
|
7675
|
+
function timeoutBlock(reading) {
|
|
7676
|
+
return reading.testTimeout === void 0 ? "" : ` testTimeout: ${reading.testTimeout},`;
|
|
7677
|
+
}
|
|
7678
|
+
function globalSetupBlock(reading) {
|
|
7679
|
+
if (reading.globalSetup === void 0) return "";
|
|
7680
|
+
const files = reading.globalSetup.map((file) => `'${withoutRootDir(file)}'`).join(", ");
|
|
7681
|
+
return ` globalSetup: [${files}],`;
|
|
7682
|
+
}
|
|
7683
|
+
function includeBlock(reading) {
|
|
7684
|
+
if (reading.testMatch === void 0) return "";
|
|
7685
|
+
const globs = reading.testMatch.map((glob) => `'${withoutRootDir(glob)}'`).join(", ");
|
|
7686
|
+
return ` include: [${globs}],`;
|
|
7687
|
+
}
|
|
7688
|
+
function excludeBlock(reading) {
|
|
7689
|
+
if (reading.excludeGlobs === void 0 || reading.excludeGlobs.length === 0) return "";
|
|
7690
|
+
const globs = reading.excludeGlobs.map((glob) => `'${glob}'`).join(", ");
|
|
7691
|
+
return ` exclude: [...configDefaults.exclude, ${globs}],`;
|
|
7692
|
+
}
|
|
7693
|
+
function isPackage(replacement) {
|
|
7694
|
+
return !replacement.startsWith(".") && !replacement.startsWith("/") && !replacement.toLowerCase().includes("<rootdir>");
|
|
7695
|
+
}
|
|
7696
|
+
function aliasBlock(reading) {
|
|
7697
|
+
if (reading.aliases === void 0 || reading.aliases.length === 0) return "";
|
|
7698
|
+
const entries = reading.aliases.map((alias) => {
|
|
7699
|
+
const pattern = alias.find.replaceAll(String.raw`\\`, "\\").replaceAll("/", String.raw`\/`);
|
|
7700
|
+
const target = isPackage(alias.replacement) ? `'${alias.replacement}'` : `path.resolve(import.meta.dirname, '${withoutRootDir(alias.replacement)}')`;
|
|
7701
|
+
return ` { find: /${pattern}/, replacement: ${target} },`;
|
|
7702
|
+
});
|
|
7703
|
+
return [" resolve: {", " alias: [", ...entries, " ],", " },"].join("\n");
|
|
7704
|
+
}
|
|
7705
|
+
function generateNestTestConfig(cwd, reading, target = "test") {
|
|
7706
|
+
if (reading.refusals.length > 0) return { refusals: reading.refusals };
|
|
7707
|
+
const workspaceRoot = findWorkspaceRoot(cwd);
|
|
7708
|
+
if (workspaceRoot === void 0) {
|
|
7709
|
+
return { refusals: ["the workspace root could not be found from this module"] };
|
|
7710
|
+
}
|
|
7711
|
+
const traits = readModuleTraits(cwd);
|
|
7712
|
+
const up = hopsToRoot(cwd, workspaceRoot);
|
|
7713
|
+
const test = [
|
|
7714
|
+
environmentBlock(reading),
|
|
7715
|
+
setupBlock(reading, traits.msw),
|
|
7716
|
+
envBlock(reading),
|
|
7717
|
+
timeoutBlock(reading),
|
|
7718
|
+
concurrencyBlock(cwd, reading, target),
|
|
7719
|
+
globalSetupBlock(reading),
|
|
7720
|
+
includeBlock(reading),
|
|
7721
|
+
excludeBlock(reading),
|
|
7722
|
+
inlineBlock(traits.automockedPackages),
|
|
7723
|
+
coverageBlock(reading)
|
|
7724
|
+
].filter((line) => line !== "");
|
|
7725
|
+
const needsDefaults = test.some((line) => line.includes("configDefaults"));
|
|
7726
|
+
const imports = [
|
|
7727
|
+
`import path from 'node:path'`,
|
|
7728
|
+
"",
|
|
7729
|
+
...needsDefaults ? [`import { configDefaults } from 'vitest/config'`, ""] : [],
|
|
7730
|
+
`import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
|
|
7731
|
+
].join("\n");
|
|
7732
|
+
const overrides = [aliasBlock(reading), " test: {", ...test, " },"].filter(
|
|
7733
|
+
(line) => line !== ""
|
|
7734
|
+
);
|
|
7735
|
+
const decoratorLine = traits.decoratedAbstractMember ? "\n lowerDecoratorsWithTypeScript: true," : "";
|
|
7736
|
+
return {
|
|
7737
|
+
refusals: [],
|
|
7738
|
+
source: `${imports}
|
|
7739
|
+
|
|
7740
|
+
export default defineConfig(
|
|
7741
|
+
nestTestConfig({
|
|
7742
|
+
root: import.meta.dirname,
|
|
7743
|
+
workspaceRoot: path.resolve(import.meta.dirname, '${up}'),${decoratorLine}
|
|
7744
|
+
overrides: {
|
|
7745
|
+
${overrides.join("\n")}
|
|
7746
|
+
},
|
|
7747
|
+
}),
|
|
7748
|
+
)
|
|
7749
|
+
`
|
|
7750
|
+
};
|
|
7751
|
+
}
|
|
7752
|
+
|
|
7753
|
+
// src/roles/test/nest/read-jest-config.ts
|
|
7754
|
+
import { existsSync as existsSync26, readFileSync as readFileSync27 } from "fs";
|
|
7755
|
+
import { join as join33 } from "path";
|
|
7756
|
+
var CARRIED = [
|
|
7757
|
+
"testEnvironment",
|
|
7758
|
+
"coverageDirectory",
|
|
7759
|
+
"setupFilesAfterEnv",
|
|
7760
|
+
"setupFiles",
|
|
7761
|
+
"testTimeout",
|
|
7762
|
+
"maxWorkers",
|
|
7763
|
+
"globalSetup",
|
|
7764
|
+
"testMatch",
|
|
7765
|
+
"collectCoverageFrom",
|
|
7766
|
+
"testPathIgnorePatterns",
|
|
7767
|
+
"moduleNameMapper"
|
|
7768
|
+
];
|
|
7769
|
+
var DISCARDED = [
|
|
7770
|
+
"preset",
|
|
7771
|
+
"displayName",
|
|
7772
|
+
"moduleFileExtensions",
|
|
7773
|
+
"transform",
|
|
7774
|
+
"extensionsToTreatAsEsm"
|
|
7775
|
+
];
|
|
7776
|
+
var STRING = String.raw`['"\`]([^'"\`]*)['"\`]`;
|
|
7777
|
+
function readString(body, key) {
|
|
7778
|
+
const value = new RegExp(String.raw`\b${key}\s*:\s*${STRING}`).exec(body)?.[1];
|
|
7779
|
+
return value?.includes("${") === true ? void 0 : value;
|
|
7780
|
+
}
|
|
7781
|
+
function readNumber(body, key) {
|
|
7782
|
+
return new RegExp(String.raw`\b${key}\s*:\s*(\d+)`).exec(body)?.[1];
|
|
7783
|
+
}
|
|
7784
|
+
function objectBody(source, key) {
|
|
7785
|
+
const at = new RegExp(String.raw`\b${key}\s*:\s*\{`).exec(source);
|
|
7786
|
+
if (at === null) return void 0;
|
|
7787
|
+
const from = at.index + at[0].length - 1;
|
|
7788
|
+
let depth = 0;
|
|
7789
|
+
for (let i = from; i < source.length; i += 1) {
|
|
7790
|
+
if (source[i] === "{") depth += 1;
|
|
7791
|
+
else if (source[i] === "}") {
|
|
7792
|
+
depth -= 1;
|
|
7793
|
+
if (depth === 0) return source.slice(from + 1, i);
|
|
7794
|
+
}
|
|
7795
|
+
}
|
|
7796
|
+
return void 0;
|
|
7797
|
+
}
|
|
7798
|
+
function objectBodyAfter(source, offset) {
|
|
7799
|
+
const open = source.indexOf("{", offset);
|
|
7800
|
+
if (open === -1) return void 0;
|
|
7801
|
+
let depth = 0;
|
|
7802
|
+
for (let i = open; i < source.length; i += 1) {
|
|
7803
|
+
if (source[i] === "{") depth += 1;
|
|
7804
|
+
else if (source[i] === "}") {
|
|
7805
|
+
depth -= 1;
|
|
7806
|
+
if (depth === 0) return source.slice(open + 1, i);
|
|
7807
|
+
}
|
|
7808
|
+
}
|
|
7809
|
+
return void 0;
|
|
7810
|
+
}
|
|
7811
|
+
function readEnvironment(source) {
|
|
7812
|
+
const entries = [];
|
|
7813
|
+
const at = /process\.env\s*=\s*Object\.assign\(\s*process\.env\s*,/.exec(source);
|
|
7814
|
+
if (at !== null) {
|
|
7815
|
+
const block = objectBody(source.slice(at.index), "assign") ?? objectBodyAfter(source, at.index);
|
|
7816
|
+
for (const raw of (block ?? "").split(/,(?![^[\]{}]*[\]}])/)) {
|
|
7817
|
+
const entry = /^\s*(?:\/\/[^\n]*\n\s*)?([A-Za-z_][\w]*)\s*:\s*([\s\S]+?)\s*$/.exec(raw);
|
|
7818
|
+
if (entry !== null) entries.push({ name: entry[1], value: entry[2] });
|
|
7819
|
+
}
|
|
7820
|
+
}
|
|
7821
|
+
for (const match of source.matchAll(/process\.env\.(\w+)\s*\?\?=\s*(.+?),?\s*$/gm)) {
|
|
7822
|
+
entries.push({ name: match[1], value: match[2] });
|
|
7823
|
+
}
|
|
7824
|
+
return entries;
|
|
7825
|
+
}
|
|
7826
|
+
function readCoverageFrom(body) {
|
|
7827
|
+
const globs = readStringArray(body, "collectCoverageFrom");
|
|
7828
|
+
if (globs === void 0) return void 0;
|
|
7829
|
+
return {
|
|
7830
|
+
include: globs.filter((glob) => !glob.startsWith("!")),
|
|
7831
|
+
exclude: globs.filter((glob) => glob.startsWith("!")).map((glob) => glob.slice(1))
|
|
7832
|
+
};
|
|
7833
|
+
}
|
|
7834
|
+
function translateIgnorePattern(raw) {
|
|
7835
|
+
const pattern = raw.replaceAll("\\\\", "\\");
|
|
7836
|
+
if (pattern === "/node_modules/" || pattern === "node_modules") return {};
|
|
7837
|
+
const suffix = /^\\\.([\w.\\-]+)\$$/.exec(pattern);
|
|
7838
|
+
if (suffix !== null) return { glob: `**/*.${suffix[1].replaceAll("\\.", ".")}` };
|
|
7839
|
+
const directory = /^\/([\w\-/]+)\/$/.exec(pattern);
|
|
7840
|
+
if (directory !== null) return { glob: `**/${directory[1]}/**` };
|
|
7841
|
+
return {
|
|
7842
|
+
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`
|
|
7843
|
+
};
|
|
7844
|
+
}
|
|
7845
|
+
function readModuleNameMapper(body) {
|
|
7846
|
+
const block = objectBody(body, "moduleNameMapper");
|
|
7847
|
+
if (block === void 0) return { aliases: [], dropped: [] };
|
|
7848
|
+
const aliases = [];
|
|
7849
|
+
const dropped = [];
|
|
7850
|
+
for (const entry of block.matchAll(
|
|
7851
|
+
new RegExp(String.raw`${STRING}\s*:\s*\n?\s*${STRING}`, "g")
|
|
7852
|
+
)) {
|
|
7853
|
+
const find = entry[1];
|
|
7854
|
+
const replacement = entry[2];
|
|
7855
|
+
const named = find.replace(/^\^/, "").replace(/\$$/, "").replaceAll("\\", "");
|
|
7856
|
+
if (named !== "" && replacement.includes(`node_modules/${named}/`)) {
|
|
7857
|
+
dropped.push(find);
|
|
7858
|
+
continue;
|
|
7859
|
+
}
|
|
7860
|
+
aliases.push({ find, replacement: replacement.replace(/^<rootDir>\//, "") });
|
|
7861
|
+
}
|
|
7862
|
+
return { aliases, dropped };
|
|
7863
|
+
}
|
|
7864
|
+
function readStringArray(body, key) {
|
|
7865
|
+
const raw = new RegExp(String.raw`\b${key}\s*:\s*\[([^\]]*)\]`).exec(body)?.[1];
|
|
7866
|
+
if (raw === void 0) return void 0;
|
|
7867
|
+
return [...raw.matchAll(new RegExp(STRING, "g"))].map((match) => match[1]);
|
|
7868
|
+
}
|
|
7869
|
+
function configBody(source) {
|
|
7870
|
+
const start = /(?:const\s+\w+(?::\s*\w+)?\s*=|export\s+default)\s*\{/.exec(source);
|
|
7871
|
+
if (start === null) return void 0;
|
|
7872
|
+
let depth = 0;
|
|
7873
|
+
const from = start.index + start[0].length - 1;
|
|
7874
|
+
for (let i = from; i < source.length; i += 1) {
|
|
7875
|
+
const char = source[i];
|
|
7876
|
+
if (char === "{") depth += 1;
|
|
7877
|
+
else if (char === "}") {
|
|
7878
|
+
depth -= 1;
|
|
7879
|
+
if (depth === 0) return source.slice(from + 1, i);
|
|
7880
|
+
}
|
|
7881
|
+
}
|
|
7882
|
+
return void 0;
|
|
7883
|
+
}
|
|
7884
|
+
function declaredKeys(body) {
|
|
7885
|
+
const keys = [];
|
|
7886
|
+
let depth = 0;
|
|
7887
|
+
let quote;
|
|
7888
|
+
let token = "";
|
|
7889
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
7890
|
+
const char = body[i];
|
|
7891
|
+
if (quote !== void 0) {
|
|
7892
|
+
if (char === quote && body[i - 1] !== "\\") quote = void 0;
|
|
7893
|
+
continue;
|
|
7894
|
+
}
|
|
7895
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
7896
|
+
quote = char;
|
|
7897
|
+
continue;
|
|
7898
|
+
}
|
|
7899
|
+
if (char === "/" && body[i + 1] === "/") {
|
|
7900
|
+
const end = body.indexOf("\n", i);
|
|
7901
|
+
i = end === -1 ? body.length : end;
|
|
7902
|
+
token = "";
|
|
7903
|
+
continue;
|
|
7904
|
+
}
|
|
7905
|
+
if (char === "/" && body[i + 1] === "*") {
|
|
7906
|
+
const end = body.indexOf("*/", i + 2);
|
|
7907
|
+
i = end === -1 ? body.length : end + 1;
|
|
7908
|
+
token = "";
|
|
7909
|
+
continue;
|
|
7910
|
+
}
|
|
7911
|
+
if (char === "{" || char === "[" || char === "(") {
|
|
7912
|
+
depth += 1;
|
|
7913
|
+
token = "";
|
|
7914
|
+
continue;
|
|
7915
|
+
}
|
|
7916
|
+
if (char === "}" || char === "]" || char === ")") {
|
|
7917
|
+
depth -= 1;
|
|
7918
|
+
token = "";
|
|
7919
|
+
continue;
|
|
7920
|
+
}
|
|
7921
|
+
if (depth === 0 && char === ":") {
|
|
7922
|
+
const key = /([A-Za-z_$][\w$]*)\s*$/.exec(token)?.[1];
|
|
7923
|
+
if (key !== void 0) keys.push(key);
|
|
7924
|
+
token = "";
|
|
7925
|
+
continue;
|
|
7926
|
+
}
|
|
7927
|
+
if (depth === 0 && char === ",") {
|
|
7928
|
+
token = "";
|
|
7929
|
+
continue;
|
|
7930
|
+
}
|
|
7931
|
+
token += char;
|
|
7932
|
+
}
|
|
7933
|
+
return keys;
|
|
7934
|
+
}
|
|
7935
|
+
function extendedConfig(source, cwd) {
|
|
7936
|
+
if (cwd === void 0) return void 0;
|
|
7937
|
+
for (const line of source.split("\n")) {
|
|
7938
|
+
const match = /^\s*import\s+(?!type\b)(\w+)\s+from\s+['"](\.[^'"]*)['"]/.exec(line);
|
|
7939
|
+
if (match === null) continue;
|
|
7940
|
+
const [, binding, specifier] = match;
|
|
7941
|
+
const candidates = [specifier, `${specifier}.ts`, `${specifier}.js`].map(
|
|
7942
|
+
(name) => name.replace(/^\.\//, "")
|
|
7943
|
+
);
|
|
7944
|
+
const file = candidates.find(
|
|
7945
|
+
(name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync26(join33(cwd, name))
|
|
7946
|
+
);
|
|
7947
|
+
if (file === void 0) continue;
|
|
7948
|
+
if (!new RegExp(String.raw`\{\s*\.\.\.${binding}\b`).test(source)) continue;
|
|
7949
|
+
return { binding, file };
|
|
7950
|
+
}
|
|
7951
|
+
return void 0;
|
|
7952
|
+
}
|
|
7953
|
+
function sideEffects(source, extended) {
|
|
7954
|
+
const found = [];
|
|
7955
|
+
for (const line of source.split("\n")) {
|
|
7956
|
+
const valueImport = /^\s*import\s+(?!type\b)(.*)$/.exec(line);
|
|
7957
|
+
if (valueImport === null || /from\s+['"]jest['"]/.test(line)) continue;
|
|
7958
|
+
const bindings = [
|
|
7959
|
+
...(/\{([^}]*)\}/.exec(valueImport[1])?.[1] ?? "").split(","),
|
|
7960
|
+
/^\s*(\w+)\s*(?:,|from)/.exec(valueImport[1])?.[1] ?? ""
|
|
7961
|
+
].map((name) => name.split(" as ").pop()?.trim() ?? "").filter((name) => name !== "");
|
|
7962
|
+
if (extended !== void 0 && bindings.includes(extended.binding)) continue;
|
|
7963
|
+
const used = bindings.some((name) => {
|
|
7964
|
+
const uses = source.match(new RegExp(String.raw`\b${name}\b`, "g")) ?? [];
|
|
7965
|
+
return uses.length > 1;
|
|
7966
|
+
});
|
|
7967
|
+
if (used) found.push(`it imports a value it uses (\`${line.trim()}\`)`);
|
|
7968
|
+
}
|
|
7969
|
+
return found;
|
|
7970
|
+
}
|
|
7971
|
+
function readJestConfig(cwd, named) {
|
|
7972
|
+
const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync26(join33(cwd, name)));
|
|
7973
|
+
if (file === void 0 || !existsSync26(join33(cwd, file))) return void 0;
|
|
7974
|
+
let source;
|
|
7975
|
+
try {
|
|
7976
|
+
source = readFileSync27(join33(cwd, file), "utf8");
|
|
7977
|
+
} catch {
|
|
7978
|
+
return { file, refusals: [`${file} could not be read`], notes: [] };
|
|
7979
|
+
}
|
|
7980
|
+
const extended = extendedConfig(source, cwd);
|
|
7981
|
+
const refusals = sideEffects(source, extended);
|
|
7982
|
+
const body = configBody(source);
|
|
7983
|
+
if (body === void 0) {
|
|
7984
|
+
return { file, refusals: [...refusals, "its config object could not be located"], notes: [] };
|
|
7985
|
+
}
|
|
7986
|
+
const known = /* @__PURE__ */ new Set([...CARRIED, ...DISCARDED]);
|
|
7987
|
+
for (const key of declaredKeys(body)) {
|
|
7988
|
+
if (!known.has(key)) refusals.push(`it declares \`${key}\`, which is not transposed`);
|
|
7989
|
+
}
|
|
7990
|
+
const notes = [];
|
|
7991
|
+
const afterEnv = readStringArray(body, "setupFilesAfterEnv");
|
|
7992
|
+
const before = readStringArray(body, "setupFiles");
|
|
7993
|
+
if (afterEnv !== void 0 && before !== void 0) {
|
|
7994
|
+
notes.push(
|
|
7995
|
+
"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"
|
|
7996
|
+
);
|
|
7997
|
+
}
|
|
7998
|
+
const ignore = readStringArray(body, "testPathIgnorePatterns") ?? [];
|
|
7999
|
+
const excludeGlobs = [];
|
|
8000
|
+
for (const pattern of ignore) {
|
|
8001
|
+
const { glob, refusal } = translateIgnorePattern(pattern);
|
|
8002
|
+
if (refusal !== void 0) refusals.push(refusal);
|
|
8003
|
+
else if (glob !== void 0) excludeGlobs.push(glob);
|
|
8004
|
+
}
|
|
8005
|
+
const mapper = readModuleNameMapper(body);
|
|
8006
|
+
for (const find of mapper.dropped) {
|
|
8007
|
+
notes.push(
|
|
8008
|
+
`\`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`
|
|
8009
|
+
);
|
|
8010
|
+
}
|
|
8011
|
+
for (const alias of mapper.aliases) {
|
|
8012
|
+
if (/<rootdir>/i.test(alias.replacement) && !alias.replacement.includes("<rootDir>")) {
|
|
8013
|
+
notes.push(
|
|
8014
|
+
`\`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`
|
|
8015
|
+
);
|
|
8016
|
+
}
|
|
8017
|
+
}
|
|
8018
|
+
const coverageFrom = readCoverageFrom(body);
|
|
8019
|
+
const env = readEnvironment(source);
|
|
8020
|
+
const globalSetup = readString(body, "globalSetup");
|
|
8021
|
+
const testTimeout = readNumber(body, "testTimeout");
|
|
8022
|
+
const maxWorkers = readNumber(body, "maxWorkers");
|
|
8023
|
+
const testMatch = readStringArray(body, "testMatch");
|
|
8024
|
+
const base = extended === void 0 ? void 0 : readJestConfig(cwd, extended.file);
|
|
8025
|
+
const inherited = base === void 0 ? {} : { ...base, refusals: [], notes: [] };
|
|
8026
|
+
for (const [jestKey, field] of [
|
|
8027
|
+
["testPathIgnorePatterns", "excludeGlobs"],
|
|
8028
|
+
["setupFilesAfterEnv", "setupFilesAfterEnv"],
|
|
8029
|
+
["setupFiles", "setupFiles"],
|
|
8030
|
+
["testMatch", "testMatch"],
|
|
8031
|
+
["collectCoverageFrom", "coverageFrom"],
|
|
8032
|
+
["moduleNameMapper", "aliases"]
|
|
8033
|
+
]) {
|
|
8034
|
+
if (declaredKeys(body).includes(jestKey)) delete inherited[field];
|
|
8035
|
+
}
|
|
8036
|
+
return {
|
|
8037
|
+
...inherited,
|
|
8038
|
+
file,
|
|
8039
|
+
...readString(body, "testEnvironment") !== void 0 ? { testEnvironment: readString(body, "testEnvironment") } : {},
|
|
8040
|
+
...readString(body, "coverageDirectory") !== void 0 ? { coverageDirectory: readString(body, "coverageDirectory") } : {},
|
|
8041
|
+
...afterEnv !== void 0 ? { setupFilesAfterEnv: afterEnv } : {},
|
|
8042
|
+
...before !== void 0 ? { setupFiles: before } : {},
|
|
8043
|
+
...testTimeout !== void 0 ? { testTimeout } : {},
|
|
8044
|
+
...maxWorkers !== void 0 ? { maxWorkers } : {},
|
|
8045
|
+
...globalSetup !== void 0 ? { globalSetup: [globalSetup] } : {},
|
|
8046
|
+
...testMatch !== void 0 ? { testMatch } : {},
|
|
8047
|
+
...coverageFrom !== void 0 ? { coverageFrom } : {},
|
|
8048
|
+
...excludeGlobs.length > 0 ? { excludeGlobs } : {},
|
|
8049
|
+
...mapper.aliases.length > 0 ? { aliases: mapper.aliases } : {},
|
|
8050
|
+
...env.length > 0 ? { env } : {},
|
|
8051
|
+
refusals: [...base?.refusals ?? [], ...refusals],
|
|
8052
|
+
notes: [...base?.notes ?? [], ...notes]
|
|
8053
|
+
};
|
|
8054
|
+
}
|
|
8055
|
+
|
|
8056
|
+
// src/roles/test/nest/run-modes.ts
|
|
8057
|
+
import { existsSync as existsSync27 } from "fs";
|
|
8058
|
+
import { basename as basename2, join as join34 } from "path";
|
|
8059
|
+
var JEST_EXECUTOR = "@nx/jest:jest";
|
|
8060
|
+
var PRIMARY_CONFIG_FILE = "vitest.config.mts";
|
|
8061
|
+
var PRIMARY_TARGET = "test";
|
|
8062
|
+
function configName(declared) {
|
|
8063
|
+
return basename2(declared.replaceAll("\\", "/"));
|
|
8064
|
+
}
|
|
8065
|
+
function configFileFor(target) {
|
|
8066
|
+
if (target === PRIMARY_TARGET) return PRIMARY_CONFIG_FILE;
|
|
8067
|
+
const suffix = target.startsWith("test-") ? target.slice("test-".length) : target;
|
|
8068
|
+
return `vitest.${suffix}.config.mts`;
|
|
8069
|
+
}
|
|
8070
|
+
function runModes(cwd, fallback) {
|
|
8071
|
+
const targets = projectTargets(cwd) ?? {};
|
|
8072
|
+
const declared = [];
|
|
8073
|
+
for (const [target, raw] of Object.entries(targets)) {
|
|
8074
|
+
const entry = raw;
|
|
8075
|
+
if (entry.executor !== JEST_EXECUTOR) continue;
|
|
8076
|
+
const jestConfig = entry.options?.jestConfig;
|
|
8077
|
+
if (jestConfig === void 0) continue;
|
|
8078
|
+
const name = configName(jestConfig);
|
|
8079
|
+
if (!existsSync27(join34(cwd, name))) continue;
|
|
8080
|
+
declared.push({
|
|
8081
|
+
target,
|
|
8082
|
+
jestConfig: name,
|
|
8083
|
+
vitestConfig: configFileFor(target),
|
|
8084
|
+
primary: target === PRIMARY_TARGET
|
|
8085
|
+
});
|
|
8086
|
+
}
|
|
8087
|
+
if (declared.length === 0) {
|
|
8088
|
+
const [only] = fallback;
|
|
8089
|
+
return only === void 0 ? [] : [
|
|
8090
|
+
{
|
|
8091
|
+
target: PRIMARY_TARGET,
|
|
8092
|
+
jestConfig: only,
|
|
8093
|
+
vitestConfig: PRIMARY_CONFIG_FILE,
|
|
8094
|
+
primary: true
|
|
8095
|
+
}
|
|
8096
|
+
];
|
|
8097
|
+
}
|
|
8098
|
+
return declared.sort(
|
|
8099
|
+
(left, right) => left.primary === right.primary ? left.target.localeCompare(right.target) : Number(right.primary) - Number(left.primary)
|
|
8100
|
+
);
|
|
8101
|
+
}
|
|
8102
|
+
|
|
8103
|
+
// src/roles/test/jest-migration.ts
|
|
8104
|
+
var SPEC_TSCONFIG = "tsconfig.spec.json";
|
|
8105
|
+
var VITEST_GLOBAL_TYPES = "vitest/globals";
|
|
8106
|
+
function specTypesOperation(cwd) {
|
|
8107
|
+
const path = join35(cwd, SPEC_TSCONFIG);
|
|
8108
|
+
if (!existsSync28(path)) return void 0;
|
|
8109
|
+
let config;
|
|
8110
|
+
try {
|
|
8111
|
+
config = parseJsonc(
|
|
8112
|
+
readFileSync28(path, "utf8"),
|
|
8113
|
+
SPEC_TSCONFIG
|
|
8114
|
+
);
|
|
8115
|
+
} catch {
|
|
8116
|
+
return void 0;
|
|
8117
|
+
}
|
|
8118
|
+
const types = config?.compilerOptions?.types;
|
|
8119
|
+
if (!Array.isArray(types) || !types.includes("jest")) return void 0;
|
|
8120
|
+
return {
|
|
8121
|
+
kind: "merge-json",
|
|
8122
|
+
path: SPEC_TSCONFIG,
|
|
8123
|
+
value: {
|
|
8124
|
+
compilerOptions: {
|
|
8125
|
+
types: types.map((entry) => entry === "jest" ? VITEST_GLOBAL_TYPES : entry)
|
|
8126
|
+
}
|
|
8127
|
+
}
|
|
8128
|
+
};
|
|
8129
|
+
}
|
|
8130
|
+
function siblingScriptRewrites(cwd) {
|
|
8131
|
+
const rewrites = {};
|
|
8132
|
+
for (const [name, command] of Object.entries(moduleScripts(cwd))) {
|
|
8133
|
+
if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
|
|
8134
|
+
const rewritten = rewriteVitestScript(command);
|
|
8135
|
+
if (rewritten !== command) rewrites[name] = rewritten;
|
|
8136
|
+
}
|
|
8137
|
+
return rewrites;
|
|
8138
|
+
}
|
|
8139
|
+
function blocked(reason) {
|
|
8140
|
+
return { operations: [], blocked: reason };
|
|
8141
|
+
}
|
|
8142
|
+
function planJestMigration(context, adoption) {
|
|
8143
|
+
const modes = runModes(context.cwd, adoption.jestConfigs);
|
|
8144
|
+
if (modes.length === 0) {
|
|
8145
|
+
return blocked(`no jest config could be found for this module, so nothing was written.`);
|
|
8146
|
+
}
|
|
8147
|
+
const prepared = [];
|
|
8148
|
+
for (const mode of modes) {
|
|
8149
|
+
const reading = readJestConfig(context.cwd, mode.jestConfig);
|
|
8150
|
+
if (reading === void 0) {
|
|
8151
|
+
return blocked(`${mode.jestConfig} could not be read, so nothing was written.`);
|
|
8152
|
+
}
|
|
8153
|
+
const generated = generateNestTestConfig(context.cwd, reading, mode.target);
|
|
8154
|
+
if (generated.source === void 0) {
|
|
8155
|
+
return blocked(
|
|
8156
|
+
`${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.`
|
|
8157
|
+
);
|
|
8158
|
+
}
|
|
8159
|
+
prepared.push({ mode, reading, source: generated.source });
|
|
8160
|
+
}
|
|
8161
|
+
const migration = migrateModuleTests(
|
|
8162
|
+
context.cwd,
|
|
8163
|
+
prepared.flatMap(({ reading }) => [
|
|
8164
|
+
...reading.setupFiles ?? [],
|
|
8165
|
+
...reading.setupFilesAfterEnv ?? [],
|
|
8166
|
+
...reading.globalSetup ?? []
|
|
8167
|
+
])
|
|
8168
|
+
);
|
|
8169
|
+
if (migration.blockers.length > 0) {
|
|
8170
|
+
const listed = migration.blockers.map((blocker) => `
|
|
8171
|
+
${blocker.file}: ${blocker.reason}`).join("");
|
|
8172
|
+
return blocked(
|
|
8173
|
+
`${migration.blockers.length} thing(s) in this module's test files need a human before it can move. Nothing was written, because a config without its files is a suite calling jest.* under Vitest.${listed}`
|
|
8174
|
+
);
|
|
8175
|
+
}
|
|
8176
|
+
const targets = projectTargets(context.cwd) ?? {};
|
|
8177
|
+
const existingFor = (name) => targets[name];
|
|
8178
|
+
const siblings = siblingScriptRewrites(context.cwd);
|
|
8179
|
+
const specTypes = specTypesOperation(context.cwd);
|
|
8180
|
+
const scripts = Object.fromEntries(
|
|
8181
|
+
prepared.map(({ mode }) => [
|
|
8182
|
+
mode.target,
|
|
8183
|
+
mode.primary ? SENTINEL_TEST_COMMAND : `${SENTINEL_TEST_COMMAND} --config ${mode.vitestConfig}`
|
|
8184
|
+
])
|
|
8185
|
+
);
|
|
8186
|
+
const operations = [
|
|
8187
|
+
...prepared.map(({ mode, source }) => ({
|
|
8188
|
+
kind: "write",
|
|
8189
|
+
path: mode.vitestConfig,
|
|
8190
|
+
contents: source
|
|
8191
|
+
})),
|
|
8192
|
+
...migration.operations,
|
|
8193
|
+
...specTypes === void 0 ? [] : [specTypes],
|
|
8194
|
+
manifestOperation(context.cwd, { ...scripts, ...siblings }),
|
|
8195
|
+
...prepared.flatMap(
|
|
8196
|
+
({ mode }) => nxTargetOperations({
|
|
8197
|
+
cwd: context.cwd,
|
|
8198
|
+
targets: testTarget({
|
|
8199
|
+
name: mode.target,
|
|
8200
|
+
configFiles: [mode.vitestConfig],
|
|
8201
|
+
outputs: existingFor(mode.target)?.outputs ?? [],
|
|
8202
|
+
dependsOn: existingFor(mode.target)?.dependsOn ?? [],
|
|
8203
|
+
configurations: existingFor(mode.target)?.configurations ?? {}
|
|
8204
|
+
})
|
|
8205
|
+
})
|
|
8206
|
+
),
|
|
8207
|
+
...[.../* @__PURE__ */ new Set([...adoption.jestConfigs, ...prepared.map(({ mode }) => mode.jestConfig)])].map(
|
|
8208
|
+
(name) => ({ kind: "delete", path: name })
|
|
8209
|
+
)
|
|
8210
|
+
];
|
|
8211
|
+
const notes = [
|
|
8212
|
+
`${migration.files} test file(s) migrated from jest to Vitest, ${migration.renamed} site(s) rewritten${migration.hoisted > 0 ? `, ${migration.hoisted} of them moving a mock factory's captures into vi.hoisted` : ""}.`,
|
|
8213
|
+
`${NEST_TEST_CONFIG_FILE} written from what ${adoption.jestConfigs.join(", ")} declared, and that config deleted. Every specifier in the new one is a package name: nothing in it reaches outside this module.`
|
|
8214
|
+
];
|
|
8215
|
+
if (specTypes !== void 0) {
|
|
8216
|
+
notes.push(
|
|
8217
|
+
`${SPEC_TSCONFIG} now declares \`${VITEST_GLOBAL_TYPES}\` instead of \`jest\`. Without it the module keeps depending on @types/jest and \`jest.Mock\` still resolves, so the migration would look finished while the dependency it cuts is still declared.`
|
|
8218
|
+
);
|
|
8219
|
+
}
|
|
8220
|
+
notes.push(
|
|
8221
|
+
`run this module's own lint --fix (or its formatter) after installing: the Vitest type imports are written beside the existing ones, and ordering belongs to the tool that enforces it.`
|
|
8222
|
+
);
|
|
8223
|
+
notes.push(
|
|
8224
|
+
`then run this module's TYPECHECK, not only its tests. The migration rewrites types as well as calls, and a wrong type is invisible to a green suite: measured on a real module, \`--run --test\` passed while \`--run --typescript\` reported ten errors it had not had before.`
|
|
8225
|
+
);
|
|
8226
|
+
if (Object.keys(siblings).length > 0) {
|
|
8227
|
+
notes.push(`rewrote ${Object.keys(siblings).join(", ")} to run through sentinel.`);
|
|
8228
|
+
}
|
|
8229
|
+
return { operations, notes };
|
|
8230
|
+
}
|
|
8231
|
+
|
|
8232
|
+
// src/roles/test/plan.ts
|
|
8233
|
+
function ownedTestDependencies(cwd, declared) {
|
|
8234
|
+
return declared.filter((entry) => SENTINEL_OWNED_TEST_PACKAGES.includes(entry.name)).flatMap((entry) => [
|
|
8235
|
+
["dependencies", entry.name],
|
|
8236
|
+
["devDependencies", entry.name]
|
|
8237
|
+
]).filter(([section]) => section !== void 0);
|
|
8238
|
+
}
|
|
8239
|
+
var DEFERRED = {
|
|
8240
|
+
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`
|
|
8241
|
+
};
|
|
8242
|
+
function plan3(context) {
|
|
8243
|
+
const deferred = DEFERRED[context.preset];
|
|
8244
|
+
if (deferred !== void 0) {
|
|
8245
|
+
return {
|
|
8246
|
+
operations: [],
|
|
8247
|
+
skipped: `the test role defers ${context.preset} for now. It defers because ${deferred}.`
|
|
8248
|
+
};
|
|
8249
|
+
}
|
|
8250
|
+
const configFile = testConfigFile(context.cwd);
|
|
8251
|
+
const adoption = readTestAdoption(context.cwd);
|
|
8252
|
+
if (adoption.state === "no-tests") {
|
|
8253
|
+
const operations2 = adoption.jestConfigs.map((name) => ({
|
|
8254
|
+
kind: "delete",
|
|
8255
|
+
path: name
|
|
8256
|
+
}));
|
|
8257
|
+
const owned2 = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
|
|
8258
|
+
if (owned2.length > 0) {
|
|
8259
|
+
operations2.push({ kind: "remove-json-keys", path: "package.json", keys: owned2 });
|
|
8260
|
+
}
|
|
8261
|
+
const hasTarget = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME] !== void 0;
|
|
8262
|
+
if (hasTarget) {
|
|
8263
|
+
operations2.push({
|
|
8264
|
+
kind: "remove-json-keys",
|
|
8265
|
+
path: "project.json",
|
|
8266
|
+
keys: [["targets", TEST_SCRIPT_NAME]]
|
|
8267
|
+
});
|
|
8268
|
+
}
|
|
8269
|
+
if (operations2.length === 0) {
|
|
8270
|
+
return {
|
|
8271
|
+
operations: [],
|
|
8272
|
+
skipped: `this module has no test and no jest leftovers, so there is nothing to do here.`
|
|
8273
|
+
};
|
|
8274
|
+
}
|
|
8275
|
+
return {
|
|
8276
|
+
operations: operations2,
|
|
8277
|
+
notes: [
|
|
8278
|
+
// Names what is actually removed, never a shape this module does not have: half of these
|
|
8279
|
+
// modules carry a config with no target behind it, and claiming otherwise would describe
|
|
8280
|
+
// an operation the plan never performs.
|
|
8281
|
+
`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.`
|
|
8282
|
+
]
|
|
8283
|
+
};
|
|
8284
|
+
}
|
|
8285
|
+
if (configFile === void 0) {
|
|
8286
|
+
if (adoption.state === "jest") return planJestMigration(context, adoption);
|
|
8287
|
+
return {
|
|
8288
|
+
operations: [],
|
|
8289
|
+
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.`
|
|
8290
|
+
};
|
|
8291
|
+
}
|
|
8292
|
+
const operations = [];
|
|
8293
|
+
const notes = [];
|
|
8294
|
+
const rewrite = rewriteToolchainImports(
|
|
8295
|
+
testToolchain(context.preset),
|
|
8296
|
+
readTestConfigSource(context.cwd, configFile),
|
|
8297
|
+
configFile
|
|
8298
|
+
);
|
|
8299
|
+
if (rewrite.kind === "blocked") {
|
|
8300
|
+
return {
|
|
8301
|
+
operations: [],
|
|
8302
|
+
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.`
|
|
8303
|
+
};
|
|
8304
|
+
}
|
|
8305
|
+
if (rewrite.kind === "rewritten") {
|
|
8306
|
+
operations.push({ kind: "write", path: configFile, contents: rewrite.source });
|
|
8307
|
+
notes.push(
|
|
8308
|
+
`${configFile} now imports ${rewrite.moved.join(", ")} from sentinel. Only the import lines changed: every alias, setup file, timeout and coverage path is untouched, so this suite runs what it ran before.`
|
|
8309
|
+
);
|
|
8310
|
+
}
|
|
8311
|
+
const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
|
|
8312
|
+
const outputs = existing?.outputs ?? [];
|
|
8313
|
+
const dependsOn = existing?.dependsOn ?? [];
|
|
8314
|
+
const configurations = existing?.configurations ?? {};
|
|
8315
|
+
if (dependsOn.length > 0) {
|
|
8316
|
+
notes.push(`carried \`dependsOn\` from the replaced target: ${dependsOn.join(", ")}.`);
|
|
8317
|
+
}
|
|
8318
|
+
if (Object.keys(configurations).length > 0) {
|
|
8319
|
+
notes.push(
|
|
8320
|
+
`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.`
|
|
8321
|
+
);
|
|
8322
|
+
}
|
|
8323
|
+
if (outputs.length === 0) {
|
|
8324
|
+
notes.push(
|
|
8325
|
+
`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.`
|
|
8326
|
+
);
|
|
8327
|
+
}
|
|
8328
|
+
const siblingScripts = {};
|
|
8329
|
+
for (const [name, command] of Object.entries(moduleScripts(context.cwd))) {
|
|
8330
|
+
if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
|
|
8331
|
+
const rewritten = rewriteVitestScript(command);
|
|
8332
|
+
if (rewritten !== command) siblingScripts[name] = rewritten;
|
|
8333
|
+
}
|
|
8334
|
+
if (Object.keys(siblingScripts).length > 0) {
|
|
8335
|
+
notes.push(
|
|
8336
|
+
`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\`.`
|
|
8337
|
+
);
|
|
8338
|
+
}
|
|
8339
|
+
operations.push(
|
|
8340
|
+
manifestOperation(context.cwd, {
|
|
8341
|
+
[TEST_SCRIPT_NAME]: SENTINEL_TEST_COMMAND,
|
|
8342
|
+
...siblingScripts
|
|
8343
|
+
})
|
|
8344
|
+
);
|
|
8345
|
+
operations.push(
|
|
8346
|
+
...nxTargetOperations({
|
|
8347
|
+
cwd: context.cwd,
|
|
8348
|
+
targets: testTarget({ configFiles: [configFile], outputs, dependsOn, configurations })
|
|
8349
|
+
})
|
|
8350
|
+
);
|
|
8351
|
+
const owned = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
|
|
8352
|
+
if (owned.length > 0) {
|
|
8353
|
+
operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
|
|
8354
|
+
notes.push(
|
|
8355
|
+
`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.`
|
|
8356
|
+
);
|
|
8357
|
+
}
|
|
8358
|
+
if (adoption.jestConfigs.length > 0) {
|
|
8359
|
+
notes.push(
|
|
8360
|
+
`${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.`
|
|
8361
|
+
);
|
|
8362
|
+
}
|
|
8363
|
+
return { operations, notes };
|
|
8364
|
+
}
|
|
8365
|
+
|
|
8366
|
+
// src/roles/test/adapters/vitest/vitest.adapter.ts
|
|
8367
|
+
function resolveVitest(cwd) {
|
|
8368
|
+
const fromSentinel = binFromOwnInstall("vitest", "vitest");
|
|
8369
|
+
if (fromSentinel !== void 0) return { bin: fromSentinel, origin: "sentinel" };
|
|
8370
|
+
const fromModule = resolveBin(cwd, "vitest");
|
|
8371
|
+
return fromModule !== void 0 ? { bin: fromModule, origin: "module" } : { bin: void 0, origin: "module" };
|
|
8372
|
+
}
|
|
8373
|
+
var VitestAdapter = class extends BaseAdapter {
|
|
8374
|
+
target = "test";
|
|
8375
|
+
runner = "vitest";
|
|
8376
|
+
appliesTo(_preset) {
|
|
8377
|
+
return true;
|
|
8378
|
+
}
|
|
8379
|
+
plan(context) {
|
|
8380
|
+
return plan3(context);
|
|
8381
|
+
}
|
|
8382
|
+
async status(ctx) {
|
|
8383
|
+
return Promise.resolve(readTestAdoption(ctx.cwd));
|
|
8384
|
+
}
|
|
8385
|
+
async inspect(ctx) {
|
|
8386
|
+
const adoption = readTestAdoption(ctx.cwd);
|
|
8387
|
+
return Promise.resolve({
|
|
8388
|
+
state: adoption.state,
|
|
8389
|
+
runner: adoption.runner,
|
|
8390
|
+
configFile: adoption.configFile,
|
|
8391
|
+
jestConfigs: adoption.jestConfigs,
|
|
8392
|
+
adopted: adoption.adopted,
|
|
8393
|
+
conformant: adoption.conformant,
|
|
8394
|
+
drift: adoption.drift,
|
|
8395
|
+
unreadable: adoption.unreadable
|
|
8396
|
+
});
|
|
8397
|
+
}
|
|
8398
|
+
/**
|
|
8399
|
+
* Run the module's tests.
|
|
8400
|
+
*
|
|
8401
|
+
* A module that has not adopted is reported and PASSES, the stance every role takes: `--run`
|
|
8402
|
+
* with no named target sweeps the workspace, so failing here would exit non-zero on the 107
|
|
8403
|
+
* modules still on jest, which says nothing anyone can act on.
|
|
8404
|
+
*/
|
|
8405
|
+
async run(ctx) {
|
|
8406
|
+
const adoption = readTestAdoption(ctx.cwd);
|
|
8407
|
+
if (!adoption.adopted) {
|
|
8408
|
+
this.say(
|
|
8409
|
+
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.`
|
|
8410
|
+
);
|
|
8411
|
+
return Promise.resolve({ ok: true, code: 0 });
|
|
8412
|
+
}
|
|
8413
|
+
const { bin, origin } = resolveVitest(ctx.cwd);
|
|
8414
|
+
if (bin === void 0) {
|
|
8415
|
+
this.say(`could not find the vitest binary. Run \`pnpm install\` in the module.`);
|
|
8416
|
+
return Promise.resolve({ ok: false, code: 1 });
|
|
8417
|
+
}
|
|
8418
|
+
if (origin === "module") {
|
|
8419
|
+
this.say(
|
|
8420
|
+
`using the MODULE's vitest, not sentinel's. An adopted config gets its plugins from sentinel, and two copies fail in ways that never mention a version. Remove vitest from this module's package.json.`
|
|
8421
|
+
);
|
|
8422
|
+
}
|
|
8423
|
+
const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
|
|
8424
|
+
const watching = options.includes("--watch") || options.includes("-w");
|
|
8425
|
+
const argv = watching ? [...options, ...paths] : ["run", ...options, ...paths];
|
|
8426
|
+
const result = spawnSync5(bin, argv, { cwd: ctx.cwd, stdio: "inherit" });
|
|
8427
|
+
if (result.error) {
|
|
8428
|
+
this.say(`could not run vitest (${result.error.message})`);
|
|
8429
|
+
return Promise.resolve({ ok: false, code: 1 });
|
|
8430
|
+
}
|
|
8431
|
+
const code = result.status ?? 1;
|
|
8432
|
+
return Promise.resolve({ ok: code === 0, code });
|
|
8433
|
+
}
|
|
8434
|
+
say(message) {
|
|
8435
|
+
process.stderr.write(`sentinel test(vitest): ${message}
|
|
8436
|
+
`);
|
|
8437
|
+
}
|
|
8438
|
+
};
|
|
8439
|
+
|
|
8440
|
+
// src/roles/test/register.ts
|
|
8441
|
+
function registerTest() {
|
|
8442
|
+
register(new VitestAdapter());
|
|
8443
|
+
setDefaultRunner("test", "vitest");
|
|
8444
|
+
}
|
|
8445
|
+
|
|
8446
|
+
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
8447
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
8448
|
+
import { existsSync as existsSync29, readFileSync as readFileSync30 } from "fs";
|
|
8449
|
+
import { createRequire as createRequire6 } from "module";
|
|
8450
|
+
import { join as join37 } from "path";
|
|
8451
|
+
|
|
8452
|
+
// src/roles/typescript/presets/nest.json
|
|
8453
|
+
var nest_default2 = {
|
|
8454
|
+
compilerOptions: {
|
|
8455
|
+
module: "esnext",
|
|
8456
|
+
moduleResolution: "bundler",
|
|
8457
|
+
types: ["node"],
|
|
8458
|
+
experimentalDecorators: true,
|
|
8459
|
+
emitDecoratorMetadata: true,
|
|
8460
|
+
declaration: true,
|
|
8461
|
+
sourceMap: true
|
|
8462
|
+
},
|
|
8463
|
+
notes: {
|
|
8464
|
+
module: "the backends are webpack-bundled and tsc only typechecks, so the module format is the bundler's concern; `bundler` resolution is what the repo already uses, and the only Node-friendly option TS7 still accepts since it removed classic `node`",
|
|
8465
|
+
target: "deliberately NOT set, here or in lib: those are environment the repo base owns. Overriding target flips useDefineForClassFields (TS2612) and narrowing lib drops DOM (GLfloat); both REPLACE the base rather than merge"
|
|
8466
|
+
}
|
|
8467
|
+
};
|
|
8468
|
+
|
|
8469
|
+
// src/roles/typescript/presets/node.json
|
|
8470
|
+
var node_default2 = {
|
|
8471
|
+
compilerOptions: {
|
|
8472
|
+
module: "esnext",
|
|
8473
|
+
moduleResolution: "bundler",
|
|
8474
|
+
types: ["node"],
|
|
8475
|
+
declaration: true
|
|
8476
|
+
},
|
|
8477
|
+
notes: {
|
|
8478
|
+
module: "esnext/bundler like the react and nest presets, matching a monorepo whose libs are bundler-resolved in practice. NOT NodeNext: that would force its stricter ESM rules onto libs the repo resolves through a bundler"
|
|
8479
|
+
}
|
|
8480
|
+
};
|
|
8481
|
+
|
|
8482
|
+
// src/roles/typescript/presets/react.json
|
|
8483
|
+
var react_default2 = {
|
|
8484
|
+
compilerOptions: {
|
|
8485
|
+
target: "ES2023",
|
|
8486
|
+
lib: ["ES2023", "DOM", "DOM.Iterable"],
|
|
8487
|
+
module: "ESNext",
|
|
8488
|
+
moduleResolution: "bundler",
|
|
8489
|
+
jsx: "react-jsx",
|
|
8490
|
+
useDefineForClassFields: true,
|
|
8491
|
+
isolatedModules: true,
|
|
8492
|
+
moduleDetection: "force",
|
|
8493
|
+
noEmit: true,
|
|
8494
|
+
noUncheckedSideEffectImports: true
|
|
8495
|
+
},
|
|
8496
|
+
notes: {
|
|
8497
|
+
noEmit: "vite owns the build; tsc only typechecks here"
|
|
8498
|
+
}
|
|
8499
|
+
};
|
|
8500
|
+
|
|
8501
|
+
// src/roles/typescript/presets/shared.json
|
|
8502
|
+
var shared_default2 = {
|
|
8503
|
+
$schema: "https://json.schemastore.org/tsconfig",
|
|
8504
|
+
compilerOptions: {
|
|
8505
|
+
strict: true,
|
|
8506
|
+
noFallthroughCasesInSwitch: true,
|
|
8507
|
+
forceConsistentCasingInFileNames: true,
|
|
8508
|
+
esModuleInterop: true,
|
|
8509
|
+
skipLibCheck: true
|
|
8510
|
+
},
|
|
8511
|
+
deferred: {
|
|
8512
|
+
noImplicitAny: {
|
|
8513
|
+
phase: 2,
|
|
8514
|
+
reason: "the implicit-any migration (TS70xx)"
|
|
8515
|
+
},
|
|
8516
|
+
noUnusedLocals: {
|
|
8517
|
+
phase: 2,
|
|
8518
|
+
reason: "unused-local cleanup (TS6133)"
|
|
8519
|
+
},
|
|
8520
|
+
noUnusedParameters: {
|
|
8521
|
+
phase: 2,
|
|
8522
|
+
reason: "unused-parameter cleanup (TS6133)"
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8525
|
+
};
|
|
8526
|
+
|
|
8527
|
+
// src/roles/typescript/preset-data.ts
|
|
8528
|
+
var SHARED = shared_default2;
|
|
5713
8529
|
var LAYERS = {
|
|
5714
8530
|
nest: nest_default2,
|
|
5715
8531
|
node: node_default2,
|
|
@@ -5812,8 +8628,8 @@ function readTsconfigAdoption(cwd) {
|
|
|
5812
8628
|
}
|
|
5813
8629
|
|
|
5814
8630
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
5815
|
-
import { readFileSync as
|
|
5816
|
-
import { join as
|
|
8631
|
+
import { readFileSync as readFileSync29 } from "fs";
|
|
8632
|
+
import { join as join36 } from "path";
|
|
5817
8633
|
|
|
5818
8634
|
// src/roles/typescript/typecheck-script.ts
|
|
5819
8635
|
var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
|
|
@@ -5906,7 +8722,7 @@ function planAdoption(context) {
|
|
|
5906
8722
|
};
|
|
5907
8723
|
}
|
|
5908
8724
|
const existing = parseJsonc(
|
|
5909
|
-
|
|
8725
|
+
readFileSync29(join36(context.cwd, target.path), "utf8"),
|
|
5910
8726
|
target.path
|
|
5911
8727
|
);
|
|
5912
8728
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -6044,7 +8860,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6044
8860
|
let chain;
|
|
6045
8861
|
try {
|
|
6046
8862
|
const parsed = parseJsonc(
|
|
6047
|
-
|
|
8863
|
+
readFileSync30(join37(cwd, target.path), "utf8"),
|
|
6048
8864
|
target.path
|
|
6049
8865
|
);
|
|
6050
8866
|
chain = parsed.extends;
|
|
@@ -6057,7 +8873,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6057
8873
|
);
|
|
6058
8874
|
if (preset === void 0) return void 0;
|
|
6059
8875
|
try {
|
|
6060
|
-
|
|
8876
|
+
createRequire6(join37(cwd, "noop.js")).resolve(preset);
|
|
6061
8877
|
return void 0;
|
|
6062
8878
|
} catch {
|
|
6063
8879
|
return preset;
|
|
@@ -6098,7 +8914,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6098
8914
|
return { ok: false, code: 1 };
|
|
6099
8915
|
}
|
|
6100
8916
|
if (options.length > 0) return this.runWithOptions(ctx, tsc, config);
|
|
6101
|
-
const result =
|
|
8917
|
+
const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
|
|
6102
8918
|
if (result.error) {
|
|
6103
8919
|
process.stderr.write(
|
|
6104
8920
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -6133,7 +8949,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6133
8949
|
let worst = 0;
|
|
6134
8950
|
for (const project of this.referencedProjects(ctx.cwd, config)) {
|
|
6135
8951
|
const args = ["-p", project, "--noEmit", "--composite", "false", ...ctx.toolArgs ?? []];
|
|
6136
|
-
const result =
|
|
8952
|
+
const result = spawnSync6(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
|
|
6137
8953
|
if (result.error) {
|
|
6138
8954
|
process.stderr.write(
|
|
6139
8955
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -6153,7 +8969,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6153
8969
|
referencedProjects(cwd, config) {
|
|
6154
8970
|
try {
|
|
6155
8971
|
const parsed = parseJsonc(
|
|
6156
|
-
|
|
8972
|
+
readFileSync30(join37(cwd, config), "utf8"),
|
|
6157
8973
|
config
|
|
6158
8974
|
);
|
|
6159
8975
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
@@ -6169,7 +8985,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6169
8985
|
* check.
|
|
6170
8986
|
*/
|
|
6171
8987
|
typecheckTarget(cwd) {
|
|
6172
|
-
if (
|
|
8988
|
+
if (existsSync29(join37(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
6173
8989
|
const target = resolveTsconfigTarget(cwd);
|
|
6174
8990
|
return target.reason === "none" ? null : target.path;
|
|
6175
8991
|
}
|
|
@@ -6222,7 +9038,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6222
9038
|
return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
|
|
6223
9039
|
}
|
|
6224
9040
|
const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
|
|
6225
|
-
const result =
|
|
9041
|
+
const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
|
|
6226
9042
|
if (result.error) {
|
|
6227
9043
|
process.stderr.write(
|
|
6228
9044
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -6265,7 +9081,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6265
9081
|
* errors in the output mean the rule must be on.
|
|
6266
9082
|
*/
|
|
6267
9083
|
noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
|
|
6268
|
-
const shown =
|
|
9084
|
+
const shown = spawnSync6(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
|
|
6269
9085
|
if (shown.status === 0 && shown.stdout) {
|
|
6270
9086
|
try {
|
|
6271
9087
|
const co = parseJsonc(
|
|
@@ -6292,6 +9108,7 @@ function registerAdapters() {
|
|
|
6292
9108
|
registerLint();
|
|
6293
9109
|
registerFormat();
|
|
6294
9110
|
registerBuild();
|
|
9111
|
+
registerTest();
|
|
6295
9112
|
}
|
|
6296
9113
|
|
|
6297
9114
|
// src/core/detect-framework.ts
|
|
@@ -6373,9 +9190,9 @@ function replaceLines(current, replacements) {
|
|
|
6373
9190
|
}
|
|
6374
9191
|
|
|
6375
9192
|
// src/core/apply-plan.ts
|
|
6376
|
-
import { existsSync as
|
|
6377
|
-
import { resolve as
|
|
6378
|
-
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
9193
|
+
import { existsSync as existsSync30, readFileSync as readFileSync31, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
9194
|
+
import { resolve as resolve5, sep } from "path";
|
|
9195
|
+
import { applyEdits as applyEdits3, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
6379
9196
|
|
|
6380
9197
|
// src/shared/deep-merge.ts
|
|
6381
9198
|
function isPlainObject(value) {
|
|
@@ -6384,15 +9201,15 @@ function isPlainObject(value) {
|
|
|
6384
9201
|
|
|
6385
9202
|
// src/core/apply-plan.ts
|
|
6386
9203
|
function resolveWithinRoot(cwd, relativePath) {
|
|
6387
|
-
const root =
|
|
6388
|
-
const absolutePath =
|
|
9204
|
+
const root = resolve5(cwd);
|
|
9205
|
+
const absolutePath = resolve5(root, relativePath);
|
|
6389
9206
|
if (absolutePath !== root && !absolutePath.startsWith(root + sep)) {
|
|
6390
9207
|
throw new Error(`Refusing to write outside the module root: "${relativePath}".`);
|
|
6391
9208
|
}
|
|
6392
9209
|
return absolutePath;
|
|
6393
9210
|
}
|
|
6394
9211
|
function readIfExists(absolutePath) {
|
|
6395
|
-
return
|
|
9212
|
+
return existsSync30(absolutePath) ? readFileSync31(absolutePath, "utf8") : void 0;
|
|
6396
9213
|
}
|
|
6397
9214
|
function* leaves(value, prefix = []) {
|
|
6398
9215
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -6407,7 +9224,7 @@ function mergeJsonc(current, value) {
|
|
|
6407
9224
|
const edits = modify(text, path, leaf, {
|
|
6408
9225
|
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
6409
9226
|
});
|
|
6410
|
-
text =
|
|
9227
|
+
text = applyEdits3(text, edits);
|
|
6411
9228
|
}
|
|
6412
9229
|
return text.endsWith("\n") ? text : text + "\n";
|
|
6413
9230
|
}
|
|
@@ -6419,7 +9236,7 @@ function removeJsoncKeys(current, keys) {
|
|
|
6419
9236
|
const edits = modify(text, path, void 0, {
|
|
6420
9237
|
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
6421
9238
|
});
|
|
6422
|
-
text =
|
|
9239
|
+
text = applyEdits3(text, edits);
|
|
6423
9240
|
}
|
|
6424
9241
|
return text.endsWith("\n") ? text : text + "\n";
|
|
6425
9242
|
}
|
|
@@ -6473,9 +9290,9 @@ function applyOperationTo(current, operation) {
|
|
|
6473
9290
|
}
|
|
6474
9291
|
}
|
|
6475
9292
|
}
|
|
6476
|
-
function preparePlan(cwd,
|
|
9293
|
+
function preparePlan(cwd, plan4) {
|
|
6477
9294
|
const prepared = /* @__PURE__ */ new Map();
|
|
6478
|
-
for (const operation of
|
|
9295
|
+
for (const operation of plan4.operations) {
|
|
6479
9296
|
const absolutePath = resolveWithinRoot(cwd, operation.path);
|
|
6480
9297
|
const existing = prepared.get(operation.path);
|
|
6481
9298
|
const before = existing?.before ?? readIfExists(absolutePath) ?? "";
|
|
@@ -6494,11 +9311,11 @@ function preparePlan(cwd, plan3) {
|
|
|
6494
9311
|
}
|
|
6495
9312
|
function writeFileAtomic(absolutePath, contents) {
|
|
6496
9313
|
const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
|
|
6497
|
-
|
|
9314
|
+
writeFileSync3(tempPath, contents);
|
|
6498
9315
|
renameSync(tempPath, absolutePath);
|
|
6499
9316
|
}
|
|
6500
|
-
function applyPlan(cwd,
|
|
6501
|
-
const changed = preparePlan(cwd,
|
|
9317
|
+
function applyPlan(cwd, plan4) {
|
|
9318
|
+
const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
|
|
6502
9319
|
for (const file of changed) {
|
|
6503
9320
|
if (file.deleted) {
|
|
6504
9321
|
rmSync3(file.absolutePath, { force: true });
|
|
@@ -6510,8 +9327,8 @@ function applyPlan(cwd, plan3) {
|
|
|
6510
9327
|
}
|
|
6511
9328
|
|
|
6512
9329
|
// src/core/config/preset-evidence.ts
|
|
6513
|
-
import { existsSync as
|
|
6514
|
-
import { join as
|
|
9330
|
+
import { existsSync as existsSync31, readdirSync as readdirSync8, readFileSync as readFileSync32 } from "fs";
|
|
9331
|
+
import { join as join38 } from "path";
|
|
6515
9332
|
var PATH_SIGNALS = [
|
|
6516
9333
|
{
|
|
6517
9334
|
preset: "nest",
|
|
@@ -6528,12 +9345,42 @@ var DEPENDENCY_SIGNALS = [
|
|
|
6528
9345
|
{ preset: "svelte", pattern: /^svelte$|^@sveltejs\// },
|
|
6529
9346
|
{ preset: "nest", pattern: /^@nestjs\// }
|
|
6530
9347
|
];
|
|
9348
|
+
var IMPORT_SIGNALS = [
|
|
9349
|
+
{ preset: "nest", pattern: /@nestjs\//, evidence: 'its code imports "@nestjs/"' }
|
|
9350
|
+
];
|
|
9351
|
+
var SOURCE_FILE2 = /\.[cm]?[jt]sx?$/;
|
|
9352
|
+
var NOT_SCANNED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo", "build"]);
|
|
9353
|
+
function sourceImports(cwd, pattern) {
|
|
9354
|
+
const stack = [cwd];
|
|
9355
|
+
while (stack.length > 0) {
|
|
9356
|
+
const dir = stack.pop();
|
|
9357
|
+
let entries;
|
|
9358
|
+
try {
|
|
9359
|
+
entries = readdirSync8(dir, { withFileTypes: true });
|
|
9360
|
+
} catch {
|
|
9361
|
+
continue;
|
|
9362
|
+
}
|
|
9363
|
+
for (const entry of entries) {
|
|
9364
|
+
if (entry.isDirectory()) {
|
|
9365
|
+
if (!NOT_SCANNED.has(entry.name)) stack.push(join38(dir, entry.name));
|
|
9366
|
+
continue;
|
|
9367
|
+
}
|
|
9368
|
+
if (!SOURCE_FILE2.test(entry.name)) continue;
|
|
9369
|
+
try {
|
|
9370
|
+
if (pattern.test(readFileSync32(join38(dir, entry.name), "utf8"))) return true;
|
|
9371
|
+
} catch {
|
|
9372
|
+
continue;
|
|
9373
|
+
}
|
|
9374
|
+
}
|
|
9375
|
+
}
|
|
9376
|
+
return false;
|
|
9377
|
+
}
|
|
6531
9378
|
function dependencyNames(cwd) {
|
|
6532
|
-
const path =
|
|
6533
|
-
if (!
|
|
9379
|
+
const path = join38(cwd, "package.json");
|
|
9380
|
+
if (!existsSync31(path)) return [];
|
|
6534
9381
|
try {
|
|
6535
9382
|
const manifest = parseJsonc(
|
|
6536
|
-
|
|
9383
|
+
readFileSync32(path, "utf8"),
|
|
6537
9384
|
path
|
|
6538
9385
|
);
|
|
6539
9386
|
return [
|
|
@@ -6547,7 +9394,7 @@ function dependencyNames(cwd) {
|
|
|
6547
9394
|
function declaresJsx(cwd) {
|
|
6548
9395
|
let entries;
|
|
6549
9396
|
try {
|
|
6550
|
-
entries =
|
|
9397
|
+
entries = readdirSync8(cwd).filter(
|
|
6551
9398
|
(name) => name.startsWith("tsconfig") && name.endsWith(".json")
|
|
6552
9399
|
);
|
|
6553
9400
|
} catch {
|
|
@@ -6556,7 +9403,7 @@ function declaresJsx(cwd) {
|
|
|
6556
9403
|
for (const name of entries) {
|
|
6557
9404
|
try {
|
|
6558
9405
|
const config = parseJsonc(
|
|
6559
|
-
|
|
9406
|
+
readFileSync32(join38(cwd, name), "utf8"),
|
|
6560
9407
|
name
|
|
6561
9408
|
);
|
|
6562
9409
|
if (config.compilerOptions?.jsx !== void 0) return true;
|
|
@@ -6581,6 +9428,10 @@ function presetSignals(cwd) {
|
|
|
6581
9428
|
signals.push({ preset, evidence });
|
|
6582
9429
|
}
|
|
6583
9430
|
}
|
|
9431
|
+
for (const { preset, pattern, evidence } of IMPORT_SIGNALS) {
|
|
9432
|
+
if (signals.some((signal) => signal.preset === preset)) continue;
|
|
9433
|
+
if (sourceImports(cwd, pattern)) signals.push({ preset, evidence });
|
|
9434
|
+
}
|
|
6584
9435
|
return signals;
|
|
6585
9436
|
}
|
|
6586
9437
|
function correctingSignal(detected, cwd) {
|
|
@@ -6611,14 +9462,14 @@ function resolveFlavour(opts) {
|
|
|
6611
9462
|
}
|
|
6612
9463
|
return detection.preset;
|
|
6613
9464
|
}
|
|
6614
|
-
function previewPlan(opts,
|
|
6615
|
-
const changed = preparePlan(opts.cwd,
|
|
9465
|
+
function previewPlan(opts, plan4) {
|
|
9466
|
+
const changed = preparePlan(opts.cwd, plan4).filter((file) => file.before !== file.after);
|
|
6616
9467
|
if (opts.json) {
|
|
6617
9468
|
process.stdout.write(
|
|
6618
9469
|
JSON.stringify(
|
|
6619
9470
|
{
|
|
6620
9471
|
dryRun: true,
|
|
6621
|
-
notes:
|
|
9472
|
+
notes: plan4.notes ?? [],
|
|
6622
9473
|
files: changed.map(({ path, before, after, deleted }) => ({
|
|
6623
9474
|
path,
|
|
6624
9475
|
action: deleted ? "delete" : before.length === 0 ? "create" : "update",
|
|
@@ -6633,7 +9484,7 @@ function previewPlan(opts, plan3) {
|
|
|
6633
9484
|
return 0;
|
|
6634
9485
|
}
|
|
6635
9486
|
process.stderr.write(" dry run: no files written\n");
|
|
6636
|
-
for (const note of
|
|
9487
|
+
for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
|
|
6637
9488
|
`);
|
|
6638
9489
|
if (changed.length === 0) {
|
|
6639
9490
|
process.stderr.write(" nothing to change\n");
|
|
@@ -6655,6 +9506,12 @@ async function dispatch(opts) {
|
|
|
6655
9506
|
}
|
|
6656
9507
|
const detected = resolveFlavour(opts);
|
|
6657
9508
|
const resolutionPreset = opts.preset ?? declaredPresetFor(opts.target, opts.cwd) ?? detected;
|
|
9509
|
+
if (resolutionPreset === "tools") {
|
|
9510
|
+
const message = `tools modules are not adopted one at a time: they sometimes hold configuration shared with the workspace root, so they move WITH the root, as one reviewed change owned by the tooling team rather than by the module's team.`;
|
|
9511
|
+
process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${message}`)}
|
|
9512
|
+
`);
|
|
9513
|
+
return 0;
|
|
9514
|
+
}
|
|
6658
9515
|
const adapter = resolve(opts.target, resolutionPreset, opts.runner);
|
|
6659
9516
|
const preset = opts.preset ?? adapter.declaredPreset?.(opts.cwd) ?? detected;
|
|
6660
9517
|
if (opts.preset !== void 0) {
|
|
@@ -6677,25 +9534,25 @@ async function dispatch(opts) {
|
|
|
6677
9534
|
}
|
|
6678
9535
|
}
|
|
6679
9536
|
const context = { cwd: opts.cwd, preset: effective };
|
|
6680
|
-
const
|
|
6681
|
-
if (
|
|
6682
|
-
process.stderr.write(`sentinel (${opts.target}): ${
|
|
9537
|
+
const plan4 = await adapter.plan(context);
|
|
9538
|
+
if (plan4.blocked) {
|
|
9539
|
+
process.stderr.write(`sentinel (${opts.target}): ${plan4.blocked}
|
|
6683
9540
|
`);
|
|
6684
9541
|
return 1;
|
|
6685
9542
|
}
|
|
6686
|
-
if (
|
|
6687
|
-
process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${
|
|
9543
|
+
if (plan4.skipped) {
|
|
9544
|
+
process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan4.skipped}`)}
|
|
6688
9545
|
`);
|
|
6689
9546
|
return 0;
|
|
6690
9547
|
}
|
|
6691
9548
|
if (opts.dryRun) {
|
|
6692
|
-
return previewPlan(opts,
|
|
9549
|
+
return previewPlan(opts, plan4);
|
|
6693
9550
|
}
|
|
6694
|
-
for (const change of applyPlan(opts.cwd,
|
|
9551
|
+
for (const change of applyPlan(opts.cwd, plan4)) {
|
|
6695
9552
|
process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
|
|
6696
9553
|
`);
|
|
6697
9554
|
}
|
|
6698
|
-
for (const note of
|
|
9555
|
+
for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
|
|
6699
9556
|
`);
|
|
6700
9557
|
if (adapter.afterInit) {
|
|
6701
9558
|
process.stderr.write(` fixing what ${opts.target} can fix automatically...
|
|
@@ -6719,16 +9576,9 @@ export {
|
|
|
6719
9576
|
declaredPresetFor,
|
|
6720
9577
|
availableTargets,
|
|
6721
9578
|
resolve,
|
|
6722
|
-
readOwnVersion,
|
|
6723
|
-
readProjectPackageJson,
|
|
6724
|
-
moduleName,
|
|
6725
9579
|
buildConfigFile,
|
|
6726
9580
|
BaseAdapter,
|
|
6727
9581
|
resolveBin,
|
|
6728
|
-
WORKSPACE_ROOT_MARKER,
|
|
6729
|
-
findWorkspaceRoot,
|
|
6730
|
-
ensureWorkspacePrep,
|
|
6731
|
-
inspectWorkspacePrep,
|
|
6732
9582
|
VERBS,
|
|
6733
9583
|
TARGETS,
|
|
6734
9584
|
LONG_RUNNING_TARGETS,
|
|
@@ -6741,4 +9591,4 @@ export {
|
|
|
6741
9591
|
detectFramework,
|
|
6742
9592
|
dispatch
|
|
6743
9593
|
};
|
|
6744
|
-
//# sourceMappingURL=chunk-
|
|
9594
|
+
//# sourceMappingURL=chunk-4UIZJ3TR.js.map
|