@hublo/sentinel 1.2.0-alpha.34 → 1.2.0-alpha.35
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-GQID2U34.js} +895 -41
- 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;
|
|
@@ -5654,9 +5661,17 @@ function testTarget(options) {
|
|
|
5654
5661
|
};
|
|
5655
5662
|
}
|
|
5656
5663
|
|
|
5664
|
+
// src/roles/test/jest-migration.ts
|
|
5665
|
+
import { existsSync as existsSync27, readFileSync as readFileSync26 } from "fs";
|
|
5666
|
+
import { join as join32 } from "path";
|
|
5667
|
+
|
|
5668
|
+
// src/roles/test/codemod/migrate-module.ts
|
|
5669
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
5670
|
+
import { join as join30 } from "path";
|
|
5671
|
+
|
|
5657
5672
|
// src/roles/test/read-adoption.ts
|
|
5658
5673
|
import { existsSync as existsSync25, readFileSync as readFileSync23, readdirSync as readdirSync3 } from "fs";
|
|
5659
|
-
import { join as join29 } from "path";
|
|
5674
|
+
import { join as join29, relative as relative5 } from "path";
|
|
5660
5675
|
var JEST_CONFIG_FILES = [
|
|
5661
5676
|
"jest.config.ts",
|
|
5662
5677
|
"jest.config.js",
|
|
@@ -5678,6 +5693,13 @@ function rootJestConfigFiles(cwd) {
|
|
|
5678
5693
|
var TEST_FILE = /[.-](spec|test)\.[cm]?[jt]sx?$/;
|
|
5679
5694
|
var NOT_WALKED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo"]);
|
|
5680
5695
|
function hasTestFiles(cwd) {
|
|
5696
|
+
return walkTestFiles(cwd, true).length > 0;
|
|
5697
|
+
}
|
|
5698
|
+
function listTestFiles(cwd) {
|
|
5699
|
+
return walkTestFiles(cwd, false).sort();
|
|
5700
|
+
}
|
|
5701
|
+
function walkTestFiles(cwd, stopAtFirst) {
|
|
5702
|
+
const found = [];
|
|
5681
5703
|
const stack = [cwd];
|
|
5682
5704
|
while (stack.length > 0) {
|
|
5683
5705
|
const dir = stack.pop();
|
|
@@ -5691,11 +5713,12 @@ function hasTestFiles(cwd) {
|
|
|
5691
5713
|
if (entry.isDirectory()) {
|
|
5692
5714
|
if (!NOT_WALKED.has(entry.name)) stack.push(join29(dir, entry.name));
|
|
5693
5715
|
} else if (TEST_FILE.test(entry.name)) {
|
|
5694
|
-
|
|
5716
|
+
found.push(relative5(cwd, join29(dir, entry.name)).replaceAll("\\", "/"));
|
|
5717
|
+
if (stopAtFirst) return found;
|
|
5695
5718
|
}
|
|
5696
5719
|
}
|
|
5697
5720
|
}
|
|
5698
|
-
return
|
|
5721
|
+
return found;
|
|
5699
5722
|
}
|
|
5700
5723
|
function readTestConfigSource(cwd, fileName) {
|
|
5701
5724
|
try {
|
|
@@ -5766,6 +5789,769 @@ function readTestAdoption(cwd) {
|
|
|
5766
5789
|
};
|
|
5767
5790
|
}
|
|
5768
5791
|
|
|
5792
|
+
// src/roles/test/codemod/migrate-file.ts
|
|
5793
|
+
import { parseSync as parseSync5 } from "oxc-parser";
|
|
5794
|
+
|
|
5795
|
+
// src/roles/test/codemod/scope.ts
|
|
5796
|
+
function readIdentifiers(node) {
|
|
5797
|
+
const names = [];
|
|
5798
|
+
walk(node, (current) => {
|
|
5799
|
+
if (current.type === "Property" || current.type === "ObjectProperty") {
|
|
5800
|
+
if (current.shorthand === true) {
|
|
5801
|
+
names.push(...readIdentifiers(current.value));
|
|
5802
|
+
return SKIP;
|
|
5803
|
+
}
|
|
5804
|
+
if (current.computed === true) return;
|
|
5805
|
+
const key = current.key;
|
|
5806
|
+
if (key?.type !== "Identifier") return;
|
|
5807
|
+
names.push(...readIdentifiers(current.value));
|
|
5808
|
+
return SKIP;
|
|
5809
|
+
}
|
|
5810
|
+
if (current.type === "MemberExpression" || current.type === "StaticMemberExpression") {
|
|
5811
|
+
if (current.computed === true) return;
|
|
5812
|
+
names.push(...readIdentifiers(current.object));
|
|
5813
|
+
return SKIP;
|
|
5814
|
+
}
|
|
5815
|
+
if (current.type === "Identifier") {
|
|
5816
|
+
names.push(current.name);
|
|
5817
|
+
return SKIP;
|
|
5818
|
+
}
|
|
5819
|
+
return void 0;
|
|
5820
|
+
});
|
|
5821
|
+
return names;
|
|
5822
|
+
}
|
|
5823
|
+
function patternNames(pattern) {
|
|
5824
|
+
const names = [];
|
|
5825
|
+
const visit = (node) => {
|
|
5826
|
+
if (node === null || typeof node !== "object") return;
|
|
5827
|
+
const current = node;
|
|
5828
|
+
switch (current.type) {
|
|
5829
|
+
case "Identifier":
|
|
5830
|
+
names.push(current.name);
|
|
5831
|
+
return;
|
|
5832
|
+
case "ObjectPattern":
|
|
5833
|
+
for (const property of current.properties ?? []) {
|
|
5834
|
+
visit(property.type === "RestElement" ? property.argument : property.value);
|
|
5835
|
+
}
|
|
5836
|
+
return;
|
|
5837
|
+
case "ArrayPattern":
|
|
5838
|
+
for (const element of current.elements ?? []) visit(element);
|
|
5839
|
+
return;
|
|
5840
|
+
case "AssignmentPattern":
|
|
5841
|
+
visit(current.left);
|
|
5842
|
+
return;
|
|
5843
|
+
case "RestElement":
|
|
5844
|
+
visit(current.argument);
|
|
5845
|
+
return;
|
|
5846
|
+
default:
|
|
5847
|
+
return;
|
|
5848
|
+
}
|
|
5849
|
+
};
|
|
5850
|
+
visit(pattern);
|
|
5851
|
+
return names;
|
|
5852
|
+
}
|
|
5853
|
+
function moduleBindings(program) {
|
|
5854
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
5855
|
+
for (const statement of program.body ?? []) {
|
|
5856
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
5857
|
+
for (const declarator of statement.declarations ?? []) {
|
|
5858
|
+
const init = declarator.init ?? void 0;
|
|
5859
|
+
for (const name of patternNames(declarator.id)) bindings.set(name, init);
|
|
5860
|
+
}
|
|
5861
|
+
}
|
|
5862
|
+
return bindings;
|
|
5863
|
+
}
|
|
5864
|
+
function importedNames(program) {
|
|
5865
|
+
const names = /* @__PURE__ */ new Set();
|
|
5866
|
+
for (const statement of program.body ?? []) {
|
|
5867
|
+
if (statement.type !== "ImportDeclaration") continue;
|
|
5868
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
5869
|
+
const local = specifier.local;
|
|
5870
|
+
if (local?.type === "Identifier") names.add(local.name);
|
|
5871
|
+
}
|
|
5872
|
+
}
|
|
5873
|
+
return names;
|
|
5874
|
+
}
|
|
5875
|
+
function declaredInside(factory) {
|
|
5876
|
+
const names = /* @__PURE__ */ new Set();
|
|
5877
|
+
walk(factory, (node) => {
|
|
5878
|
+
if (node.type === "VariableDeclarator" || node.type === "FunctionDeclaration") {
|
|
5879
|
+
for (const name of patternNames(node.id)) names.add(name);
|
|
5880
|
+
}
|
|
5881
|
+
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") {
|
|
5882
|
+
for (const parameter of node.params ?? []) {
|
|
5883
|
+
for (const name of patternNames(parameter)) names.add(name);
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
});
|
|
5887
|
+
return names;
|
|
5888
|
+
}
|
|
5889
|
+
function capturedBindings(factory, bindings) {
|
|
5890
|
+
const own = declaredInside(factory);
|
|
5891
|
+
const captured = /* @__PURE__ */ new Set();
|
|
5892
|
+
for (const name of readIdentifiers(factory)) {
|
|
5893
|
+
if (bindings.has(name) && !own.has(name)) captured.add(name);
|
|
5894
|
+
}
|
|
5895
|
+
return [...captured];
|
|
5896
|
+
}
|
|
5897
|
+
function hoistable(name, bindings, imported, seen = /* @__PURE__ */ new Set()) {
|
|
5898
|
+
if (seen.has(name)) return true;
|
|
5899
|
+
seen.add(name);
|
|
5900
|
+
const init = bindings.get(name);
|
|
5901
|
+
if (init === void 0) return false;
|
|
5902
|
+
for (const read2 of readIdentifiers(init)) {
|
|
5903
|
+
if (imported.has(read2)) return false;
|
|
5904
|
+
if (bindings.has(read2) && !hoistable(read2, bindings, imported, seen)) return false;
|
|
5905
|
+
}
|
|
5906
|
+
return true;
|
|
5907
|
+
}
|
|
5908
|
+
|
|
5909
|
+
// src/roles/test/codemod/hoist.ts
|
|
5910
|
+
function simpleDeclarations(program) {
|
|
5911
|
+
const found = /* @__PURE__ */ new Map();
|
|
5912
|
+
for (const statement of program.body ?? []) {
|
|
5913
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
5914
|
+
const declarations = statement.declarations ?? [];
|
|
5915
|
+
if (declarations.length !== 1) continue;
|
|
5916
|
+
const declarator = declarations[0];
|
|
5917
|
+
const id = declarator.id;
|
|
5918
|
+
const init = declarator.init;
|
|
5919
|
+
if (id?.type !== "Identifier" || init === void 0 || init === null) continue;
|
|
5920
|
+
found.set(id.name, { statement, init });
|
|
5921
|
+
}
|
|
5922
|
+
return found;
|
|
5923
|
+
}
|
|
5924
|
+
function interdependent(names, bindings) {
|
|
5925
|
+
for (const name of names) {
|
|
5926
|
+
for (const read2 of readIdentifiers(bindings.get(name))) {
|
|
5927
|
+
if (read2 !== name && names.includes(read2)) return `${name} reads ${read2}`;
|
|
5928
|
+
}
|
|
5929
|
+
}
|
|
5930
|
+
return void 0;
|
|
5931
|
+
}
|
|
5932
|
+
function statementSpan(source, statement) {
|
|
5933
|
+
let end = statement.end;
|
|
5934
|
+
if (source[end] === "\r") end++;
|
|
5935
|
+
if (source[end] === "\n") end++;
|
|
5936
|
+
return { start: statement.start, end };
|
|
5937
|
+
}
|
|
5938
|
+
function hoistCapturedBindings(source, program, factories, bindings, imported) {
|
|
5939
|
+
const captured = [...new Set(factories.flatMap((factory) => capturedBindings(factory, bindings)))];
|
|
5940
|
+
if (captured.length === 0) return { source, hoisted: [], refusals: [] };
|
|
5941
|
+
const refusals = [];
|
|
5942
|
+
for (const name of captured) {
|
|
5943
|
+
if (hoistable(name, bindings, imported)) continue;
|
|
5944
|
+
refusals.push({
|
|
5945
|
+
binding: name,
|
|
5946
|
+
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`
|
|
5947
|
+
});
|
|
5948
|
+
}
|
|
5949
|
+
const declarations = simpleDeclarations(program);
|
|
5950
|
+
for (const name of captured) {
|
|
5951
|
+
if (!declarations.has(name) && !refusals.some((refusal) => refusal.binding === name)) {
|
|
5952
|
+
refusals.push({
|
|
5953
|
+
binding: name,
|
|
5954
|
+
reason: `it is not declared as a plain \`const ${name} = ...\` at module scope`
|
|
5955
|
+
});
|
|
5956
|
+
}
|
|
5957
|
+
}
|
|
5958
|
+
const cycle = interdependent(captured, bindings);
|
|
5959
|
+
if (cycle !== void 0) {
|
|
5960
|
+
refusals.push({
|
|
5961
|
+
binding: captured.join(", "),
|
|
5962
|
+
reason: `${cycle}, and an object literal cannot refer to its own keys`
|
|
5963
|
+
});
|
|
5964
|
+
}
|
|
5965
|
+
if (refusals.length > 0) return { source, hoisted: [], refusals };
|
|
5966
|
+
const ordered = captured.map((name) => ({ name, ...declarations.get(name) })).sort((left, right) => left.statement.start - right.statement.start);
|
|
5967
|
+
const entries = ordered.map(({ name, init }) => `${name}: ${source.slice(init.start, init.end)}`).join(", ");
|
|
5968
|
+
const first = ordered[0];
|
|
5969
|
+
const semi = source.slice(first.statement.start, first.statement.end).trimEnd().endsWith(";");
|
|
5970
|
+
const hoistedLine = `const { ${ordered.map(({ name }) => name).join(", ")} } = vi.hoisted(() => ({ ${entries} }))${semi ? ";" : ""}
|
|
5971
|
+
`;
|
|
5972
|
+
const edits = ordered.map(({ statement }, index) => {
|
|
5973
|
+
const span = statementSpan(source, statement);
|
|
5974
|
+
return { ...span, text: index === 0 ? hoistedLine : "" };
|
|
5975
|
+
});
|
|
5976
|
+
let rewritten = source;
|
|
5977
|
+
for (const edit of edits.sort((left, right) => right.start - left.start)) {
|
|
5978
|
+
rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
|
|
5979
|
+
}
|
|
5980
|
+
return { source: rewritten, hoisted: ordered.map(({ name }) => name), refusals: [] };
|
|
5981
|
+
}
|
|
5982
|
+
|
|
5983
|
+
// src/roles/test/codemod/inject-type-import.ts
|
|
5984
|
+
import { parseSync as parseSync4 } from "oxc-parser";
|
|
5985
|
+
|
|
5986
|
+
// src/roles/test/codemod/rename.ts
|
|
5987
|
+
import { parseSync as parseSync3 } from "oxc-parser";
|
|
5988
|
+
var VITEST_TYPE_SOURCE = "vitest";
|
|
5989
|
+
var RUNTIME_RENAMES = /* @__PURE__ */ new Set([
|
|
5990
|
+
// Mocks
|
|
5991
|
+
"clearAllMocks",
|
|
5992
|
+
"doMock",
|
|
5993
|
+
"fn",
|
|
5994
|
+
"mock",
|
|
5995
|
+
"mocked",
|
|
5996
|
+
"resetAllMocks",
|
|
5997
|
+
"resetModules",
|
|
5998
|
+
"restoreAllMocks",
|
|
5999
|
+
"spyOn",
|
|
6000
|
+
"unmock",
|
|
6001
|
+
/*
|
|
6002
|
+
* Timers, as a family rather than as the two the corpus happens to use today. Running this over
|
|
6003
|
+
* the repo reported `jest.runAllTimers` as unknown, which was a hole in the table rather than a
|
|
6004
|
+
* finding about the code. Each name here was then checked against a running Vitest, so adding the
|
|
6005
|
+
* siblings is not speculation: a file gaining one tomorrow gets the same answer as one using it
|
|
6006
|
+
* today, instead of a report about a member that was always going to be a rename.
|
|
6007
|
+
*/
|
|
6008
|
+
"advanceTimersByTime",
|
|
6009
|
+
"advanceTimersByTimeAsync",
|
|
6010
|
+
"advanceTimersToNextTimer",
|
|
6011
|
+
"clearAllTimers",
|
|
6012
|
+
"getRealSystemTime",
|
|
6013
|
+
"getTimerCount",
|
|
6014
|
+
"runAllTimers",
|
|
6015
|
+
"runAllTimersAsync",
|
|
6016
|
+
"runOnlyPendingTimers",
|
|
6017
|
+
"setSystemTime",
|
|
6018
|
+
"useFakeTimers",
|
|
6019
|
+
"useRealTimers"
|
|
6020
|
+
]);
|
|
6021
|
+
var TYPE_RENAMES = {
|
|
6022
|
+
Mock: "Mock",
|
|
6023
|
+
MockedClass: "MockedClass",
|
|
6024
|
+
MockedFunction: "MockedFunction",
|
|
6025
|
+
MockedObject: "MockedObject",
|
|
6026
|
+
Mocked: "Mocked",
|
|
6027
|
+
SpiedFunction: "MockInstance",
|
|
6028
|
+
SpyInstance: "MockInstance"
|
|
6029
|
+
};
|
|
6030
|
+
var NAMED_RULE = {
|
|
6031
|
+
isolateModules: `vi.resetModules() followed by a dynamic import, since vi.resetModules() with a require() returns the CACHED module`,
|
|
6032
|
+
requireActual: `await vi.importActual(...), which makes the surrounding factory async`,
|
|
6033
|
+
requireMock: `await vi.importMock(...), which makes the surrounding factory async`,
|
|
6034
|
+
setTimeout: `vi.setConfig({ testTimeout: ... }), which is configuration rather than a call`
|
|
6035
|
+
};
|
|
6036
|
+
function lineOf(source, offset) {
|
|
6037
|
+
let line = 1;
|
|
6038
|
+
for (let index = 0; index < offset && index < source.length; index++) {
|
|
6039
|
+
if (source[index] === "\n") line++;
|
|
6040
|
+
}
|
|
6041
|
+
return line;
|
|
6042
|
+
}
|
|
6043
|
+
function valueEdit(node, source, outcome) {
|
|
6044
|
+
const object = node.object;
|
|
6045
|
+
const property = node.property;
|
|
6046
|
+
if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
|
|
6047
|
+
if (property?.type !== "Identifier") return void 0;
|
|
6048
|
+
const member = property.name;
|
|
6049
|
+
if (RUNTIME_RENAMES.has(member)) {
|
|
6050
|
+
outcome.rewritten++;
|
|
6051
|
+
return { start: object.start, end: object.end, text: "vi" };
|
|
6052
|
+
}
|
|
6053
|
+
outcome.unhandled.push({
|
|
6054
|
+
member: `jest.${member}`,
|
|
6055
|
+
line: lineOf(source, object.start),
|
|
6056
|
+
...NAMED_RULE[member] === void 0 ? {} : { rule: NAMED_RULE[member] }
|
|
6057
|
+
});
|
|
6058
|
+
return void 0;
|
|
6059
|
+
}
|
|
6060
|
+
function typeEdit(node, source, outcome, needed) {
|
|
6061
|
+
const left = node.left;
|
|
6062
|
+
const right = node.right;
|
|
6063
|
+
if (left?.type !== "Identifier" || left.name !== "jest") return void 0;
|
|
6064
|
+
if (right?.type !== "Identifier") return void 0;
|
|
6065
|
+
const member = right.name;
|
|
6066
|
+
const replacement = TYPE_RENAMES[member];
|
|
6067
|
+
if (replacement === void 0) {
|
|
6068
|
+
outcome.unhandled.push({ member: `jest.${member}`, line: lineOf(source, node.start) });
|
|
6069
|
+
return void 0;
|
|
6070
|
+
}
|
|
6071
|
+
needed.add(replacement);
|
|
6072
|
+
outcome.rewritten++;
|
|
6073
|
+
return { start: node.start, end: node.end, text: replacement };
|
|
6074
|
+
}
|
|
6075
|
+
function renameJestToVitest(fileName, source) {
|
|
6076
|
+
const outcome = { source, typeImports: [], rewritten: 0, unhandled: [] };
|
|
6077
|
+
const { program, errors } = parseSync3(fileName, source, { sourceType: "module" });
|
|
6078
|
+
if (errors.length > 0) return outcome;
|
|
6079
|
+
const edits = [];
|
|
6080
|
+
const needed = /* @__PURE__ */ new Set();
|
|
6081
|
+
walk(program, (node) => {
|
|
6082
|
+
if (node.type === "MemberExpression" || node.type === "StaticMemberExpression") {
|
|
6083
|
+
const edit = valueEdit(node, source, outcome);
|
|
6084
|
+
if (edit !== void 0) edits.push(edit);
|
|
6085
|
+
return;
|
|
6086
|
+
}
|
|
6087
|
+
if (node.type === "TSQualifiedName") {
|
|
6088
|
+
const edit = typeEdit(node, source, outcome, needed);
|
|
6089
|
+
if (edit !== void 0) edits.push(edit);
|
|
6090
|
+
}
|
|
6091
|
+
});
|
|
6092
|
+
outcome.typeImports = [...needed].sort();
|
|
6093
|
+
let rewritten = source;
|
|
6094
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
6095
|
+
rewritten = rewritten.slice(0, edit.start) + edit.text + rewritten.slice(edit.end);
|
|
6096
|
+
}
|
|
6097
|
+
outcome.source = rewritten;
|
|
6098
|
+
return outcome;
|
|
6099
|
+
}
|
|
6100
|
+
|
|
6101
|
+
// src/roles/test/codemod/inject-type-import.ts
|
|
6102
|
+
function renderTypeImport(names, semi) {
|
|
6103
|
+
return `import type { ${names.join(", ")} } from '${VITEST_TYPE_SOURCE}'${semi ? ";" : ""}`;
|
|
6104
|
+
}
|
|
6105
|
+
function importDeclarations(program) {
|
|
6106
|
+
return (program.body ?? []).filter((node) => node.type === "ImportDeclaration");
|
|
6107
|
+
}
|
|
6108
|
+
function usesSemicolons(source, imports) {
|
|
6109
|
+
const first = imports[0];
|
|
6110
|
+
if (first === void 0) return false;
|
|
6111
|
+
return source.slice(first.start, first.end).trimEnd().endsWith(";");
|
|
6112
|
+
}
|
|
6113
|
+
function existingVitestImport(imports) {
|
|
6114
|
+
return imports.find(
|
|
6115
|
+
(node) => node.source?.value === VITEST_TYPE_SOURCE
|
|
6116
|
+
);
|
|
6117
|
+
}
|
|
6118
|
+
function alreadyImported(declaration) {
|
|
6119
|
+
return (declaration.specifiers ?? []).filter((specifier) => specifier.type === "ImportSpecifier").map((specifier) => specifier.imported?.name).filter((name) => name !== void 0);
|
|
6120
|
+
}
|
|
6121
|
+
function injectTypeImport(fileName, source, typeNames) {
|
|
6122
|
+
if (typeNames.length === 0) return source;
|
|
6123
|
+
const { program, errors } = parseSync4(fileName, source, { sourceType: "module" });
|
|
6124
|
+
if (errors.length > 0) return source;
|
|
6125
|
+
const imports = importDeclarations(program);
|
|
6126
|
+
const semi = usesSemicolons(source, imports);
|
|
6127
|
+
const existing = existingVitestImport(imports);
|
|
6128
|
+
if (existing !== void 0) {
|
|
6129
|
+
const merged = [.../* @__PURE__ */ new Set([...alreadyImported(existing), ...typeNames])].sort();
|
|
6130
|
+
if (merged.length === alreadyImported(existing).length) return source;
|
|
6131
|
+
return source.slice(0, existing.start) + renderTypeImport(merged, semi) + source.slice(existing.end);
|
|
6132
|
+
}
|
|
6133
|
+
const line = renderTypeImport([...typeNames].sort(), semi);
|
|
6134
|
+
const first = imports[0];
|
|
6135
|
+
if (first === void 0) return `${line}
|
|
6136
|
+
|
|
6137
|
+
${source}`;
|
|
6138
|
+
return source.slice(0, first.start) + line + "\n" + source.slice(first.start);
|
|
6139
|
+
}
|
|
6140
|
+
|
|
6141
|
+
// src/roles/test/codemod/named-rules.ts
|
|
6142
|
+
var ASYNC_REPLACEMENTS = {
|
|
6143
|
+
requireActual: "importActual",
|
|
6144
|
+
requireMock: "importMock"
|
|
6145
|
+
};
|
|
6146
|
+
var FUNCTIONS = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
|
|
6147
|
+
function walkWithEnclosingFunction(node, enclosing, visit) {
|
|
6148
|
+
if (node === null || typeof node !== "object") return;
|
|
6149
|
+
if (Array.isArray(node)) {
|
|
6150
|
+
for (const entry of node) walkWithEnclosingFunction(entry, enclosing, visit);
|
|
6151
|
+
return;
|
|
6152
|
+
}
|
|
6153
|
+
const current = node;
|
|
6154
|
+
const next = FUNCTIONS.has(current.type) ? current : enclosing;
|
|
6155
|
+
if (typeof current.type === "string") visit(current, enclosing);
|
|
6156
|
+
for (const value of Object.values(current)) {
|
|
6157
|
+
if (value !== null && typeof value === "object") walkWithEnclosingFunction(value, next, visit);
|
|
6158
|
+
}
|
|
6159
|
+
}
|
|
6160
|
+
function ownedCall(node) {
|
|
6161
|
+
if (node.type !== "CallExpression") return void 0;
|
|
6162
|
+
const callee = node.callee;
|
|
6163
|
+
if (callee?.type !== "MemberExpression" && callee?.type !== "StaticMemberExpression") {
|
|
6164
|
+
return void 0;
|
|
6165
|
+
}
|
|
6166
|
+
const object = callee.object;
|
|
6167
|
+
const property = callee.property;
|
|
6168
|
+
if (object?.type !== "Identifier" || object.name !== "jest") return void 0;
|
|
6169
|
+
return property?.type === "Identifier" ? property.name : void 0;
|
|
6170
|
+
}
|
|
6171
|
+
function argumentText(source, call) {
|
|
6172
|
+
const args = call.arguments ?? [];
|
|
6173
|
+
if (args.length === 0) return "";
|
|
6174
|
+
const first = args[0];
|
|
6175
|
+
const last = args[args.length - 1];
|
|
6176
|
+
return source.slice(first.start, last.end);
|
|
6177
|
+
}
|
|
6178
|
+
function applyEdits(source, edits) {
|
|
6179
|
+
let result = source;
|
|
6180
|
+
for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
|
|
6181
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
6182
|
+
}
|
|
6183
|
+
return result;
|
|
6184
|
+
}
|
|
6185
|
+
function applyNamedRules(source, program) {
|
|
6186
|
+
const edits = [];
|
|
6187
|
+
const applied = /* @__PURE__ */ new Set();
|
|
6188
|
+
const asyncify = /* @__PURE__ */ new Set();
|
|
6189
|
+
walkWithEnclosingFunction(program, void 0, (node, enclosing) => {
|
|
6190
|
+
const member = ownedCall(node);
|
|
6191
|
+
if (member === void 0) return;
|
|
6192
|
+
const asyncReplacement = ASYNC_REPLACEMENTS[member];
|
|
6193
|
+
if (asyncReplacement !== void 0) {
|
|
6194
|
+
edits.push({
|
|
6195
|
+
start: node.start,
|
|
6196
|
+
end: node.end,
|
|
6197
|
+
text: `(await vi.${asyncReplacement}(${argumentText(source, node)}))`
|
|
6198
|
+
});
|
|
6199
|
+
applied.add(`jest.${member}`);
|
|
6200
|
+
if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
|
|
6201
|
+
return;
|
|
6202
|
+
}
|
|
6203
|
+
if (member === "setTimeout") {
|
|
6204
|
+
edits.push({
|
|
6205
|
+
start: node.start,
|
|
6206
|
+
end: node.end,
|
|
6207
|
+
text: `vi.setConfig({ testTimeout: ${argumentText(source, node)} })`
|
|
6208
|
+
});
|
|
6209
|
+
applied.add("jest.setTimeout");
|
|
6210
|
+
}
|
|
6211
|
+
});
|
|
6212
|
+
for (const fn of asyncify) {
|
|
6213
|
+
edits.push({ start: fn.start, end: fn.start, text: "async " });
|
|
6214
|
+
}
|
|
6215
|
+
return { source: applyEdits(source, edits), applied: [...applied].sort() };
|
|
6216
|
+
}
|
|
6217
|
+
|
|
6218
|
+
// src/roles/test/codemod/migrate-file.ts
|
|
6219
|
+
function mockFactories(program) {
|
|
6220
|
+
const factories = [];
|
|
6221
|
+
walk(program, (node) => {
|
|
6222
|
+
if (node.type !== "CallExpression") return;
|
|
6223
|
+
const callee = node.callee;
|
|
6224
|
+
if (callee?.type !== "MemberExpression" && callee?.type !== "StaticMemberExpression") return;
|
|
6225
|
+
const object = callee.object;
|
|
6226
|
+
const property = callee.property;
|
|
6227
|
+
if (object?.type !== "Identifier") return;
|
|
6228
|
+
if (object.name !== "jest" && object.name !== "vi") return;
|
|
6229
|
+
if (property?.name !== "mock") return;
|
|
6230
|
+
const args = node.arguments ?? [];
|
|
6231
|
+
if (args.length > 1) factories.push(args[1]);
|
|
6232
|
+
});
|
|
6233
|
+
return factories;
|
|
6234
|
+
}
|
|
6235
|
+
function parse2(fileName, source) {
|
|
6236
|
+
const { program, errors } = parseSync5(fileName, source, { sourceType: "module" });
|
|
6237
|
+
return errors.length > 0 ? void 0 : program;
|
|
6238
|
+
}
|
|
6239
|
+
function migrateTestFile(fileName, source) {
|
|
6240
|
+
const untouched = {
|
|
6241
|
+
source,
|
|
6242
|
+
changed: false,
|
|
6243
|
+
renamed: 0,
|
|
6244
|
+
hoisted: [],
|
|
6245
|
+
unhandled: [],
|
|
6246
|
+
refusals: [],
|
|
6247
|
+
rulesApplied: []
|
|
6248
|
+
};
|
|
6249
|
+
const original = parse2(fileName, source);
|
|
6250
|
+
if (original === void 0) return untouched;
|
|
6251
|
+
const ruled = applyNamedRules(source, original);
|
|
6252
|
+
const renamed = renameJestToVitest(fileName, ruled.source);
|
|
6253
|
+
const program = parse2(fileName, renamed.source);
|
|
6254
|
+
if (program === void 0) return untouched;
|
|
6255
|
+
const hoist = hoistCapturedBindings(
|
|
6256
|
+
renamed.source,
|
|
6257
|
+
program,
|
|
6258
|
+
mockFactories(program),
|
|
6259
|
+
moduleBindings(program),
|
|
6260
|
+
importedNames(program)
|
|
6261
|
+
);
|
|
6262
|
+
const withImport = injectTypeImport(fileName, hoist.source, renamed.typeImports);
|
|
6263
|
+
return {
|
|
6264
|
+
source: withImport,
|
|
6265
|
+
changed: withImport !== source,
|
|
6266
|
+
renamed: renamed.rewritten,
|
|
6267
|
+
hoisted: hoist.hoisted,
|
|
6268
|
+
unhandled: renamed.unhandled,
|
|
6269
|
+
refusals: hoist.refusals,
|
|
6270
|
+
rulesApplied: ruled.applied
|
|
6271
|
+
};
|
|
6272
|
+
}
|
|
6273
|
+
|
|
6274
|
+
// src/roles/test/codemod/migrate-module.ts
|
|
6275
|
+
function read(path) {
|
|
6276
|
+
try {
|
|
6277
|
+
return readFileSync24(path, "utf8");
|
|
6278
|
+
} catch {
|
|
6279
|
+
return void 0;
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
function migrateModuleTests(cwd) {
|
|
6283
|
+
const operations = [];
|
|
6284
|
+
const blockers = [];
|
|
6285
|
+
let files = 0;
|
|
6286
|
+
let renamed = 0;
|
|
6287
|
+
let hoisted = 0;
|
|
6288
|
+
for (const relative7 of listTestFiles(cwd)) {
|
|
6289
|
+
const source = read(join30(cwd, relative7));
|
|
6290
|
+
if (source === void 0) {
|
|
6291
|
+
blockers.push({ file: relative7, reason: "this file could not be read" });
|
|
6292
|
+
continue;
|
|
6293
|
+
}
|
|
6294
|
+
const migrated = migrateTestFile(join30(cwd, relative7), source);
|
|
6295
|
+
for (const member of migrated.unhandled) {
|
|
6296
|
+
blockers.push({
|
|
6297
|
+
file: `${relative7}:${member.line}`,
|
|
6298
|
+
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}`
|
|
6299
|
+
});
|
|
6300
|
+
}
|
|
6301
|
+
for (const refusal of migrated.refusals) {
|
|
6302
|
+
blockers.push({
|
|
6303
|
+
file: relative7,
|
|
6304
|
+
reason: `\`${refusal.binding}\` cannot move into vi.hoisted: ${refusal.reason}`
|
|
6305
|
+
});
|
|
6306
|
+
}
|
|
6307
|
+
if (!migrated.changed) continue;
|
|
6308
|
+
files++;
|
|
6309
|
+
renamed += migrated.renamed;
|
|
6310
|
+
if (migrated.hoisted.length > 0) hoisted++;
|
|
6311
|
+
operations.push({ kind: "write", path: relative7, contents: migrated.source });
|
|
6312
|
+
}
|
|
6313
|
+
return blockers.length > 0 ? { operations: [], files, renamed, hoisted, blockers } : { operations, files, renamed, hoisted, blockers: [] };
|
|
6314
|
+
}
|
|
6315
|
+
|
|
6316
|
+
// src/roles/test/nest/generate-config.ts
|
|
6317
|
+
import { relative as relative6 } from "path";
|
|
6318
|
+
var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
|
|
6319
|
+
var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/nest";
|
|
6320
|
+
var NEST_TEST_CONFIG_FILE = "vitest.config.mts";
|
|
6321
|
+
function hopsToRoot(cwd, workspaceRoot) {
|
|
6322
|
+
const up = relative6(cwd, workspaceRoot).replaceAll("\\", "/");
|
|
6323
|
+
return up === "" ? "." : up;
|
|
6324
|
+
}
|
|
6325
|
+
function coverageBlock(reading) {
|
|
6326
|
+
return reading.coverageDirectory === void 0 ? "" : ` coverage: { reportsDirectory: '${reading.coverageDirectory}' },`;
|
|
6327
|
+
}
|
|
6328
|
+
function environmentBlock(reading) {
|
|
6329
|
+
return reading.testEnvironment === void 0 ? "" : ` environment: '${reading.testEnvironment}',`;
|
|
6330
|
+
}
|
|
6331
|
+
function generateNestTestConfig(cwd, reading) {
|
|
6332
|
+
if (reading.refusals.length > 0) return { refusals: reading.refusals };
|
|
6333
|
+
const workspaceRoot = findWorkspaceRoot(cwd);
|
|
6334
|
+
if (workspaceRoot === void 0) {
|
|
6335
|
+
return { refusals: ["the workspace root could not be found from this module"] };
|
|
6336
|
+
}
|
|
6337
|
+
const up = hopsToRoot(cwd, workspaceRoot);
|
|
6338
|
+
const test = [
|
|
6339
|
+
environmentBlock(reading),
|
|
6340
|
+
` setupFiles: ['${SENTINEL_TEST_SETUP}'],`,
|
|
6341
|
+
coverageBlock(reading)
|
|
6342
|
+
].filter((line) => line !== "");
|
|
6343
|
+
return {
|
|
6344
|
+
refusals: [],
|
|
6345
|
+
source: `import path from 'node:path'
|
|
6346
|
+
|
|
6347
|
+
import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'
|
|
6348
|
+
|
|
6349
|
+
export default defineConfig(
|
|
6350
|
+
nestTestConfig({
|
|
6351
|
+
root: import.meta.dirname,
|
|
6352
|
+
workspaceRoot: path.resolve(import.meta.dirname, '${up}'),
|
|
6353
|
+
overrides: {
|
|
6354
|
+
test: {
|
|
6355
|
+
${test.join("\n")}
|
|
6356
|
+
},
|
|
6357
|
+
},
|
|
6358
|
+
}),
|
|
6359
|
+
)
|
|
6360
|
+
`
|
|
6361
|
+
};
|
|
6362
|
+
}
|
|
6363
|
+
|
|
6364
|
+
// src/roles/test/nest/read-jest-config.ts
|
|
6365
|
+
import { existsSync as existsSync26, readFileSync as readFileSync25 } from "fs";
|
|
6366
|
+
import { join as join31 } from "path";
|
|
6367
|
+
var CARRIED = ["testEnvironment", "coverageDirectory", "setupFilesAfterEnv"];
|
|
6368
|
+
var DISCARDED = ["preset", "displayName", "moduleFileExtensions"];
|
|
6369
|
+
var STRING = String.raw`['"]([^'"]*)['"]`;
|
|
6370
|
+
function readString(body, key) {
|
|
6371
|
+
return new RegExp(String.raw`\b${key}\s*:\s*${STRING}`).exec(body)?.[1];
|
|
6372
|
+
}
|
|
6373
|
+
function readStringArray(body, key) {
|
|
6374
|
+
const raw = new RegExp(String.raw`\b${key}\s*:\s*\[([^\]]*)\]`).exec(body)?.[1];
|
|
6375
|
+
if (raw === void 0) return void 0;
|
|
6376
|
+
return [...raw.matchAll(new RegExp(STRING, "g"))].map((match) => match[1]);
|
|
6377
|
+
}
|
|
6378
|
+
function configBody(source) {
|
|
6379
|
+
const start = /(?:const\s+\w+(?::\s*\w+)?\s*=|export\s+default)\s*\{/.exec(source);
|
|
6380
|
+
if (start === null) return void 0;
|
|
6381
|
+
let depth = 0;
|
|
6382
|
+
const from = start.index + start[0].length - 1;
|
|
6383
|
+
for (let i = from; i < source.length; i += 1) {
|
|
6384
|
+
const char = source[i];
|
|
6385
|
+
if (char === "{") depth += 1;
|
|
6386
|
+
else if (char === "}") {
|
|
6387
|
+
depth -= 1;
|
|
6388
|
+
if (depth === 0) return source.slice(from + 1, i);
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
return void 0;
|
|
6392
|
+
}
|
|
6393
|
+
function declaredKeys(body) {
|
|
6394
|
+
const keys = [];
|
|
6395
|
+
let depth = 0;
|
|
6396
|
+
for (const line of body.split("\n")) {
|
|
6397
|
+
if (depth === 0) {
|
|
6398
|
+
const key = /^\s*([A-Za-z_$][\w$]*)\s*:/.exec(line);
|
|
6399
|
+
if (key !== null) keys.push(key[1]);
|
|
6400
|
+
}
|
|
6401
|
+
for (const char of line) {
|
|
6402
|
+
if (char === "{" || char === "[") depth += 1;
|
|
6403
|
+
else if (char === "}" || char === "]") depth -= 1;
|
|
6404
|
+
}
|
|
6405
|
+
}
|
|
6406
|
+
return keys;
|
|
6407
|
+
}
|
|
6408
|
+
function sideEffects(source) {
|
|
6409
|
+
const found = [];
|
|
6410
|
+
if (/^\s*process\.env\s*[.=]/m.test(source)) {
|
|
6411
|
+
found.push("it sets `process.env`, which the generated config would not carry");
|
|
6412
|
+
}
|
|
6413
|
+
const valueImport = /^\s*import\s+(?!type\b)[^\n]*$/m.exec(source);
|
|
6414
|
+
if (valueImport !== null && !/from\s+['"]jest['"]/.test(valueImport[0])) {
|
|
6415
|
+
found.push(`it imports a value (\`${valueImport[0].trim()}\`)`);
|
|
6416
|
+
}
|
|
6417
|
+
return found;
|
|
6418
|
+
}
|
|
6419
|
+
function readJestConfig(cwd) {
|
|
6420
|
+
const file = JEST_CONFIG_FILES.find((name) => existsSync26(join31(cwd, name)));
|
|
6421
|
+
if (file === void 0) return void 0;
|
|
6422
|
+
let source;
|
|
6423
|
+
try {
|
|
6424
|
+
source = readFileSync25(join31(cwd, file), "utf8");
|
|
6425
|
+
} catch {
|
|
6426
|
+
return { file, refusals: [`${file} could not be read`] };
|
|
6427
|
+
}
|
|
6428
|
+
const refusals = sideEffects(source);
|
|
6429
|
+
const body = configBody(source);
|
|
6430
|
+
if (body === void 0) {
|
|
6431
|
+
return { file, refusals: [...refusals, "its config object could not be located"] };
|
|
6432
|
+
}
|
|
6433
|
+
const known = /* @__PURE__ */ new Set([...CARRIED, ...DISCARDED]);
|
|
6434
|
+
for (const key of declaredKeys(body)) {
|
|
6435
|
+
if (!known.has(key)) refusals.push(`it declares \`${key}\`, which is not transposed`);
|
|
6436
|
+
}
|
|
6437
|
+
const setup = readStringArray(source, "setupFilesAfterEnv");
|
|
6438
|
+
return {
|
|
6439
|
+
file,
|
|
6440
|
+
...readString(source, "testEnvironment") !== void 0 ? { testEnvironment: readString(source, "testEnvironment") } : {},
|
|
6441
|
+
...readString(source, "coverageDirectory") !== void 0 ? { coverageDirectory: readString(source, "coverageDirectory") } : {},
|
|
6442
|
+
...setup !== void 0 ? { setupFilesAfterEnv: setup } : {},
|
|
6443
|
+
refusals
|
|
6444
|
+
};
|
|
6445
|
+
}
|
|
6446
|
+
|
|
6447
|
+
// src/roles/test/jest-migration.ts
|
|
6448
|
+
var SPEC_TSCONFIG = "tsconfig.spec.json";
|
|
6449
|
+
var VITEST_GLOBAL_TYPES = "vitest/globals";
|
|
6450
|
+
function specTypesOperation(cwd) {
|
|
6451
|
+
const path = join32(cwd, SPEC_TSCONFIG);
|
|
6452
|
+
if (!existsSync27(path)) return void 0;
|
|
6453
|
+
let config;
|
|
6454
|
+
try {
|
|
6455
|
+
config = parseJsonc(
|
|
6456
|
+
readFileSync26(path, "utf8"),
|
|
6457
|
+
SPEC_TSCONFIG
|
|
6458
|
+
);
|
|
6459
|
+
} catch {
|
|
6460
|
+
return void 0;
|
|
6461
|
+
}
|
|
6462
|
+
const types = config?.compilerOptions?.types;
|
|
6463
|
+
if (!Array.isArray(types) || !types.includes("jest")) return void 0;
|
|
6464
|
+
return {
|
|
6465
|
+
kind: "merge-json",
|
|
6466
|
+
path: SPEC_TSCONFIG,
|
|
6467
|
+
value: {
|
|
6468
|
+
compilerOptions: {
|
|
6469
|
+
types: types.map((entry) => entry === "jest" ? VITEST_GLOBAL_TYPES : entry)
|
|
6470
|
+
}
|
|
6471
|
+
}
|
|
6472
|
+
};
|
|
6473
|
+
}
|
|
6474
|
+
function siblingScriptRewrites(cwd) {
|
|
6475
|
+
const rewrites = {};
|
|
6476
|
+
for (const [name, command] of Object.entries(moduleScripts(cwd))) {
|
|
6477
|
+
if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
|
|
6478
|
+
const rewritten = rewriteVitestScript(command);
|
|
6479
|
+
if (rewritten !== command) rewrites[name] = rewritten;
|
|
6480
|
+
}
|
|
6481
|
+
return rewrites;
|
|
6482
|
+
}
|
|
6483
|
+
function blocked(reason) {
|
|
6484
|
+
return { operations: [], blocked: reason };
|
|
6485
|
+
}
|
|
6486
|
+
function planJestMigration(context, adoption) {
|
|
6487
|
+
if (adoption.jestConfigs.length > 1) {
|
|
6488
|
+
const [primary, ...rest] = adoption.jestConfigs;
|
|
6489
|
+
return blocked(
|
|
6490
|
+
`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.`
|
|
6491
|
+
);
|
|
6492
|
+
}
|
|
6493
|
+
const reading = readJestConfig(context.cwd);
|
|
6494
|
+
if (reading === void 0) {
|
|
6495
|
+
return blocked(`${adoption.jestConfigs.join(", ")} could not be read, so nothing was written.`);
|
|
6496
|
+
}
|
|
6497
|
+
const generated = generateNestTestConfig(context.cwd, reading);
|
|
6498
|
+
if (generated.source === void 0) {
|
|
6499
|
+
return blocked(
|
|
6500
|
+
`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.`
|
|
6501
|
+
);
|
|
6502
|
+
}
|
|
6503
|
+
const migration = migrateModuleTests(context.cwd);
|
|
6504
|
+
if (migration.blockers.length > 0) {
|
|
6505
|
+
const listed = migration.blockers.map((blocker) => `
|
|
6506
|
+
${blocker.file}: ${blocker.reason}`).join("");
|
|
6507
|
+
return blocked(
|
|
6508
|
+
`${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}`
|
|
6509
|
+
);
|
|
6510
|
+
}
|
|
6511
|
+
const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
|
|
6512
|
+
const siblings = siblingScriptRewrites(context.cwd);
|
|
6513
|
+
const specTypes = specTypesOperation(context.cwd);
|
|
6514
|
+
const operations = [
|
|
6515
|
+
{ kind: "write", path: NEST_TEST_CONFIG_FILE, contents: generated.source },
|
|
6516
|
+
...migration.operations,
|
|
6517
|
+
...specTypes === void 0 ? [] : [specTypes],
|
|
6518
|
+
manifestOperation(context.cwd, { [TEST_SCRIPT_NAME]: SENTINEL_TEST_COMMAND, ...siblings }),
|
|
6519
|
+
...nxTargetOperations({
|
|
6520
|
+
cwd: context.cwd,
|
|
6521
|
+
targets: testTarget({
|
|
6522
|
+
configFiles: [NEST_TEST_CONFIG_FILE],
|
|
6523
|
+
outputs: existing?.outputs ?? [],
|
|
6524
|
+
dependsOn: existing?.dependsOn ?? [],
|
|
6525
|
+
configurations: existing?.configurations ?? {}
|
|
6526
|
+
})
|
|
6527
|
+
}),
|
|
6528
|
+
...adoption.jestConfigs.map((name) => ({ kind: "delete", path: name }))
|
|
6529
|
+
];
|
|
6530
|
+
if (existing !== void 0) {
|
|
6531
|
+
operations.push({
|
|
6532
|
+
kind: "remove-json-keys",
|
|
6533
|
+
path: "project.json",
|
|
6534
|
+
keys: [["targets", TEST_SCRIPT_NAME]]
|
|
6535
|
+
});
|
|
6536
|
+
}
|
|
6537
|
+
const notes = [
|
|
6538
|
+
`${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` : ""}.`,
|
|
6539
|
+
`${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.`
|
|
6540
|
+
];
|
|
6541
|
+
if (specTypes !== void 0) {
|
|
6542
|
+
notes.push(
|
|
6543
|
+
`${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.`
|
|
6544
|
+
);
|
|
6545
|
+
}
|
|
6546
|
+
notes.push(
|
|
6547
|
+
`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.`
|
|
6548
|
+
);
|
|
6549
|
+
if (Object.keys(siblings).length > 0) {
|
|
6550
|
+
notes.push(`rewrote ${Object.keys(siblings).join(", ")} to run through sentinel.`);
|
|
6551
|
+
}
|
|
6552
|
+
return { operations, notes };
|
|
6553
|
+
}
|
|
6554
|
+
|
|
5769
6555
|
// src/roles/test/plan.ts
|
|
5770
6556
|
function ownedTestDependencies(cwd, declared) {
|
|
5771
6557
|
return declared.filter((entry) => SENTINEL_OWNED_TEST_PACKAGES.includes(entry.name)).flatMap((entry) => [
|
|
@@ -5786,10 +6572,44 @@ function plan3(context) {
|
|
|
5786
6572
|
}
|
|
5787
6573
|
const configFile = testConfigFile(context.cwd);
|
|
5788
6574
|
const adoption = readTestAdoption(context.cwd);
|
|
6575
|
+
if (adoption.state === "no-tests") {
|
|
6576
|
+
const operations2 = adoption.jestConfigs.map((name) => ({
|
|
6577
|
+
kind: "delete",
|
|
6578
|
+
path: name
|
|
6579
|
+
}));
|
|
6580
|
+
const owned2 = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
|
|
6581
|
+
if (owned2.length > 0) {
|
|
6582
|
+
operations2.push({ kind: "remove-json-keys", path: "package.json", keys: owned2 });
|
|
6583
|
+
}
|
|
6584
|
+
const hasTarget = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME] !== void 0;
|
|
6585
|
+
if (hasTarget) {
|
|
6586
|
+
operations2.push({
|
|
6587
|
+
kind: "remove-json-keys",
|
|
6588
|
+
path: "project.json",
|
|
6589
|
+
keys: [["targets", TEST_SCRIPT_NAME]]
|
|
6590
|
+
});
|
|
6591
|
+
}
|
|
6592
|
+
if (operations2.length === 0) {
|
|
6593
|
+
return {
|
|
6594
|
+
operations: [],
|
|
6595
|
+
skipped: `this module has no test and no jest leftovers, so there is nothing to do here.`
|
|
6596
|
+
};
|
|
6597
|
+
}
|
|
6598
|
+
return {
|
|
6599
|
+
operations: operations2,
|
|
6600
|
+
notes: [
|
|
6601
|
+
// Names what is actually removed, never a shape this module does not have: half of these
|
|
6602
|
+
// modules carry a config with no target behind it, and claiming otherwise would describe
|
|
6603
|
+
// an operation the plan never performs.
|
|
6604
|
+
`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.`
|
|
6605
|
+
]
|
|
6606
|
+
};
|
|
6607
|
+
}
|
|
5789
6608
|
if (configFile === void 0) {
|
|
6609
|
+
if (adoption.state === "jest") return planJestMigration(context, adoption);
|
|
5790
6610
|
return {
|
|
5791
6611
|
operations: [],
|
|
5792
|
-
skipped:
|
|
6612
|
+
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
6613
|
};
|
|
5794
6614
|
}
|
|
5795
6615
|
const operations = [];
|
|
@@ -5948,9 +6768,9 @@ function registerTest() {
|
|
|
5948
6768
|
|
|
5949
6769
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
5950
6770
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
5951
|
-
import { existsSync as
|
|
6771
|
+
import { existsSync as existsSync28, readFileSync as readFileSync28 } from "fs";
|
|
5952
6772
|
import { createRequire as createRequire4 } from "module";
|
|
5953
|
-
import { join as
|
|
6773
|
+
import { join as join34 } from "path";
|
|
5954
6774
|
|
|
5955
6775
|
// src/roles/typescript/presets/nest.json
|
|
5956
6776
|
var nest_default2 = {
|
|
@@ -6131,8 +6951,8 @@ function readTsconfigAdoption(cwd) {
|
|
|
6131
6951
|
}
|
|
6132
6952
|
|
|
6133
6953
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
6134
|
-
import { readFileSync as
|
|
6135
|
-
import { join as
|
|
6954
|
+
import { readFileSync as readFileSync27 } from "fs";
|
|
6955
|
+
import { join as join33 } from "path";
|
|
6136
6956
|
|
|
6137
6957
|
// src/roles/typescript/typecheck-script.ts
|
|
6138
6958
|
var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
|
|
@@ -6225,7 +7045,7 @@ function planAdoption(context) {
|
|
|
6225
7045
|
};
|
|
6226
7046
|
}
|
|
6227
7047
|
const existing = parseJsonc(
|
|
6228
|
-
|
|
7048
|
+
readFileSync27(join33(context.cwd, target.path), "utf8"),
|
|
6229
7049
|
target.path
|
|
6230
7050
|
);
|
|
6231
7051
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -6363,7 +7183,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6363
7183
|
let chain;
|
|
6364
7184
|
try {
|
|
6365
7185
|
const parsed = parseJsonc(
|
|
6366
|
-
|
|
7186
|
+
readFileSync28(join34(cwd, target.path), "utf8"),
|
|
6367
7187
|
target.path
|
|
6368
7188
|
);
|
|
6369
7189
|
chain = parsed.extends;
|
|
@@ -6376,7 +7196,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6376
7196
|
);
|
|
6377
7197
|
if (preset === void 0) return void 0;
|
|
6378
7198
|
try {
|
|
6379
|
-
createRequire4(
|
|
7199
|
+
createRequire4(join34(cwd, "noop.js")).resolve(preset);
|
|
6380
7200
|
return void 0;
|
|
6381
7201
|
} catch {
|
|
6382
7202
|
return preset;
|
|
@@ -6472,7 +7292,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6472
7292
|
referencedProjects(cwd, config) {
|
|
6473
7293
|
try {
|
|
6474
7294
|
const parsed = parseJsonc(
|
|
6475
|
-
|
|
7295
|
+
readFileSync28(join34(cwd, config), "utf8"),
|
|
6476
7296
|
config
|
|
6477
7297
|
);
|
|
6478
7298
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
@@ -6488,7 +7308,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6488
7308
|
* check.
|
|
6489
7309
|
*/
|
|
6490
7310
|
typecheckTarget(cwd) {
|
|
6491
|
-
if (
|
|
7311
|
+
if (existsSync28(join34(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
6492
7312
|
const target = resolveTsconfigTarget(cwd);
|
|
6493
7313
|
return target.reason === "none" ? null : target.path;
|
|
6494
7314
|
}
|
|
@@ -6693,9 +7513,9 @@ function replaceLines(current, replacements) {
|
|
|
6693
7513
|
}
|
|
6694
7514
|
|
|
6695
7515
|
// src/core/apply-plan.ts
|
|
6696
|
-
import { existsSync as
|
|
7516
|
+
import { existsSync as existsSync29, readFileSync as readFileSync29, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
6697
7517
|
import { resolve as resolve4, sep } from "path";
|
|
6698
|
-
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
7518
|
+
import { applyEdits as applyEdits2, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
6699
7519
|
|
|
6700
7520
|
// src/shared/deep-merge.ts
|
|
6701
7521
|
function isPlainObject(value) {
|
|
@@ -6712,7 +7532,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
6712
7532
|
return absolutePath;
|
|
6713
7533
|
}
|
|
6714
7534
|
function readIfExists(absolutePath) {
|
|
6715
|
-
return
|
|
7535
|
+
return existsSync29(absolutePath) ? readFileSync29(absolutePath, "utf8") : void 0;
|
|
6716
7536
|
}
|
|
6717
7537
|
function* leaves(value, prefix = []) {
|
|
6718
7538
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -6727,7 +7547,7 @@ function mergeJsonc(current, value) {
|
|
|
6727
7547
|
const edits = modify(text, path, leaf, {
|
|
6728
7548
|
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
6729
7549
|
});
|
|
6730
|
-
text =
|
|
7550
|
+
text = applyEdits2(text, edits);
|
|
6731
7551
|
}
|
|
6732
7552
|
return text.endsWith("\n") ? text : text + "\n";
|
|
6733
7553
|
}
|
|
@@ -6739,7 +7559,7 @@ function removeJsoncKeys(current, keys) {
|
|
|
6739
7559
|
const edits = modify(text, path, void 0, {
|
|
6740
7560
|
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
6741
7561
|
});
|
|
6742
|
-
text =
|
|
7562
|
+
text = applyEdits2(text, edits);
|
|
6743
7563
|
}
|
|
6744
7564
|
return text.endsWith("\n") ? text : text + "\n";
|
|
6745
7565
|
}
|
|
@@ -6830,8 +7650,8 @@ function applyPlan(cwd, plan4) {
|
|
|
6830
7650
|
}
|
|
6831
7651
|
|
|
6832
7652
|
// src/core/config/preset-evidence.ts
|
|
6833
|
-
import { existsSync as
|
|
6834
|
-
import { join as
|
|
7653
|
+
import { existsSync as existsSync30, readdirSync as readdirSync4, readFileSync as readFileSync30 } from "fs";
|
|
7654
|
+
import { join as join35 } from "path";
|
|
6835
7655
|
var PATH_SIGNALS = [
|
|
6836
7656
|
{
|
|
6837
7657
|
preset: "nest",
|
|
@@ -6844,12 +7664,42 @@ var DEPENDENCY_SIGNALS = [
|
|
|
6844
7664
|
{ preset: "svelte", pattern: /^svelte$|^@sveltejs\// },
|
|
6845
7665
|
{ preset: "nest", pattern: /^@nestjs\// }
|
|
6846
7666
|
];
|
|
7667
|
+
var IMPORT_SIGNALS = [
|
|
7668
|
+
{ preset: "nest", pattern: /@nestjs\//, evidence: 'its code imports "@nestjs/"' }
|
|
7669
|
+
];
|
|
7670
|
+
var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
|
|
7671
|
+
var NOT_SCANNED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo", "build"]);
|
|
7672
|
+
function sourceImports(cwd, pattern) {
|
|
7673
|
+
const stack = [cwd];
|
|
7674
|
+
while (stack.length > 0) {
|
|
7675
|
+
const dir = stack.pop();
|
|
7676
|
+
let entries;
|
|
7677
|
+
try {
|
|
7678
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
7679
|
+
} catch {
|
|
7680
|
+
continue;
|
|
7681
|
+
}
|
|
7682
|
+
for (const entry of entries) {
|
|
7683
|
+
if (entry.isDirectory()) {
|
|
7684
|
+
if (!NOT_SCANNED.has(entry.name)) stack.push(join35(dir, entry.name));
|
|
7685
|
+
continue;
|
|
7686
|
+
}
|
|
7687
|
+
if (!SOURCE_FILE.test(entry.name)) continue;
|
|
7688
|
+
try {
|
|
7689
|
+
if (pattern.test(readFileSync30(join35(dir, entry.name), "utf8"))) return true;
|
|
7690
|
+
} catch {
|
|
7691
|
+
continue;
|
|
7692
|
+
}
|
|
7693
|
+
}
|
|
7694
|
+
}
|
|
7695
|
+
return false;
|
|
7696
|
+
}
|
|
6847
7697
|
function dependencyNames(cwd) {
|
|
6848
|
-
const path =
|
|
6849
|
-
if (!
|
|
7698
|
+
const path = join35(cwd, "package.json");
|
|
7699
|
+
if (!existsSync30(path)) return [];
|
|
6850
7700
|
try {
|
|
6851
7701
|
const manifest = parseJsonc(
|
|
6852
|
-
|
|
7702
|
+
readFileSync30(path, "utf8"),
|
|
6853
7703
|
path
|
|
6854
7704
|
);
|
|
6855
7705
|
return [
|
|
@@ -6872,7 +7722,7 @@ function declaresJsx(cwd) {
|
|
|
6872
7722
|
for (const name of entries) {
|
|
6873
7723
|
try {
|
|
6874
7724
|
const config = parseJsonc(
|
|
6875
|
-
|
|
7725
|
+
readFileSync30(join35(cwd, name), "utf8"),
|
|
6876
7726
|
name
|
|
6877
7727
|
);
|
|
6878
7728
|
if (config.compilerOptions?.jsx !== void 0) return true;
|
|
@@ -6897,6 +7747,10 @@ function presetSignals(cwd) {
|
|
|
6897
7747
|
signals.push({ preset, evidence });
|
|
6898
7748
|
}
|
|
6899
7749
|
}
|
|
7750
|
+
for (const { preset, pattern, evidence } of IMPORT_SIGNALS) {
|
|
7751
|
+
if (signals.some((signal) => signal.preset === preset)) continue;
|
|
7752
|
+
if (sourceImports(cwd, pattern)) signals.push({ preset, evidence });
|
|
7753
|
+
}
|
|
6900
7754
|
return signals;
|
|
6901
7755
|
}
|
|
6902
7756
|
function presetContradiction(declared, cwd) {
|
|
@@ -7058,4 +7912,4 @@ export {
|
|
|
7058
7912
|
detectFramework,
|
|
7059
7913
|
dispatch
|
|
7060
7914
|
};
|
|
7061
|
-
//# sourceMappingURL=chunk-
|
|
7915
|
+
//# sourceMappingURL=chunk-GQID2U34.js.map
|