@hublo/sentinel 1.4.0-alpha.31 → 1.4.0-alpha.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/bin/sentinel.js +3 -3
- package/dist/{chunk-QYIF3HXC.js → chunk-63NQP75B.js} +1 -1
- package/dist/{chunk-XM65P73B.js → chunk-UBXL567Z.js} +355 -119
- package/dist/index.js +2 -2
- package/dist/{validate-YRLGMILJ.js → validate-JUNITP37.js} +1 -1
- package/docs/test-adoption.md +27 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ A large monorepo accumulates:
|
|
|
24
24
|
- **One source of truth for config** — every project just `extends @hublo/sentinel/...`; the actual rules live in one versioned place. Change a rule once, everyone gets it on the next version bump.
|
|
25
25
|
- **One source of truth for tooling dependencies** — a project depends on `@hublo/sentinel`, not on a scattered pile of eslint / vitest / plugin devDeps. Bump one version and the whole toolchain moves, atomically, tested in isolation first.
|
|
26
26
|
- **`--init` sets a module up** — the one command generates the stubs the first time (**adopt**), regenerates them after a change like a runner swap (**refresh**), and applies the workspace prep the module needs. Run it module by module to roll out gradually. (`--migrate`, for changing an already-initialized setup, is a reserved future verb.)
|
|
27
|
+
- **`--validate` proves the move did not change anything** — a verb of its own, because what separates it from `--init` is what SURVIVES the run: `--init` and `--migrate` write state that REMAINS, `--validate` may instrument the module it is proving provided nothing it writes stays. Run before a migration it records what the module does today; run after, it compares and refuses a run that lost a test or invented one. It composes with every type, and answers "not available yet" for the ones without a proof rather than calling a valid command wrong.
|
|
27
28
|
- **Move one module at a time** — installed per module, so you adopt at your pace; a module can adopt sentinel while its neighbour keeps the old setup. No big-bang.
|
|
28
29
|
- **Swap tools without touching projects** — change eslint → biome (or benchmark them) in one place; `--init` regenerates the stubs.
|
|
29
30
|
- **No silent drift** — the guard keeps every project's config converged on the source of truth.
|
package/dist/bin/sentinel.js
CHANGED
|
@@ -15,10 +15,10 @@ import {
|
|
|
15
15
|
palette,
|
|
16
16
|
registerAdapters,
|
|
17
17
|
resolve
|
|
18
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-63NQP75B.js";
|
|
19
19
|
import {
|
|
20
20
|
resolveContext
|
|
21
|
-
} from "../chunk-
|
|
21
|
+
} from "../chunk-UBXL567Z.js";
|
|
22
22
|
import "../chunk-O7REVMOC.js";
|
|
23
23
|
import {
|
|
24
24
|
ensureWorkspacePrep,
|
|
@@ -144,7 +144,7 @@ async function runValidate(ctx) {
|
|
|
144
144
|
);
|
|
145
145
|
}
|
|
146
146
|
if (!ctx.targets.includes("test")) return 1;
|
|
147
|
-
const { validateModuleTests } = await import("../validate-
|
|
147
|
+
const { validateModuleTests } = await import("../validate-JUNITP37.js");
|
|
148
148
|
const outcomes = modules.flatMap(
|
|
149
149
|
(module) => validateModuleTests(module.name, module.root)
|
|
150
150
|
);
|
|
@@ -723,7 +723,7 @@ function resolveVitest(cwd) {
|
|
|
723
723
|
|
|
724
724
|
// src/roles/test/adapters/vitest/vitest.adapter.ts
|
|
725
725
|
import { spawnSync } from "node:child_process";
|
|
726
|
-
import { isAbsolute as isAbsolute2, resolve as
|
|
726
|
+
import { isAbsolute as isAbsolute2, resolve as resolve8 } from "node:path";
|
|
727
727
|
|
|
728
728
|
// src/core/config/tool-args.ts
|
|
729
729
|
import { existsSync as existsSync6 } from "node:fs";
|
|
@@ -873,9 +873,9 @@ function rewriteToolchainImports(toolchain, source, fileName) {
|
|
|
873
873
|
}
|
|
874
874
|
|
|
875
875
|
// src/roles/test/jest-migration.ts
|
|
876
|
-
import { existsSync as
|
|
876
|
+
import { existsSync as existsSync19, readFileSync as readFileSync18, readdirSync as readdirSync6 } from "node:fs";
|
|
877
877
|
import { createRequire as createRequire5 } from "node:module";
|
|
878
|
-
import { dirname as
|
|
878
|
+
import { dirname as dirname11, join as join23, relative as relative5 } from "node:path";
|
|
879
879
|
|
|
880
880
|
// src/shared/jsonc.ts
|
|
881
881
|
import { parse as parse2, printParseErrorCode } from "jsonc-parser";
|
|
@@ -890,9 +890,9 @@ function parseJsonc(text, source = "config") {
|
|
|
890
890
|
}
|
|
891
891
|
|
|
892
892
|
// src/roles/test/codemod/migrate-module.ts
|
|
893
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
894
|
-
import { join as
|
|
895
|
-
import { parseSync as
|
|
893
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
|
|
894
|
+
import { join as join16 } from "node:path";
|
|
895
|
+
import { parseSync as parseSync8 } from "oxc-parser";
|
|
896
896
|
|
|
897
897
|
// src/roles/test/msw-lifecycle.ts
|
|
898
898
|
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
|
|
@@ -1121,8 +1121,8 @@ ${source}`;
|
|
|
1121
1121
|
}
|
|
1122
1122
|
|
|
1123
1123
|
// src/roles/test/codemod/migrate-file.ts
|
|
1124
|
-
import { dirname as
|
|
1125
|
-
import { parseSync as
|
|
1124
|
+
import { dirname as dirname7 } from "node:path";
|
|
1125
|
+
import { parseSync as parseSync7 } from "oxc-parser";
|
|
1126
1126
|
|
|
1127
1127
|
// src/roles/test/codemod/assertion-return-value.ts
|
|
1128
1128
|
var MEMBER_TYPES = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
@@ -1295,6 +1295,20 @@ function assertedMockedClass(source, node) {
|
|
|
1295
1295
|
if (object === void 0) return false;
|
|
1296
1296
|
return /\bMockedClass\b/.test(source.slice(object.start, object.end));
|
|
1297
1297
|
}
|
|
1298
|
+
function classValuedProperties(factories) {
|
|
1299
|
+
const found = [];
|
|
1300
|
+
for (const factory of factories) {
|
|
1301
|
+
walk(factory, (node) => {
|
|
1302
|
+
if (node.type !== "Property" && node.type !== "ObjectProperty") return;
|
|
1303
|
+
const key = node.key;
|
|
1304
|
+
const name = key?.type === "Identifier" ? key.name : key?.type === "StringLiteral" || key?.type === "Literal" ? String(key.value) : void 0;
|
|
1305
|
+
if (name === void 0 || !/^[A-Z]/.test(name)) return;
|
|
1306
|
+
const value = node.value;
|
|
1307
|
+
if (value !== void 0) found.push(value);
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
return found;
|
|
1311
|
+
}
|
|
1298
1312
|
function readsThis(node) {
|
|
1299
1313
|
let found = false;
|
|
1300
1314
|
walk(node, (inner) => {
|
|
@@ -1307,7 +1321,7 @@ function rewriteConstructibleMocks(source, program, factories) {
|
|
|
1307
1321
|
const refusals = [];
|
|
1308
1322
|
let rewritten = 0;
|
|
1309
1323
|
const seen = /* @__PURE__ */ new Set();
|
|
1310
|
-
const subtrees = [...factories];
|
|
1324
|
+
const subtrees = [...classValuedProperties(factories)];
|
|
1311
1325
|
walk(program, (node) => {
|
|
1312
1326
|
if (assertedMockedClass(source, node)) subtrees.push(node);
|
|
1313
1327
|
});
|
|
@@ -1867,9 +1881,165 @@ function rewriteHookReturnValue(source, program) {
|
|
|
1867
1881
|
}
|
|
1868
1882
|
|
|
1869
1883
|
// src/roles/test/codemod/named-rules.ts
|
|
1870
|
-
import { existsSync as
|
|
1884
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
|
|
1871
1885
|
import { createRequire as createRequire2 } from "node:module";
|
|
1872
|
-
import { dirname as
|
|
1886
|
+
import { dirname as dirname3, join as join10 } from "node:path";
|
|
1887
|
+
|
|
1888
|
+
// src/roles/test/codemod/static-import-for-require.ts
|
|
1889
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, statSync } from "node:fs";
|
|
1890
|
+
import { dirname as dirname2, join as join9, resolve as resolve3 } from "node:path";
|
|
1891
|
+
import { parseSync as parseSync4 } from "oxc-parser";
|
|
1892
|
+
var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
1893
|
+
function resolveRelative(fromFile, specifier) {
|
|
1894
|
+
const base = resolve3(dirname2(fromFile), specifier);
|
|
1895
|
+
const candidates = [
|
|
1896
|
+
base,
|
|
1897
|
+
...EXTENSIONS.map((extension) => `${base}${extension}`),
|
|
1898
|
+
...EXTENSIONS.map((extension) => join9(base, `index${extension}`))
|
|
1899
|
+
];
|
|
1900
|
+
for (const candidate of candidates) {
|
|
1901
|
+
try {
|
|
1902
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
1903
|
+
} catch {
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
return void 0;
|
|
1907
|
+
}
|
|
1908
|
+
function relativeSpecifiers(source) {
|
|
1909
|
+
const found = [];
|
|
1910
|
+
const patterns = [
|
|
1911
|
+
/\bfrom\s+['"](\.[^'"]*)['"]/g,
|
|
1912
|
+
/\bimport\s+['"](\.[^'"]*)['"]/g,
|
|
1913
|
+
/\bimport\s*\(\s*['"](\.[^'"]*)['"]\s*\)/g,
|
|
1914
|
+
/\brequire\s*\(\s*['"](\.[^'"]*)['"]\s*\)/g
|
|
1915
|
+
];
|
|
1916
|
+
for (const pattern of patterns) {
|
|
1917
|
+
for (const match of source.matchAll(pattern)) found.push(match[1]);
|
|
1918
|
+
}
|
|
1919
|
+
return found;
|
|
1920
|
+
}
|
|
1921
|
+
function reachesBack(fromFile, specifier) {
|
|
1922
|
+
const entry = resolveRelative(fromFile, specifier);
|
|
1923
|
+
if (entry === void 0) return true;
|
|
1924
|
+
const target = resolve3(fromFile);
|
|
1925
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1926
|
+
const stack = [entry];
|
|
1927
|
+
while (stack.length > 0 && seen.size < 200) {
|
|
1928
|
+
const file = stack.pop();
|
|
1929
|
+
if (seen.has(file)) continue;
|
|
1930
|
+
seen.add(file);
|
|
1931
|
+
if (resolve3(file) === target) return true;
|
|
1932
|
+
let source;
|
|
1933
|
+
try {
|
|
1934
|
+
source = readFileSync7(file, "utf8");
|
|
1935
|
+
} catch {
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
for (const relative6 of relativeSpecifiers(source)) {
|
|
1939
|
+
const next = resolveRelative(file, relative6);
|
|
1940
|
+
if (next !== void 0) stack.push(next);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
return seen.size >= 200;
|
|
1944
|
+
}
|
|
1945
|
+
function namedRequireDeclarations(program) {
|
|
1946
|
+
const found = /* @__PURE__ */ new Map();
|
|
1947
|
+
walk(program, (node) => {
|
|
1948
|
+
if (node.type !== "VariableDeclaration") return;
|
|
1949
|
+
const declarators = node.declarations ?? [];
|
|
1950
|
+
if (declarators.length !== 1) return;
|
|
1951
|
+
const declarator = declarators[0];
|
|
1952
|
+
const init = declarator.init;
|
|
1953
|
+
if (init?.type !== "CallExpression") return;
|
|
1954
|
+
const callee = init.callee;
|
|
1955
|
+
if (callee?.type !== "Identifier" || callee.name !== "require") return;
|
|
1956
|
+
const [argument] = init.arguments ?? [];
|
|
1957
|
+
if (argument?.type !== "StringLiteral" && argument?.type !== "Literal") return;
|
|
1958
|
+
const specifier = String(argument.value);
|
|
1959
|
+
if (!specifier.startsWith(".")) return;
|
|
1960
|
+
const id = declarator.id;
|
|
1961
|
+
if (id?.type !== "ObjectPattern") return;
|
|
1962
|
+
const names = [];
|
|
1963
|
+
for (const property of id.properties ?? []) {
|
|
1964
|
+
if (property.type !== "Property" && property.type !== "ObjectProperty") return;
|
|
1965
|
+
if (property.computed === true || property.shorthand !== true) return;
|
|
1966
|
+
const key = property.key;
|
|
1967
|
+
const value = property.value;
|
|
1968
|
+
if (key?.type !== "Identifier" || value?.type !== "Identifier") return;
|
|
1969
|
+
if (key.name !== value.name) return;
|
|
1970
|
+
names.push(key.name);
|
|
1971
|
+
}
|
|
1972
|
+
if (names.length === 0) return;
|
|
1973
|
+
found.set(init, { names, specifier, start: node.start, end: node.end });
|
|
1974
|
+
});
|
|
1975
|
+
return found;
|
|
1976
|
+
}
|
|
1977
|
+
function topLevelNames(program) {
|
|
1978
|
+
const names = /* @__PURE__ */ new Set();
|
|
1979
|
+
for (const node of program.body ?? []) {
|
|
1980
|
+
if (node.type === "ImportDeclaration") {
|
|
1981
|
+
for (const specifier of node.specifiers ?? []) {
|
|
1982
|
+
const local = specifier.local;
|
|
1983
|
+
if (local?.type === "Identifier") names.add(local.name);
|
|
1984
|
+
}
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
if (node.type === "VariableDeclaration") {
|
|
1988
|
+
for (const declarator of node.declarations ?? []) {
|
|
1989
|
+
const id = declarator.id;
|
|
1990
|
+
if (id?.type === "Identifier") names.add(id.name);
|
|
1991
|
+
}
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
if ((node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") && node.id?.type === "Identifier") {
|
|
1995
|
+
names.add(node.id.name);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
return names;
|
|
1999
|
+
}
|
|
2000
|
+
function withLeadingComments(source, comments, start) {
|
|
2001
|
+
let from = start;
|
|
2002
|
+
for (; ; ) {
|
|
2003
|
+
const before = source.slice(0, from);
|
|
2004
|
+
const blank = before.length - before.trimEnd().length;
|
|
2005
|
+
const candidate = comments.find((comment) => comment.end === from - blank);
|
|
2006
|
+
if (candidate === void 0) break;
|
|
2007
|
+
const lineStart = source.lastIndexOf("\n", candidate.start) + 1;
|
|
2008
|
+
if (source.slice(lineStart, candidate.start).trim() !== "") break;
|
|
2009
|
+
from = lineStart;
|
|
2010
|
+
}
|
|
2011
|
+
return from;
|
|
2012
|
+
}
|
|
2013
|
+
function planStaticImport(fileName, source, program, requireCall2, semi) {
|
|
2014
|
+
const declaration = namedRequireDeclarations(program).get(requireCall2);
|
|
2015
|
+
if (declaration === void 0) return void 0;
|
|
2016
|
+
const bound = topLevelNames(program);
|
|
2017
|
+
if (declaration.names.some((name) => bound.has(name))) return void 0;
|
|
2018
|
+
if (reachesBack(fileName, declaration.specifier)) return void 0;
|
|
2019
|
+
const { comments } = parseSync4(fileName, source, { sourceType: "module" });
|
|
2020
|
+
const lineStart = source.lastIndexOf("\n", declaration.start - 1) + 1;
|
|
2021
|
+
const ownsItsLine = source.slice(lineStart, declaration.start).trim() === "";
|
|
2022
|
+
const start = withLeadingComments(
|
|
2023
|
+
source,
|
|
2024
|
+
comments,
|
|
2025
|
+
ownsItsLine ? lineStart : declaration.start
|
|
2026
|
+
);
|
|
2027
|
+
return {
|
|
2028
|
+
line: `import { ${declaration.names.join(", ")} } from '${declaration.specifier}'${semi ? ";" : ""}`,
|
|
2029
|
+
// To the end of the line, so the statement does not leave an empty one behind.
|
|
2030
|
+
remove: { start, end: endOfLine(source, declaration.end) }
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
function endOfLine(source, end) {
|
|
2034
|
+
let at = end;
|
|
2035
|
+
if (source[at] === ";") at += 1;
|
|
2036
|
+
while (source[at] === " " || source[at] === " ") at += 1;
|
|
2037
|
+
if (source[at] === "\r") at += 1;
|
|
2038
|
+
if (source[at] === "\n") at += 1;
|
|
2039
|
+
return at;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// src/roles/test/codemod/named-rules.ts
|
|
1873
2043
|
function isCommonJsPackage(fromFile, specifier) {
|
|
1874
2044
|
if (specifier.startsWith(".") || specifier.startsWith("/")) return false;
|
|
1875
2045
|
let resolved;
|
|
@@ -1881,23 +2051,27 @@ function isCommonJsPackage(fromFile, specifier) {
|
|
|
1881
2051
|
if (!resolved.includes(`${"/"}node_modules${"/"}`)) return false;
|
|
1882
2052
|
if (resolved.endsWith(".mjs")) return false;
|
|
1883
2053
|
if (resolved.endsWith(".cjs")) return true;
|
|
1884
|
-
let dir =
|
|
2054
|
+
let dir = dirname3(resolved);
|
|
1885
2055
|
for (let depth = 0; depth < 12; depth += 1) {
|
|
1886
|
-
const manifest =
|
|
1887
|
-
if (
|
|
2056
|
+
const manifest = join10(dir, "package.json");
|
|
2057
|
+
if (existsSync11(manifest)) {
|
|
1888
2058
|
try {
|
|
1889
|
-
return JSON.parse(
|
|
2059
|
+
return JSON.parse(readFileSync8(manifest, "utf8")).type !== "module";
|
|
1890
2060
|
} catch {
|
|
1891
2061
|
return false;
|
|
1892
2062
|
}
|
|
1893
2063
|
}
|
|
1894
|
-
const parent =
|
|
2064
|
+
const parent = dirname3(dir);
|
|
1895
2065
|
if (parent === dir) break;
|
|
1896
2066
|
dir = parent;
|
|
1897
2067
|
}
|
|
1898
2068
|
return false;
|
|
1899
2069
|
}
|
|
1900
2070
|
var MEMBER_TYPES9 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
2071
|
+
function fileUsesSemicolons(source) {
|
|
2072
|
+
const first = /^import\s[^\n]*$/m.exec(source);
|
|
2073
|
+
return first !== null && first[0].trimEnd().endsWith(";");
|
|
2074
|
+
}
|
|
1901
2075
|
var ASYNC_REPLACEMENTS = {
|
|
1902
2076
|
requireActual: "importActual",
|
|
1903
2077
|
requireMock: "importMock"
|
|
@@ -1920,6 +2094,16 @@ function mockedSpecifiers(program) {
|
|
|
1920
2094
|
});
|
|
1921
2095
|
return found;
|
|
1922
2096
|
}
|
|
2097
|
+
function methodValues(program) {
|
|
2098
|
+
const found = /* @__PURE__ */ new Set();
|
|
2099
|
+
walk(program, (node) => {
|
|
2100
|
+
const isMethod = node.type === "MethodDefinition" || node.type === "ClassMethod" || node.type === "ObjectMethod" || (node.type === "Property" || node.type === "ObjectProperty") && (node.kind === "get" || node.kind === "set" || node.method === true);
|
|
2101
|
+
if (!isMethod) return;
|
|
2102
|
+
const value = node.value ?? node.body;
|
|
2103
|
+
if (value !== void 0) found.add(value);
|
|
2104
|
+
});
|
|
2105
|
+
return found;
|
|
2106
|
+
}
|
|
1923
2107
|
var FUNCTIONS = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
|
|
1924
2108
|
function walkWithEnclosingFunction(node, enclosing, visit) {
|
|
1925
2109
|
if (node === null || typeof node !== "object") return;
|
|
@@ -2002,7 +2186,25 @@ function applyNamedRules(source, program, fileName = "") {
|
|
|
2002
2186
|
const edits = [];
|
|
2003
2187
|
const applied = /* @__PURE__ */ new Set();
|
|
2004
2188
|
const asyncify = /* @__PURE__ */ new Set();
|
|
2189
|
+
const refusals = [];
|
|
2190
|
+
const staticImports = [];
|
|
2005
2191
|
const mocked = mockedSpecifiers(program);
|
|
2192
|
+
const methods = methodValues(program);
|
|
2193
|
+
const cannotAwaitHere = (enclosing, what, call) => {
|
|
2194
|
+
if (enclosing === void 0 || !methods.has(enclosing)) return false;
|
|
2195
|
+
const plan2 = planStaticImport(fileName, source, program, call, fileUsesSemicolons(source));
|
|
2196
|
+
if (plan2 !== void 0) {
|
|
2197
|
+
edits.push({ start: plan2.remove.start, end: plan2.remove.end, text: "" });
|
|
2198
|
+
staticImports.push(plan2.line);
|
|
2199
|
+
applied.add(`${what} as a static import`);
|
|
2200
|
+
return true;
|
|
2201
|
+
}
|
|
2202
|
+
refusals.push({
|
|
2203
|
+
binding: what,
|
|
2204
|
+
reason: `\`${what}\` sits inside a class or object METHOD, which cannot become async here: \`async get\` is not valid, and making a method that returns void return a promise changes what every caller gets. Left as it was. Under Vitest \`require\` is Node's own resolver, which knows nothing of \`.ts\`, so this one needs a person: a static import, or the dynamic one kept on purpose.`
|
|
2205
|
+
});
|
|
2206
|
+
return true;
|
|
2207
|
+
};
|
|
2006
2208
|
const wholeStatement = /* @__PURE__ */ new Map();
|
|
2007
2209
|
walk(program, (node) => {
|
|
2008
2210
|
if (node.type !== "ExpressionStatement") return;
|
|
@@ -2070,6 +2272,7 @@ function applyNamedRules(source, program, fileName = "") {
|
|
|
2070
2272
|
const [specifier] = node.arguments ?? [];
|
|
2071
2273
|
const literal = specifier?.type === "StringLiteral" || specifier?.type === "Literal" ? String(specifier.value) : void 0;
|
|
2072
2274
|
if (literal !== void 0 && mocked.has(literal)) {
|
|
2275
|
+
if (cannotAwaitHere(enclosing, "jest.requireMock", node)) return;
|
|
2073
2276
|
edits.push({
|
|
2074
2277
|
start: node.start,
|
|
2075
2278
|
end: node.end,
|
|
@@ -2082,6 +2285,7 @@ function applyNamedRules(source, program, fileName = "") {
|
|
|
2082
2285
|
}
|
|
2083
2286
|
const asyncReplacement = member === void 0 ? void 0 : ASYNC_REPLACEMENTS[member];
|
|
2084
2287
|
if (asyncReplacement !== void 0) {
|
|
2288
|
+
if (cannotAwaitHere(enclosing, `jest.${member}`, node)) return;
|
|
2085
2289
|
edits.push({
|
|
2086
2290
|
start: node.start,
|
|
2087
2291
|
end: node.end,
|
|
@@ -2095,6 +2299,7 @@ function applyNamedRules(source, program, fileName = "") {
|
|
|
2095
2299
|
return;
|
|
2096
2300
|
}
|
|
2097
2301
|
if (bareRequire(node)) {
|
|
2302
|
+
if (cannotAwaitHere(enclosing, "require", node)) return;
|
|
2098
2303
|
const [specifier] = node.arguments ?? [];
|
|
2099
2304
|
const literal = specifier?.type === "StringLiteral" || specifier?.type === "Literal" ? String(specifier.value) : void 0;
|
|
2100
2305
|
const interop = literal !== void 0 && !readsANameOffIt.has(node) && !handedToTheRunnerAsAModule.has(node) && !wholeStatement.has(node) && isCommonJsPackage(fileName, literal) ? ".default" : "";
|
|
@@ -2129,7 +2334,27 @@ function applyNamedRules(source, program, fileName = "") {
|
|
|
2129
2334
|
for (const fn of asyncify) {
|
|
2130
2335
|
edits.push({ start: fn.start, end: fn.start, text: "async " });
|
|
2131
2336
|
}
|
|
2132
|
-
|
|
2337
|
+
if (staticImports.length > 0) {
|
|
2338
|
+
const first = (program.body ?? []).find(
|
|
2339
|
+
(node) => node.type === "ImportDeclaration"
|
|
2340
|
+
);
|
|
2341
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
2342
|
+
for (const line of staticImports) {
|
|
2343
|
+
const parsed = /^import \{ (.+) \} from '(.+)'(;?)$/.exec(line);
|
|
2344
|
+
if (parsed === null) continue;
|
|
2345
|
+
const names = byModule.get(parsed[2]) ?? /* @__PURE__ */ new Set();
|
|
2346
|
+
for (const name of parsed[1].split(", ")) names.add(name);
|
|
2347
|
+
byModule.set(parsed[2], names);
|
|
2348
|
+
}
|
|
2349
|
+
const semi = staticImports[0]?.endsWith(";") === true ? ";" : "";
|
|
2350
|
+
const block = `${[...byModule].map(([specifier, names]) => `import { ${[...names].join(", ")} } from '${specifier}'${semi}`).join("\n")}
|
|
2351
|
+
`;
|
|
2352
|
+
edits.push(
|
|
2353
|
+
first === void 0 ? { start: 0, end: 0, text: `${block}
|
|
2354
|
+
` } : { start: first.start, end: first.start, text: block }
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
return { source: applyEdits(source, edits), applied: [...applied].sort(), refusals };
|
|
2133
2358
|
}
|
|
2134
2359
|
|
|
2135
2360
|
// src/roles/test/codemod/isolate-modules.ts
|
|
@@ -2525,7 +2750,7 @@ function annotateMockedBindings(source, program) {
|
|
|
2525
2750
|
}
|
|
2526
2751
|
|
|
2527
2752
|
// src/roles/test/codemod/propagate-await.ts
|
|
2528
|
-
import { parseSync as
|
|
2753
|
+
import { parseSync as parseSync5 } from "oxc-parser";
|
|
2529
2754
|
var FUNCTIONS3 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
|
|
2530
2755
|
var MEMBER_TYPES14 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
2531
2756
|
var ALREADY_A_PROMISE = /* @__PURE__ */ new Set(["then", "catch", "finally"]);
|
|
@@ -2621,7 +2846,7 @@ function propagateAwait(fileName, source, original, elsewhere = /* @__PURE__ */
|
|
|
2621
2846
|
for (let round = 0; round < 12; round += 1) {
|
|
2622
2847
|
let parsed;
|
|
2623
2848
|
try {
|
|
2624
|
-
parsed =
|
|
2849
|
+
parsed = parseSync5(fileName, current);
|
|
2625
2850
|
} catch {
|
|
2626
2851
|
return { source: current, rewritten };
|
|
2627
2852
|
}
|
|
@@ -2651,9 +2876,9 @@ function propagateAwait(fileName, source, original, elsewhere = /* @__PURE__ */
|
|
|
2651
2876
|
}
|
|
2652
2877
|
|
|
2653
2878
|
// src/roles/test/codemod/reexport-barrel-mock.ts
|
|
2654
|
-
import { readFileSync as
|
|
2879
|
+
import { readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
2655
2880
|
import { createRequire as createRequire3 } from "node:module";
|
|
2656
|
-
import { dirname as
|
|
2881
|
+
import { dirname as dirname4, join as join11 } from "node:path";
|
|
2657
2882
|
var MEMBER_TYPES15 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
|
|
2658
2883
|
var PACKAGE_SUBPATH = /^(@[^/]+\/[^/]+|[^@.][^/]*)\/(.+)$/;
|
|
2659
2884
|
var PURE_REEXPORT = /^module\.exports\s*=\s*require\(\s*(['"])([^'"]+)\1\s*\)\s*;?$/;
|
|
@@ -2682,7 +2907,7 @@ function mockCalls(program) {
|
|
|
2682
2907
|
}
|
|
2683
2908
|
function packageRoot(fromFile, name) {
|
|
2684
2909
|
try {
|
|
2685
|
-
return
|
|
2910
|
+
return dirname4(createRequire3(fromFile).resolve(`${name}/package.json`));
|
|
2686
2911
|
} catch {
|
|
2687
2912
|
return void 0;
|
|
2688
2913
|
}
|
|
@@ -2706,11 +2931,11 @@ function barrelsFor(fromFile, name, target) {
|
|
|
2706
2931
|
const aliases = [];
|
|
2707
2932
|
for (const entry of entries) {
|
|
2708
2933
|
if (!entry.endsWith(".js")) continue;
|
|
2709
|
-
const path =
|
|
2934
|
+
const path = join11(root, entry);
|
|
2710
2935
|
let body;
|
|
2711
2936
|
try {
|
|
2712
|
-
if (!
|
|
2713
|
-
body =
|
|
2937
|
+
if (!statSync2(path).isFile()) continue;
|
|
2938
|
+
body = readFileSync9(path, "utf8");
|
|
2714
2939
|
} catch {
|
|
2715
2940
|
continue;
|
|
2716
2941
|
}
|
|
@@ -2750,7 +2975,7 @@ ${text}` });
|
|
|
2750
2975
|
}
|
|
2751
2976
|
|
|
2752
2977
|
// src/roles/test/codemod/repoint-msw.ts
|
|
2753
|
-
import { parseSync as
|
|
2978
|
+
import { parseSync as parseSync6 } from "oxc-parser";
|
|
2754
2979
|
var SENTINEL_MSW_SERVER = "@hublo/sentinel/test/msw";
|
|
2755
2980
|
var SENTINEL_MSW_API = "@hublo/sentinel/test/tools/msw";
|
|
2756
2981
|
var MSW_PACKAGE = /^msw$/;
|
|
@@ -2763,7 +2988,7 @@ function importsServer(declaration) {
|
|
|
2763
2988
|
});
|
|
2764
2989
|
}
|
|
2765
2990
|
function repointMswServer(fileName, source) {
|
|
2766
|
-
const { program, errors } =
|
|
2991
|
+
const { program, errors } = parseSync6(fileName, source, { sourceType: "module" });
|
|
2767
2992
|
if (errors.length > 0) return { source, repointed: false };
|
|
2768
2993
|
const edits = [];
|
|
2769
2994
|
walk(program, (node) => {
|
|
@@ -2789,17 +3014,17 @@ function repointMswServer(fileName, source) {
|
|
|
2789
3014
|
}
|
|
2790
3015
|
|
|
2791
3016
|
// src/roles/test/codemod/setup-static-imports.ts
|
|
2792
|
-
import { dirname as
|
|
3017
|
+
import { dirname as dirname6, resolve as resolve6 } from "node:path";
|
|
2793
3018
|
|
|
2794
3019
|
// src/core/context.ts
|
|
2795
|
-
import { existsSync as
|
|
2796
|
-
import { dirname as
|
|
3020
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
3021
|
+
import { dirname as dirname5, join as join13, resolve as resolve4 } from "node:path";
|
|
2797
3022
|
|
|
2798
3023
|
// src/core/discover-modules.ts
|
|
2799
3024
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2800
|
-
import { mkdtempSync as mkdtempSync2, readFileSync as
|
|
3025
|
+
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync10, rmSync as rmSync2 } from "node:fs";
|
|
2801
3026
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
2802
|
-
import { join as
|
|
3027
|
+
import { join as join12 } from "node:path";
|
|
2803
3028
|
function runNx(cwd, args) {
|
|
2804
3029
|
const nx = resolveBin(cwd, "nx") ?? "nx";
|
|
2805
3030
|
try {
|
|
@@ -2817,11 +3042,11 @@ function runNx(cwd, args) {
|
|
|
2817
3042
|
}
|
|
2818
3043
|
}
|
|
2819
3044
|
function readGraph(cwd) {
|
|
2820
|
-
const dir = mkdtempSync2(
|
|
2821
|
-
const file =
|
|
3045
|
+
const dir = mkdtempSync2(join12(tmpdir2(), "sentinel-nx-"));
|
|
3046
|
+
const file = join12(dir, "graph.json");
|
|
2822
3047
|
try {
|
|
2823
3048
|
runNx(cwd, ["graph", "--file", file]);
|
|
2824
|
-
const parsed = JSON.parse(
|
|
3049
|
+
const parsed = JSON.parse(readFileSync10(file, "utf8"));
|
|
2825
3050
|
const nodes = parsed.graph?.nodes;
|
|
2826
3051
|
if (!nodes || typeof nodes !== "object") {
|
|
2827
3052
|
throw new Error(
|
|
@@ -2830,7 +3055,7 @@ function readGraph(cwd) {
|
|
|
2830
3055
|
}
|
|
2831
3056
|
return Object.entries(nodes).map(([name, node]) => ({
|
|
2832
3057
|
name,
|
|
2833
|
-
root:
|
|
3058
|
+
root: join12(cwd, node.data.root)
|
|
2834
3059
|
}));
|
|
2835
3060
|
} finally {
|
|
2836
3061
|
rmSync2(dir, { recursive: true, force: true });
|
|
@@ -2848,20 +3073,20 @@ function discoverModules(cwd, options = {}) {
|
|
|
2848
3073
|
// src/core/context.ts
|
|
2849
3074
|
var MODULE_MARKERS = ["package.json", "project.json"];
|
|
2850
3075
|
function isModuleDir(cwd) {
|
|
2851
|
-
return MODULE_MARKERS.some((marker) =>
|
|
3076
|
+
return MODULE_MARKERS.some((marker) => existsSync12(join13(cwd, marker)));
|
|
2852
3077
|
}
|
|
2853
3078
|
function findModuleRoot(startDir, workspaceRoot) {
|
|
2854
|
-
let dir =
|
|
3079
|
+
let dir = resolve4(startDir);
|
|
2855
3080
|
for (; ; ) {
|
|
2856
|
-
if (workspaceRoot !== void 0 && dir ===
|
|
3081
|
+
if (workspaceRoot !== void 0 && dir === resolve4(workspaceRoot)) return void 0;
|
|
2857
3082
|
if (isModuleDir(dir)) return dir;
|
|
2858
|
-
const parent =
|
|
3083
|
+
const parent = dirname5(dir);
|
|
2859
3084
|
if (parent === dir) return void 0;
|
|
2860
3085
|
dir = parent;
|
|
2861
3086
|
}
|
|
2862
3087
|
}
|
|
2863
3088
|
function resolveContext(cwd, opts) {
|
|
2864
|
-
const atRoot =
|
|
3089
|
+
const atRoot = existsSync12(join13(cwd, WORKSPACE_ROOT_MARKER));
|
|
2865
3090
|
if (!atRoot && isModuleDir(cwd)) {
|
|
2866
3091
|
if (opts.module) {
|
|
2867
3092
|
throw new Error(
|
|
@@ -2889,16 +3114,16 @@ function resolveContext(cwd, opts) {
|
|
|
2889
3114
|
}
|
|
2890
3115
|
|
|
2891
3116
|
// src/roles/test/codemod/workspace-specifier.ts
|
|
2892
|
-
import { existsSync as
|
|
2893
|
-
import { join as
|
|
2894
|
-
var
|
|
3117
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11 } from "node:fs";
|
|
3118
|
+
import { join as join14, relative as relative2, resolve as resolve5 } from "node:path";
|
|
3119
|
+
var EXTENSIONS2 = ["", ".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", "/index.ts", "/index.js"];
|
|
2895
3120
|
function pathEntries(workspaceRoot) {
|
|
2896
|
-
const file =
|
|
2897
|
-
if (!
|
|
3121
|
+
const file = join14(workspaceRoot, "tsconfig.base.json");
|
|
3122
|
+
if (!existsSync13(file)) return [];
|
|
2898
3123
|
let paths;
|
|
2899
3124
|
try {
|
|
2900
3125
|
paths = parseJsonc(
|
|
2901
|
-
|
|
3126
|
+
readFileSync11(file, "utf8"),
|
|
2902
3127
|
"tsconfig.base.json"
|
|
2903
3128
|
)?.compilerOptions?.paths;
|
|
2904
3129
|
} catch {
|
|
@@ -2911,17 +3136,17 @@ function pathEntries(workspaceRoot) {
|
|
|
2911
3136
|
const wildcard = key.endsWith("/*") && target.endsWith("/*");
|
|
2912
3137
|
entries.push({
|
|
2913
3138
|
key: wildcard ? key.slice(0, -2) : key,
|
|
2914
|
-
target:
|
|
3139
|
+
target: resolve5(workspaceRoot, wildcard ? target.slice(0, -2) : target),
|
|
2915
3140
|
wildcard
|
|
2916
3141
|
});
|
|
2917
3142
|
}
|
|
2918
3143
|
return entries.sort((left, right) => right.target.length - left.target.length);
|
|
2919
3144
|
}
|
|
2920
3145
|
function workspaceSpecifier(workspaceRoot, file) {
|
|
2921
|
-
const absolute =
|
|
3146
|
+
const absolute = resolve5(file);
|
|
2922
3147
|
for (const entry of pathEntries(workspaceRoot)) {
|
|
2923
3148
|
if (!entry.wildcard) {
|
|
2924
|
-
if (
|
|
3149
|
+
if (EXTENSIONS2.some((extension) => `${entry.target}${extension}` === absolute)) {
|
|
2925
3150
|
return entry.key;
|
|
2926
3151
|
}
|
|
2927
3152
|
continue;
|
|
@@ -2947,9 +3172,9 @@ function requireCall(source, node) {
|
|
|
2947
3172
|
}
|
|
2948
3173
|
function importable(written, fileName, workspaceRoot) {
|
|
2949
3174
|
if (!written.startsWith(".")) return written;
|
|
2950
|
-
const fileDir =
|
|
3175
|
+
const fileDir = dirname6(fileName);
|
|
2951
3176
|
const moduleRoot = findModuleRoot(fileDir, workspaceRoot) ?? fileDir;
|
|
2952
|
-
const target =
|
|
3177
|
+
const target = resolve6(fileDir, written);
|
|
2953
3178
|
if (target.startsWith(`${moduleRoot}/`)) return written;
|
|
2954
3179
|
if (workspaceRoot === void 0) return void 0;
|
|
2955
3180
|
return workspaceSpecifier(workspaceRoot, target);
|
|
@@ -3090,7 +3315,7 @@ function mockFactories(program) {
|
|
|
3090
3315
|
return factories;
|
|
3091
3316
|
}
|
|
3092
3317
|
function parse3(fileName, source) {
|
|
3093
|
-
const { program, errors } =
|
|
3318
|
+
const { program, errors } = parseSync7(fileName, source, { sourceType: "module" });
|
|
3094
3319
|
return errors.length > 0 ? void 0 : program;
|
|
3095
3320
|
}
|
|
3096
3321
|
function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new Set(), asyncHelpers = /* @__PURE__ */ new Set(), modulesOwnMsw = false) {
|
|
@@ -3120,7 +3345,7 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
|
|
|
3120
3345
|
fileName,
|
|
3121
3346
|
source,
|
|
3122
3347
|
beforeStatic,
|
|
3123
|
-
findWorkspaceRoot(
|
|
3348
|
+
findWorkspaceRoot(dirname7(fileName))
|
|
3124
3349
|
);
|
|
3125
3350
|
source = statics.source;
|
|
3126
3351
|
const original = statics.rewritten === 0 ? beforeStatic : parse3(fileName, source);
|
|
@@ -3235,7 +3460,8 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
|
|
|
3235
3460
|
...hoist.refusals,
|
|
3236
3461
|
...mocks.refusals.map((refusal) => ({ binding: refusal.binding, reason: refusal.reason })),
|
|
3237
3462
|
...constructible.refusals,
|
|
3238
|
-
...isolated.refusals
|
|
3463
|
+
...isolated.refusals,
|
|
3464
|
+
...ruled.refusals
|
|
3239
3465
|
],
|
|
3240
3466
|
warnings: fakeTimers.warnings,
|
|
3241
3467
|
mswRepointed: repointed.repointed
|
|
@@ -3243,13 +3469,13 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
|
|
|
3243
3469
|
}
|
|
3244
3470
|
|
|
3245
3471
|
// src/roles/test/codemod/test-support-files.ts
|
|
3246
|
-
import { existsSync as
|
|
3247
|
-
import { dirname as
|
|
3472
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
|
|
3473
|
+
import { dirname as dirname8, join as join15, relative as relative3, resolve as resolve7 } from "node:path";
|
|
3248
3474
|
var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
|
|
3249
3475
|
var TEST_FILE2 = /\.(spec|test)\.[cm]?[jt]sx?$/;
|
|
3250
3476
|
var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
|
|
3251
|
-
var
|
|
3252
|
-
function
|
|
3477
|
+
var EXTENSIONS3 = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"];
|
|
3478
|
+
function relativeSpecifiers2(source) {
|
|
3253
3479
|
const found = [];
|
|
3254
3480
|
const patterns = [
|
|
3255
3481
|
/\bfrom\s+['"](\.[^'"]*)['"]/g,
|
|
@@ -3263,14 +3489,14 @@ function relativeSpecifiers(source) {
|
|
|
3263
3489
|
return found;
|
|
3264
3490
|
}
|
|
3265
3491
|
function resolveSpecifier(fromFile, specifier) {
|
|
3266
|
-
const base =
|
|
3267
|
-
if (
|
|
3268
|
-
for (const extension of
|
|
3269
|
-
if (
|
|
3492
|
+
const base = resolve7(dirname8(fromFile), specifier);
|
|
3493
|
+
if (existsSync14(base) && statSync3(base).isFile()) return base;
|
|
3494
|
+
for (const extension of EXTENSIONS3) {
|
|
3495
|
+
if (existsSync14(`${base}${extension}`)) return `${base}${extension}`;
|
|
3270
3496
|
}
|
|
3271
|
-
for (const extension of
|
|
3272
|
-
const index =
|
|
3273
|
-
if (
|
|
3497
|
+
for (const extension of EXTENSIONS3) {
|
|
3498
|
+
const index = join15(base, `index${extension}`);
|
|
3499
|
+
if (existsSync14(index)) return index;
|
|
3274
3500
|
}
|
|
3275
3501
|
return void 0;
|
|
3276
3502
|
}
|
|
@@ -3287,9 +3513,9 @@ function moduleSources(cwd) {
|
|
|
3287
3513
|
}
|
|
3288
3514
|
for (const entry of entries) {
|
|
3289
3515
|
if (entry.isDirectory()) {
|
|
3290
|
-
if (!SKIPPED.has(entry.name)) stack.push(
|
|
3516
|
+
if (!SKIPPED.has(entry.name)) stack.push(join15(dir, entry.name));
|
|
3291
3517
|
} else if (SOURCE_FILE.test(entry.name)) {
|
|
3292
|
-
found.push(
|
|
3518
|
+
found.push(join15(dir, entry.name));
|
|
3293
3519
|
}
|
|
3294
3520
|
}
|
|
3295
3521
|
}
|
|
@@ -3297,7 +3523,7 @@ function moduleSources(cwd) {
|
|
|
3297
3523
|
}
|
|
3298
3524
|
function read(path) {
|
|
3299
3525
|
try {
|
|
3300
|
-
return
|
|
3526
|
+
return readFileSync12(path, "utf8");
|
|
3301
3527
|
} catch {
|
|
3302
3528
|
return void 0;
|
|
3303
3529
|
}
|
|
@@ -3306,10 +3532,10 @@ function setupFilesInside(cwd, named) {
|
|
|
3306
3532
|
const kept = [];
|
|
3307
3533
|
for (const raw of named) {
|
|
3308
3534
|
const withoutToken = raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", ".");
|
|
3309
|
-
const full =
|
|
3535
|
+
const full = resolve7(cwd, withoutToken);
|
|
3310
3536
|
const inside = relative3(cwd, full);
|
|
3311
3537
|
if (inside.startsWith("..") || inside === "") continue;
|
|
3312
|
-
if (!
|
|
3538
|
+
if (!existsSync14(full)) continue;
|
|
3313
3539
|
kept.push(inside.replaceAll("\\", "/"));
|
|
3314
3540
|
}
|
|
3315
3541
|
return kept;
|
|
@@ -3323,7 +3549,7 @@ function listTestSupportFiles(cwd, setupFiles = []) {
|
|
|
3323
3549
|
}
|
|
3324
3550
|
const importedBy = /* @__PURE__ */ new Map();
|
|
3325
3551
|
for (const [file, source] of contents) {
|
|
3326
|
-
for (const specifier of
|
|
3552
|
+
for (const specifier of relativeSpecifiers2(source)) {
|
|
3327
3553
|
const target = resolveSpecifier(file, specifier);
|
|
3328
3554
|
if (target === void 0 || !contents.has(target)) continue;
|
|
3329
3555
|
importedBy.set(target, [...importedBy.get(target) ?? [], file]);
|
|
@@ -3336,7 +3562,7 @@ function listTestSupportFiles(cwd, setupFiles = []) {
|
|
|
3336
3562
|
);
|
|
3337
3563
|
while (queue.length > 0) {
|
|
3338
3564
|
const file = queue.shift();
|
|
3339
|
-
for (const specifier of
|
|
3565
|
+
for (const specifier of relativeSpecifiers2(contents.get(file) ?? "")) {
|
|
3340
3566
|
const target = resolveSpecifier(file, specifier);
|
|
3341
3567
|
if (target === void 0 || TEST_FILE2.test(target) || testOnly.has(target)) continue;
|
|
3342
3568
|
if (!isTestOnly(target)) continue;
|
|
@@ -3350,12 +3576,12 @@ function listTestSupportFiles(cwd, setupFiles = []) {
|
|
|
3350
3576
|
|
|
3351
3577
|
// src/roles/test/codemod/migrate-module.ts
|
|
3352
3578
|
function parse4(source) {
|
|
3353
|
-
const { program, errors } =
|
|
3579
|
+
const { program, errors } = parseSync8("file.ts", source, { sourceType: "module" });
|
|
3354
3580
|
return errors.length > 0 ? void 0 : program;
|
|
3355
3581
|
}
|
|
3356
3582
|
function read2(path) {
|
|
3357
3583
|
try {
|
|
3358
|
-
return
|
|
3584
|
+
return readFileSync13(path, "utf8");
|
|
3359
3585
|
} catch {
|
|
3360
3586
|
return void 0;
|
|
3361
3587
|
}
|
|
@@ -3373,10 +3599,10 @@ function defaultImportedSpecifiers(cwd) {
|
|
|
3373
3599
|
}
|
|
3374
3600
|
for (const entry of entries) {
|
|
3375
3601
|
if (SKIPPED2.has(entry)) continue;
|
|
3376
|
-
const path =
|
|
3602
|
+
const path = join16(dir, entry);
|
|
3377
3603
|
let directory;
|
|
3378
3604
|
try {
|
|
3379
|
-
directory =
|
|
3605
|
+
directory = statSync4(path).isDirectory();
|
|
3380
3606
|
} catch {
|
|
3381
3607
|
continue;
|
|
3382
3608
|
}
|
|
@@ -3404,18 +3630,28 @@ function migrateModuleTests(cwd, setupFiles = []) {
|
|
|
3404
3630
|
const defaultImported = defaultImportedSpecifiers(cwd);
|
|
3405
3631
|
const asyncHelpers = /* @__PURE__ */ new Set();
|
|
3406
3632
|
for (const relative6 of listTestSupportFiles(cwd, setupFiles)) {
|
|
3407
|
-
const source = read2(
|
|
3633
|
+
const source = read2(join16(cwd, relative6));
|
|
3408
3634
|
if (source === void 0) {
|
|
3409
3635
|
blockers.push({ file: relative6, reason: "this file could not be read" });
|
|
3410
3636
|
continue;
|
|
3411
3637
|
}
|
|
3412
3638
|
const migrated = migrateTestFile(
|
|
3413
|
-
|
|
3639
|
+
join16(cwd, relative6),
|
|
3414
3640
|
source,
|
|
3415
3641
|
defaultImported,
|
|
3416
3642
|
/* @__PURE__ */ new Set(),
|
|
3417
3643
|
modulesOwnMsw
|
|
3418
3644
|
);
|
|
3645
|
+
for (const member of migrated.unhandled) {
|
|
3646
|
+
blockers.push({
|
|
3647
|
+
file: `${relative6}:${member.line}`,
|
|
3648
|
+
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}`
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
for (const refusal of migrated.refusals) {
|
|
3652
|
+
blockers.push({ file: relative6, reason: refusal.reason });
|
|
3653
|
+
}
|
|
3654
|
+
collected.push(...migrated.warnings);
|
|
3419
3655
|
const renamedTo = vitestSupportName(relative6);
|
|
3420
3656
|
if (!migrated.changed && renamedTo === relative6) continue;
|
|
3421
3657
|
if (renamedTo !== relative6) operations.push({ kind: "delete", path: relative6 });
|
|
@@ -3429,17 +3665,17 @@ function migrateModuleTests(cwd, setupFiles = []) {
|
|
|
3429
3665
|
}
|
|
3430
3666
|
files++;
|
|
3431
3667
|
renamed += migrated.renamed;
|
|
3432
|
-
const contents = /\bvi\./.test(migrated.source) ? injectViImport(
|
|
3668
|
+
const contents = /\bvi\./.test(migrated.source) ? injectViImport(join16(cwd, renamedTo), migrated.source) : migrated.source;
|
|
3433
3669
|
operations.push({ kind: "write", path: renamedTo, contents });
|
|
3434
3670
|
}
|
|
3435
3671
|
for (const relative6 of listTestFiles(cwd)) {
|
|
3436
|
-
const source = read2(
|
|
3672
|
+
const source = read2(join16(cwd, relative6));
|
|
3437
3673
|
if (source === void 0) {
|
|
3438
3674
|
blockers.push({ file: relative6, reason: "this file could not be read" });
|
|
3439
3675
|
continue;
|
|
3440
3676
|
}
|
|
3441
3677
|
const migrated = migrateTestFile(
|
|
3442
|
-
|
|
3678
|
+
join16(cwd, relative6),
|
|
3443
3679
|
source,
|
|
3444
3680
|
defaultImported,
|
|
3445
3681
|
asyncHelpers,
|
|
@@ -3466,8 +3702,8 @@ function migrateModuleTests(cwd, setupFiles = []) {
|
|
|
3466
3702
|
}
|
|
3467
3703
|
|
|
3468
3704
|
// src/roles/test/nest/generate-config.ts
|
|
3469
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
3470
|
-
import { join as
|
|
3705
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
|
|
3706
|
+
import { join as join17, relative as relative4 } from "node:path";
|
|
3471
3707
|
var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
|
|
3472
3708
|
var REACT_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
|
|
3473
3709
|
function flavourOf(reading) {
|
|
@@ -3513,10 +3749,10 @@ function readModuleTraits(cwd) {
|
|
|
3513
3749
|
}
|
|
3514
3750
|
for (const entry of entries) {
|
|
3515
3751
|
if (SKIPPED3.has(entry)) continue;
|
|
3516
|
-
const path =
|
|
3752
|
+
const path = join17(dir, entry);
|
|
3517
3753
|
let directory;
|
|
3518
3754
|
try {
|
|
3519
|
-
directory =
|
|
3755
|
+
directory = statSync5(path).isDirectory();
|
|
3520
3756
|
} catch {
|
|
3521
3757
|
continue;
|
|
3522
3758
|
}
|
|
@@ -3527,7 +3763,7 @@ function readModuleTraits(cwd) {
|
|
|
3527
3763
|
if (!/\.tsx?$/.test(entry)) continue;
|
|
3528
3764
|
let source;
|
|
3529
3765
|
try {
|
|
3530
|
-
source =
|
|
3766
|
+
source = readFileSync14(path, "utf8");
|
|
3531
3767
|
} catch {
|
|
3532
3768
|
continue;
|
|
3533
3769
|
}
|
|
@@ -3660,8 +3896,8 @@ ${overrides.join("\n")}
|
|
|
3660
3896
|
}
|
|
3661
3897
|
|
|
3662
3898
|
// src/roles/test/nest/read-jest-config.ts
|
|
3663
|
-
import { existsSync as
|
|
3664
|
-
import { join as
|
|
3899
|
+
import { existsSync as existsSync15, readFileSync as readFileSync15 } from "node:fs";
|
|
3900
|
+
import { join as join18 } from "node:path";
|
|
3665
3901
|
var CARRIED = [
|
|
3666
3902
|
"testEnvironment",
|
|
3667
3903
|
"coverageDirectory",
|
|
@@ -3851,7 +4087,7 @@ function extendedConfig(source, cwd) {
|
|
|
3851
4087
|
(name) => name.replace(/^\.\//, "")
|
|
3852
4088
|
);
|
|
3853
4089
|
const file = candidates.find(
|
|
3854
|
-
(name) => /^jest[\w.-]*\.(ts|js)$/.test(name) &&
|
|
4090
|
+
(name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync15(join18(cwd, name))
|
|
3855
4091
|
);
|
|
3856
4092
|
if (file === void 0) continue;
|
|
3857
4093
|
if (!new RegExp(String.raw`\{\s*\.\.\.${binding}\b`).test(source)) continue;
|
|
@@ -3878,11 +4114,11 @@ function sideEffects(source, extended) {
|
|
|
3878
4114
|
return found;
|
|
3879
4115
|
}
|
|
3880
4116
|
function readJestConfig(cwd, named) {
|
|
3881
|
-
const file = named ?? JEST_CONFIG_FILES.find((name) =>
|
|
3882
|
-
if (file === void 0 || !
|
|
4117
|
+
const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync15(join18(cwd, name)));
|
|
4118
|
+
if (file === void 0 || !existsSync15(join18(cwd, file))) return void 0;
|
|
3883
4119
|
let source;
|
|
3884
4120
|
try {
|
|
3885
|
-
source =
|
|
4121
|
+
source = readFileSync15(join18(cwd, file), "utf8");
|
|
3886
4122
|
} catch {
|
|
3887
4123
|
return { file, refusals: [`${file} could not be read`], notes: [] };
|
|
3888
4124
|
}
|
|
@@ -3963,8 +4199,8 @@ function readJestConfig(cwd, named) {
|
|
|
3963
4199
|
}
|
|
3964
4200
|
|
|
3965
4201
|
// src/roles/test/nest/run-modes.ts
|
|
3966
|
-
import { existsSync as
|
|
3967
|
-
import { basename as basename2, join as
|
|
4202
|
+
import { existsSync as existsSync16 } from "node:fs";
|
|
4203
|
+
import { basename as basename2, join as join19 } from "node:path";
|
|
3968
4204
|
var JEST_EXECUTOR = "@nx/jest:jest";
|
|
3969
4205
|
var PRIMARY_CONFIG_FILE = "vitest.config.mts";
|
|
3970
4206
|
var PRIMARY_TARGET = "test";
|
|
@@ -3985,7 +4221,7 @@ function runModes(cwd, fallback) {
|
|
|
3985
4221
|
const jestConfig = entry.options?.jestConfig;
|
|
3986
4222
|
if (jestConfig === void 0) continue;
|
|
3987
4223
|
const name = configName(jestConfig);
|
|
3988
|
-
if (!
|
|
4224
|
+
if (!existsSync16(join19(cwd, name))) continue;
|
|
3989
4225
|
declared.push({
|
|
3990
4226
|
target,
|
|
3991
4227
|
jestConfig: name,
|
|
@@ -4010,12 +4246,12 @@ function runModes(cwd, fallback) {
|
|
|
4010
4246
|
}
|
|
4011
4247
|
|
|
4012
4248
|
// src/roles/test/written-file-formatter.ts
|
|
4013
|
-
import { existsSync as
|
|
4014
|
-
import { dirname as
|
|
4249
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
4250
|
+
import { dirname as dirname10, join as join22 } from "node:path";
|
|
4015
4251
|
|
|
4016
4252
|
// src/roles/format/read-adoption.ts
|
|
4017
|
-
import { existsSync as
|
|
4018
|
-
import { join as
|
|
4253
|
+
import { existsSync as existsSync17, readFileSync as readFileSync16 } from "node:fs";
|
|
4254
|
+
import { join as join20 } from "node:path";
|
|
4019
4255
|
|
|
4020
4256
|
// src/roles/format/config-policy.ts
|
|
4021
4257
|
var FORMAT_CONFIG_FILE = ".oxfmtrc.json";
|
|
@@ -4121,11 +4357,11 @@ function sameValue(a, b) {
|
|
|
4121
4357
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
4122
4358
|
}
|
|
4123
4359
|
function readFormatAdoption(cwd) {
|
|
4124
|
-
const path =
|
|
4125
|
-
if (!
|
|
4360
|
+
const path = join20(cwd, FORMAT_CONFIG_FILE);
|
|
4361
|
+
if (!existsSync17(path)) return NOT_ADOPTED(null);
|
|
4126
4362
|
let parsed;
|
|
4127
4363
|
try {
|
|
4128
|
-
parsed = parseJsonc(
|
|
4364
|
+
parsed = parseJsonc(readFileSync16(path, "utf8"), FORMAT_CONFIG_FILE);
|
|
4129
4365
|
} catch (error) {
|
|
4130
4366
|
const reason = error instanceof Error ? error.message : String(error);
|
|
4131
4367
|
return NOT_ADOPTED(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
|
|
@@ -4169,9 +4405,9 @@ function readFormatAdoption(cwd) {
|
|
|
4169
4405
|
}
|
|
4170
4406
|
|
|
4171
4407
|
// src/roles/format/resolve-oxfmt.ts
|
|
4172
|
-
import { readFileSync as
|
|
4408
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
4173
4409
|
import { createRequire as createRequire4 } from "node:module";
|
|
4174
|
-
import { dirname as
|
|
4410
|
+
import { dirname as dirname9, join as join21 } from "node:path";
|
|
4175
4411
|
function resolveOxfmt(cwd) {
|
|
4176
4412
|
return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
|
|
4177
4413
|
}
|
|
@@ -4200,8 +4436,8 @@ function configSchema() {
|
|
|
4200
4436
|
function readConfigSchema() {
|
|
4201
4437
|
try {
|
|
4202
4438
|
const manifest = createRequire4(import.meta.url).resolve("oxfmt/package.json");
|
|
4203
|
-
const schemaPath =
|
|
4204
|
-
return JSON.parse(
|
|
4439
|
+
const schemaPath = join21(dirname9(manifest), "configuration_schema.json");
|
|
4440
|
+
return JSON.parse(readFileSync17(schemaPath, "utf8"));
|
|
4205
4441
|
} catch {
|
|
4206
4442
|
return void 0;
|
|
4207
4443
|
}
|
|
@@ -4226,9 +4462,9 @@ function prettierConfigFrom(cwd) {
|
|
|
4226
4462
|
let dir = cwd;
|
|
4227
4463
|
for (; ; ) {
|
|
4228
4464
|
for (const name of PRETTIER_CONFIGS) {
|
|
4229
|
-
if (
|
|
4465
|
+
if (existsSync18(join22(dir, name))) return join22(dir, name);
|
|
4230
4466
|
}
|
|
4231
|
-
const parent =
|
|
4467
|
+
const parent = dirname10(dir);
|
|
4232
4468
|
if (parent === dir) return void 0;
|
|
4233
4469
|
dir = parent;
|
|
4234
4470
|
}
|
|
@@ -4256,12 +4492,12 @@ function writtenFileFormatter(cwd) {
|
|
|
4256
4492
|
var SPEC_TSCONFIG = "tsconfig.spec.json";
|
|
4257
4493
|
var VITEST_GLOBAL_TYPES = "vitest/globals";
|
|
4258
4494
|
function specTypesOperation(cwd) {
|
|
4259
|
-
const path =
|
|
4260
|
-
if (!
|
|
4495
|
+
const path = join23(cwd, SPEC_TSCONFIG);
|
|
4496
|
+
if (!existsSync19(path)) return void 0;
|
|
4261
4497
|
let config;
|
|
4262
4498
|
try {
|
|
4263
4499
|
config = parseJsonc(
|
|
4264
|
-
|
|
4500
|
+
readFileSync18(path, "utf8"),
|
|
4265
4501
|
SPEC_TSCONFIG
|
|
4266
4502
|
);
|
|
4267
4503
|
} catch {
|
|
@@ -4281,9 +4517,9 @@ function specTypesOperation(cwd) {
|
|
|
4281
4517
|
}
|
|
4282
4518
|
function shippedTypesPath(cwd, declaration) {
|
|
4283
4519
|
try {
|
|
4284
|
-
const manifest = createRequire5(
|
|
4285
|
-
const file =
|
|
4286
|
-
if (
|
|
4520
|
+
const manifest = createRequire5(join23(cwd, "noop.cjs")).resolve("@hublo/sentinel/package.json");
|
|
4521
|
+
const file = join23(dirname11(manifest), declaration);
|
|
4522
|
+
if (existsSync19(file)) {
|
|
4287
4523
|
const path = relative5(cwd, file);
|
|
4288
4524
|
return path.startsWith(".") ? path : `./${path}`;
|
|
4289
4525
|
}
|
|
@@ -4302,7 +4538,7 @@ function helpersStillCallingJest(cwd) {
|
|
|
4302
4538
|
return;
|
|
4303
4539
|
}
|
|
4304
4540
|
for (const entry of entries) {
|
|
4305
|
-
const path =
|
|
4541
|
+
const path = join23(dir, entry.name);
|
|
4306
4542
|
if (entry.isDirectory()) {
|
|
4307
4543
|
if (!skip.has(entry.name)) walk2(path);
|
|
4308
4544
|
continue;
|
|
@@ -4312,7 +4548,7 @@ function helpersStillCallingJest(cwd) {
|
|
|
4312
4548
|
if (/^(jest|vitest)\./.test(entry.name)) continue;
|
|
4313
4549
|
let source;
|
|
4314
4550
|
try {
|
|
4315
|
-
source =
|
|
4551
|
+
source = readFileSync18(path, "utf8");
|
|
4316
4552
|
} catch {
|
|
4317
4553
|
continue;
|
|
4318
4554
|
}
|
|
@@ -4348,7 +4584,7 @@ function tsconfigFileListOperations(cwd) {
|
|
|
4348
4584
|
let config;
|
|
4349
4585
|
try {
|
|
4350
4586
|
config = parseJsonc(
|
|
4351
|
-
|
|
4587
|
+
readFileSync18(join23(cwd, name), "utf8"),
|
|
4352
4588
|
name
|
|
4353
4589
|
);
|
|
4354
4590
|
} catch {
|
|
@@ -4816,7 +5052,7 @@ function fromWorkspaceRoot(moduleDir, options, paths) {
|
|
|
4816
5052
|
if (root === void 0 || root === moduleDir) {
|
|
4817
5053
|
return { cwd: moduleDir, options: [...options], paths: [...paths] };
|
|
4818
5054
|
}
|
|
4819
|
-
const absolute = (value) => isAbsolute2(value) ? value :
|
|
5055
|
+
const absolute = (value) => isAbsolute2(value) ? value : resolve8(moduleDir, value);
|
|
4820
5056
|
const moved = [];
|
|
4821
5057
|
let previousWasConfig = false;
|
|
4822
5058
|
for (const option of options) {
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
registerAdapters,
|
|
7
7
|
resolve,
|
|
8
8
|
setDefaultRunner
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-63NQP75B.js";
|
|
10
10
|
import {
|
|
11
11
|
BaseAdapter
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-UBXL567Z.js";
|
|
13
13
|
import "./chunk-O7REVMOC.js";
|
|
14
14
|
import "./chunk-NW6UHNYX.js";
|
|
15
15
|
export {
|
package/docs/test-adoption.md
CHANGED
|
@@ -10,6 +10,7 @@ that decided a design choice say which choice.
|
|
|
10
10
|
- [The one line that makes a per-module migration possible](#the-one-line-that-makes-a-per-module-migration-possible)
|
|
11
11
|
- [What `--init --test` rewrites, and what it refuses](#what---init---test-rewrites-and-what-it-refuses)
|
|
12
12
|
- [Nest services](#nest-services)
|
|
13
|
+
- [The two DOM matchers, which a react module gets without asking](#the-two-dom-matchers-which-a-react-module-gets-without-asking)
|
|
13
14
|
- [The check that decides whether you are done](#the-check-that-decides-whether-you-are-done)
|
|
14
15
|
|
|
15
16
|
## What the ground actually looks like
|
|
@@ -292,16 +293,36 @@ member NAMES, which `any` gave up too. Applied only when the SAME file mocks tha
|
|
|
292
293
|
## The check that decides whether you are done
|
|
293
294
|
|
|
294
295
|
Not "the suite is green". **The suite runs the same tests as before**. You do not have to arrange
|
|
295
|
-
that comparison;
|
|
296
|
+
that comparison; `--validate` does it, on either side of the migration:
|
|
296
297
|
|
|
297
298
|
```console
|
|
298
|
-
$ sentinel --
|
|
299
|
-
$
|
|
300
|
-
$
|
|
299
|
+
$ sentinel --validate --test # on the JEST state: records every passing test name
|
|
300
|
+
$ sentinel --init --test # migrates
|
|
301
|
+
$ pnpm install # the module's dependencies changed
|
|
302
|
+
$ sentinel --validate --test # runs vitest, compares against that recording, then spends it
|
|
301
303
|
```
|
|
302
304
|
|
|
303
305
|
A codemod touching thousands of files cannot be reviewed by hand. This comparison is the review.
|
|
304
306
|
|
|
307
|
+
⚠️ **The first command comes first, and the tool enforces it.** Start with `--init` and the jest
|
|
308
|
+
config is already gone, so there is nothing left to record and nothing to compare against:
|
|
309
|
+
|
|
310
|
+
```console
|
|
311
|
+
✗ recruitment — already migrated, and no reference was recorded before it was, so there is
|
|
312
|
+
nothing to compare against. A proof has to be started BEFORE the migration:
|
|
313
|
+
`sentinel --validate --test` on the jest state, then `--init --test`, then this again.
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Why the proof is its own verb
|
|
317
|
+
|
|
318
|
+
Because of what SURVIVES a run. `--init` and `--migrate` write state that stays, which is their
|
|
319
|
+
whole purpose and why they are strict about what they touch. `--validate` may instrument the module
|
|
320
|
+
it is proving, provided nothing it writes remains. Those are different contracts, so they are
|
|
321
|
+
different verbs, and the proof can grow without touching a migration that is already proven.
|
|
322
|
+
|
|
323
|
+
It answers every type, too: `sentinel --validate --lint` is a well-formed command, so it says "not
|
|
324
|
+
available yet" rather than calling your sentence wrong.
|
|
325
|
+
|
|
305
326
|
### Why it cannot be one command
|
|
306
327
|
|
|
307
328
|
The reference only exists BEFORE the migration, and the thing that produces it, the jest config, is
|
|
@@ -321,7 +342,7 @@ shorter.
|
|
|
321
342
|
So the gate refuses a run that lost a name or invented one, and says which:
|
|
322
343
|
|
|
323
344
|
```console
|
|
324
|
-
$ sentinel --
|
|
345
|
+
$ sentinel --validate --test
|
|
325
346
|
1 test(s) no longer exist: useHublerNetworkProfilesQuery builds the URL with employment
|
|
326
347
|
statuses only. (reference: jest.config.ts, 2026-09-23T21:10:00.000Z) The reference is KEPT
|
|
327
348
|
until a run passes it, so this does not go green by running again. Fix the suite and re-run,
|
|
@@ -356,6 +377,7 @@ have read that improvement as a regression.
|
|
|
356
377
|
```console
|
|
357
378
|
$ nx run <module>:typecheck # BEFORE, and write that down too
|
|
358
379
|
$ sentinel --init --test
|
|
380
|
+
$ pnpm install
|
|
359
381
|
$ nx run <module>:typecheck # after
|
|
360
382
|
```
|
|
361
383
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.4.0-alpha.
|
|
3
|
+
"version": "1.4.0-alpha.33",
|
|
4
4
|
"description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|