@hublo/sentinel 1.2.0-alpha.34 → 1.2.0-alpha.36
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/dist/bin/sentinel.js +1 -1
- package/dist/chunk-3TDUIKVQ.js +178 -0
- package/dist/{chunk-XZLF7FBK.js → chunk-U2KUKV4C.js} +912 -49
- package/dist/index.js +1 -1
- package/dist/roles/build/nest/toolchain.d.ts +4 -36
- package/dist/roles/build/nest/toolchain.js +10 -178
- package/dist/roles/test/nest/toolchain.d.ts +17 -0
- package/dist/roles/test/nest/toolchain.js +103 -0
- package/dist/roles/test/setup/nest.d.ts +15 -0
- package/dist/roles/test/setup/nest.js +50 -0
- package/dist/tsconfig-aliases-Ce6axdJ4.d.ts +36 -0
- package/package.json +11 -1
|
@@ -596,9 +596,9 @@ function binFromOwnInstall(packageName, binName) {
|
|
|
596
596
|
try {
|
|
597
597
|
const manifest = require2.resolve(`${packageName}/package.json`);
|
|
598
598
|
const bin = require2(manifest).bin;
|
|
599
|
-
const
|
|
600
|
-
if (!
|
|
601
|
-
const executable = join6(dirname2(manifest),
|
|
599
|
+
const relative7 = typeof bin === "string" ? bin : bin?.[binName];
|
|
600
|
+
if (!relative7) return void 0;
|
|
601
|
+
const executable = join6(dirname2(manifest), relative7);
|
|
602
602
|
return existsSync7(executable) ? executable : void 0;
|
|
603
603
|
} catch {
|
|
604
604
|
return void 0;
|
|
@@ -1005,9 +1005,9 @@ function nestAdoptionOperations(cwd, target) {
|
|
|
1005
1005
|
|
|
1006
1006
|
// src/roles/build/requirements.ts
|
|
1007
1007
|
function atLeast(declared, needed) {
|
|
1008
|
-
const
|
|
1009
|
-
const left =
|
|
1010
|
-
const right =
|
|
1008
|
+
const parse3 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
1009
|
+
const left = parse3(declared);
|
|
1010
|
+
const right = parse3(needed);
|
|
1011
1011
|
for (let at = 0; at < Math.max(left.length, right.length); at++) {
|
|
1012
1012
|
const a = left[at] ?? 0;
|
|
1013
1013
|
const b = right[at] ?? 0;
|
|
@@ -4692,6 +4692,23 @@ import { join as join27 } from "path";
|
|
|
4692
4692
|
import { existsSync as existsSync22, readFileSync as readFileSync19 } from "fs";
|
|
4693
4693
|
import { join as join25 } from "path";
|
|
4694
4694
|
import { parseSync as parseSync2 } from "oxc-parser";
|
|
4695
|
+
|
|
4696
|
+
// src/shared/ast.ts
|
|
4697
|
+
var SKIP = /* @__PURE__ */ Symbol("skip-subtree");
|
|
4698
|
+
function walk(node, visit) {
|
|
4699
|
+
if (node === null || typeof node !== "object") return;
|
|
4700
|
+
if (Array.isArray(node)) {
|
|
4701
|
+
for (const entry of node) walk(entry, visit);
|
|
4702
|
+
return;
|
|
4703
|
+
}
|
|
4704
|
+
const current = node;
|
|
4705
|
+
if (typeof current.type === "string" && visit(current) === SKIP) return;
|
|
4706
|
+
for (const value of Object.values(current)) {
|
|
4707
|
+
if (value !== null && typeof value === "object") walk(value, visit);
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4711
|
+
// src/roles/lint/eslint-ignores.ts
|
|
4695
4712
|
var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
|
|
4696
4713
|
function toOxlintPattern(pattern) {
|
|
4697
4714
|
return pattern.replace(/^\.\//, "");
|
|
@@ -4737,16 +4754,6 @@ function readEslintIgnores(cwd) {
|
|
|
4737
4754
|
});
|
|
4738
4755
|
return { patterns: [...patterns], unresolved: [...unresolved] };
|
|
4739
4756
|
}
|
|
4740
|
-
function walk(node, visit) {
|
|
4741
|
-
if (!node || typeof node !== "object") return;
|
|
4742
|
-
const current = node;
|
|
4743
|
-
if (typeof current.type === "string") visit(current);
|
|
4744
|
-
for (const key of Object.keys(current)) {
|
|
4745
|
-
const value = current[key];
|
|
4746
|
-
if (Array.isArray(value)) for (const entry of value) walk(entry, visit);
|
|
4747
|
-
else if (value && typeof value === "object") walk(value, visit);
|
|
4748
|
-
}
|
|
4749
|
-
}
|
|
4750
4757
|
function calleeName(node) {
|
|
4751
4758
|
const callee = node.callee;
|
|
4752
4759
|
return callee?.type === "Identifier" ? callee.name : void 0;
|
|
@@ -5611,7 +5618,15 @@ var toolchain_default2 = {
|
|
|
5611
5618
|
var OWNED_TEST_PACKAGES = toolchain_default2.owned;
|
|
5612
5619
|
|
|
5613
5620
|
// src/roles/test/config-policy.ts
|
|
5614
|
-
var
|
|
5621
|
+
var TEST_PRESET_SPECIFIERS = {
|
|
5622
|
+
react: "@hublo/sentinel/test/react",
|
|
5623
|
+
nest: "@hublo/sentinel/test/nest"
|
|
5624
|
+
};
|
|
5625
|
+
function testPresetSpecifier(preset) {
|
|
5626
|
+
return TEST_PRESET_SPECIFIERS[preset] ?? TEST_PRESET_SPECIFIERS.react;
|
|
5627
|
+
}
|
|
5628
|
+
var ALL_TEST_PRESET_SPECIFIERS = Object.values(TEST_PRESET_SPECIFIERS);
|
|
5629
|
+
var TEST_PRESET_SPECIFIER = TEST_PRESET_SPECIFIERS.react;
|
|
5615
5630
|
var TEST_CONFIG_FILES = [
|
|
5616
5631
|
"vitest.config.ts",
|
|
5617
5632
|
"vitest.config.mts",
|
|
@@ -5622,10 +5637,9 @@ var TEST_OWNED_PACKAGES = OWNED_TEST_PACKAGES;
|
|
|
5622
5637
|
var SENTINEL_OWNED_TEST_PACKAGES = TEST_OWNED_PACKAGES.filter(
|
|
5623
5638
|
(entry) => entry.package !== "vite"
|
|
5624
5639
|
).map((entry) => entry.package);
|
|
5625
|
-
|
|
5626
|
-
presetSpecifier:
|
|
5627
|
-
|
|
5628
|
-
};
|
|
5640
|
+
function testToolchain(preset) {
|
|
5641
|
+
return { presetSpecifier: testPresetSpecifier(preset), owned: TEST_OWNED_PACKAGES };
|
|
5642
|
+
}
|
|
5629
5643
|
var TEST_SCRIPT_NAME = "test";
|
|
5630
5644
|
var SENTINEL_TEST_COMMAND = "sentinel --run --test --";
|
|
5631
5645
|
var VITEST_INVOCATION = /(^|&&\s*|\|\|\s*|;\s*)(?:npx\s+|pnpm\s+(?:exec\s+|dlx\s+)?)?vitest\b([^&|;]*)/g;
|
|
@@ -5654,9 +5668,17 @@ function testTarget(options) {
|
|
|
5654
5668
|
};
|
|
5655
5669
|
}
|
|
5656
5670
|
|
|
5671
|
+
// src/roles/test/jest-migration.ts
|
|
5672
|
+
import { existsSync as existsSync27, readFileSync as readFileSync26 } from "fs";
|
|
5673
|
+
import { join as join32 } from "path";
|
|
5674
|
+
|
|
5675
|
+
// src/roles/test/codemod/migrate-module.ts
|
|
5676
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
5677
|
+
import { join as join30 } from "path";
|
|
5678
|
+
|
|
5657
5679
|
// src/roles/test/read-adoption.ts
|
|
5658
5680
|
import { existsSync as existsSync25, readFileSync as readFileSync23, readdirSync as readdirSync3 } from "fs";
|
|
5659
|
-
import { join as join29 } from "path";
|
|
5681
|
+
import { join as join29, relative as relative5 } from "path";
|
|
5660
5682
|
var JEST_CONFIG_FILES = [
|
|
5661
5683
|
"jest.config.ts",
|
|
5662
5684
|
"jest.config.js",
|
|
@@ -5678,6 +5700,13 @@ function rootJestConfigFiles(cwd) {
|
|
|
5678
5700
|
var TEST_FILE = /[.-](spec|test)\.[cm]?[jt]sx?$/;
|
|
5679
5701
|
var NOT_WALKED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo"]);
|
|
5680
5702
|
function hasTestFiles(cwd) {
|
|
5703
|
+
return walkTestFiles(cwd, true).length > 0;
|
|
5704
|
+
}
|
|
5705
|
+
function listTestFiles(cwd) {
|
|
5706
|
+
return walkTestFiles(cwd, false).sort();
|
|
5707
|
+
}
|
|
5708
|
+
function walkTestFiles(cwd, stopAtFirst) {
|
|
5709
|
+
const found = [];
|
|
5681
5710
|
const stack = [cwd];
|
|
5682
5711
|
while (stack.length > 0) {
|
|
5683
5712
|
const dir = stack.pop();
|
|
@@ -5691,11 +5720,12 @@ function hasTestFiles(cwd) {
|
|
|
5691
5720
|
if (entry.isDirectory()) {
|
|
5692
5721
|
if (!NOT_WALKED.has(entry.name)) stack.push(join29(dir, entry.name));
|
|
5693
5722
|
} else if (TEST_FILE.test(entry.name)) {
|
|
5694
|
-
|
|
5723
|
+
found.push(relative5(cwd, join29(dir, entry.name)).replaceAll("\\", "/"));
|
|
5724
|
+
if (stopAtFirst) return found;
|
|
5695
5725
|
}
|
|
5696
5726
|
}
|
|
5697
5727
|
}
|
|
5698
|
-
return
|
|
5728
|
+
return found;
|
|
5699
5729
|
}
|
|
5700
5730
|
function readTestConfigSource(cwd, fileName) {
|
|
5701
5731
|
try {
|
|
@@ -5705,8 +5735,10 @@ function readTestConfigSource(cwd, fileName) {
|
|
|
5705
5735
|
}
|
|
5706
5736
|
}
|
|
5707
5737
|
function importsPreset2(source) {
|
|
5708
|
-
|
|
5709
|
-
|
|
5738
|
+
return ALL_TEST_PRESET_SPECIFIERS.some((specifier) => {
|
|
5739
|
+
const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5740
|
+
return new RegExp(String.raw`(?:from|import)\s*\(?\s*['"]${escaped}['"]`).test(source);
|
|
5741
|
+
});
|
|
5710
5742
|
}
|
|
5711
5743
|
function readTestAdoption(cwd) {
|
|
5712
5744
|
const manifest = readProjectPackageJson(cwd);
|
|
@@ -5766,6 +5798,769 @@ function readTestAdoption(cwd) {
|
|
|
5766
5798
|
};
|
|
5767
5799
|
}
|
|
5768
5800
|
|
|
5801
|
+
// src/roles/test/codemod/migrate-file.ts
|
|
5802
|
+
import { parseSync as parseSync5 } from "oxc-parser";
|
|
5803
|
+
|
|
5804
|
+
// src/roles/test/codemod/scope.ts
|
|
5805
|
+
function readIdentifiers(node) {
|
|
5806
|
+
const names = [];
|
|
5807
|
+
walk(node, (current) => {
|
|
5808
|
+
if (current.type === "Property" || current.type === "ObjectProperty") {
|
|
5809
|
+
if (current.shorthand === true) {
|
|
5810
|
+
names.push(...readIdentifiers(current.value));
|
|
5811
|
+
return SKIP;
|
|
5812
|
+
}
|
|
5813
|
+
if (current.computed === true) return;
|
|
5814
|
+
const key = current.key;
|
|
5815
|
+
if (key?.type !== "Identifier") return;
|
|
5816
|
+
names.push(...readIdentifiers(current.value));
|
|
5817
|
+
return SKIP;
|
|
5818
|
+
}
|
|
5819
|
+
if (current.type === "MemberExpression" || current.type === "StaticMemberExpression") {
|
|
5820
|
+
if (current.computed === true) return;
|
|
5821
|
+
names.push(...readIdentifiers(current.object));
|
|
5822
|
+
return SKIP;
|
|
5823
|
+
}
|
|
5824
|
+
if (current.type === "Identifier") {
|
|
5825
|
+
names.push(current.name);
|
|
5826
|
+
return SKIP;
|
|
5827
|
+
}
|
|
5828
|
+
return void 0;
|
|
5829
|
+
});
|
|
5830
|
+
return names;
|
|
5831
|
+
}
|
|
5832
|
+
function patternNames(pattern) {
|
|
5833
|
+
const names = [];
|
|
5834
|
+
const visit = (node) => {
|
|
5835
|
+
if (node === null || typeof node !== "object") return;
|
|
5836
|
+
const current = node;
|
|
5837
|
+
switch (current.type) {
|
|
5838
|
+
case "Identifier":
|
|
5839
|
+
names.push(current.name);
|
|
5840
|
+
return;
|
|
5841
|
+
case "ObjectPattern":
|
|
5842
|
+
for (const property of current.properties ?? []) {
|
|
5843
|
+
visit(property.type === "RestElement" ? property.argument : property.value);
|
|
5844
|
+
}
|
|
5845
|
+
return;
|
|
5846
|
+
case "ArrayPattern":
|
|
5847
|
+
for (const element of current.elements ?? []) visit(element);
|
|
5848
|
+
return;
|
|
5849
|
+
case "AssignmentPattern":
|
|
5850
|
+
visit(current.left);
|
|
5851
|
+
return;
|
|
5852
|
+
case "RestElement":
|
|
5853
|
+
visit(current.argument);
|
|
5854
|
+
return;
|
|
5855
|
+
default:
|
|
5856
|
+
return;
|
|
5857
|
+
}
|
|
5858
|
+
};
|
|
5859
|
+
visit(pattern);
|
|
5860
|
+
return names;
|
|
5861
|
+
}
|
|
5862
|
+
function moduleBindings(program) {
|
|
5863
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
5864
|
+
for (const statement of program.body ?? []) {
|
|
5865
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
5866
|
+
for (const declarator of statement.declarations ?? []) {
|
|
5867
|
+
const init = declarator.init ?? void 0;
|
|
5868
|
+
for (const name of patternNames(declarator.id)) bindings.set(name, init);
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
return bindings;
|
|
5872
|
+
}
|
|
5873
|
+
function importedNames(program) {
|
|
5874
|
+
const names = /* @__PURE__ */ new Set();
|
|
5875
|
+
for (const statement of program.body ?? []) {
|
|
5876
|
+
if (statement.type !== "ImportDeclaration") continue;
|
|
5877
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
5878
|
+
const local = specifier.local;
|
|
5879
|
+
if (local?.type === "Identifier") names.add(local.name);
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
return names;
|
|
5883
|
+
}
|
|
5884
|
+
function declaredInside(factory) {
|
|
5885
|
+
const names = /* @__PURE__ */ new Set();
|
|
5886
|
+
walk(factory, (node) => {
|
|
5887
|
+
if (node.type === "VariableDeclarator" || node.type === "FunctionDeclaration") {
|
|
5888
|
+
for (const name of patternNames(node.id)) names.add(name);
|
|
5889
|
+
}
|
|
5890
|
+
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") {
|
|
5891
|
+
for (const parameter of node.params ?? []) {
|
|
5892
|
+
for (const name of patternNames(parameter)) names.add(name);
|
|
5893
|
+
}
|
|
5894
|
+
}
|
|
5895
|
+
});
|
|
5896
|
+
return names;
|
|
5897
|
+
}
|
|
5898
|
+
function capturedBindings(factory, bindings) {
|
|
5899
|
+
const own = declaredInside(factory);
|
|
5900
|
+
const captured = /* @__PURE__ */ new Set();
|
|
5901
|
+
for (const name of readIdentifiers(factory)) {
|
|
5902
|
+
if (bindings.has(name) && !own.has(name)) captured.add(name);
|
|
5903
|
+
}
|
|
5904
|
+
return [...captured];
|
|
5905
|
+
}
|
|
5906
|
+
function hoistable(name, bindings, imported, seen = /* @__PURE__ */ new Set()) {
|
|
5907
|
+
if (seen.has(name)) return true;
|
|
5908
|
+
seen.add(name);
|
|
5909
|
+
const init = bindings.get(name);
|
|
5910
|
+
if (init === void 0) return false;
|
|
5911
|
+
for (const read2 of readIdentifiers(init)) {
|
|
5912
|
+
if (imported.has(read2)) return false;
|
|
5913
|
+
if (bindings.has(read2) && !hoistable(read2, bindings, imported, seen)) return false;
|
|
5914
|
+
}
|
|
5915
|
+
return true;
|
|
5916
|
+
}
|
|
5917
|
+
|
|
5918
|
+
// src/roles/test/codemod/hoist.ts
|
|
5919
|
+
function simpleDeclarations(program) {
|
|
5920
|
+
const found = /* @__PURE__ */ new Map();
|
|
5921
|
+
for (const statement of program.body ?? []) {
|
|
5922
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
5923
|
+
const declarations = statement.declarations ?? [];
|
|
5924
|
+
if (declarations.length !== 1) continue;
|
|
5925
|
+
const declarator = declarations[0];
|
|
5926
|
+
const id = declarator.id;
|
|
5927
|
+
const init = declarator.init;
|
|
5928
|
+
if (id?.type !== "Identifier" || init === void 0 || init === null) continue;
|
|
5929
|
+
found.set(id.name, { statement, init });
|
|
5930
|
+
}
|
|
5931
|
+
return found;
|
|
5932
|
+
}
|
|
5933
|
+
function interdependent(names, bindings) {
|
|
5934
|
+
for (const name of names) {
|
|
5935
|
+
for (const read2 of readIdentifiers(bindings.get(name))) {
|
|
5936
|
+
if (read2 !== name && names.includes(read2)) return `${name} reads ${read2}`;
|
|
5937
|
+
}
|
|
5938
|
+
}
|
|
5939
|
+
return void 0;
|
|
5940
|
+
}
|
|
5941
|
+
function statementSpan(source, statement) {
|
|
5942
|
+
let end = statement.end;
|
|
5943
|
+
if (source[end] === "\r") end++;
|
|
5944
|
+
if (source[end] === "\n") end++;
|
|
5945
|
+
return { start: statement.start, end };
|
|
5946
|
+
}
|
|
5947
|
+
function hoistCapturedBindings(source, program, factories, bindings, imported) {
|
|
5948
|
+
const captured = [...new Set(factories.flatMap((factory) => capturedBindings(factory, bindings)))];
|
|
5949
|
+
if (captured.length === 0) return { source, hoisted: [], refusals: [] };
|
|
5950
|
+
const refusals = [];
|
|
5951
|
+
for (const name of captured) {
|
|
5952
|
+
if (hoistable(name, bindings, imported)) continue;
|
|
5953
|
+
refusals.push({
|
|
5954
|
+
binding: name,
|
|
5955
|
+
reason: bindings.get(name) === void 0 ? `it is declared without an initializer, so there is nothing for vi.hoisted to carry` : `its initializer reads something imported, so it cannot live above the import graph`
|
|
5956
|
+
});
|
|
5957
|
+
}
|
|
5958
|
+
const declarations = simpleDeclarations(program);
|
|
5959
|
+
for (const name of captured) {
|
|
5960
|
+
if (!declarations.has(name) && !refusals.some((refusal) => refusal.binding === name)) {
|
|
5961
|
+
refusals.push({
|
|
5962
|
+
binding: name,
|
|
5963
|
+
reason: `it is not declared as a plain \`const ${name} = ...\` at module scope`
|
|
5964
|
+
});
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5967
|
+
const cycle = interdependent(captured, bindings);
|
|
5968
|
+
if (cycle !== void 0) {
|
|
5969
|
+
refusals.push({
|
|
5970
|
+
binding: captured.join(", "),
|
|
5971
|
+
reason: `${cycle}, and an object literal cannot refer to its own keys`
|
|
5972
|
+
});
|
|
5973
|
+
}
|
|
5974
|
+
if (refusals.length > 0) return { source, hoisted: [], refusals };
|
|
5975
|
+
const ordered = captured.map((name) => ({ name, ...declarations.get(name) })).sort((left, right) => left.statement.start - right.statement.start);
|
|
5976
|
+
const entries = ordered.map(({ name, init }) => `${name}: ${source.slice(init.start, init.end)}`).join(", ");
|
|
5977
|
+
const first = ordered[0];
|
|
5978
|
+
const semi = source.slice(first.statement.start, first.statement.end).trimEnd().endsWith(";");
|
|
5979
|
+
const hoistedLine = `const { ${ordered.map(({ name }) => name).join(", ")} } = vi.hoisted(() => ({ ${entries} }))${semi ? ";" : ""}
|
|
5980
|
+
`;
|
|
5981
|
+
const edits = ordered.map(({ statement }, index) => {
|
|
5982
|
+
const span = statementSpan(source, statement);
|
|
5983
|
+
return { ...span, text: index === 0 ? hoistedLine : "" };
|
|
5984
|
+
});
|
|
5985
|
+
let rewritten = source;
|
|
5986
|
+
for (const edit of edits.sort((left, right) => right.start - left.start)) {
|
|
5987
|
+
rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
|
|
5988
|
+
}
|
|
5989
|
+
return { source: rewritten, hoisted: ordered.map(({ name }) => name), refusals: [] };
|
|
5990
|
+
}
|
|
5991
|
+
|
|
5992
|
+
// src/roles/test/codemod/inject-type-import.ts
|
|
5993
|
+
import { parseSync as parseSync4 } from "oxc-parser";
|
|
5994
|
+
|
|
5995
|
+
// src/roles/test/codemod/rename.ts
|
|
5996
|
+
import { parseSync as parseSync3 } from "oxc-parser";
|
|
5997
|
+
var VITEST_TYPE_SOURCE = "vitest";
|
|
5998
|
+
var RUNTIME_RENAMES = /* @__PURE__ */ new Set([
|
|
5999
|
+
// Mocks
|
|
6000
|
+
"clearAllMocks",
|
|
6001
|
+
"doMock",
|
|
6002
|
+
"fn",
|
|
6003
|
+
"mock",
|
|
6004
|
+
"mocked",
|
|
6005
|
+
"resetAllMocks",
|
|
6006
|
+
"resetModules",
|
|
6007
|
+
"restoreAllMocks",
|
|
6008
|
+
"spyOn",
|
|
6009
|
+
"unmock",
|
|
6010
|
+
/*
|
|
6011
|
+
* Timers, as a family rather than as the two the corpus happens to use today. Running this over
|
|
6012
|
+
* the repo reported `jest.runAllTimers` as unknown, which was a hole in the table rather than a
|
|
6013
|
+
* finding about the code. Each name here was then checked against a running Vitest, so adding the
|
|
6014
|
+
* siblings is not speculation: a file gaining one tomorrow gets the same answer as one using it
|
|
6015
|
+
* today, instead of a report about a member that was always going to be a rename.
|
|
6016
|
+
*/
|
|
6017
|
+
"advanceTimersByTime",
|
|
6018
|
+
"advanceTimersByTimeAsync",
|
|
6019
|
+
"advanceTimersToNextTimer",
|
|
6020
|
+
"clearAllTimers",
|
|
6021
|
+
"getRealSystemTime",
|
|
6022
|
+
"getTimerCount",
|
|
6023
|
+
"runAllTimers",
|
|
6024
|
+
"runAllTimersAsync",
|
|
6025
|
+
"runOnlyPendingTimers",
|
|
6026
|
+
"setSystemTime",
|
|
6027
|
+
"useFakeTimers",
|
|
6028
|
+
"useRealTimers"
|
|
6029
|
+
]);
|
|
6030
|
+
var TYPE_RENAMES = {
|
|
6031
|
+
Mock: "Mock",
|
|
6032
|
+
MockedClass: "MockedClass",
|
|
6033
|
+
MockedFunction: "MockedFunction",
|
|
6034
|
+
MockedObject: "MockedObject",
|
|
6035
|
+
Mocked: "Mocked",
|
|
6036
|
+
SpiedFunction: "MockInstance",
|
|
6037
|
+
SpyInstance: "MockInstance"
|
|
6038
|
+
};
|
|
6039
|
+
var NAMED_RULE = {
|
|
6040
|
+
isolateModules: `vi.resetModules() followed by a dynamic import, since vi.resetModules() with a require() returns the CACHED module`,
|
|
6041
|
+
requireActual: `await vi.importActual(...), which makes the surrounding factory async`,
|
|
6042
|
+
requireMock: `await vi.importMock(...), which makes the surrounding factory async`,
|
|
6043
|
+
setTimeout: `vi.setConfig({ testTimeout: ... }), which is configuration rather than a call`
|
|
6044
|
+
};
|
|
6045
|
+
function lineOf(source, offset) {
|
|
6046
|
+
let line = 1;
|
|
6047
|
+
for (let index = 0; index < offset && index < source.length; index++) {
|
|
6048
|
+
if (source[index] === "\n") line++;
|
|
6049
|
+
}
|
|
6050
|
+
return line;
|
|
6051
|
+
}
|
|
6052
|
+
function valueEdit(node, source, outcome) {
|
|
6053
|
+
const object = node.object;
|
|
6054
|
+
const property = node.property;
|
|
6055
|
+
if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
|
|
6056
|
+
if (property?.type !== "Identifier") return void 0;
|
|
6057
|
+
const member = property.name;
|
|
6058
|
+
if (RUNTIME_RENAMES.has(member)) {
|
|
6059
|
+
outcome.rewritten++;
|
|
6060
|
+
return { start: object.start, end: object.end, text: "vi" };
|
|
6061
|
+
}
|
|
6062
|
+
outcome.unhandled.push({
|
|
6063
|
+
member: `jest.${member}`,
|
|
6064
|
+
line: lineOf(source, object.start),
|
|
6065
|
+
...NAMED_RULE[member] === void 0 ? {} : { rule: NAMED_RULE[member] }
|
|
6066
|
+
});
|
|
6067
|
+
return void 0;
|
|
6068
|
+
}
|
|
6069
|
+
function typeEdit(node, source, outcome, needed) {
|
|
6070
|
+
const left = node.left;
|
|
6071
|
+
const right = node.right;
|
|
6072
|
+
if (left?.type !== "Identifier" || left.name !== "jest") return void 0;
|
|
6073
|
+
if (right?.type !== "Identifier") return void 0;
|
|
6074
|
+
const member = right.name;
|
|
6075
|
+
const replacement = TYPE_RENAMES[member];
|
|
6076
|
+
if (replacement === void 0) {
|
|
6077
|
+
outcome.unhandled.push({ member: `jest.${member}`, line: lineOf(source, node.start) });
|
|
6078
|
+
return void 0;
|
|
6079
|
+
}
|
|
6080
|
+
needed.add(replacement);
|
|
6081
|
+
outcome.rewritten++;
|
|
6082
|
+
return { start: node.start, end: node.end, text: replacement };
|
|
6083
|
+
}
|
|
6084
|
+
function renameJestToVitest(fileName, source) {
|
|
6085
|
+
const outcome = { source, typeImports: [], rewritten: 0, unhandled: [] };
|
|
6086
|
+
const { program, errors } = parseSync3(fileName, source, { sourceType: "module" });
|
|
6087
|
+
if (errors.length > 0) return outcome;
|
|
6088
|
+
const edits = [];
|
|
6089
|
+
const needed = /* @__PURE__ */ new Set();
|
|
6090
|
+
walk(program, (node) => {
|
|
6091
|
+
if (node.type === "MemberExpression" || node.type === "StaticMemberExpression") {
|
|
6092
|
+
const edit = valueEdit(node, source, outcome);
|
|
6093
|
+
if (edit !== void 0) edits.push(edit);
|
|
6094
|
+
return;
|
|
6095
|
+
}
|
|
6096
|
+
if (node.type === "TSQualifiedName") {
|
|
6097
|
+
const edit = typeEdit(node, source, outcome, needed);
|
|
6098
|
+
if (edit !== void 0) edits.push(edit);
|
|
6099
|
+
}
|
|
6100
|
+
});
|
|
6101
|
+
outcome.typeImports = [...needed].sort();
|
|
6102
|
+
let rewritten = source;
|
|
6103
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
6104
|
+
rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
|
|
6105
|
+
}
|
|
6106
|
+
outcome.source = rewritten;
|
|
6107
|
+
return outcome;
|
|
6108
|
+
}
|
|
6109
|
+
|
|
6110
|
+
// src/roles/test/codemod/inject-type-import.ts
|
|
6111
|
+
function renderTypeImport(names, semi) {
|
|
6112
|
+
return `import type { ${names.join(", ")} } from '${VITEST_TYPE_SOURCE}'${semi ? ";" : ""}`;
|
|
6113
|
+
}
|
|
6114
|
+
function importDeclarations(program) {
|
|
6115
|
+
return (program.body ?? []).filter((node) => node.type === "ImportDeclaration");
|
|
6116
|
+
}
|
|
6117
|
+
function usesSemicolons(source, imports) {
|
|
6118
|
+
const first = imports[0];
|
|
6119
|
+
if (first === void 0) return false;
|
|
6120
|
+
return source.slice(first.start, first.end).trimEnd().endsWith(";");
|
|
6121
|
+
}
|
|
6122
|
+
function existingVitestImport(imports) {
|
|
6123
|
+
return imports.find(
|
|
6124
|
+
(node) => node.source?.value === VITEST_TYPE_SOURCE
|
|
6125
|
+
);
|
|
6126
|
+
}
|
|
6127
|
+
function alreadyImported(declaration) {
|
|
6128
|
+
return (declaration.specifiers ?? []).filter((specifier) => specifier.type === "ImportSpecifier").map((specifier) => specifier.imported?.name).filter((name) => name !== void 0);
|
|
6129
|
+
}
|
|
6130
|
+
function injectTypeImport(fileName, source, typeNames) {
|
|
6131
|
+
if (typeNames.length === 0) return source;
|
|
6132
|
+
const { program, errors } = parseSync4(fileName, source, { sourceType: "module" });
|
|
6133
|
+
if (errors.length > 0) return source;
|
|
6134
|
+
const imports = importDeclarations(program);
|
|
6135
|
+
const semi = usesSemicolons(source, imports);
|
|
6136
|
+
const existing = existingVitestImport(imports);
|
|
6137
|
+
if (existing !== void 0) {
|
|
6138
|
+
const merged = [.../* @__PURE__ */ new Set([...alreadyImported(existing), ...typeNames])].sort();
|
|
6139
|
+
if (merged.length === alreadyImported(existing).length) return source;
|
|
6140
|
+
return source.slice(0, existing.start) + renderTypeImport(merged, semi) + source.slice(existing.end);
|
|
6141
|
+
}
|
|
6142
|
+
const line = renderTypeImport([...typeNames].sort(), semi);
|
|
6143
|
+
const first = imports[0];
|
|
6144
|
+
if (first === void 0) return `${line}
|
|
6145
|
+
|
|
6146
|
+
${source}`;
|
|
6147
|
+
return source.slice(0, first.start) + line + "\n" + source.slice(first.start);
|
|
6148
|
+
}
|
|
6149
|
+
|
|
6150
|
+
// src/roles/test/codemod/named-rules.ts
|
|
6151
|
+
var ASYNC_REPLACEMENTS = {
|
|
6152
|
+
requireActual: "importActual",
|
|
6153
|
+
requireMock: "importMock"
|
|
6154
|
+
};
|
|
6155
|
+
var FUNCTIONS = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
|
|
6156
|
+
function walkWithEnclosingFunction(node, enclosing, visit) {
|
|
6157
|
+
if (node === null || typeof node !== "object") return;
|
|
6158
|
+
if (Array.isArray(node)) {
|
|
6159
|
+
for (const entry of node) walkWithEnclosingFunction(entry, enclosing, visit);
|
|
6160
|
+
return;
|
|
6161
|
+
}
|
|
6162
|
+
const current = node;
|
|
6163
|
+
const next = FUNCTIONS.has(current.type) ? current : enclosing;
|
|
6164
|
+
if (typeof current.type === "string") visit(current, enclosing);
|
|
6165
|
+
for (const value of Object.values(current)) {
|
|
6166
|
+
if (value !== null && typeof value === "object") walkWithEnclosingFunction(value, next, visit);
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
function ownedCall(node) {
|
|
6170
|
+
if (node.type !== "CallExpression") return void 0;
|
|
6171
|
+
const callee = node.callee;
|
|
6172
|
+
if (callee?.type !== "MemberExpression" && callee?.type !== "StaticMemberExpression") {
|
|
6173
|
+
return void 0;
|
|
6174
|
+
}
|
|
6175
|
+
const object = callee.object;
|
|
6176
|
+
const property = callee.property;
|
|
6177
|
+
if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
|
|
6178
|
+
return property?.type === "Identifier" ? property.name : void 0;
|
|
6179
|
+
}
|
|
6180
|
+
function argumentText(source, call) {
|
|
6181
|
+
const args = call.arguments ?? [];
|
|
6182
|
+
if (args.length === 0) return "";
|
|
6183
|
+
const first = args[0];
|
|
6184
|
+
const last = args[args.length - 1];
|
|
6185
|
+
return source.slice(first.start, last.end);
|
|
6186
|
+
}
|
|
6187
|
+
function applyEdits(source, edits) {
|
|
6188
|
+
let result = source;
|
|
6189
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
6190
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
6191
|
+
}
|
|
6192
|
+
return result;
|
|
6193
|
+
}
|
|
6194
|
+
function applyNamedRules(source, program) {
|
|
6195
|
+
const edits = [];
|
|
6196
|
+
const applied = /* @__PURE__ */ new Set();
|
|
6197
|
+
const asyncify = /* @__PURE__ */ new Set();
|
|
6198
|
+
walkWithEnclosingFunction(program, void 0, (node, enclosing) => {
|
|
6199
|
+
const member = ownedCall(node);
|
|
6200
|
+
if (member === void 0) return;
|
|
6201
|
+
const asyncReplacement = ASYNC_REPLACEMENTS[member];
|
|
6202
|
+
if (asyncReplacement !== void 0) {
|
|
6203
|
+
edits.push({
|
|
6204
|
+
start: node.start,
|
|
6205
|
+
end: node.end,
|
|
6206
|
+
text: `(await vi.${asyncReplacement}(${argumentText(source, node)}))`
|
|
6207
|
+
});
|
|
6208
|
+
applied.add(`jest.${member}`);
|
|
6209
|
+
if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
|
|
6210
|
+
return;
|
|
6211
|
+
}
|
|
6212
|
+
if (member === "setTimeout") {
|
|
6213
|
+
edits.push({
|
|
6214
|
+
start: node.start,
|
|
6215
|
+
end: node.end,
|
|
6216
|
+
text: `vi.setConfig({ testTimeout: ${argumentText(source, node)} })`
|
|
6217
|
+
});
|
|
6218
|
+
applied.add("jest.setTimeout");
|
|
6219
|
+
}
|
|
6220
|
+
});
|
|
6221
|
+
for (const fn of asyncify) {
|
|
6222
|
+
edits.push({ start: fn.start, end: fn.start, text: "async " });
|
|
6223
|
+
}
|
|
6224
|
+
return { source: applyEdits(source, edits), applied: [...applied].sort() };
|
|
6225
|
+
}
|
|
6226
|
+
|
|
6227
|
+
// src/roles/test/codemod/migrate-file.ts
|
|
6228
|
+
function mockFactories(program) {
|
|
6229
|
+
const factories = [];
|
|
6230
|
+
walk(program, (node) => {
|
|
6231
|
+
if (node.type !== "CallExpression") return;
|
|
6232
|
+
const callee = node.callee;
|
|
6233
|
+
if (callee?.type !== "MemberExpression" && callee?.type !== "StaticMemberExpression") return;
|
|
6234
|
+
const object = callee.object;
|
|
6235
|
+
const property = callee.property;
|
|
6236
|
+
if (object?.type !== "Identifier") return;
|
|
6237
|
+
if (object.name !== "jest" && object.name !== "vi") return;
|
|
6238
|
+
if (property?.name !== "mock") return;
|
|
6239
|
+
const args = node.arguments ?? [];
|
|
6240
|
+
if (args.length > 1) factories.push(args[1]);
|
|
6241
|
+
});
|
|
6242
|
+
return factories;
|
|
6243
|
+
}
|
|
6244
|
+
function parse2(fileName, source) {
|
|
6245
|
+
const { program, errors } = parseSync5(fileName, source, { sourceType: "module" });
|
|
6246
|
+
return errors.length > 0 ? void 0 : program;
|
|
6247
|
+
}
|
|
6248
|
+
function migrateTestFile(fileName, source) {
|
|
6249
|
+
const untouched = {
|
|
6250
|
+
source,
|
|
6251
|
+
changed: false,
|
|
6252
|
+
renamed: 0,
|
|
6253
|
+
hoisted: [],
|
|
6254
|
+
unhandled: [],
|
|
6255
|
+
refusals: [],
|
|
6256
|
+
rulesApplied: []
|
|
6257
|
+
};
|
|
6258
|
+
const original = parse2(fileName, source);
|
|
6259
|
+
if (original === void 0) return untouched;
|
|
6260
|
+
const ruled = applyNamedRules(source, original);
|
|
6261
|
+
const renamed = renameJestToVitest(fileName, ruled.source);
|
|
6262
|
+
const program = parse2(fileName, renamed.source);
|
|
6263
|
+
if (program === void 0) return untouched;
|
|
6264
|
+
const hoist = hoistCapturedBindings(
|
|
6265
|
+
renamed.source,
|
|
6266
|
+
program,
|
|
6267
|
+
mockFactories(program),
|
|
6268
|
+
moduleBindings(program),
|
|
6269
|
+
importedNames(program)
|
|
6270
|
+
);
|
|
6271
|
+
const withImport = injectTypeImport(fileName, hoist.source, renamed.typeImports);
|
|
6272
|
+
return {
|
|
6273
|
+
source: withImport,
|
|
6274
|
+
changed: withImport !== source,
|
|
6275
|
+
renamed: renamed.rewritten,
|
|
6276
|
+
hoisted: hoist.hoisted,
|
|
6277
|
+
unhandled: renamed.unhandled,
|
|
6278
|
+
refusals: hoist.refusals,
|
|
6279
|
+
rulesApplied: ruled.applied
|
|
6280
|
+
};
|
|
6281
|
+
}
|
|
6282
|
+
|
|
6283
|
+
// src/roles/test/codemod/migrate-module.ts
|
|
6284
|
+
function read(path) {
|
|
6285
|
+
try {
|
|
6286
|
+
return readFileSync24(path, "utf8");
|
|
6287
|
+
} catch {
|
|
6288
|
+
return void 0;
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
6291
|
+
function migrateModuleTests(cwd) {
|
|
6292
|
+
const operations = [];
|
|
6293
|
+
const blockers = [];
|
|
6294
|
+
let files = 0;
|
|
6295
|
+
let renamed = 0;
|
|
6296
|
+
let hoisted = 0;
|
|
6297
|
+
for (const relative7 of listTestFiles(cwd)) {
|
|
6298
|
+
const source = read(join30(cwd, relative7));
|
|
6299
|
+
if (source === void 0) {
|
|
6300
|
+
blockers.push({ file: relative7, reason: "this file could not be read" });
|
|
6301
|
+
continue;
|
|
6302
|
+
}
|
|
6303
|
+
const migrated = migrateTestFile(join30(cwd, relative7), source);
|
|
6304
|
+
for (const member of migrated.unhandled) {
|
|
6305
|
+
blockers.push({
|
|
6306
|
+
file: `${relative7}:${member.line}`,
|
|
6307
|
+
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}`
|
|
6308
|
+
});
|
|
6309
|
+
}
|
|
6310
|
+
for (const refusal of migrated.refusals) {
|
|
6311
|
+
blockers.push({
|
|
6312
|
+
file: relative7,
|
|
6313
|
+
reason: `\`${refusal.binding}\` cannot move into vi.hoisted: ${refusal.reason}`
|
|
6314
|
+
});
|
|
6315
|
+
}
|
|
6316
|
+
if (!migrated.changed) continue;
|
|
6317
|
+
files++;
|
|
6318
|
+
renamed += migrated.renamed;
|
|
6319
|
+
if (migrated.hoisted.length > 0) hoisted++;
|
|
6320
|
+
operations.push({ kind: "write", path: relative7, contents: migrated.source });
|
|
6321
|
+
}
|
|
6322
|
+
return blockers.length > 0 ? { operations: [], files, renamed, hoisted, blockers } : { operations, files, renamed, hoisted, blockers: [] };
|
|
6323
|
+
}
|
|
6324
|
+
|
|
6325
|
+
// src/roles/test/nest/generate-config.ts
|
|
6326
|
+
import { relative as relative6 } from "path";
|
|
6327
|
+
var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
|
|
6328
|
+
var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/nest";
|
|
6329
|
+
var NEST_TEST_CONFIG_FILE = "vitest.config.mts";
|
|
6330
|
+
function hopsToRoot(cwd, workspaceRoot) {
|
|
6331
|
+
const up = relative6(cwd, workspaceRoot).replaceAll("\\", "/");
|
|
6332
|
+
return up === "" ? "." : up;
|
|
6333
|
+
}
|
|
6334
|
+
function coverageBlock(reading) {
|
|
6335
|
+
return reading.coverageDirectory === void 0 ? "" : ` coverage: { reportsDirectory: '${reading.coverageDirectory}' },`;
|
|
6336
|
+
}
|
|
6337
|
+
function environmentBlock(reading) {
|
|
6338
|
+
return reading.testEnvironment === void 0 ? "" : ` environment: '${reading.testEnvironment}',`;
|
|
6339
|
+
}
|
|
6340
|
+
function generateNestTestConfig(cwd, reading) {
|
|
6341
|
+
if (reading.refusals.length > 0) return { refusals: reading.refusals };
|
|
6342
|
+
const workspaceRoot = findWorkspaceRoot(cwd);
|
|
6343
|
+
if (workspaceRoot === void 0) {
|
|
6344
|
+
return { refusals: ["the workspace root could not be found from this module"] };
|
|
6345
|
+
}
|
|
6346
|
+
const up = hopsToRoot(cwd, workspaceRoot);
|
|
6347
|
+
const test = [
|
|
6348
|
+
environmentBlock(reading),
|
|
6349
|
+
` setupFiles: ['${SENTINEL_TEST_SETUP}'],`,
|
|
6350
|
+
coverageBlock(reading)
|
|
6351
|
+
].filter((line) => line !== "");
|
|
6352
|
+
return {
|
|
6353
|
+
refusals: [],
|
|
6354
|
+
source: `import path from 'node:path'
|
|
6355
|
+
|
|
6356
|
+
import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'
|
|
6357
|
+
|
|
6358
|
+
export default defineConfig(
|
|
6359
|
+
nestTestConfig({
|
|
6360
|
+
root: import.meta.dirname,
|
|
6361
|
+
workspaceRoot: path.resolve(import.meta.dirname, '${up}'),
|
|
6362
|
+
overrides: {
|
|
6363
|
+
test: {
|
|
6364
|
+
${test.join("\n")}
|
|
6365
|
+
},
|
|
6366
|
+
},
|
|
6367
|
+
}),
|
|
6368
|
+
)
|
|
6369
|
+
`
|
|
6370
|
+
};
|
|
6371
|
+
}
|
|
6372
|
+
|
|
6373
|
+
// src/roles/test/nest/read-jest-config.ts
|
|
6374
|
+
import { existsSync as existsSync26, readFileSync as readFileSync25 } from "fs";
|
|
6375
|
+
import { join as join31 } from "path";
|
|
6376
|
+
var CARRIED = ["testEnvironment", "coverageDirectory", "setupFilesAfterEnv"];
|
|
6377
|
+
var DISCARDED = ["preset", "displayName", "moduleFileExtensions"];
|
|
6378
|
+
var STRING = String.raw`['"]([^'"]*)['"]`;
|
|
6379
|
+
function readString(body, key) {
|
|
6380
|
+
return new RegExp(String.raw`\b${key}\s*:\s*${STRING}`).exec(body)?.[1];
|
|
6381
|
+
}
|
|
6382
|
+
function readStringArray(body, key) {
|
|
6383
|
+
const raw = new RegExp(String.raw`\b${key}\s*:\s*\[([^\]]*)\]`).exec(body)?.[1];
|
|
6384
|
+
if (raw === void 0) return void 0;
|
|
6385
|
+
return [...raw.matchAll(new RegExp(STRING, "g"))].map((match) => match[1]);
|
|
6386
|
+
}
|
|
6387
|
+
function configBody(source) {
|
|
6388
|
+
const start = /(?:const\s+\w+(?::\s*\w+)?\s*=|export\s+default)\s*\{/.exec(source);
|
|
6389
|
+
if (start === null) return void 0;
|
|
6390
|
+
let depth = 0;
|
|
6391
|
+
const from = start.index + start[0].length - 1;
|
|
6392
|
+
for (let i = from; i < source.length; i += 1) {
|
|
6393
|
+
const char = source[i];
|
|
6394
|
+
if (char === "{") depth += 1;
|
|
6395
|
+
else if (char === "}") {
|
|
6396
|
+
depth -= 1;
|
|
6397
|
+
if (depth === 0) return source.slice(from + 1, i);
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
return void 0;
|
|
6401
|
+
}
|
|
6402
|
+
function declaredKeys(body) {
|
|
6403
|
+
const keys = [];
|
|
6404
|
+
let depth = 0;
|
|
6405
|
+
for (const line of body.split("\n")) {
|
|
6406
|
+
if (depth === 0) {
|
|
6407
|
+
const key = /^\s*([A-Za-z_$][\w$]*)\s*:/.exec(line);
|
|
6408
|
+
if (key !== null) keys.push(key[1]);
|
|
6409
|
+
}
|
|
6410
|
+
for (const char of line) {
|
|
6411
|
+
if (char === "{" || char === "[") depth += 1;
|
|
6412
|
+
else if (char === "}" || char === "]") depth -= 1;
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6415
|
+
return keys;
|
|
6416
|
+
}
|
|
6417
|
+
function sideEffects(source) {
|
|
6418
|
+
const found = [];
|
|
6419
|
+
if (/^\s*process\.env\s*[.=]/m.test(source)) {
|
|
6420
|
+
found.push("it sets `process.env`, which the generated config would not carry");
|
|
6421
|
+
}
|
|
6422
|
+
const valueImport = /^\s*import\s+(?!type\b)[^\n]*$/m.exec(source);
|
|
6423
|
+
if (valueImport !== null && !/from\s+['"]jest['"]/.test(valueImport[0])) {
|
|
6424
|
+
found.push(`it imports a value (\`${valueImport[0].trim()}\`)`);
|
|
6425
|
+
}
|
|
6426
|
+
return found;
|
|
6427
|
+
}
|
|
6428
|
+
function readJestConfig(cwd) {
|
|
6429
|
+
const file = JEST_CONFIG_FILES.find((name) => existsSync26(join31(cwd, name)));
|
|
6430
|
+
if (file === void 0) return void 0;
|
|
6431
|
+
let source;
|
|
6432
|
+
try {
|
|
6433
|
+
source = readFileSync25(join31(cwd, file), "utf8");
|
|
6434
|
+
} catch {
|
|
6435
|
+
return { file, refusals: [`${file} could not be read`] };
|
|
6436
|
+
}
|
|
6437
|
+
const refusals = sideEffects(source);
|
|
6438
|
+
const body = configBody(source);
|
|
6439
|
+
if (body === void 0) {
|
|
6440
|
+
return { file, refusals: [...refusals, "its config object could not be located"] };
|
|
6441
|
+
}
|
|
6442
|
+
const known = /* @__PURE__ */ new Set([...CARRIED, ...DISCARDED]);
|
|
6443
|
+
for (const key of declaredKeys(body)) {
|
|
6444
|
+
if (!known.has(key)) refusals.push(`it declares \`${key}\`, which is not transposed`);
|
|
6445
|
+
}
|
|
6446
|
+
const setup = readStringArray(source, "setupFilesAfterEnv");
|
|
6447
|
+
return {
|
|
6448
|
+
file,
|
|
6449
|
+
...readString(source, "testEnvironment") !== void 0 ? { testEnvironment: readString(source, "testEnvironment") } : {},
|
|
6450
|
+
...readString(source, "coverageDirectory") !== void 0 ? { coverageDirectory: readString(source, "coverageDirectory") } : {},
|
|
6451
|
+
...setup !== void 0 ? { setupFilesAfterEnv: setup } : {},
|
|
6452
|
+
refusals
|
|
6453
|
+
};
|
|
6454
|
+
}
|
|
6455
|
+
|
|
6456
|
+
// src/roles/test/jest-migration.ts
|
|
6457
|
+
var SPEC_TSCONFIG = "tsconfig.spec.json";
|
|
6458
|
+
var VITEST_GLOBAL_TYPES = "vitest/globals";
|
|
6459
|
+
function specTypesOperation(cwd) {
|
|
6460
|
+
const path = join32(cwd, SPEC_TSCONFIG);
|
|
6461
|
+
if (!existsSync27(path)) return void 0;
|
|
6462
|
+
let config;
|
|
6463
|
+
try {
|
|
6464
|
+
config = parseJsonc(
|
|
6465
|
+
readFileSync26(path, "utf8"),
|
|
6466
|
+
SPEC_TSCONFIG
|
|
6467
|
+
);
|
|
6468
|
+
} catch {
|
|
6469
|
+
return void 0;
|
|
6470
|
+
}
|
|
6471
|
+
const types = config?.compilerOptions?.types;
|
|
6472
|
+
if (!Array.isArray(types) || !types.includes("jest")) return void 0;
|
|
6473
|
+
return {
|
|
6474
|
+
kind: "merge-json",
|
|
6475
|
+
path: SPEC_TSCONFIG,
|
|
6476
|
+
value: {
|
|
6477
|
+
compilerOptions: {
|
|
6478
|
+
types: types.map((entry) => entry === "jest" ? VITEST_GLOBAL_TYPES : entry)
|
|
6479
|
+
}
|
|
6480
|
+
}
|
|
6481
|
+
};
|
|
6482
|
+
}
|
|
6483
|
+
function siblingScriptRewrites(cwd) {
|
|
6484
|
+
const rewrites = {};
|
|
6485
|
+
for (const [name, command] of Object.entries(moduleScripts(cwd))) {
|
|
6486
|
+
if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
|
|
6487
|
+
const rewritten = rewriteVitestScript(command);
|
|
6488
|
+
if (rewritten !== command) rewrites[name] = rewritten;
|
|
6489
|
+
}
|
|
6490
|
+
return rewrites;
|
|
6491
|
+
}
|
|
6492
|
+
function blocked(reason) {
|
|
6493
|
+
return { operations: [], blocked: reason };
|
|
6494
|
+
}
|
|
6495
|
+
function planJestMigration(context, adoption) {
|
|
6496
|
+
if (adoption.jestConfigs.length > 1) {
|
|
6497
|
+
const [primary, ...rest] = adoption.jestConfigs;
|
|
6498
|
+
return blocked(
|
|
6499
|
+
`this module has a second run mode (${rest.join(", ")}) beside ${primary}, with its own target. One config file cannot stand for both, and how run modes are expressed under Vitest is an open question here, so this module is not migrated yet.`
|
|
6500
|
+
);
|
|
6501
|
+
}
|
|
6502
|
+
const reading = readJestConfig(context.cwd);
|
|
6503
|
+
if (reading === void 0) {
|
|
6504
|
+
return blocked(`${adoption.jestConfigs.join(", ")} could not be read, so nothing was written.`);
|
|
6505
|
+
}
|
|
6506
|
+
const generated = generateNestTestConfig(context.cwd, reading);
|
|
6507
|
+
if (generated.source === void 0) {
|
|
6508
|
+
return blocked(
|
|
6509
|
+
`this module's jest config does not transpose as it stands: ${generated.refusals.join("; ")}. Nothing was written; this needs a look before the migration runs.`
|
|
6510
|
+
);
|
|
6511
|
+
}
|
|
6512
|
+
const migration = migrateModuleTests(context.cwd);
|
|
6513
|
+
if (migration.blockers.length > 0) {
|
|
6514
|
+
const listed = migration.blockers.map((blocker) => `
|
|
6515
|
+
${blocker.file}: ${blocker.reason}`).join("");
|
|
6516
|
+
return blocked(
|
|
6517
|
+
`${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}`
|
|
6518
|
+
);
|
|
6519
|
+
}
|
|
6520
|
+
const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
|
|
6521
|
+
const siblings = siblingScriptRewrites(context.cwd);
|
|
6522
|
+
const specTypes = specTypesOperation(context.cwd);
|
|
6523
|
+
const operations = [
|
|
6524
|
+
{ kind: "write", path: NEST_TEST_CONFIG_FILE, contents: generated.source },
|
|
6525
|
+
...migration.operations,
|
|
6526
|
+
...specTypes === void 0 ? [] : [specTypes],
|
|
6527
|
+
manifestOperation(context.cwd, { [TEST_SCRIPT_NAME]: SENTINEL_TEST_COMMAND, ...siblings }),
|
|
6528
|
+
...nxTargetOperations({
|
|
6529
|
+
cwd: context.cwd,
|
|
6530
|
+
targets: testTarget({
|
|
6531
|
+
configFiles: [NEST_TEST_CONFIG_FILE],
|
|
6532
|
+
outputs: existing?.outputs ?? [],
|
|
6533
|
+
dependsOn: existing?.dependsOn ?? [],
|
|
6534
|
+
configurations: existing?.configurations ?? {}
|
|
6535
|
+
})
|
|
6536
|
+
}),
|
|
6537
|
+
...adoption.jestConfigs.map((name) => ({ kind: "delete", path: name }))
|
|
6538
|
+
];
|
|
6539
|
+
if (existing !== void 0) {
|
|
6540
|
+
operations.push({
|
|
6541
|
+
kind: "remove-json-keys",
|
|
6542
|
+
path: "project.json",
|
|
6543
|
+
keys: [["targets", TEST_SCRIPT_NAME]]
|
|
6544
|
+
});
|
|
6545
|
+
}
|
|
6546
|
+
const notes = [
|
|
6547
|
+
`${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` : ""}.`,
|
|
6548
|
+
`${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.`
|
|
6549
|
+
];
|
|
6550
|
+
if (specTypes !== void 0) {
|
|
6551
|
+
notes.push(
|
|
6552
|
+
`${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.`
|
|
6553
|
+
);
|
|
6554
|
+
}
|
|
6555
|
+
notes.push(
|
|
6556
|
+
`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.`
|
|
6557
|
+
);
|
|
6558
|
+
if (Object.keys(siblings).length > 0) {
|
|
6559
|
+
notes.push(`rewrote ${Object.keys(siblings).join(", ")} to run through sentinel.`);
|
|
6560
|
+
}
|
|
6561
|
+
return { operations, notes };
|
|
6562
|
+
}
|
|
6563
|
+
|
|
5769
6564
|
// src/roles/test/plan.ts
|
|
5770
6565
|
function ownedTestDependencies(cwd, declared) {
|
|
5771
6566
|
return declared.filter((entry) => SENTINEL_OWNED_TEST_PACKAGES.includes(entry.name)).flatMap((entry) => [
|
|
@@ -5786,16 +6581,50 @@ function plan3(context) {
|
|
|
5786
6581
|
}
|
|
5787
6582
|
const configFile = testConfigFile(context.cwd);
|
|
5788
6583
|
const adoption = readTestAdoption(context.cwd);
|
|
6584
|
+
if (adoption.state === "no-tests") {
|
|
6585
|
+
const operations2 = adoption.jestConfigs.map((name) => ({
|
|
6586
|
+
kind: "delete",
|
|
6587
|
+
path: name
|
|
6588
|
+
}));
|
|
6589
|
+
const owned2 = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
|
|
6590
|
+
if (owned2.length > 0) {
|
|
6591
|
+
operations2.push({ kind: "remove-json-keys", path: "package.json", keys: owned2 });
|
|
6592
|
+
}
|
|
6593
|
+
const hasTarget = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME] !== void 0;
|
|
6594
|
+
if (hasTarget) {
|
|
6595
|
+
operations2.push({
|
|
6596
|
+
kind: "remove-json-keys",
|
|
6597
|
+
path: "project.json",
|
|
6598
|
+
keys: [["targets", TEST_SCRIPT_NAME]]
|
|
6599
|
+
});
|
|
6600
|
+
}
|
|
6601
|
+
if (operations2.length === 0) {
|
|
6602
|
+
return {
|
|
6603
|
+
operations: [],
|
|
6604
|
+
skipped: `this module has no test and no jest leftovers, so there is nothing to do here.`
|
|
6605
|
+
};
|
|
6606
|
+
}
|
|
6607
|
+
return {
|
|
6608
|
+
operations: operations2,
|
|
6609
|
+
notes: [
|
|
6610
|
+
// Names what is actually removed, never a shape this module does not have: half of these
|
|
6611
|
+
// modules carry a config with no target behind it, and claiming otherwise would describe
|
|
6612
|
+
// an operation the plan never performs.
|
|
6613
|
+
`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.`
|
|
6614
|
+
]
|
|
6615
|
+
};
|
|
6616
|
+
}
|
|
5789
6617
|
if (configFile === void 0) {
|
|
6618
|
+
if (adoption.state === "jest") return planJestMigration(context, adoption);
|
|
5790
6619
|
return {
|
|
5791
6620
|
operations: [],
|
|
5792
|
-
skipped:
|
|
6621
|
+
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.`
|
|
5793
6622
|
};
|
|
5794
6623
|
}
|
|
5795
6624
|
const operations = [];
|
|
5796
6625
|
const notes = [];
|
|
5797
6626
|
const rewrite = rewriteToolchainImports(
|
|
5798
|
-
|
|
6627
|
+
testToolchain(context.preset),
|
|
5799
6628
|
readTestConfigSource(context.cwd, configFile),
|
|
5800
6629
|
configFile
|
|
5801
6630
|
);
|
|
@@ -5948,9 +6777,9 @@ function registerTest() {
|
|
|
5948
6777
|
|
|
5949
6778
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
5950
6779
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
5951
|
-
import { existsSync as
|
|
6780
|
+
import { existsSync as existsSync28, readFileSync as readFileSync28 } from "fs";
|
|
5952
6781
|
import { createRequire as createRequire4 } from "module";
|
|
5953
|
-
import { join as
|
|
6782
|
+
import { join as join34 } from "path";
|
|
5954
6783
|
|
|
5955
6784
|
// src/roles/typescript/presets/nest.json
|
|
5956
6785
|
var nest_default2 = {
|
|
@@ -6131,8 +6960,8 @@ function readTsconfigAdoption(cwd) {
|
|
|
6131
6960
|
}
|
|
6132
6961
|
|
|
6133
6962
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
6134
|
-
import { readFileSync as
|
|
6135
|
-
import { join as
|
|
6963
|
+
import { readFileSync as readFileSync27 } from "fs";
|
|
6964
|
+
import { join as join33 } from "path";
|
|
6136
6965
|
|
|
6137
6966
|
// src/roles/typescript/typecheck-script.ts
|
|
6138
6967
|
var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
|
|
@@ -6225,7 +7054,7 @@ function planAdoption(context) {
|
|
|
6225
7054
|
};
|
|
6226
7055
|
}
|
|
6227
7056
|
const existing = parseJsonc(
|
|
6228
|
-
|
|
7057
|
+
readFileSync27(join33(context.cwd, target.path), "utf8"),
|
|
6229
7058
|
target.path
|
|
6230
7059
|
);
|
|
6231
7060
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -6363,7 +7192,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6363
7192
|
let chain;
|
|
6364
7193
|
try {
|
|
6365
7194
|
const parsed = parseJsonc(
|
|
6366
|
-
|
|
7195
|
+
readFileSync28(join34(cwd, target.path), "utf8"),
|
|
6367
7196
|
target.path
|
|
6368
7197
|
);
|
|
6369
7198
|
chain = parsed.extends;
|
|
@@ -6376,7 +7205,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6376
7205
|
);
|
|
6377
7206
|
if (preset === void 0) return void 0;
|
|
6378
7207
|
try {
|
|
6379
|
-
createRequire4(
|
|
7208
|
+
createRequire4(join34(cwd, "noop.js")).resolve(preset);
|
|
6380
7209
|
return void 0;
|
|
6381
7210
|
} catch {
|
|
6382
7211
|
return preset;
|
|
@@ -6472,7 +7301,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6472
7301
|
referencedProjects(cwd, config) {
|
|
6473
7302
|
try {
|
|
6474
7303
|
const parsed = parseJsonc(
|
|
6475
|
-
|
|
7304
|
+
readFileSync28(join34(cwd, config), "utf8"),
|
|
6476
7305
|
config
|
|
6477
7306
|
);
|
|
6478
7307
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
@@ -6488,7 +7317,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6488
7317
|
* check.
|
|
6489
7318
|
*/
|
|
6490
7319
|
typecheckTarget(cwd) {
|
|
6491
|
-
if (
|
|
7320
|
+
if (existsSync28(join34(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
6492
7321
|
const target = resolveTsconfigTarget(cwd);
|
|
6493
7322
|
return target.reason === "none" ? null : target.path;
|
|
6494
7323
|
}
|
|
@@ -6693,9 +7522,9 @@ function replaceLines(current, replacements) {
|
|
|
6693
7522
|
}
|
|
6694
7523
|
|
|
6695
7524
|
// src/core/apply-plan.ts
|
|
6696
|
-
import { existsSync as
|
|
7525
|
+
import { existsSync as existsSync29, readFileSync as readFileSync29, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
6697
7526
|
import { resolve as resolve4, sep } from "path";
|
|
6698
|
-
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
7527
|
+
import { applyEdits as applyEdits2, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
6699
7528
|
|
|
6700
7529
|
// src/shared/deep-merge.ts
|
|
6701
7530
|
function isPlainObject(value) {
|
|
@@ -6712,7 +7541,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
6712
7541
|
return absolutePath;
|
|
6713
7542
|
}
|
|
6714
7543
|
function readIfExists(absolutePath) {
|
|
6715
|
-
return
|
|
7544
|
+
return existsSync29(absolutePath) ? readFileSync29(absolutePath, "utf8") : void 0;
|
|
6716
7545
|
}
|
|
6717
7546
|
function* leaves(value, prefix = []) {
|
|
6718
7547
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -6727,7 +7556,7 @@ function mergeJsonc(current, value) {
|
|
|
6727
7556
|
const edits = modify(text, path, leaf, {
|
|
6728
7557
|
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
6729
7558
|
});
|
|
6730
|
-
text =
|
|
7559
|
+
text = applyEdits2(text, edits);
|
|
6731
7560
|
}
|
|
6732
7561
|
return text.endsWith("\n") ? text : text + "\n";
|
|
6733
7562
|
}
|
|
@@ -6739,7 +7568,7 @@ function removeJsoncKeys(current, keys) {
|
|
|
6739
7568
|
const edits = modify(text, path, void 0, {
|
|
6740
7569
|
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
6741
7570
|
});
|
|
6742
|
-
text =
|
|
7571
|
+
text = applyEdits2(text, edits);
|
|
6743
7572
|
}
|
|
6744
7573
|
return text.endsWith("\n") ? text : text + "\n";
|
|
6745
7574
|
}
|
|
@@ -6830,8 +7659,8 @@ function applyPlan(cwd, plan4) {
|
|
|
6830
7659
|
}
|
|
6831
7660
|
|
|
6832
7661
|
// src/core/config/preset-evidence.ts
|
|
6833
|
-
import { existsSync as
|
|
6834
|
-
import { join as
|
|
7662
|
+
import { existsSync as existsSync30, readdirSync as readdirSync4, readFileSync as readFileSync30 } from "fs";
|
|
7663
|
+
import { join as join35 } from "path";
|
|
6835
7664
|
var PATH_SIGNALS = [
|
|
6836
7665
|
{
|
|
6837
7666
|
preset: "nest",
|
|
@@ -6844,12 +7673,42 @@ var DEPENDENCY_SIGNALS = [
|
|
|
6844
7673
|
{ preset: "svelte", pattern: /^svelte$|^@sveltejs\// },
|
|
6845
7674
|
{ preset: "nest", pattern: /^@nestjs\// }
|
|
6846
7675
|
];
|
|
7676
|
+
var IMPORT_SIGNALS = [
|
|
7677
|
+
{ preset: "nest", pattern: /@nestjs\//, evidence: 'its code imports "@nestjs/"' }
|
|
7678
|
+
];
|
|
7679
|
+
var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
|
|
7680
|
+
var NOT_SCANNED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo", "build"]);
|
|
7681
|
+
function sourceImports(cwd, pattern) {
|
|
7682
|
+
const stack = [cwd];
|
|
7683
|
+
while (stack.length > 0) {
|
|
7684
|
+
const dir = stack.pop();
|
|
7685
|
+
let entries;
|
|
7686
|
+
try {
|
|
7687
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
7688
|
+
} catch {
|
|
7689
|
+
continue;
|
|
7690
|
+
}
|
|
7691
|
+
for (const entry of entries) {
|
|
7692
|
+
if (entry.isDirectory()) {
|
|
7693
|
+
if (!NOT_SCANNED.has(entry.name)) stack.push(join35(dir, entry.name));
|
|
7694
|
+
continue;
|
|
7695
|
+
}
|
|
7696
|
+
if (!SOURCE_FILE.test(entry.name)) continue;
|
|
7697
|
+
try {
|
|
7698
|
+
if (pattern.test(readFileSync30(join35(dir, entry.name), "utf8"))) return true;
|
|
7699
|
+
} catch {
|
|
7700
|
+
continue;
|
|
7701
|
+
}
|
|
7702
|
+
}
|
|
7703
|
+
}
|
|
7704
|
+
return false;
|
|
7705
|
+
}
|
|
6847
7706
|
function dependencyNames(cwd) {
|
|
6848
|
-
const path =
|
|
6849
|
-
if (!
|
|
7707
|
+
const path = join35(cwd, "package.json");
|
|
7708
|
+
if (!existsSync30(path)) return [];
|
|
6850
7709
|
try {
|
|
6851
7710
|
const manifest = parseJsonc(
|
|
6852
|
-
|
|
7711
|
+
readFileSync30(path, "utf8"),
|
|
6853
7712
|
path
|
|
6854
7713
|
);
|
|
6855
7714
|
return [
|
|
@@ -6872,7 +7731,7 @@ function declaresJsx(cwd) {
|
|
|
6872
7731
|
for (const name of entries) {
|
|
6873
7732
|
try {
|
|
6874
7733
|
const config = parseJsonc(
|
|
6875
|
-
|
|
7734
|
+
readFileSync30(join35(cwd, name), "utf8"),
|
|
6876
7735
|
name
|
|
6877
7736
|
);
|
|
6878
7737
|
if (config.compilerOptions?.jsx !== void 0) return true;
|
|
@@ -6897,6 +7756,10 @@ function presetSignals(cwd) {
|
|
|
6897
7756
|
signals.push({ preset, evidence });
|
|
6898
7757
|
}
|
|
6899
7758
|
}
|
|
7759
|
+
for (const { preset, pattern, evidence } of IMPORT_SIGNALS) {
|
|
7760
|
+
if (signals.some((signal) => signal.preset === preset)) continue;
|
|
7761
|
+
if (sourceImports(cwd, pattern)) signals.push({ preset, evidence });
|
|
7762
|
+
}
|
|
6900
7763
|
return signals;
|
|
6901
7764
|
}
|
|
6902
7765
|
function presetContradiction(declared, cwd) {
|
|
@@ -7058,4 +7921,4 @@ export {
|
|
|
7058
7921
|
detectFramework,
|
|
7059
7922
|
dispatch
|
|
7060
7923
|
};
|
|
7061
|
-
//# sourceMappingURL=chunk-
|
|
7924
|
+
//# sourceMappingURL=chunk-U2KUKV4C.js.map
|