@hublo/sentinel 1.4.0-alpha.2 → 1.4.0-alpha.4
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-4UIZJ3TR.js → chunk-ASBZQAXV.js} +487 -60
- package/dist/chunk-MT7VQXRA.js +336 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -1
- package/dist/roles/test/nest/toolchain.d.ts +3 -23
- package/dist/roles/test/nest/toolchain.js +4 -268
- package/dist/roles/test/nest/toolchain.js.map +1 -1
- package/dist/roles/test/react/toolchain.d.ts +5 -1
- package/dist/roles/test/react/toolchain.js +11 -3
- package/dist/roles/test/react/toolchain.js.map +1 -1
- package/dist/roles/test/setup/{nest.js → jest-parity.js} +3 -3
- package/dist/roles/test/setup/jest-parity.js.map +1 -0
- package/dist/roles/test/setup/workspace-entry.js.map +1 -1
- package/dist/roles/test/shared-test-config.d.ts +32 -0
- package/dist/roles/test/shared-test-config.js +8 -0
- package/dist/roles/test/shared-test-config.js.map +1 -0
- package/docs/test-adoption.md +54 -10
- package/docs/using-sentinel.md +25 -10
- package/package.json +4 -4
- package/dist/chunk-NX4GHIHF.js +0 -25
- package/dist/roles/test/setup/nest.js.map +0 -1
- /package/dist/roles/test/setup/{nest.d.ts → jest-parity.d.ts} +0 -0
|
@@ -220,6 +220,14 @@ function moduleScripts(cwd) {
|
|
|
220
220
|
return {};
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
|
+
function manifestTargets(cwd) {
|
|
224
|
+
try {
|
|
225
|
+
const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
|
|
226
|
+
return pkg.nx?.targets ?? {};
|
|
227
|
+
} catch {
|
|
228
|
+
return {};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
223
231
|
function moduleName2(cwd) {
|
|
224
232
|
try {
|
|
225
233
|
const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
|
|
@@ -5394,6 +5402,9 @@ function registerLint() {
|
|
|
5394
5402
|
|
|
5395
5403
|
// src/roles/test/adapters/vitest/vitest.adapter.ts
|
|
5396
5404
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
5405
|
+
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync30 } from "fs";
|
|
5406
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
5407
|
+
import { join as join37 } from "path";
|
|
5397
5408
|
|
|
5398
5409
|
// src/roles/test/presets/toolchain.json
|
|
5399
5410
|
var toolchain_default2 = {
|
|
@@ -5470,7 +5481,7 @@ function testTarget(options) {
|
|
|
5470
5481
|
}
|
|
5471
5482
|
|
|
5472
5483
|
// src/roles/test/jest-migration.ts
|
|
5473
|
-
import { existsSync as
|
|
5484
|
+
import { existsSync as existsSync29, readFileSync as readFileSync28 } from "fs";
|
|
5474
5485
|
import { join as join35 } from "path";
|
|
5475
5486
|
|
|
5476
5487
|
// src/roles/test/codemod/migrate-module.ts
|
|
@@ -6581,12 +6592,47 @@ function applyNamedRules(source, program, fileName = "") {
|
|
|
6581
6592
|
const applied = /* @__PURE__ */ new Set();
|
|
6582
6593
|
const asyncify = /* @__PURE__ */ new Set();
|
|
6583
6594
|
const mocked = mockedSpecifiers(program);
|
|
6584
|
-
const wholeStatement = /* @__PURE__ */ new
|
|
6595
|
+
const wholeStatement = /* @__PURE__ */ new Map();
|
|
6585
6596
|
walk(program, (node) => {
|
|
6586
6597
|
if (node.type !== "ExpressionStatement") return;
|
|
6587
6598
|
const expression = node.expression;
|
|
6588
|
-
if (expression !== void 0) wholeStatement.
|
|
6599
|
+
if (expression !== void 0) wholeStatement.set(expression, node);
|
|
6600
|
+
});
|
|
6601
|
+
const standsAlone = /* @__PURE__ */ new Set();
|
|
6602
|
+
const alone = (node) => {
|
|
6603
|
+
if (node !== void 0) standsAlone.add(node);
|
|
6604
|
+
};
|
|
6605
|
+
walk(program, (node) => {
|
|
6606
|
+
switch (node.type) {
|
|
6607
|
+
case "VariableDeclarator":
|
|
6608
|
+
return alone(node.init);
|
|
6609
|
+
case "AssignmentExpression":
|
|
6610
|
+
return alone(node.right);
|
|
6611
|
+
case "ReturnStatement":
|
|
6612
|
+
case "ExportDefaultDeclaration":
|
|
6613
|
+
return alone(node.argument ?? node.declaration);
|
|
6614
|
+
case "ArrowFunctionExpression":
|
|
6615
|
+
return node.expression === true ? alone(node.body) : void 0;
|
|
6616
|
+
case "ConditionalExpression":
|
|
6617
|
+
alone(node.consequent);
|
|
6618
|
+
return alone(node.alternate);
|
|
6619
|
+
case "ArrayExpression":
|
|
6620
|
+
return (node.elements ?? []).forEach((e) => alone(e ?? void 0));
|
|
6621
|
+
case "Property":
|
|
6622
|
+
case "ObjectProperty":
|
|
6623
|
+
return node.shorthand === true ? void 0 : alone(node.value);
|
|
6624
|
+
case "CallExpression":
|
|
6625
|
+
case "NewExpression":
|
|
6626
|
+
return (node.arguments ?? []).forEach((a) => alone(a));
|
|
6627
|
+
default:
|
|
6628
|
+
return;
|
|
6629
|
+
}
|
|
6589
6630
|
});
|
|
6631
|
+
const placed = (node, expression, trailing = "") => {
|
|
6632
|
+
if (trailing !== "") return `(${expression})${trailing}`;
|
|
6633
|
+
if (wholeStatement.has(node)) return expression;
|
|
6634
|
+
return standsAlone.has(node) ? expression : `(${expression})`;
|
|
6635
|
+
};
|
|
6590
6636
|
const handedToTheRunnerAsAModule = /* @__PURE__ */ new Set();
|
|
6591
6637
|
walk(program, (node) => {
|
|
6592
6638
|
const returned = mockFactoryReturn(node);
|
|
@@ -6616,7 +6662,7 @@ function applyNamedRules(source, program, fileName = "") {
|
|
|
6616
6662
|
edits.push({
|
|
6617
6663
|
start: node.start,
|
|
6618
6664
|
end: node.end,
|
|
6619
|
-
text: `
|
|
6665
|
+
text: placed(node, `await import(${argumentText(source, node)})`)
|
|
6620
6666
|
});
|
|
6621
6667
|
applied.add("jest.requireMock");
|
|
6622
6668
|
if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
|
|
@@ -6628,7 +6674,7 @@ function applyNamedRules(source, program, fileName = "") {
|
|
|
6628
6674
|
edits.push({
|
|
6629
6675
|
start: node.start,
|
|
6630
6676
|
end: node.end,
|
|
6631
|
-
text: `
|
|
6677
|
+
text: placed(node, `await vi.${asyncReplacement}(${argumentText(source, node)})`)
|
|
6632
6678
|
});
|
|
6633
6679
|
applied.add(`jest.${member}`);
|
|
6634
6680
|
if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
|
|
@@ -6641,7 +6687,7 @@ function applyNamedRules(source, program, fileName = "") {
|
|
|
6641
6687
|
edits.push({
|
|
6642
6688
|
start: node.start,
|
|
6643
6689
|
end: node.end,
|
|
6644
|
-
text:
|
|
6690
|
+
text: placed(node, `await import(${argumentText(source, node)})`, interop)
|
|
6645
6691
|
});
|
|
6646
6692
|
applied.add("require");
|
|
6647
6693
|
if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
|
|
@@ -7161,11 +7207,6 @@ ${text}` });
|
|
|
7161
7207
|
import { parseSync as parseSync6 } from "oxc-parser";
|
|
7162
7208
|
var SENTINEL_MSW_SERVER = "@hublo/sentinel/test/msw";
|
|
7163
7209
|
var MSW_SERVER_MODULE = /(^|\/)msw\/server$/;
|
|
7164
|
-
var MSW_PACKAGE = /^msw$/;
|
|
7165
|
-
function importsMswPackage(declaration) {
|
|
7166
|
-
const source = declaration.source?.value;
|
|
7167
|
-
return source !== void 0 && MSW_PACKAGE.test(source);
|
|
7168
|
-
}
|
|
7169
7210
|
function importsServer(declaration) {
|
|
7170
7211
|
return (declaration.specifiers ?? []).some((specifier) => {
|
|
7171
7212
|
const imported = specifier.imported;
|
|
@@ -7181,8 +7222,7 @@ function repointMswServer(fileName, source) {
|
|
|
7181
7222
|
if (node.type !== "ImportDeclaration") return;
|
|
7182
7223
|
const specifier = node.source;
|
|
7183
7224
|
if (specifier?.value === void 0) return;
|
|
7184
|
-
|
|
7185
|
-
if (!isServerModule && !importsMswPackage(node)) return;
|
|
7225
|
+
if (!MSW_SERVER_MODULE.test(specifier.value) || !importsServer(node)) return;
|
|
7186
7226
|
edits.push({ start: specifier.start, end: specifier.end });
|
|
7187
7227
|
});
|
|
7188
7228
|
if (edits.length === 0) return { source, repointed: false };
|
|
@@ -7570,13 +7610,16 @@ function migrateModuleTests(cwd, setupFiles = []) {
|
|
|
7570
7610
|
}
|
|
7571
7611
|
|
|
7572
7612
|
// src/roles/test/nest/generate-config.ts
|
|
7573
|
-
import { readdirSync as readdirSync7, readFileSync as readFileSync26, statSync as statSync7 } from "fs";
|
|
7613
|
+
import { existsSync as existsSync26, readdirSync as readdirSync7, readFileSync as readFileSync26, statSync as statSync7 } from "fs";
|
|
7574
7614
|
import { join as join32, relative as relative6 } from "path";
|
|
7575
7615
|
var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
|
|
7576
|
-
var
|
|
7616
|
+
var REACT_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
|
|
7617
|
+
function flavourOf(reading) {
|
|
7618
|
+
return reading.testEnvironment === "jsdom" ? "react" : "nest";
|
|
7619
|
+
}
|
|
7620
|
+
var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/jest-parity";
|
|
7577
7621
|
var SENTINEL_MSW_SETUP = "@hublo/sentinel/test/setup/msw-lifecycle";
|
|
7578
7622
|
var SENTINEL_WORKSPACE_SETUP = "@hublo/sentinel/test/setup/workspace";
|
|
7579
|
-
var NEST_TEST_CONFIG_FILE = "vitest.config.mts";
|
|
7580
7623
|
function hopsToRoot(cwd, workspaceRoot) {
|
|
7581
7624
|
const up = relative6(cwd, workspaceRoot).replaceAll("\\", "/");
|
|
7582
7625
|
return up === "" ? "." : up;
|
|
@@ -7643,14 +7686,29 @@ function readModuleTraits(cwd) {
|
|
|
7643
7686
|
return traits;
|
|
7644
7687
|
}
|
|
7645
7688
|
var withoutRootDir = (value) => value.replaceAll("<rootDir>/", "./").replaceAll("<rootDir>", ".");
|
|
7646
|
-
function
|
|
7689
|
+
function ownsMswLifecycle(cwd, reading) {
|
|
7690
|
+
const declared = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []].filter(
|
|
7691
|
+
(file) => !file.endsWith("jest.setup.after.env.js")
|
|
7692
|
+
);
|
|
7693
|
+
for (const raw of declared) {
|
|
7694
|
+
const file = join32(cwd, raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", "."));
|
|
7695
|
+
if (!existsSync26(file)) continue;
|
|
7696
|
+
try {
|
|
7697
|
+
if (/\.listen\s*\(/.test(readFileSync26(file, "utf8"))) return true;
|
|
7698
|
+
} catch {
|
|
7699
|
+
continue;
|
|
7700
|
+
}
|
|
7701
|
+
}
|
|
7702
|
+
return false;
|
|
7703
|
+
}
|
|
7704
|
+
function setupBlock(cwd, reading, msw) {
|
|
7647
7705
|
const declared = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []];
|
|
7648
7706
|
const namedTheRootSetup = declared.some((file) => file.endsWith("jest.setup.after.env.js"));
|
|
7649
7707
|
const own = declared.filter((file) => !file.endsWith("jest.setup.after.env.js")).map((file) => `'${withoutRootDir(file)}'`);
|
|
7650
7708
|
const sentinel = [
|
|
7651
7709
|
`'${SENTINEL_TEST_SETUP}'`,
|
|
7652
7710
|
...namedTheRootSetup ? [`'${SENTINEL_WORKSPACE_SETUP}'`] : [],
|
|
7653
|
-
...msw ? [`'${SENTINEL_MSW_SETUP}'`] : []
|
|
7711
|
+
...msw && !ownsMswLifecycle(cwd, reading) ? [`'${SENTINEL_MSW_SETUP}'`] : []
|
|
7654
7712
|
];
|
|
7655
7713
|
return ` setupFiles: [${[...sentinel, ...own].join(", ")}],`;
|
|
7656
7714
|
}
|
|
@@ -7697,7 +7755,8 @@ function aliasBlock(reading) {
|
|
|
7697
7755
|
if (reading.aliases === void 0 || reading.aliases.length === 0) return "";
|
|
7698
7756
|
const entries = reading.aliases.map((alias) => {
|
|
7699
7757
|
const pattern = alias.find.replaceAll(String.raw`\\`, "\\").replaceAll("/", String.raw`\/`);
|
|
7700
|
-
const
|
|
7758
|
+
const deadToken = /<rootdir>/i.test(alias.replacement) && !alias.replacement.includes("<rootDir>");
|
|
7759
|
+
const target = isPackage(alias.replacement) ? `'${alias.replacement}'` : deadToken ? `'${alias.replacement}'` : `path.resolve(import.meta.dirname, '${withoutRootDir(alias.replacement)}')`;
|
|
7701
7760
|
return ` { find: /${pattern}/, replacement: ${target} },`;
|
|
7702
7761
|
});
|
|
7703
7762
|
return [" resolve: {", " alias: [", ...entries, " ],", " },"].join("\n");
|
|
@@ -7712,7 +7771,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
|
|
|
7712
7771
|
const up = hopsToRoot(cwd, workspaceRoot);
|
|
7713
7772
|
const test = [
|
|
7714
7773
|
environmentBlock(reading),
|
|
7715
|
-
setupBlock(reading, traits.msw),
|
|
7774
|
+
setupBlock(cwd, reading, traits.msw),
|
|
7716
7775
|
envBlock(reading),
|
|
7717
7776
|
timeoutBlock(reading),
|
|
7718
7777
|
concurrencyBlock(cwd, reading, target),
|
|
@@ -7727,7 +7786,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
|
|
|
7727
7786
|
`import path from 'node:path'`,
|
|
7728
7787
|
"",
|
|
7729
7788
|
...needsDefaults ? [`import { configDefaults } from 'vitest/config'`, ""] : [],
|
|
7730
|
-
`import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
|
|
7789
|
+
flavourOf(reading) === "react" ? `import { defineConfig, reactTestConfig } from '${REACT_TEST_PRESET_SPECIFIER}'` : `import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
|
|
7731
7790
|
].join("\n");
|
|
7732
7791
|
const overrides = [aliasBlock(reading), " test: {", ...test, " },"].filter(
|
|
7733
7792
|
(line) => line !== ""
|
|
@@ -7738,7 +7797,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
|
|
|
7738
7797
|
source: `${imports}
|
|
7739
7798
|
|
|
7740
7799
|
export default defineConfig(
|
|
7741
|
-
nestTestConfig({
|
|
7800
|
+
${flavourOf(reading) === "react" ? "reactTestConfig" : "nestTestConfig"}({
|
|
7742
7801
|
root: import.meta.dirname,
|
|
7743
7802
|
workspaceRoot: path.resolve(import.meta.dirname, '${up}'),${decoratorLine}
|
|
7744
7803
|
overrides: {
|
|
@@ -7751,7 +7810,7 @@ ${overrides.join("\n")}
|
|
|
7751
7810
|
}
|
|
7752
7811
|
|
|
7753
7812
|
// src/roles/test/nest/read-jest-config.ts
|
|
7754
|
-
import { existsSync as
|
|
7813
|
+
import { existsSync as existsSync27, readFileSync as readFileSync27 } from "fs";
|
|
7755
7814
|
import { join as join33 } from "path";
|
|
7756
7815
|
var CARRIED = [
|
|
7757
7816
|
"testEnvironment",
|
|
@@ -7942,7 +8001,7 @@ function extendedConfig(source, cwd) {
|
|
|
7942
8001
|
(name) => name.replace(/^\.\//, "")
|
|
7943
8002
|
);
|
|
7944
8003
|
const file = candidates.find(
|
|
7945
|
-
(name) => /^jest[\w.-]*\.(ts|js)$/.test(name) &&
|
|
8004
|
+
(name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync27(join33(cwd, name))
|
|
7946
8005
|
);
|
|
7947
8006
|
if (file === void 0) continue;
|
|
7948
8007
|
if (!new RegExp(String.raw`\{\s*\.\.\.${binding}\b`).test(source)) continue;
|
|
@@ -7969,8 +8028,8 @@ function sideEffects(source, extended) {
|
|
|
7969
8028
|
return found;
|
|
7970
8029
|
}
|
|
7971
8030
|
function readJestConfig(cwd, named) {
|
|
7972
|
-
const file = named ?? JEST_CONFIG_FILES.find((name) =>
|
|
7973
|
-
if (file === void 0 || !
|
|
8031
|
+
const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync27(join33(cwd, name)));
|
|
8032
|
+
if (file === void 0 || !existsSync27(join33(cwd, file))) return void 0;
|
|
7974
8033
|
let source;
|
|
7975
8034
|
try {
|
|
7976
8035
|
source = readFileSync27(join33(cwd, file), "utf8");
|
|
@@ -8054,7 +8113,7 @@ function readJestConfig(cwd, named) {
|
|
|
8054
8113
|
}
|
|
8055
8114
|
|
|
8056
8115
|
// src/roles/test/nest/run-modes.ts
|
|
8057
|
-
import { existsSync as
|
|
8116
|
+
import { existsSync as existsSync28 } from "fs";
|
|
8058
8117
|
import { basename as basename2, join as join34 } from "path";
|
|
8059
8118
|
var JEST_EXECUTOR = "@nx/jest:jest";
|
|
8060
8119
|
var PRIMARY_CONFIG_FILE = "vitest.config.mts";
|
|
@@ -8076,7 +8135,7 @@ function runModes(cwd, fallback) {
|
|
|
8076
8135
|
const jestConfig = entry.options?.jestConfig;
|
|
8077
8136
|
if (jestConfig === void 0) continue;
|
|
8078
8137
|
const name = configName(jestConfig);
|
|
8079
|
-
if (!
|
|
8138
|
+
if (!existsSync28(join34(cwd, name))) continue;
|
|
8080
8139
|
declared.push({
|
|
8081
8140
|
target,
|
|
8082
8141
|
jestConfig: name,
|
|
@@ -8105,7 +8164,7 @@ var SPEC_TSCONFIG = "tsconfig.spec.json";
|
|
|
8105
8164
|
var VITEST_GLOBAL_TYPES = "vitest/globals";
|
|
8106
8165
|
function specTypesOperation(cwd) {
|
|
8107
8166
|
const path = join35(cwd, SPEC_TSCONFIG);
|
|
8108
|
-
if (!
|
|
8167
|
+
if (!existsSync29(path)) return void 0;
|
|
8109
8168
|
let config;
|
|
8110
8169
|
try {
|
|
8111
8170
|
config = parseJsonc(
|
|
@@ -8173,8 +8232,9 @@ function planJestMigration(context, adoption) {
|
|
|
8173
8232
|
`${migration.blockers.length} thing(s) in this module's test files need a human before it can move. Nothing was written, because a config without its files is a suite calling jest.* under Vitest.${listed}`
|
|
8174
8233
|
);
|
|
8175
8234
|
}
|
|
8176
|
-
const
|
|
8177
|
-
const
|
|
8235
|
+
const fromProject = projectTargets(context.cwd) ?? {};
|
|
8236
|
+
const fromManifest = manifestTargets(context.cwd);
|
|
8237
|
+
const existingFor = (name) => fromProject[name] ?? fromManifest[name];
|
|
8178
8238
|
const siblings = siblingScriptRewrites(context.cwd);
|
|
8179
8239
|
const specTypes = specTypesOperation(context.cwd);
|
|
8180
8240
|
const scripts = Object.fromEntries(
|
|
@@ -8208,9 +8268,10 @@ function planJestMigration(context, adoption) {
|
|
|
8208
8268
|
(name) => ({ kind: "delete", path: name })
|
|
8209
8269
|
)
|
|
8210
8270
|
];
|
|
8271
|
+
const escaping = (prepared[0]?.reading.aliases ?? []).map((alias) => alias.replacement).filter((replacement) => replacement.includes(".."));
|
|
8211
8272
|
const notes = [
|
|
8212
8273
|
`${migration.files} test file(s) migrated from jest to Vitest, ${migration.renamed} site(s) rewritten${migration.hoisted > 0 ? `, ${migration.hoisted} of them moving a mock factory's captures into vi.hoisted` : ""}.`,
|
|
8213
|
-
`${
|
|
8274
|
+
`${prepared.map(({ mode }) => mode.vitestConfig).join(", ")} written from what ${prepared.map(({ mode }) => mode.jestConfig).join(", ")} declared, and that config deleted.` + (escaping.length === 0 ? ` Every specifier in the new one is a package name: nothing in it reaches outside this module.` : ` \u26A0\uFE0F ${escaping.length} alias(es) carried from that config reach OUTSIDE this module: ${escaping.slice(0, 3).join(", ")}. They came from its \`moduleNameMapper\` and are kept as they were, but a module that reaches out cannot be moved on its own.`)
|
|
8214
8275
|
];
|
|
8215
8276
|
if (specTypes !== void 0) {
|
|
8216
8277
|
notes.push(
|
|
@@ -8218,7 +8279,7 @@ function planJestMigration(context, adoption) {
|
|
|
8218
8279
|
);
|
|
8219
8280
|
}
|
|
8220
8281
|
notes.push(
|
|
8221
|
-
`run this module's own lint --fix
|
|
8282
|
+
`run this module's own formatter (or lint --fix) on what this wrote, after installing. Two things need it: the Vitest type imports are written beside the existing ones, and a rewritten call is often SHORTER than the one it replaced, so the line breaks around it are no longer the ones your formatter would pick. Neither is a defect, and both fail a format check. Layout is a repository setting, so sentinel leaves it to the tool that owns it.`
|
|
8222
8283
|
);
|
|
8223
8284
|
notes.push(
|
|
8224
8285
|
`then run this module's TYPECHECK, not only its tests. The migration rewrites types as well as calls, and a wrong type is invisible to a green suite: measured on a real module, \`--run --test\` passed while \`--run --typescript\` reported ten errors it had not had before.`
|
|
@@ -8308,7 +8369,7 @@ function plan3(context) {
|
|
|
8308
8369
|
`${configFile} now imports ${rewrite.moved.join(", ")} from sentinel. Only the import lines changed: every alias, setup file, timeout and coverage path is untouched, so this suite runs what it ran before.`
|
|
8309
8370
|
);
|
|
8310
8371
|
}
|
|
8311
|
-
const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
|
|
8372
|
+
const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME] ?? manifestTargets(context.cwd)[TEST_SCRIPT_NAME];
|
|
8312
8373
|
const outputs = existing?.outputs ?? [];
|
|
8313
8374
|
const dependsOn = existing?.dependsOn ?? [];
|
|
8314
8375
|
const configurations = existing?.configurations ?? {};
|
|
@@ -8363,6 +8424,329 @@ function plan3(context) {
|
|
|
8363
8424
|
return { operations, notes };
|
|
8364
8425
|
}
|
|
8365
8426
|
|
|
8427
|
+
// src/roles/test/reference.ts
|
|
8428
|
+
import { execFileSync } from "child_process";
|
|
8429
|
+
import { createHash } from "crypto";
|
|
8430
|
+
import { existsSync as existsSync30, mkdirSync, mkdtempSync, readFileSync as readFileSync29, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
8431
|
+
import { tmpdir } from "os";
|
|
8432
|
+
import { join as join36, resolve as resolve5 } from "path";
|
|
8433
|
+
|
|
8434
|
+
// src/roles/test/baseline.ts
|
|
8435
|
+
function readSnapshot(report) {
|
|
8436
|
+
const files = report?.testResults ?? [];
|
|
8437
|
+
const passing = [];
|
|
8438
|
+
const failing = [];
|
|
8439
|
+
const unloadable = [];
|
|
8440
|
+
for (const file of files) {
|
|
8441
|
+
const assertions = file?.assertionResults ?? [];
|
|
8442
|
+
if (file?.status === "failed" && assertions.length === 0) {
|
|
8443
|
+
unloadable.push(typeof file.name === "string" ? file.name : "an unnamed file");
|
|
8444
|
+
}
|
|
8445
|
+
for (const assertion of assertions) {
|
|
8446
|
+
const name = assertion?.fullName;
|
|
8447
|
+
if (typeof name !== "string" || name === "") continue;
|
|
8448
|
+
if (assertion.status === "passed") passing.push(name);
|
|
8449
|
+
else if (assertion.status === "failed") failing.push(name);
|
|
8450
|
+
}
|
|
8451
|
+
}
|
|
8452
|
+
const claimed = report?.numFailedTests;
|
|
8453
|
+
return {
|
|
8454
|
+
files: files.length,
|
|
8455
|
+
passing: passing.sort(),
|
|
8456
|
+
failing: failing.sort(),
|
|
8457
|
+
unloadable: unloadable.sort(),
|
|
8458
|
+
claimedFailing: typeof claimed === "number" ? claimed : failing.length
|
|
8459
|
+
};
|
|
8460
|
+
}
|
|
8461
|
+
function difference(left, right) {
|
|
8462
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
8463
|
+
for (const name of right) remaining.set(name, (remaining.get(name) ?? 0) + 1);
|
|
8464
|
+
const missing = [];
|
|
8465
|
+
for (const name of left) {
|
|
8466
|
+
const left_ = remaining.get(name) ?? 0;
|
|
8467
|
+
if (left_ === 0) {
|
|
8468
|
+
missing.push(name);
|
|
8469
|
+
continue;
|
|
8470
|
+
}
|
|
8471
|
+
remaining.set(name, left_ - 1);
|
|
8472
|
+
}
|
|
8473
|
+
return missing.sort();
|
|
8474
|
+
}
|
|
8475
|
+
var withoutRenderingQuotes = (name) => name.replaceAll(String.raw`\'`, "'").replaceAll(String.raw`\"`, '"').replaceAll(/['"`]\s*\+\s*['"`]/g, "").replaceAll(/(^|[\s:,[{(])(''|"")($|[\s,)\]}])/g, "$1<empty-string>$3").replaceAll(/['"`]/g, "");
|
|
8476
|
+
function withoutRenderingNoise(name) {
|
|
8477
|
+
const spelled = withSortedKeys(
|
|
8478
|
+
withoutUndefinedEntries(
|
|
8479
|
+
withoutErrorRendering(
|
|
8480
|
+
withoutNaN(
|
|
8481
|
+
withoutTimestamps(withoutRenderingQuotes(withoutClassBodies(withoutStackFrames(name))))
|
|
8482
|
+
)
|
|
8483
|
+
)
|
|
8484
|
+
)
|
|
8485
|
+
);
|
|
8486
|
+
return withoutArrayBrackets(
|
|
8487
|
+
spelled.replaceAll(/\[\s+/g, "[").replaceAll(/\s+\]/g, "]").replaceAll(/\{\s+/g, "{").replaceAll(/\s+\}/g, "}").replaceAll(/(?<=[[{,\s])\+0\b/g, "0").replaceAll(/:\s+/g, ":").replaceAll(/,\s+/g, ",")
|
|
8488
|
+
);
|
|
8489
|
+
}
|
|
8490
|
+
function withoutArrayBrackets(name) {
|
|
8491
|
+
return name.replaceAll(
|
|
8492
|
+
/\[([^[\]]*)\]/g,
|
|
8493
|
+
(match, inner) => (
|
|
8494
|
+
// `[Object]` and `[Array]` are the formatter saying it STOPPED, not an array literal.
|
|
8495
|
+
// Stripping their brackets would erase the marker `collapsedOf` pairs on.
|
|
8496
|
+
/^(?:Object|Array)$/.test(inner) ? match : inner.replaceAll(/,\s+/g, ",")
|
|
8497
|
+
)
|
|
8498
|
+
);
|
|
8499
|
+
}
|
|
8500
|
+
var withoutClassBodies = (name) => name.replaceAll(
|
|
8501
|
+
/class\s+([A-Za-z_$][\w$]*)\s*\{\s*\}/g,
|
|
8502
|
+
(_match, className) => `class ${className}`
|
|
8503
|
+
).replaceAll(
|
|
8504
|
+
/class\s+([A-Za-z_$][\w$]*)[\s\S]*?\n\}/g,
|
|
8505
|
+
(_match, className) => `class ${className}`
|
|
8506
|
+
);
|
|
8507
|
+
var withoutTimestamps = (name) => name.replaceAll(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, "<instant>");
|
|
8508
|
+
var withoutNaN = (name) => name.replaceAll(/\bNaN\b/g, "null");
|
|
8509
|
+
function entriesOf(inner) {
|
|
8510
|
+
const parts = [];
|
|
8511
|
+
let depth = 0;
|
|
8512
|
+
let start = 0;
|
|
8513
|
+
for (let index = 0; index < inner.length; index += 1) {
|
|
8514
|
+
const character = inner[index];
|
|
8515
|
+
if (character === "[" || character === "(") depth += 1;
|
|
8516
|
+
else if (character === "]" || character === ")") depth -= 1;
|
|
8517
|
+
else if (character === "," && depth === 0) {
|
|
8518
|
+
parts.push(inner.slice(start, index));
|
|
8519
|
+
start = index + 1;
|
|
8520
|
+
}
|
|
8521
|
+
if (depth < 0) return void 0;
|
|
8522
|
+
}
|
|
8523
|
+
if (depth !== 0) return void 0;
|
|
8524
|
+
parts.push(inner.slice(start));
|
|
8525
|
+
return parts.map((part) => part.trim()).filter(Boolean);
|
|
8526
|
+
}
|
|
8527
|
+
var sortedGroup = (match, inner) => {
|
|
8528
|
+
if (!inner.includes(":")) return match;
|
|
8529
|
+
const parts = entriesOf(inner);
|
|
8530
|
+
if (parts === void 0) return match;
|
|
8531
|
+
if (parts.length < 2 || !parts.every((part) => part.includes(":"))) return match;
|
|
8532
|
+
return `{${[...parts].sort().join(",")}}`;
|
|
8533
|
+
};
|
|
8534
|
+
var HELD = "\u241F";
|
|
8535
|
+
var withSortedKeys = (name) => {
|
|
8536
|
+
const held = [];
|
|
8537
|
+
let current = name;
|
|
8538
|
+
for (let depth = 0; depth < 24; depth += 1) {
|
|
8539
|
+
const next = current.replaceAll(/\{([^{}]*)\}/g, (match, inner) => {
|
|
8540
|
+
held.push(sortedGroup(match, inner));
|
|
8541
|
+
return `${HELD}${held.length - 1}${HELD}`;
|
|
8542
|
+
});
|
|
8543
|
+
if (next === current) break;
|
|
8544
|
+
current = next;
|
|
8545
|
+
}
|
|
8546
|
+
for (let depth = 0; depth < 24 && current.includes(HELD); depth += 1) {
|
|
8547
|
+
current = current.replaceAll(
|
|
8548
|
+
new RegExp(`${HELD}(\\d+)${HELD}`, "g"),
|
|
8549
|
+
(_match, index) => held[Number(index)]
|
|
8550
|
+
);
|
|
8551
|
+
}
|
|
8552
|
+
return current;
|
|
8553
|
+
};
|
|
8554
|
+
var withoutStackFrames = (name) => {
|
|
8555
|
+
const frame = name.search(/\n\s+at\s/);
|
|
8556
|
+
return frame === -1 ? name : name.slice(0, frame);
|
|
8557
|
+
};
|
|
8558
|
+
var withoutErrorRendering = (name) => name.replaceAll(/\[[A-Za-z]*(?:Error|Exception):[^\][]*\]/g, "{}");
|
|
8559
|
+
var withoutUndefinedEntries = (name) => name.replaceAll(/([{,])\s*[A-Za-z_$][\w$]*\s*:\s*undefined\s*(?=[,}])/g, "$1").replaceAll(/\{\s*,/g, "{").replaceAll(/,\s*\}/g, "}").replaceAll(/\{\s*\}/g, "{}");
|
|
8560
|
+
function collapsedOf(candidate, name) {
|
|
8561
|
+
if (!/\[(?:Object|Array)\]/.test(candidate)) return false;
|
|
8562
|
+
const pattern = candidate.split(/\[(?:Object|Array)\]/).map((part) => part.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
8563
|
+
return new RegExp(`^${pattern}$`, "s").test(name);
|
|
8564
|
+
}
|
|
8565
|
+
function truncationOf(candidate, name) {
|
|
8566
|
+
const cut = candidate.indexOf("\u2026");
|
|
8567
|
+
if (cut === -1) return false;
|
|
8568
|
+
const head = candidate.slice(0, cut);
|
|
8569
|
+
const tail = candidate.slice(cut + 1);
|
|
8570
|
+
return name.startsWith(head) && name.endsWith(tail);
|
|
8571
|
+
}
|
|
8572
|
+
function pairRenamed(missing, added) {
|
|
8573
|
+
const remaining = [...added];
|
|
8574
|
+
const renamed = [];
|
|
8575
|
+
const unmatched = [];
|
|
8576
|
+
for (const name of missing) {
|
|
8577
|
+
const normalised = withoutRenderingNoise(name);
|
|
8578
|
+
const at = remaining.findIndex((candidate) => withoutRenderingNoise(candidate) === normalised);
|
|
8579
|
+
if (at === -1) {
|
|
8580
|
+
unmatched.push(name);
|
|
8581
|
+
continue;
|
|
8582
|
+
}
|
|
8583
|
+
renamed.push({ before: name, after: remaining[at] });
|
|
8584
|
+
remaining.splice(at, 1);
|
|
8585
|
+
}
|
|
8586
|
+
const lost = [];
|
|
8587
|
+
for (const name of unmatched) {
|
|
8588
|
+
const normalised = withoutRenderingNoise(name);
|
|
8589
|
+
const at = remaining.findIndex(
|
|
8590
|
+
(candidate) => truncationOf(withoutRenderingNoise(candidate), normalised) || collapsedOf(normalised, withoutRenderingNoise(candidate))
|
|
8591
|
+
);
|
|
8592
|
+
if (at === -1) {
|
|
8593
|
+
lost.push(name);
|
|
8594
|
+
continue;
|
|
8595
|
+
}
|
|
8596
|
+
renamed.push({ before: name, after: remaining[at] });
|
|
8597
|
+
remaining.splice(at, 1);
|
|
8598
|
+
}
|
|
8599
|
+
return { renamed, missing: lost, added: remaining };
|
|
8600
|
+
}
|
|
8601
|
+
function compareSnapshots(before, after) {
|
|
8602
|
+
const beforeAll = [...before.passing, ...before.failing];
|
|
8603
|
+
const afterAll = [...after.passing, ...after.failing];
|
|
8604
|
+
const paired = pairRenamed(difference(beforeAll, afterAll), difference(afterAll, beforeAll));
|
|
8605
|
+
const renamedBefore = new Set(paired.renamed.map((pair) => pair.before));
|
|
8606
|
+
const regressed = difference(before.passing, after.passing).filter(
|
|
8607
|
+
(name) => !paired.missing.includes(name) && !renamedBefore.has(name)
|
|
8608
|
+
);
|
|
8609
|
+
const failedBefore = new Set(before.failing);
|
|
8610
|
+
const recovered = after.passing.filter((name) => failedBefore.has(name));
|
|
8611
|
+
return {
|
|
8612
|
+
identical: paired.missing.length === 0 && paired.added.length === 0 && regressed.length === 0,
|
|
8613
|
+
missing: paired.missing,
|
|
8614
|
+
added: paired.added,
|
|
8615
|
+
regressed,
|
|
8616
|
+
renamed: paired.renamed,
|
|
8617
|
+
recovered,
|
|
8618
|
+
alreadyFailing: before.failing.length
|
|
8619
|
+
};
|
|
8620
|
+
}
|
|
8621
|
+
function unusableAsBaseline(snapshot) {
|
|
8622
|
+
if (snapshot.unloadable.length > 0) {
|
|
8623
|
+
return `${snapshot.unloadable.length} file(s) in the reference run did not load, so they reported no test at all and the run is short by however many they hold: ${snapshot.unloadable.slice(0, 3).join(", ")}. A reference that quietly lost tests cannot prove anything about the run after it`;
|
|
8624
|
+
}
|
|
8625
|
+
if (snapshot.claimedFailing > snapshot.failing.length) {
|
|
8626
|
+
return `the reference run says ${snapshot.claimedFailing} test(s) failed and names only ${snapshot.failing.length} of them, so the comparison has nothing to pair the rest against and would report them as invented by the migration`;
|
|
8627
|
+
}
|
|
8628
|
+
if (snapshot.passing.length === 0) {
|
|
8629
|
+
return `the reference run reports no passing test at all, so a green run after would prove nothing: there is no claim to preserve`;
|
|
8630
|
+
}
|
|
8631
|
+
return void 0;
|
|
8632
|
+
}
|
|
8633
|
+
function renamedExample(before, after) {
|
|
8634
|
+
let shared = 0;
|
|
8635
|
+
while (shared < before.length && shared < after.length && before[shared] === after[shared]) {
|
|
8636
|
+
shared += 1;
|
|
8637
|
+
}
|
|
8638
|
+
const boundary = before.lastIndexOf(" ", shared);
|
|
8639
|
+
const from = boundary > 0 ? boundary + 1 : 0;
|
|
8640
|
+
const ellipsis = from > 0 ? "\u2026" : "";
|
|
8641
|
+
return `"${ellipsis}${before.slice(from)}" -> "${ellipsis}${after.slice(from)}"`;
|
|
8642
|
+
}
|
|
8643
|
+
function describeComparison(comparison) {
|
|
8644
|
+
const firstRename = comparison.renamed[0];
|
|
8645
|
+
const renamedNote = firstRename === void 0 ? "" : ` ${comparison.renamed.length} test(s) kept their identity and changed name, because the runners render an each title's interpolated value differently (e.g. ${renamedExample(firstRename.before, firstRename.after)}).`;
|
|
8646
|
+
const alreadyRedNote = comparison.alreadyFailing > 0 ? ` ${comparison.alreadyFailing} test(s) were already failing before the migration and are outside this comparison, which covers only what passed.` : "";
|
|
8647
|
+
const recoveredNote = comparison.recovered.length > 0 ? ` ${comparison.recovered.length} test(s) were failing before and pass now.` : "";
|
|
8648
|
+
if (comparison.identical) {
|
|
8649
|
+
return `every test that ran before runs and passes now.${recoveredNote}${renamedNote}${alreadyRedNote}`;
|
|
8650
|
+
}
|
|
8651
|
+
const parts = [];
|
|
8652
|
+
if (comparison.recovered.length > 0) {
|
|
8653
|
+
parts.push(`${comparison.recovered.length} test(s) were failing before and pass now`);
|
|
8654
|
+
}
|
|
8655
|
+
if (comparison.missing.length > 0) {
|
|
8656
|
+
parts.push(
|
|
8657
|
+
`${comparison.missing.length} test(s) no longer exist: ${comparison.missing.slice(0, 3).join(", ")}`
|
|
8658
|
+
);
|
|
8659
|
+
}
|
|
8660
|
+
if (comparison.added.length > 0) {
|
|
8661
|
+
parts.push(
|
|
8662
|
+
`${comparison.added.length} test(s) appeared, which a migration should not do: ${comparison.added.slice(0, 3).join(", ")}`
|
|
8663
|
+
);
|
|
8664
|
+
}
|
|
8665
|
+
if (comparison.regressed.length > 0) {
|
|
8666
|
+
parts.push(
|
|
8667
|
+
`${comparison.regressed.length} test(s) passed before and do not now: ${comparison.regressed.slice(0, 3).join(", ")}`
|
|
8668
|
+
);
|
|
8669
|
+
}
|
|
8670
|
+
return `${parts.join("; ")}.${renamedNote}${alreadyRedNote}`;
|
|
8671
|
+
}
|
|
8672
|
+
|
|
8673
|
+
// src/roles/test/reference.ts
|
|
8674
|
+
function referencePath(cwd) {
|
|
8675
|
+
const key = createHash("sha256").update(resolve5(cwd)).digest("hex").slice(0, 16);
|
|
8676
|
+
return join36(tmpdir(), "sentinel-test-reference", `${key}.json`);
|
|
8677
|
+
}
|
|
8678
|
+
function runJest(cwd, config) {
|
|
8679
|
+
const bin = resolveBin(cwd, "jest");
|
|
8680
|
+
if (bin === void 0) return void 0;
|
|
8681
|
+
const root = findWorkspaceRoot(cwd) ?? cwd;
|
|
8682
|
+
const out = join36(mkdtempSync(join36(tmpdir(), "sentinel-reference-")), "jest.json");
|
|
8683
|
+
try {
|
|
8684
|
+
execFileSync(bin, ["-c", join36(cwd, config), "--json", `--outputFile=${out}`, "--silent"], {
|
|
8685
|
+
cwd: root,
|
|
8686
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
8687
|
+
maxBuffer: 1 << 28
|
|
8688
|
+
});
|
|
8689
|
+
} catch {
|
|
8690
|
+
}
|
|
8691
|
+
if (!existsSync30(out)) return void 0;
|
|
8692
|
+
try {
|
|
8693
|
+
return JSON.parse(readFileSync29(out, "utf8"));
|
|
8694
|
+
} catch {
|
|
8695
|
+
return void 0;
|
|
8696
|
+
}
|
|
8697
|
+
}
|
|
8698
|
+
function captureReference(cwd, config) {
|
|
8699
|
+
const report = runJest(cwd, config);
|
|
8700
|
+
if (report === void 0) {
|
|
8701
|
+
return {
|
|
8702
|
+
captured: false,
|
|
8703
|
+
message: `could not run jest to record a reference, so this migration cannot be PROVED. It is still applied: a module whose jest run is broken can move, it just cannot be compared.`
|
|
8704
|
+
};
|
|
8705
|
+
}
|
|
8706
|
+
const snapshot = readSnapshot(report);
|
|
8707
|
+
const unusable = unusableAsBaseline(snapshot);
|
|
8708
|
+
if (unusable !== void 0) {
|
|
8709
|
+
return {
|
|
8710
|
+
captured: false,
|
|
8711
|
+
message: `no reference recorded: ${unusable} The migration is applied anyway, unproved.`
|
|
8712
|
+
};
|
|
8713
|
+
}
|
|
8714
|
+
const stored = {
|
|
8715
|
+
runner: "jest",
|
|
8716
|
+
config,
|
|
8717
|
+
takenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8718
|
+
snapshot
|
|
8719
|
+
};
|
|
8720
|
+
const path = referencePath(cwd);
|
|
8721
|
+
mkdirSync(join36(path, ".."), { recursive: true });
|
|
8722
|
+
writeFileSync3(path, `${JSON.stringify(stored, void 0, 2)}
|
|
8723
|
+
`);
|
|
8724
|
+
return {
|
|
8725
|
+
captured: true,
|
|
8726
|
+
message: `recorded ${snapshot.passing.length} passing test name(s) from ${config} as the reference. The next \`sentinel --run --test\` compares against it.`
|
|
8727
|
+
};
|
|
8728
|
+
}
|
|
8729
|
+
function hasReference(cwd) {
|
|
8730
|
+
return existsSync30(referencePath(cwd));
|
|
8731
|
+
}
|
|
8732
|
+
function compareAgainstReference(cwd, report) {
|
|
8733
|
+
const path = referencePath(cwd);
|
|
8734
|
+
let stored;
|
|
8735
|
+
try {
|
|
8736
|
+
stored = JSON.parse(readFileSync29(path, "utf8"));
|
|
8737
|
+
} catch {
|
|
8738
|
+
rmSync3(path, { force: true });
|
|
8739
|
+
return { ok: true, message: `the recorded reference could not be read, so nothing was proved.` };
|
|
8740
|
+
}
|
|
8741
|
+
const comparison = compareSnapshots(stored.snapshot, readSnapshot(report));
|
|
8742
|
+
if (comparison.identical) rmSync3(path, { force: true });
|
|
8743
|
+
const kept = comparison.identical ? "" : ` The reference is KEPT until a run passes it, so this does not go green by running again. Fix the suite and re-run, or delete ${path} to abandon the proof on purpose.`;
|
|
8744
|
+
return {
|
|
8745
|
+
ok: comparison.identical,
|
|
8746
|
+
message: `${describeComparison(comparison)} (reference: ${stored.config}, ${stored.takenAt})${kept}`
|
|
8747
|
+
};
|
|
8748
|
+
}
|
|
8749
|
+
|
|
8366
8750
|
// src/roles/test/adapters/vitest/vitest.adapter.ts
|
|
8367
8751
|
function resolveVitest(cwd) {
|
|
8368
8752
|
const fromSentinel = binFromOwnInstall("vitest", "vitest");
|
|
@@ -8376,6 +8760,22 @@ var VitestAdapter = class extends BaseAdapter {
|
|
|
8376
8760
|
appliesTo(_preset) {
|
|
8377
8761
|
return true;
|
|
8378
8762
|
}
|
|
8763
|
+
/**
|
|
8764
|
+
* Record what this module's tests do TODAY, at the last moment it is still true.
|
|
8765
|
+
*
|
|
8766
|
+
* The plan about to be applied deletes the jest config, and with it any way of asking again. A
|
|
8767
|
+
* migration that cannot be compared to what came before is a migration nobody can check, which
|
|
8768
|
+
* is the whole reason this role has a baseline gate at all.
|
|
8769
|
+
*/
|
|
8770
|
+
async beforeInit(ctx) {
|
|
8771
|
+
const adoption = readTestAdoption(ctx.cwd);
|
|
8772
|
+
if (adoption.state !== "jest") return Promise.resolve();
|
|
8773
|
+
const [config] = adoption.jestConfigs;
|
|
8774
|
+
if (config === void 0) return Promise.resolve();
|
|
8775
|
+
this.say(`recording the jest reference before migrating...`);
|
|
8776
|
+
this.say(captureReference(ctx.cwd, config).message);
|
|
8777
|
+
return Promise.resolve();
|
|
8778
|
+
}
|
|
8379
8779
|
plan(context) {
|
|
8380
8780
|
return plan3(context);
|
|
8381
8781
|
}
|
|
@@ -8422,13 +8822,31 @@ var VitestAdapter = class extends BaseAdapter {
|
|
|
8422
8822
|
}
|
|
8423
8823
|
const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
|
|
8424
8824
|
const watching = options.includes("--watch") || options.includes("-w");
|
|
8425
|
-
const
|
|
8825
|
+
const proving = !watching && hasReference(ctx.cwd);
|
|
8826
|
+
const reportFile = proving ? join37(mkdtempSync2(join37(tmpdir2(), "sentinel-run-")), "vitest.json") : void 0;
|
|
8827
|
+
const reporters = reportFile === void 0 ? [] : ["--reporter=default", "--reporter=json", `--outputFile=${reportFile}`];
|
|
8828
|
+
const argv = watching ? [...options, ...paths] : ["run", ...reporters, ...options, ...paths];
|
|
8426
8829
|
const result = spawnSync5(bin, argv, { cwd: ctx.cwd, stdio: "inherit" });
|
|
8427
8830
|
if (result.error) {
|
|
8428
8831
|
this.say(`could not run vitest (${result.error.message})`);
|
|
8429
8832
|
return Promise.resolve({ ok: false, code: 1 });
|
|
8430
8833
|
}
|
|
8431
8834
|
const code = result.status ?? 1;
|
|
8835
|
+
if (reportFile !== void 0) {
|
|
8836
|
+
let report;
|
|
8837
|
+
try {
|
|
8838
|
+
report = JSON.parse(readFileSync30(reportFile, "utf8"));
|
|
8839
|
+
} catch {
|
|
8840
|
+
report = void 0;
|
|
8841
|
+
}
|
|
8842
|
+
if (report === void 0) {
|
|
8843
|
+
this.say(`vitest wrote no report, so this run proved nothing against the reference.`);
|
|
8844
|
+
} else {
|
|
8845
|
+
const outcome = compareAgainstReference(ctx.cwd, report);
|
|
8846
|
+
this.say(outcome.message);
|
|
8847
|
+
if (!outcome.ok) return Promise.resolve({ ok: false, code: code === 0 ? 1 : code });
|
|
8848
|
+
}
|
|
8849
|
+
}
|
|
8432
8850
|
return Promise.resolve({ ok: code === 0, code });
|
|
8433
8851
|
}
|
|
8434
8852
|
say(message) {
|
|
@@ -8445,9 +8863,9 @@ function registerTest() {
|
|
|
8445
8863
|
|
|
8446
8864
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
8447
8865
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
8448
|
-
import { existsSync as
|
|
8866
|
+
import { existsSync as existsSync31, readFileSync as readFileSync32 } from "fs";
|
|
8449
8867
|
import { createRequire as createRequire6 } from "module";
|
|
8450
|
-
import { join as
|
|
8868
|
+
import { join as join39 } from "path";
|
|
8451
8869
|
|
|
8452
8870
|
// src/roles/typescript/presets/nest.json
|
|
8453
8871
|
var nest_default2 = {
|
|
@@ -8628,8 +9046,8 @@ function readTsconfigAdoption(cwd) {
|
|
|
8628
9046
|
}
|
|
8629
9047
|
|
|
8630
9048
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
8631
|
-
import { readFileSync as
|
|
8632
|
-
import { join as
|
|
9049
|
+
import { readFileSync as readFileSync31 } from "fs";
|
|
9050
|
+
import { join as join38 } from "path";
|
|
8633
9051
|
|
|
8634
9052
|
// src/roles/typescript/typecheck-script.ts
|
|
8635
9053
|
var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
|
|
@@ -8722,7 +9140,7 @@ function planAdoption(context) {
|
|
|
8722
9140
|
};
|
|
8723
9141
|
}
|
|
8724
9142
|
const existing = parseJsonc(
|
|
8725
|
-
|
|
9143
|
+
readFileSync31(join38(context.cwd, target.path), "utf8"),
|
|
8726
9144
|
target.path
|
|
8727
9145
|
);
|
|
8728
9146
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -8860,7 +9278,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
8860
9278
|
let chain;
|
|
8861
9279
|
try {
|
|
8862
9280
|
const parsed = parseJsonc(
|
|
8863
|
-
|
|
9281
|
+
readFileSync32(join39(cwd, target.path), "utf8"),
|
|
8864
9282
|
target.path
|
|
8865
9283
|
);
|
|
8866
9284
|
chain = parsed.extends;
|
|
@@ -8873,7 +9291,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
8873
9291
|
);
|
|
8874
9292
|
if (preset === void 0) return void 0;
|
|
8875
9293
|
try {
|
|
8876
|
-
createRequire6(
|
|
9294
|
+
createRequire6(join39(cwd, "noop.js")).resolve(preset);
|
|
8877
9295
|
return void 0;
|
|
8878
9296
|
} catch {
|
|
8879
9297
|
return preset;
|
|
@@ -8969,7 +9387,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
8969
9387
|
referencedProjects(cwd, config) {
|
|
8970
9388
|
try {
|
|
8971
9389
|
const parsed = parseJsonc(
|
|
8972
|
-
|
|
9390
|
+
readFileSync32(join39(cwd, config), "utf8"),
|
|
8973
9391
|
config
|
|
8974
9392
|
);
|
|
8975
9393
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
@@ -8985,7 +9403,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
8985
9403
|
* check.
|
|
8986
9404
|
*/
|
|
8987
9405
|
typecheckTarget(cwd) {
|
|
8988
|
-
if (
|
|
9406
|
+
if (existsSync31(join39(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
8989
9407
|
const target = resolveTsconfigTarget(cwd);
|
|
8990
9408
|
return target.reason === "none" ? null : target.path;
|
|
8991
9409
|
}
|
|
@@ -9190,8 +9608,8 @@ function replaceLines(current, replacements) {
|
|
|
9190
9608
|
}
|
|
9191
9609
|
|
|
9192
9610
|
// src/core/apply-plan.ts
|
|
9193
|
-
import { existsSync as
|
|
9194
|
-
import { resolve as
|
|
9611
|
+
import { existsSync as existsSync32, readFileSync as readFileSync33, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
9612
|
+
import { resolve as resolve6, sep } from "path";
|
|
9195
9613
|
import { applyEdits as applyEdits3, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
9196
9614
|
|
|
9197
9615
|
// src/shared/deep-merge.ts
|
|
@@ -9201,15 +9619,15 @@ function isPlainObject(value) {
|
|
|
9201
9619
|
|
|
9202
9620
|
// src/core/apply-plan.ts
|
|
9203
9621
|
function resolveWithinRoot(cwd, relativePath) {
|
|
9204
|
-
const root =
|
|
9205
|
-
const absolutePath =
|
|
9622
|
+
const root = resolve6(cwd);
|
|
9623
|
+
const absolutePath = resolve6(root, relativePath);
|
|
9206
9624
|
if (absolutePath !== root && !absolutePath.startsWith(root + sep)) {
|
|
9207
9625
|
throw new Error(`Refusing to write outside the module root: "${relativePath}".`);
|
|
9208
9626
|
}
|
|
9209
9627
|
return absolutePath;
|
|
9210
9628
|
}
|
|
9211
9629
|
function readIfExists(absolutePath) {
|
|
9212
|
-
return
|
|
9630
|
+
return existsSync32(absolutePath) ? readFileSync33(absolutePath, "utf8") : void 0;
|
|
9213
9631
|
}
|
|
9214
9632
|
function* leaves(value, prefix = []) {
|
|
9215
9633
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -9311,14 +9729,14 @@ function preparePlan(cwd, plan4) {
|
|
|
9311
9729
|
}
|
|
9312
9730
|
function writeFileAtomic(absolutePath, contents) {
|
|
9313
9731
|
const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
|
|
9314
|
-
|
|
9732
|
+
writeFileSync4(tempPath, contents);
|
|
9315
9733
|
renameSync(tempPath, absolutePath);
|
|
9316
9734
|
}
|
|
9317
9735
|
function applyPlan(cwd, plan4) {
|
|
9318
9736
|
const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
|
|
9319
9737
|
for (const file of changed) {
|
|
9320
9738
|
if (file.deleted) {
|
|
9321
|
-
|
|
9739
|
+
rmSync4(file.absolutePath, { force: true });
|
|
9322
9740
|
continue;
|
|
9323
9741
|
}
|
|
9324
9742
|
writeFileAtomic(file.absolutePath, file.after);
|
|
@@ -9327,8 +9745,8 @@ function applyPlan(cwd, plan4) {
|
|
|
9327
9745
|
}
|
|
9328
9746
|
|
|
9329
9747
|
// src/core/config/preset-evidence.ts
|
|
9330
|
-
import { existsSync as
|
|
9331
|
-
import { join as
|
|
9748
|
+
import { existsSync as existsSync33, readdirSync as readdirSync8, readFileSync as readFileSync34 } from "fs";
|
|
9749
|
+
import { join as join40 } from "path";
|
|
9332
9750
|
var PATH_SIGNALS = [
|
|
9333
9751
|
{
|
|
9334
9752
|
preset: "nest",
|
|
@@ -9362,12 +9780,12 @@ function sourceImports(cwd, pattern) {
|
|
|
9362
9780
|
}
|
|
9363
9781
|
for (const entry of entries) {
|
|
9364
9782
|
if (entry.isDirectory()) {
|
|
9365
|
-
if (!NOT_SCANNED.has(entry.name)) stack.push(
|
|
9783
|
+
if (!NOT_SCANNED.has(entry.name)) stack.push(join40(dir, entry.name));
|
|
9366
9784
|
continue;
|
|
9367
9785
|
}
|
|
9368
9786
|
if (!SOURCE_FILE2.test(entry.name)) continue;
|
|
9369
9787
|
try {
|
|
9370
|
-
if (pattern.test(
|
|
9788
|
+
if (pattern.test(readFileSync34(join40(dir, entry.name), "utf8"))) return true;
|
|
9371
9789
|
} catch {
|
|
9372
9790
|
continue;
|
|
9373
9791
|
}
|
|
@@ -9376,11 +9794,11 @@ function sourceImports(cwd, pattern) {
|
|
|
9376
9794
|
return false;
|
|
9377
9795
|
}
|
|
9378
9796
|
function dependencyNames(cwd) {
|
|
9379
|
-
const path =
|
|
9380
|
-
if (!
|
|
9797
|
+
const path = join40(cwd, "package.json");
|
|
9798
|
+
if (!existsSync33(path)) return [];
|
|
9381
9799
|
try {
|
|
9382
9800
|
const manifest = parseJsonc(
|
|
9383
|
-
|
|
9801
|
+
readFileSync34(path, "utf8"),
|
|
9384
9802
|
path
|
|
9385
9803
|
);
|
|
9386
9804
|
return [
|
|
@@ -9403,7 +9821,7 @@ function declaresJsx(cwd) {
|
|
|
9403
9821
|
for (const name of entries) {
|
|
9404
9822
|
try {
|
|
9405
9823
|
const config = parseJsonc(
|
|
9406
|
-
|
|
9824
|
+
readFileSync34(join40(cwd, name), "utf8"),
|
|
9407
9825
|
name
|
|
9408
9826
|
);
|
|
9409
9827
|
if (config.compilerOptions?.jsx !== void 0) return true;
|
|
@@ -9548,6 +9966,15 @@ async function dispatch(opts) {
|
|
|
9548
9966
|
if (opts.dryRun) {
|
|
9549
9967
|
return previewPlan(opts, plan4);
|
|
9550
9968
|
}
|
|
9969
|
+
if (adapter.beforeInit) {
|
|
9970
|
+
await adapter.beforeInit({
|
|
9971
|
+
module: basename3(opts.cwd),
|
|
9972
|
+
cwd: opts.cwd,
|
|
9973
|
+
preset,
|
|
9974
|
+
ci: false,
|
|
9975
|
+
fix: false
|
|
9976
|
+
});
|
|
9977
|
+
}
|
|
9551
9978
|
for (const change of applyPlan(opts.cwd, plan4)) {
|
|
9552
9979
|
process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
|
|
9553
9980
|
`);
|
|
@@ -9591,4 +10018,4 @@ export {
|
|
|
9591
10018
|
detectFramework,
|
|
9592
10019
|
dispatch
|
|
9593
10020
|
};
|
|
9594
|
-
//# sourceMappingURL=chunk-
|
|
10021
|
+
//# sourceMappingURL=chunk-ASBZQAXV.js.map
|