@hublo/sentinel 1.2.0-alpha.33 → 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-DJAN2UTQ.js → chunk-GQID2U34.js} +911 -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) => [
|
|
@@ -5773,13 +6559,57 @@ function ownedTestDependencies(cwd, declared) {
|
|
|
5773
6559
|
["devDependencies", entry.name]
|
|
5774
6560
|
]).filter(([section]) => section !== void 0);
|
|
5775
6561
|
}
|
|
6562
|
+
var DEFERRED = {
|
|
6563
|
+
svelte: `the svelte modules here carry no svelte test at all, so adopting them would prove nothing about svelte, and their owners have a framework migration under way that moves these versions anyway`
|
|
6564
|
+
};
|
|
5776
6565
|
function plan3(context) {
|
|
6566
|
+
const deferred = DEFERRED[context.preset];
|
|
6567
|
+
if (deferred !== void 0) {
|
|
6568
|
+
return {
|
|
6569
|
+
operations: [],
|
|
6570
|
+
skipped: `the test role defers ${context.preset} for now. It defers because ${deferred}.`
|
|
6571
|
+
};
|
|
6572
|
+
}
|
|
5777
6573
|
const configFile = testConfigFile(context.cwd);
|
|
5778
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
|
+
}
|
|
5779
6608
|
if (configFile === void 0) {
|
|
6609
|
+
if (adoption.state === "jest") return planJestMigration(context, adoption);
|
|
5780
6610
|
return {
|
|
5781
6611
|
operations: [],
|
|
5782
|
-
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.`
|
|
5783
6613
|
};
|
|
5784
6614
|
}
|
|
5785
6615
|
const operations = [];
|
|
@@ -5938,9 +6768,9 @@ function registerTest() {
|
|
|
5938
6768
|
|
|
5939
6769
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
5940
6770
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
5941
|
-
import { existsSync as
|
|
6771
|
+
import { existsSync as existsSync28, readFileSync as readFileSync28 } from "fs";
|
|
5942
6772
|
import { createRequire as createRequire4 } from "module";
|
|
5943
|
-
import { join as
|
|
6773
|
+
import { join as join34 } from "path";
|
|
5944
6774
|
|
|
5945
6775
|
// src/roles/typescript/presets/nest.json
|
|
5946
6776
|
var nest_default2 = {
|
|
@@ -6121,8 +6951,8 @@ function readTsconfigAdoption(cwd) {
|
|
|
6121
6951
|
}
|
|
6122
6952
|
|
|
6123
6953
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
6124
|
-
import { readFileSync as
|
|
6125
|
-
import { join as
|
|
6954
|
+
import { readFileSync as readFileSync27 } from "fs";
|
|
6955
|
+
import { join as join33 } from "path";
|
|
6126
6956
|
|
|
6127
6957
|
// src/roles/typescript/typecheck-script.ts
|
|
6128
6958
|
var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
|
|
@@ -6215,7 +7045,7 @@ function planAdoption(context) {
|
|
|
6215
7045
|
};
|
|
6216
7046
|
}
|
|
6217
7047
|
const existing = parseJsonc(
|
|
6218
|
-
|
|
7048
|
+
readFileSync27(join33(context.cwd, target.path), "utf8"),
|
|
6219
7049
|
target.path
|
|
6220
7050
|
);
|
|
6221
7051
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -6353,7 +7183,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6353
7183
|
let chain;
|
|
6354
7184
|
try {
|
|
6355
7185
|
const parsed = parseJsonc(
|
|
6356
|
-
|
|
7186
|
+
readFileSync28(join34(cwd, target.path), "utf8"),
|
|
6357
7187
|
target.path
|
|
6358
7188
|
);
|
|
6359
7189
|
chain = parsed.extends;
|
|
@@ -6366,7 +7196,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6366
7196
|
);
|
|
6367
7197
|
if (preset === void 0) return void 0;
|
|
6368
7198
|
try {
|
|
6369
|
-
createRequire4(
|
|
7199
|
+
createRequire4(join34(cwd, "noop.js")).resolve(preset);
|
|
6370
7200
|
return void 0;
|
|
6371
7201
|
} catch {
|
|
6372
7202
|
return preset;
|
|
@@ -6462,7 +7292,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6462
7292
|
referencedProjects(cwd, config) {
|
|
6463
7293
|
try {
|
|
6464
7294
|
const parsed = parseJsonc(
|
|
6465
|
-
|
|
7295
|
+
readFileSync28(join34(cwd, config), "utf8"),
|
|
6466
7296
|
config
|
|
6467
7297
|
);
|
|
6468
7298
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
@@ -6478,7 +7308,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6478
7308
|
* check.
|
|
6479
7309
|
*/
|
|
6480
7310
|
typecheckTarget(cwd) {
|
|
6481
|
-
if (
|
|
7311
|
+
if (existsSync28(join34(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
6482
7312
|
const target = resolveTsconfigTarget(cwd);
|
|
6483
7313
|
return target.reason === "none" ? null : target.path;
|
|
6484
7314
|
}
|
|
@@ -6683,9 +7513,9 @@ function replaceLines(current, replacements) {
|
|
|
6683
7513
|
}
|
|
6684
7514
|
|
|
6685
7515
|
// src/core/apply-plan.ts
|
|
6686
|
-
import { existsSync as
|
|
7516
|
+
import { existsSync as existsSync29, readFileSync as readFileSync29, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
6687
7517
|
import { resolve as resolve4, sep } from "path";
|
|
6688
|
-
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
7518
|
+
import { applyEdits as applyEdits2, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
6689
7519
|
|
|
6690
7520
|
// src/shared/deep-merge.ts
|
|
6691
7521
|
function isPlainObject(value) {
|
|
@@ -6702,7 +7532,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
6702
7532
|
return absolutePath;
|
|
6703
7533
|
}
|
|
6704
7534
|
function readIfExists(absolutePath) {
|
|
6705
|
-
return
|
|
7535
|
+
return existsSync29(absolutePath) ? readFileSync29(absolutePath, "utf8") : void 0;
|
|
6706
7536
|
}
|
|
6707
7537
|
function* leaves(value, prefix = []) {
|
|
6708
7538
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -6717,7 +7547,7 @@ function mergeJsonc(current, value) {
|
|
|
6717
7547
|
const edits = modify(text, path, leaf, {
|
|
6718
7548
|
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
6719
7549
|
});
|
|
6720
|
-
text =
|
|
7550
|
+
text = applyEdits2(text, edits);
|
|
6721
7551
|
}
|
|
6722
7552
|
return text.endsWith("\n") ? text : text + "\n";
|
|
6723
7553
|
}
|
|
@@ -6729,7 +7559,7 @@ function removeJsoncKeys(current, keys) {
|
|
|
6729
7559
|
const edits = modify(text, path, void 0, {
|
|
6730
7560
|
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
6731
7561
|
});
|
|
6732
|
-
text =
|
|
7562
|
+
text = applyEdits2(text, edits);
|
|
6733
7563
|
}
|
|
6734
7564
|
return text.endsWith("\n") ? text : text + "\n";
|
|
6735
7565
|
}
|
|
@@ -6820,8 +7650,8 @@ function applyPlan(cwd, plan4) {
|
|
|
6820
7650
|
}
|
|
6821
7651
|
|
|
6822
7652
|
// src/core/config/preset-evidence.ts
|
|
6823
|
-
import { existsSync as
|
|
6824
|
-
import { join as
|
|
7653
|
+
import { existsSync as existsSync30, readdirSync as readdirSync4, readFileSync as readFileSync30 } from "fs";
|
|
7654
|
+
import { join as join35 } from "path";
|
|
6825
7655
|
var PATH_SIGNALS = [
|
|
6826
7656
|
{
|
|
6827
7657
|
preset: "nest",
|
|
@@ -6834,12 +7664,42 @@ var DEPENDENCY_SIGNALS = [
|
|
|
6834
7664
|
{ preset: "svelte", pattern: /^svelte$|^@sveltejs\// },
|
|
6835
7665
|
{ preset: "nest", pattern: /^@nestjs\// }
|
|
6836
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
|
+
}
|
|
6837
7697
|
function dependencyNames(cwd) {
|
|
6838
|
-
const path =
|
|
6839
|
-
if (!
|
|
7698
|
+
const path = join35(cwd, "package.json");
|
|
7699
|
+
if (!existsSync30(path)) return [];
|
|
6840
7700
|
try {
|
|
6841
7701
|
const manifest = parseJsonc(
|
|
6842
|
-
|
|
7702
|
+
readFileSync30(path, "utf8"),
|
|
6843
7703
|
path
|
|
6844
7704
|
);
|
|
6845
7705
|
return [
|
|
@@ -6862,7 +7722,7 @@ function declaresJsx(cwd) {
|
|
|
6862
7722
|
for (const name of entries) {
|
|
6863
7723
|
try {
|
|
6864
7724
|
const config = parseJsonc(
|
|
6865
|
-
|
|
7725
|
+
readFileSync30(join35(cwd, name), "utf8"),
|
|
6866
7726
|
name
|
|
6867
7727
|
);
|
|
6868
7728
|
if (config.compilerOptions?.jsx !== void 0) return true;
|
|
@@ -6887,6 +7747,10 @@ function presetSignals(cwd) {
|
|
|
6887
7747
|
signals.push({ preset, evidence });
|
|
6888
7748
|
}
|
|
6889
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
|
+
}
|
|
6890
7754
|
return signals;
|
|
6891
7755
|
}
|
|
6892
7756
|
function presetContradiction(declared, cwd) {
|
|
@@ -6957,6 +7821,12 @@ async function dispatch(opts) {
|
|
|
6957
7821
|
}
|
|
6958
7822
|
const detected = resolveFlavour(opts);
|
|
6959
7823
|
const resolutionPreset = opts.preset ?? declaredPresetFor(opts.target, opts.cwd) ?? detected;
|
|
7824
|
+
if (resolutionPreset === "tools") {
|
|
7825
|
+
const message = `tools modules are not adopted one at a time: they sometimes hold configuration shared with the workspace root, so they move WITH the root, as one reviewed change owned by the tooling team rather than by the module's team.`;
|
|
7826
|
+
process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${message}`)}
|
|
7827
|
+
`);
|
|
7828
|
+
return 0;
|
|
7829
|
+
}
|
|
6960
7830
|
const adapter = resolve(opts.target, resolutionPreset, opts.runner);
|
|
6961
7831
|
const preset = opts.preset ?? adapter.declaredPreset?.(opts.cwd) ?? detected;
|
|
6962
7832
|
if (opts.preset !== void 0) {
|
|
@@ -7042,4 +7912,4 @@ export {
|
|
|
7042
7912
|
detectFramework,
|
|
7043
7913
|
dispatch
|
|
7044
7914
|
};
|
|
7045
|
-
//# sourceMappingURL=chunk-
|
|
7915
|
+
//# sourceMappingURL=chunk-GQID2U34.js.map
|