@hublo/sentinel 1.4.0-alpha.2 → 1.4.0-alpha.3
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-MUQWAZQN.js} +407 -37
- package/dist/chunk-SX5CBE3Z.js +310 -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/shared-test-config-CGxvMlmk.d.ts +28 -0
- package/package.json +1 -1
- package/dist/chunk-NX4GHIHF.js +0 -25
|
@@ -5394,6 +5394,9 @@ function registerLint() {
|
|
|
5394
5394
|
|
|
5395
5395
|
// src/roles/test/adapters/vitest/vitest.adapter.ts
|
|
5396
5396
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
5397
|
+
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync30 } from "fs";
|
|
5398
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
5399
|
+
import { join as join37 } from "path";
|
|
5397
5400
|
|
|
5398
5401
|
// src/roles/test/presets/toolchain.json
|
|
5399
5402
|
var toolchain_default2 = {
|
|
@@ -5470,7 +5473,7 @@ function testTarget(options) {
|
|
|
5470
5473
|
}
|
|
5471
5474
|
|
|
5472
5475
|
// src/roles/test/jest-migration.ts
|
|
5473
|
-
import { existsSync as
|
|
5476
|
+
import { existsSync as existsSync29, readFileSync as readFileSync28 } from "fs";
|
|
5474
5477
|
import { join as join35 } from "path";
|
|
5475
5478
|
|
|
5476
5479
|
// src/roles/test/codemod/migrate-module.ts
|
|
@@ -7570,9 +7573,13 @@ function migrateModuleTests(cwd, setupFiles = []) {
|
|
|
7570
7573
|
}
|
|
7571
7574
|
|
|
7572
7575
|
// src/roles/test/nest/generate-config.ts
|
|
7573
|
-
import { readdirSync as readdirSync7, readFileSync as readFileSync26, statSync as statSync7 } from "fs";
|
|
7576
|
+
import { existsSync as existsSync26, readdirSync as readdirSync7, readFileSync as readFileSync26, statSync as statSync7 } from "fs";
|
|
7574
7577
|
import { join as join32, relative as relative6 } from "path";
|
|
7575
7578
|
var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
|
|
7579
|
+
var REACT_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
|
|
7580
|
+
function flavourOf(reading) {
|
|
7581
|
+
return reading.testEnvironment === "jsdom" ? "react" : "nest";
|
|
7582
|
+
}
|
|
7576
7583
|
var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/nest";
|
|
7577
7584
|
var SENTINEL_MSW_SETUP = "@hublo/sentinel/test/setup/msw-lifecycle";
|
|
7578
7585
|
var SENTINEL_WORKSPACE_SETUP = "@hublo/sentinel/test/setup/workspace";
|
|
@@ -7643,14 +7650,29 @@ function readModuleTraits(cwd) {
|
|
|
7643
7650
|
return traits;
|
|
7644
7651
|
}
|
|
7645
7652
|
var withoutRootDir = (value) => value.replaceAll("<rootDir>/", "./").replaceAll("<rootDir>", ".");
|
|
7646
|
-
function
|
|
7653
|
+
function ownsMswLifecycle(cwd, reading) {
|
|
7654
|
+
const declared = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []].filter(
|
|
7655
|
+
(file) => !file.endsWith("jest.setup.after.env.js")
|
|
7656
|
+
);
|
|
7657
|
+
for (const raw of declared) {
|
|
7658
|
+
const file = join32(cwd, raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", "."));
|
|
7659
|
+
if (!existsSync26(file)) continue;
|
|
7660
|
+
try {
|
|
7661
|
+
if (/\.listen\s*\(/.test(readFileSync26(file, "utf8"))) return true;
|
|
7662
|
+
} catch {
|
|
7663
|
+
continue;
|
|
7664
|
+
}
|
|
7665
|
+
}
|
|
7666
|
+
return false;
|
|
7667
|
+
}
|
|
7668
|
+
function setupBlock(cwd, reading, msw) {
|
|
7647
7669
|
const declared = [...reading.setupFiles ?? [], ...reading.setupFilesAfterEnv ?? []];
|
|
7648
7670
|
const namedTheRootSetup = declared.some((file) => file.endsWith("jest.setup.after.env.js"));
|
|
7649
7671
|
const own = declared.filter((file) => !file.endsWith("jest.setup.after.env.js")).map((file) => `'${withoutRootDir(file)}'`);
|
|
7650
7672
|
const sentinel = [
|
|
7651
7673
|
`'${SENTINEL_TEST_SETUP}'`,
|
|
7652
7674
|
...namedTheRootSetup ? [`'${SENTINEL_WORKSPACE_SETUP}'`] : [],
|
|
7653
|
-
...msw ? [`'${SENTINEL_MSW_SETUP}'`] : []
|
|
7675
|
+
...msw && !ownsMswLifecycle(cwd, reading) ? [`'${SENTINEL_MSW_SETUP}'`] : []
|
|
7654
7676
|
];
|
|
7655
7677
|
return ` setupFiles: [${[...sentinel, ...own].join(", ")}],`;
|
|
7656
7678
|
}
|
|
@@ -7712,7 +7734,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
|
|
|
7712
7734
|
const up = hopsToRoot(cwd, workspaceRoot);
|
|
7713
7735
|
const test = [
|
|
7714
7736
|
environmentBlock(reading),
|
|
7715
|
-
setupBlock(reading, traits.msw),
|
|
7737
|
+
setupBlock(cwd, reading, traits.msw),
|
|
7716
7738
|
envBlock(reading),
|
|
7717
7739
|
timeoutBlock(reading),
|
|
7718
7740
|
concurrencyBlock(cwd, reading, target),
|
|
@@ -7727,7 +7749,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
|
|
|
7727
7749
|
`import path from 'node:path'`,
|
|
7728
7750
|
"",
|
|
7729
7751
|
...needsDefaults ? [`import { configDefaults } from 'vitest/config'`, ""] : [],
|
|
7730
|
-
`import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
|
|
7752
|
+
flavourOf(reading) === "react" ? `import { defineConfig, reactTestConfig } from '${REACT_TEST_PRESET_SPECIFIER}'` : `import { defineConfig, nestTestConfig } from '${NEST_TEST_PRESET_SPECIFIER}'`
|
|
7731
7753
|
].join("\n");
|
|
7732
7754
|
const overrides = [aliasBlock(reading), " test: {", ...test, " },"].filter(
|
|
7733
7755
|
(line) => line !== ""
|
|
@@ -7738,7 +7760,7 @@ function generateNestTestConfig(cwd, reading, target = "test") {
|
|
|
7738
7760
|
source: `${imports}
|
|
7739
7761
|
|
|
7740
7762
|
export default defineConfig(
|
|
7741
|
-
nestTestConfig({
|
|
7763
|
+
${flavourOf(reading) === "react" ? "reactTestConfig" : "nestTestConfig"}({
|
|
7742
7764
|
root: import.meta.dirname,
|
|
7743
7765
|
workspaceRoot: path.resolve(import.meta.dirname, '${up}'),${decoratorLine}
|
|
7744
7766
|
overrides: {
|
|
@@ -7751,7 +7773,7 @@ ${overrides.join("\n")}
|
|
|
7751
7773
|
}
|
|
7752
7774
|
|
|
7753
7775
|
// src/roles/test/nest/read-jest-config.ts
|
|
7754
|
-
import { existsSync as
|
|
7776
|
+
import { existsSync as existsSync27, readFileSync as readFileSync27 } from "fs";
|
|
7755
7777
|
import { join as join33 } from "path";
|
|
7756
7778
|
var CARRIED = [
|
|
7757
7779
|
"testEnvironment",
|
|
@@ -7942,7 +7964,7 @@ function extendedConfig(source, cwd) {
|
|
|
7942
7964
|
(name) => name.replace(/^\.\//, "")
|
|
7943
7965
|
);
|
|
7944
7966
|
const file = candidates.find(
|
|
7945
|
-
(name) => /^jest[\w.-]*\.(ts|js)$/.test(name) &&
|
|
7967
|
+
(name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync27(join33(cwd, name))
|
|
7946
7968
|
);
|
|
7947
7969
|
if (file === void 0) continue;
|
|
7948
7970
|
if (!new RegExp(String.raw`\{\s*\.\.\.${binding}\b`).test(source)) continue;
|
|
@@ -7969,8 +7991,8 @@ function sideEffects(source, extended) {
|
|
|
7969
7991
|
return found;
|
|
7970
7992
|
}
|
|
7971
7993
|
function readJestConfig(cwd, named) {
|
|
7972
|
-
const file = named ?? JEST_CONFIG_FILES.find((name) =>
|
|
7973
|
-
if (file === void 0 || !
|
|
7994
|
+
const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync27(join33(cwd, name)));
|
|
7995
|
+
if (file === void 0 || !existsSync27(join33(cwd, file))) return void 0;
|
|
7974
7996
|
let source;
|
|
7975
7997
|
try {
|
|
7976
7998
|
source = readFileSync27(join33(cwd, file), "utf8");
|
|
@@ -8054,7 +8076,7 @@ function readJestConfig(cwd, named) {
|
|
|
8054
8076
|
}
|
|
8055
8077
|
|
|
8056
8078
|
// src/roles/test/nest/run-modes.ts
|
|
8057
|
-
import { existsSync as
|
|
8079
|
+
import { existsSync as existsSync28 } from "fs";
|
|
8058
8080
|
import { basename as basename2, join as join34 } from "path";
|
|
8059
8081
|
var JEST_EXECUTOR = "@nx/jest:jest";
|
|
8060
8082
|
var PRIMARY_CONFIG_FILE = "vitest.config.mts";
|
|
@@ -8076,7 +8098,7 @@ function runModes(cwd, fallback) {
|
|
|
8076
8098
|
const jestConfig = entry.options?.jestConfig;
|
|
8077
8099
|
if (jestConfig === void 0) continue;
|
|
8078
8100
|
const name = configName(jestConfig);
|
|
8079
|
-
if (!
|
|
8101
|
+
if (!existsSync28(join34(cwd, name))) continue;
|
|
8080
8102
|
declared.push({
|
|
8081
8103
|
target,
|
|
8082
8104
|
jestConfig: name,
|
|
@@ -8105,7 +8127,7 @@ var SPEC_TSCONFIG = "tsconfig.spec.json";
|
|
|
8105
8127
|
var VITEST_GLOBAL_TYPES = "vitest/globals";
|
|
8106
8128
|
function specTypesOperation(cwd) {
|
|
8107
8129
|
const path = join35(cwd, SPEC_TSCONFIG);
|
|
8108
|
-
if (!
|
|
8130
|
+
if (!existsSync29(path)) return void 0;
|
|
8109
8131
|
let config;
|
|
8110
8132
|
try {
|
|
8111
8133
|
config = parseJsonc(
|
|
@@ -8363,6 +8385,311 @@ function plan3(context) {
|
|
|
8363
8385
|
return { operations, notes };
|
|
8364
8386
|
}
|
|
8365
8387
|
|
|
8388
|
+
// src/roles/test/reference.ts
|
|
8389
|
+
import { execFileSync } from "child_process";
|
|
8390
|
+
import { existsSync as existsSync30, mkdtempSync, readFileSync as readFileSync29, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
8391
|
+
import { tmpdir } from "os";
|
|
8392
|
+
import { join as join36 } from "path";
|
|
8393
|
+
|
|
8394
|
+
// src/roles/test/baseline.ts
|
|
8395
|
+
function readSnapshot(report) {
|
|
8396
|
+
const files = report?.testResults ?? [];
|
|
8397
|
+
const passing = [];
|
|
8398
|
+
const failing = [];
|
|
8399
|
+
const unloadable = [];
|
|
8400
|
+
for (const file of files) {
|
|
8401
|
+
const assertions = file?.assertionResults ?? [];
|
|
8402
|
+
if (file?.status === "failed" && assertions.length === 0) {
|
|
8403
|
+
unloadable.push(typeof file.name === "string" ? file.name : "an unnamed file");
|
|
8404
|
+
}
|
|
8405
|
+
for (const assertion of assertions) {
|
|
8406
|
+
const name = assertion?.fullName;
|
|
8407
|
+
if (typeof name !== "string" || name === "") continue;
|
|
8408
|
+
if (assertion.status === "passed") passing.push(name);
|
|
8409
|
+
else if (assertion.status === "failed") failing.push(name);
|
|
8410
|
+
}
|
|
8411
|
+
}
|
|
8412
|
+
const claimed = report?.numFailedTests;
|
|
8413
|
+
return {
|
|
8414
|
+
files: files.length,
|
|
8415
|
+
passing: passing.sort(),
|
|
8416
|
+
failing: failing.sort(),
|
|
8417
|
+
unloadable: unloadable.sort(),
|
|
8418
|
+
claimedFailing: typeof claimed === "number" ? claimed : failing.length
|
|
8419
|
+
};
|
|
8420
|
+
}
|
|
8421
|
+
function difference(left, right) {
|
|
8422
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
8423
|
+
for (const name of right) remaining.set(name, (remaining.get(name) ?? 0) + 1);
|
|
8424
|
+
const missing = [];
|
|
8425
|
+
for (const name of left) {
|
|
8426
|
+
const left_ = remaining.get(name) ?? 0;
|
|
8427
|
+
if (left_ === 0) {
|
|
8428
|
+
missing.push(name);
|
|
8429
|
+
continue;
|
|
8430
|
+
}
|
|
8431
|
+
remaining.set(name, left_ - 1);
|
|
8432
|
+
}
|
|
8433
|
+
return missing.sort();
|
|
8434
|
+
}
|
|
8435
|
+
var withoutRenderingQuotes = (name) => name.replaceAll(String.raw`\'`, "'").replaceAll(String.raw`\"`, '"').replaceAll(/['"`]\s*\+\s*['"`]/g, "").replaceAll(/(^|[\s:,[{(])(''|"")($|[\s,)\]}])/g, "$1<empty-string>$3").replaceAll(/['"`]/g, "");
|
|
8436
|
+
function withoutRenderingNoise(name) {
|
|
8437
|
+
const spelled = withSortedKeys(
|
|
8438
|
+
withoutUndefinedEntries(
|
|
8439
|
+
withoutErrorRendering(
|
|
8440
|
+
withoutNaN(
|
|
8441
|
+
withoutTimestamps(withoutRenderingQuotes(withoutClassBodies(withoutStackFrames(name))))
|
|
8442
|
+
)
|
|
8443
|
+
)
|
|
8444
|
+
)
|
|
8445
|
+
);
|
|
8446
|
+
return withoutArrayBrackets(
|
|
8447
|
+
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, ",")
|
|
8448
|
+
);
|
|
8449
|
+
}
|
|
8450
|
+
function withoutArrayBrackets(name) {
|
|
8451
|
+
return name.replaceAll(
|
|
8452
|
+
/\[([^[\]]*)\]/g,
|
|
8453
|
+
(match, inner) => (
|
|
8454
|
+
// `[Object]` and `[Array]` are the formatter saying it STOPPED, not an array literal.
|
|
8455
|
+
// Stripping their brackets would erase the marker `collapsedOf` pairs on.
|
|
8456
|
+
/^(?:Object|Array)$/.test(inner) ? match : inner.replaceAll(/,\s+/g, ",")
|
|
8457
|
+
)
|
|
8458
|
+
);
|
|
8459
|
+
}
|
|
8460
|
+
var withoutClassBodies = (name) => name.replaceAll(
|
|
8461
|
+
/class\s+([A-Za-z_$][\w$]*)\s*\{\s*\}/g,
|
|
8462
|
+
(_match, className) => `class ${className}`
|
|
8463
|
+
).replaceAll(
|
|
8464
|
+
/class\s+([A-Za-z_$][\w$]*)[\s\S]*?\n\}/g,
|
|
8465
|
+
(_match, className) => `class ${className}`
|
|
8466
|
+
);
|
|
8467
|
+
var withoutTimestamps = (name) => name.replaceAll(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, "<instant>");
|
|
8468
|
+
var withoutNaN = (name) => name.replaceAll(/\bNaN\b/g, "null");
|
|
8469
|
+
function entriesOf(inner) {
|
|
8470
|
+
const parts = [];
|
|
8471
|
+
let depth = 0;
|
|
8472
|
+
let start = 0;
|
|
8473
|
+
for (let index = 0; index < inner.length; index += 1) {
|
|
8474
|
+
const character = inner[index];
|
|
8475
|
+
if (character === "[" || character === "(") depth += 1;
|
|
8476
|
+
else if (character === "]" || character === ")") depth -= 1;
|
|
8477
|
+
else if (character === "," && depth === 0) {
|
|
8478
|
+
parts.push(inner.slice(start, index));
|
|
8479
|
+
start = index + 1;
|
|
8480
|
+
}
|
|
8481
|
+
if (depth < 0) return void 0;
|
|
8482
|
+
}
|
|
8483
|
+
if (depth !== 0) return void 0;
|
|
8484
|
+
parts.push(inner.slice(start));
|
|
8485
|
+
return parts.map((part) => part.trim()).filter(Boolean);
|
|
8486
|
+
}
|
|
8487
|
+
var sortedGroup = (match, inner) => {
|
|
8488
|
+
if (!inner.includes(":")) return match;
|
|
8489
|
+
const parts = entriesOf(inner);
|
|
8490
|
+
if (parts === void 0) return match;
|
|
8491
|
+
if (parts.length < 2 || !parts.every((part) => part.includes(":"))) return match;
|
|
8492
|
+
return `{${[...parts].sort().join(",")}}`;
|
|
8493
|
+
};
|
|
8494
|
+
var HELD = "\u241F";
|
|
8495
|
+
var withSortedKeys = (name) => {
|
|
8496
|
+
const held = [];
|
|
8497
|
+
let current = name;
|
|
8498
|
+
for (let depth = 0; depth < 24; depth += 1) {
|
|
8499
|
+
const next = current.replaceAll(/\{([^{}]*)\}/g, (match, inner) => {
|
|
8500
|
+
held.push(sortedGroup(match, inner));
|
|
8501
|
+
return `${HELD}${held.length - 1}${HELD}`;
|
|
8502
|
+
});
|
|
8503
|
+
if (next === current) break;
|
|
8504
|
+
current = next;
|
|
8505
|
+
}
|
|
8506
|
+
for (let depth = 0; depth < 24 && current.includes(HELD); depth += 1) {
|
|
8507
|
+
current = current.replaceAll(
|
|
8508
|
+
new RegExp(`${HELD}(\\d+)${HELD}`, "g"),
|
|
8509
|
+
(_match, index) => held[Number(index)]
|
|
8510
|
+
);
|
|
8511
|
+
}
|
|
8512
|
+
return current;
|
|
8513
|
+
};
|
|
8514
|
+
var withoutStackFrames = (name) => {
|
|
8515
|
+
const frame = name.search(/\n\s+at\s/);
|
|
8516
|
+
return frame === -1 ? name : name.slice(0, frame);
|
|
8517
|
+
};
|
|
8518
|
+
var withoutErrorRendering = (name) => name.replaceAll(/\[[A-Za-z]*(?:Error|Exception):[^\][]*\]/g, "{}");
|
|
8519
|
+
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, "{}");
|
|
8520
|
+
function collapsedOf(candidate, name) {
|
|
8521
|
+
if (!/\[(?:Object|Array)\]/.test(candidate)) return false;
|
|
8522
|
+
const pattern = candidate.split(/\[(?:Object|Array)\]/).map((part) => part.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
8523
|
+
return new RegExp(`^${pattern}$`, "s").test(name);
|
|
8524
|
+
}
|
|
8525
|
+
function truncationOf(candidate, name) {
|
|
8526
|
+
const cut = candidate.indexOf("\u2026");
|
|
8527
|
+
if (cut === -1) return false;
|
|
8528
|
+
const head = candidate.slice(0, cut);
|
|
8529
|
+
const tail = candidate.slice(cut + 1);
|
|
8530
|
+
return name.startsWith(head) && name.endsWith(tail);
|
|
8531
|
+
}
|
|
8532
|
+
function pairRenamed(missing, added) {
|
|
8533
|
+
const remaining = [...added];
|
|
8534
|
+
const renamed = [];
|
|
8535
|
+
const unmatched = [];
|
|
8536
|
+
for (const name of missing) {
|
|
8537
|
+
const normalised = withoutRenderingNoise(name);
|
|
8538
|
+
const at = remaining.findIndex((candidate) => withoutRenderingNoise(candidate) === normalised);
|
|
8539
|
+
if (at === -1) {
|
|
8540
|
+
unmatched.push(name);
|
|
8541
|
+
continue;
|
|
8542
|
+
}
|
|
8543
|
+
renamed.push({ before: name, after: remaining[at] });
|
|
8544
|
+
remaining.splice(at, 1);
|
|
8545
|
+
}
|
|
8546
|
+
const lost = [];
|
|
8547
|
+
for (const name of unmatched) {
|
|
8548
|
+
const normalised = withoutRenderingNoise(name);
|
|
8549
|
+
const at = remaining.findIndex(
|
|
8550
|
+
(candidate) => truncationOf(withoutRenderingNoise(candidate), normalised) || collapsedOf(normalised, withoutRenderingNoise(candidate))
|
|
8551
|
+
);
|
|
8552
|
+
if (at === -1) {
|
|
8553
|
+
lost.push(name);
|
|
8554
|
+
continue;
|
|
8555
|
+
}
|
|
8556
|
+
renamed.push({ before: name, after: remaining[at] });
|
|
8557
|
+
remaining.splice(at, 1);
|
|
8558
|
+
}
|
|
8559
|
+
return { renamed, missing: lost, added: remaining };
|
|
8560
|
+
}
|
|
8561
|
+
function compareSnapshots(before, after) {
|
|
8562
|
+
const beforeAll = [...before.passing, ...before.failing];
|
|
8563
|
+
const afterAll = [...after.passing, ...after.failing];
|
|
8564
|
+
const paired = pairRenamed(difference(beforeAll, afterAll), difference(afterAll, beforeAll));
|
|
8565
|
+
const renamedBefore = new Set(paired.renamed.map((pair) => pair.before));
|
|
8566
|
+
const regressed = difference(before.passing, after.passing).filter(
|
|
8567
|
+
(name) => !paired.missing.includes(name) && !renamedBefore.has(name)
|
|
8568
|
+
);
|
|
8569
|
+
const failedBefore = new Set(before.failing);
|
|
8570
|
+
const recovered = after.passing.filter((name) => failedBefore.has(name));
|
|
8571
|
+
return {
|
|
8572
|
+
identical: paired.missing.length === 0 && paired.added.length === 0 && regressed.length === 0,
|
|
8573
|
+
missing: paired.missing,
|
|
8574
|
+
added: paired.added,
|
|
8575
|
+
regressed,
|
|
8576
|
+
renamed: paired.renamed,
|
|
8577
|
+
recovered,
|
|
8578
|
+
alreadyFailing: before.failing.length
|
|
8579
|
+
};
|
|
8580
|
+
}
|
|
8581
|
+
function unusableAsBaseline(snapshot) {
|
|
8582
|
+
if (snapshot.unloadable.length > 0) {
|
|
8583
|
+
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`;
|
|
8584
|
+
}
|
|
8585
|
+
if (snapshot.claimedFailing > snapshot.failing.length) {
|
|
8586
|
+
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`;
|
|
8587
|
+
}
|
|
8588
|
+
if (snapshot.passing.length === 0) {
|
|
8589
|
+
return `the reference run reports no passing test at all, so a green run after would prove nothing: there is no claim to preserve`;
|
|
8590
|
+
}
|
|
8591
|
+
return void 0;
|
|
8592
|
+
}
|
|
8593
|
+
function describeComparison(comparison) {
|
|
8594
|
+
const renamedNote = comparison.renamed.length > 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. "${comparison.renamed[0]?.before.slice(-40)}" -> "${comparison.renamed[0]?.after.slice(-40)}").` : "";
|
|
8595
|
+
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.` : "";
|
|
8596
|
+
const recoveredNote = comparison.recovered.length > 0 ? ` ${comparison.recovered.length} test(s) were failing before and pass now.` : "";
|
|
8597
|
+
if (comparison.identical) {
|
|
8598
|
+
return `every test that ran before runs and passes now.${recoveredNote}${renamedNote}${alreadyRedNote}`;
|
|
8599
|
+
}
|
|
8600
|
+
const parts = [];
|
|
8601
|
+
if (comparison.recovered.length > 0) {
|
|
8602
|
+
parts.push(`${comparison.recovered.length} test(s) were failing before and pass now`);
|
|
8603
|
+
}
|
|
8604
|
+
if (comparison.missing.length > 0) {
|
|
8605
|
+
parts.push(
|
|
8606
|
+
`${comparison.missing.length} test(s) no longer exist: ${comparison.missing.slice(0, 3).join(", ")}`
|
|
8607
|
+
);
|
|
8608
|
+
}
|
|
8609
|
+
if (comparison.added.length > 0) {
|
|
8610
|
+
parts.push(
|
|
8611
|
+
`${comparison.added.length} test(s) appeared, which a migration should not do: ${comparison.added.slice(0, 3).join(", ")}`
|
|
8612
|
+
);
|
|
8613
|
+
}
|
|
8614
|
+
if (comparison.regressed.length > 0) {
|
|
8615
|
+
parts.push(
|
|
8616
|
+
`${comparison.regressed.length} test(s) passed before and do not now: ${comparison.regressed.slice(0, 3).join(", ")}`
|
|
8617
|
+
);
|
|
8618
|
+
}
|
|
8619
|
+
return `${parts.join("; ")}${renamedNote}${alreadyRedNote}`;
|
|
8620
|
+
}
|
|
8621
|
+
|
|
8622
|
+
// src/roles/test/reference.ts
|
|
8623
|
+
var REFERENCE_FILE = ".sentinel-test-reference.json";
|
|
8624
|
+
function runJest(cwd, config) {
|
|
8625
|
+
const bin = resolveBin(cwd, "jest");
|
|
8626
|
+
if (bin === void 0) return void 0;
|
|
8627
|
+
const root = findWorkspaceRoot(cwd) ?? cwd;
|
|
8628
|
+
const out = join36(mkdtempSync(join36(tmpdir(), "sentinel-reference-")), "jest.json");
|
|
8629
|
+
try {
|
|
8630
|
+
execFileSync(bin, ["-c", join36(cwd, config), "--json", `--outputFile=${out}`, "--silent"], {
|
|
8631
|
+
cwd: root,
|
|
8632
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
8633
|
+
maxBuffer: 1 << 28
|
|
8634
|
+
});
|
|
8635
|
+
} catch {
|
|
8636
|
+
}
|
|
8637
|
+
if (!existsSync30(out)) return void 0;
|
|
8638
|
+
try {
|
|
8639
|
+
return JSON.parse(readFileSync29(out, "utf8"));
|
|
8640
|
+
} catch {
|
|
8641
|
+
return void 0;
|
|
8642
|
+
}
|
|
8643
|
+
}
|
|
8644
|
+
function captureReference(cwd, config) {
|
|
8645
|
+
const report = runJest(cwd, config);
|
|
8646
|
+
if (report === void 0) {
|
|
8647
|
+
return {
|
|
8648
|
+
captured: false,
|
|
8649
|
+
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.`
|
|
8650
|
+
};
|
|
8651
|
+
}
|
|
8652
|
+
const snapshot = readSnapshot(report);
|
|
8653
|
+
const unusable = unusableAsBaseline(snapshot);
|
|
8654
|
+
if (unusable !== void 0) {
|
|
8655
|
+
return {
|
|
8656
|
+
captured: false,
|
|
8657
|
+
message: `no reference recorded: ${unusable} The migration is applied anyway, unproved.`
|
|
8658
|
+
};
|
|
8659
|
+
}
|
|
8660
|
+
const stored = {
|
|
8661
|
+
runner: "jest",
|
|
8662
|
+
config,
|
|
8663
|
+
takenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8664
|
+
snapshot
|
|
8665
|
+
};
|
|
8666
|
+
writeFileSync3(join36(cwd, REFERENCE_FILE), `${JSON.stringify(stored, void 0, 2)}
|
|
8667
|
+
`);
|
|
8668
|
+
return {
|
|
8669
|
+
captured: true,
|
|
8670
|
+
message: `recorded ${snapshot.passing.length} passing test name(s) from ${config} as the reference. The next \`sentinel --run --test\` compares against it.`
|
|
8671
|
+
};
|
|
8672
|
+
}
|
|
8673
|
+
function hasReference(cwd) {
|
|
8674
|
+
return existsSync30(join36(cwd, REFERENCE_FILE));
|
|
8675
|
+
}
|
|
8676
|
+
function compareAgainstReference(cwd, report) {
|
|
8677
|
+
const path = join36(cwd, REFERENCE_FILE);
|
|
8678
|
+
let stored;
|
|
8679
|
+
try {
|
|
8680
|
+
stored = JSON.parse(readFileSync29(path, "utf8"));
|
|
8681
|
+
} catch {
|
|
8682
|
+
rmSync3(path, { force: true });
|
|
8683
|
+
return { ok: true, message: `the recorded reference could not be read, so nothing was proved.` };
|
|
8684
|
+
}
|
|
8685
|
+
const comparison = compareSnapshots(stored.snapshot, readSnapshot(report));
|
|
8686
|
+
rmSync3(path, { force: true });
|
|
8687
|
+
return {
|
|
8688
|
+
ok: comparison.identical,
|
|
8689
|
+
message: `${describeComparison(comparison)} (reference: ${stored.config}, ${stored.takenAt})`
|
|
8690
|
+
};
|
|
8691
|
+
}
|
|
8692
|
+
|
|
8366
8693
|
// src/roles/test/adapters/vitest/vitest.adapter.ts
|
|
8367
8694
|
function resolveVitest(cwd) {
|
|
8368
8695
|
const fromSentinel = binFromOwnInstall("vitest", "vitest");
|
|
@@ -8376,6 +8703,22 @@ var VitestAdapter = class extends BaseAdapter {
|
|
|
8376
8703
|
appliesTo(_preset) {
|
|
8377
8704
|
return true;
|
|
8378
8705
|
}
|
|
8706
|
+
/**
|
|
8707
|
+
* Record what this module's tests do TODAY, at the last moment it is still true.
|
|
8708
|
+
*
|
|
8709
|
+
* The plan about to be applied deletes the jest config, and with it any way of asking again. A
|
|
8710
|
+
* migration that cannot be compared to what came before is a migration nobody can check, which
|
|
8711
|
+
* is the whole reason this role has a baseline gate at all.
|
|
8712
|
+
*/
|
|
8713
|
+
async beforeInit(ctx) {
|
|
8714
|
+
const adoption = readTestAdoption(ctx.cwd);
|
|
8715
|
+
if (adoption.state !== "jest") return Promise.resolve();
|
|
8716
|
+
const [config] = adoption.jestConfigs;
|
|
8717
|
+
if (config === void 0) return Promise.resolve();
|
|
8718
|
+
this.say(`recording the jest reference before migrating...`);
|
|
8719
|
+
this.say(captureReference(ctx.cwd, config).message);
|
|
8720
|
+
return Promise.resolve();
|
|
8721
|
+
}
|
|
8379
8722
|
plan(context) {
|
|
8380
8723
|
return plan3(context);
|
|
8381
8724
|
}
|
|
@@ -8422,13 +8765,31 @@ var VitestAdapter = class extends BaseAdapter {
|
|
|
8422
8765
|
}
|
|
8423
8766
|
const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
|
|
8424
8767
|
const watching = options.includes("--watch") || options.includes("-w");
|
|
8425
|
-
const
|
|
8768
|
+
const proving = !watching && hasReference(ctx.cwd);
|
|
8769
|
+
const reportFile = proving ? join37(mkdtempSync2(join37(tmpdir2(), "sentinel-run-")), "vitest.json") : void 0;
|
|
8770
|
+
const reporters = reportFile === void 0 ? [] : ["--reporter=default", "--reporter=json", `--outputFile=${reportFile}`];
|
|
8771
|
+
const argv = watching ? [...options, ...paths] : ["run", ...reporters, ...options, ...paths];
|
|
8426
8772
|
const result = spawnSync5(bin, argv, { cwd: ctx.cwd, stdio: "inherit" });
|
|
8427
8773
|
if (result.error) {
|
|
8428
8774
|
this.say(`could not run vitest (${result.error.message})`);
|
|
8429
8775
|
return Promise.resolve({ ok: false, code: 1 });
|
|
8430
8776
|
}
|
|
8431
8777
|
const code = result.status ?? 1;
|
|
8778
|
+
if (reportFile !== void 0) {
|
|
8779
|
+
let report;
|
|
8780
|
+
try {
|
|
8781
|
+
report = JSON.parse(readFileSync30(reportFile, "utf8"));
|
|
8782
|
+
} catch {
|
|
8783
|
+
report = void 0;
|
|
8784
|
+
}
|
|
8785
|
+
if (report === void 0) {
|
|
8786
|
+
this.say(`vitest wrote no report, so this run proved nothing against the reference.`);
|
|
8787
|
+
} else {
|
|
8788
|
+
const outcome = compareAgainstReference(ctx.cwd, report);
|
|
8789
|
+
this.say(outcome.message);
|
|
8790
|
+
if (!outcome.ok) return Promise.resolve({ ok: false, code: code === 0 ? 1 : code });
|
|
8791
|
+
}
|
|
8792
|
+
}
|
|
8432
8793
|
return Promise.resolve({ ok: code === 0, code });
|
|
8433
8794
|
}
|
|
8434
8795
|
say(message) {
|
|
@@ -8445,9 +8806,9 @@ function registerTest() {
|
|
|
8445
8806
|
|
|
8446
8807
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
8447
8808
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
8448
|
-
import { existsSync as
|
|
8809
|
+
import { existsSync as existsSync31, readFileSync as readFileSync32 } from "fs";
|
|
8449
8810
|
import { createRequire as createRequire6 } from "module";
|
|
8450
|
-
import { join as
|
|
8811
|
+
import { join as join39 } from "path";
|
|
8451
8812
|
|
|
8452
8813
|
// src/roles/typescript/presets/nest.json
|
|
8453
8814
|
var nest_default2 = {
|
|
@@ -8628,8 +8989,8 @@ function readTsconfigAdoption(cwd) {
|
|
|
8628
8989
|
}
|
|
8629
8990
|
|
|
8630
8991
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
8631
|
-
import { readFileSync as
|
|
8632
|
-
import { join as
|
|
8992
|
+
import { readFileSync as readFileSync31 } from "fs";
|
|
8993
|
+
import { join as join38 } from "path";
|
|
8633
8994
|
|
|
8634
8995
|
// src/roles/typescript/typecheck-script.ts
|
|
8635
8996
|
var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
|
|
@@ -8722,7 +9083,7 @@ function planAdoption(context) {
|
|
|
8722
9083
|
};
|
|
8723
9084
|
}
|
|
8724
9085
|
const existing = parseJsonc(
|
|
8725
|
-
|
|
9086
|
+
readFileSync31(join38(context.cwd, target.path), "utf8"),
|
|
8726
9087
|
target.path
|
|
8727
9088
|
);
|
|
8728
9089
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -8860,7 +9221,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
8860
9221
|
let chain;
|
|
8861
9222
|
try {
|
|
8862
9223
|
const parsed = parseJsonc(
|
|
8863
|
-
|
|
9224
|
+
readFileSync32(join39(cwd, target.path), "utf8"),
|
|
8864
9225
|
target.path
|
|
8865
9226
|
);
|
|
8866
9227
|
chain = parsed.extends;
|
|
@@ -8873,7 +9234,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
8873
9234
|
);
|
|
8874
9235
|
if (preset === void 0) return void 0;
|
|
8875
9236
|
try {
|
|
8876
|
-
createRequire6(
|
|
9237
|
+
createRequire6(join39(cwd, "noop.js")).resolve(preset);
|
|
8877
9238
|
return void 0;
|
|
8878
9239
|
} catch {
|
|
8879
9240
|
return preset;
|
|
@@ -8969,7 +9330,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
8969
9330
|
referencedProjects(cwd, config) {
|
|
8970
9331
|
try {
|
|
8971
9332
|
const parsed = parseJsonc(
|
|
8972
|
-
|
|
9333
|
+
readFileSync32(join39(cwd, config), "utf8"),
|
|
8973
9334
|
config
|
|
8974
9335
|
);
|
|
8975
9336
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
@@ -8985,7 +9346,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
8985
9346
|
* check.
|
|
8986
9347
|
*/
|
|
8987
9348
|
typecheckTarget(cwd) {
|
|
8988
|
-
if (
|
|
9349
|
+
if (existsSync31(join39(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
8989
9350
|
const target = resolveTsconfigTarget(cwd);
|
|
8990
9351
|
return target.reason === "none" ? null : target.path;
|
|
8991
9352
|
}
|
|
@@ -9190,7 +9551,7 @@ function replaceLines(current, replacements) {
|
|
|
9190
9551
|
}
|
|
9191
9552
|
|
|
9192
9553
|
// src/core/apply-plan.ts
|
|
9193
|
-
import { existsSync as
|
|
9554
|
+
import { existsSync as existsSync32, readFileSync as readFileSync33, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
9194
9555
|
import { resolve as resolve5, sep } from "path";
|
|
9195
9556
|
import { applyEdits as applyEdits3, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
9196
9557
|
|
|
@@ -9209,7 +9570,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
9209
9570
|
return absolutePath;
|
|
9210
9571
|
}
|
|
9211
9572
|
function readIfExists(absolutePath) {
|
|
9212
|
-
return
|
|
9573
|
+
return existsSync32(absolutePath) ? readFileSync33(absolutePath, "utf8") : void 0;
|
|
9213
9574
|
}
|
|
9214
9575
|
function* leaves(value, prefix = []) {
|
|
9215
9576
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -9311,14 +9672,14 @@ function preparePlan(cwd, plan4) {
|
|
|
9311
9672
|
}
|
|
9312
9673
|
function writeFileAtomic(absolutePath, contents) {
|
|
9313
9674
|
const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
|
|
9314
|
-
|
|
9675
|
+
writeFileSync4(tempPath, contents);
|
|
9315
9676
|
renameSync(tempPath, absolutePath);
|
|
9316
9677
|
}
|
|
9317
9678
|
function applyPlan(cwd, plan4) {
|
|
9318
9679
|
const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
|
|
9319
9680
|
for (const file of changed) {
|
|
9320
9681
|
if (file.deleted) {
|
|
9321
|
-
|
|
9682
|
+
rmSync4(file.absolutePath, { force: true });
|
|
9322
9683
|
continue;
|
|
9323
9684
|
}
|
|
9324
9685
|
writeFileAtomic(file.absolutePath, file.after);
|
|
@@ -9327,8 +9688,8 @@ function applyPlan(cwd, plan4) {
|
|
|
9327
9688
|
}
|
|
9328
9689
|
|
|
9329
9690
|
// src/core/config/preset-evidence.ts
|
|
9330
|
-
import { existsSync as
|
|
9331
|
-
import { join as
|
|
9691
|
+
import { existsSync as existsSync33, readdirSync as readdirSync8, readFileSync as readFileSync34 } from "fs";
|
|
9692
|
+
import { join as join40 } from "path";
|
|
9332
9693
|
var PATH_SIGNALS = [
|
|
9333
9694
|
{
|
|
9334
9695
|
preset: "nest",
|
|
@@ -9362,12 +9723,12 @@ function sourceImports(cwd, pattern) {
|
|
|
9362
9723
|
}
|
|
9363
9724
|
for (const entry of entries) {
|
|
9364
9725
|
if (entry.isDirectory()) {
|
|
9365
|
-
if (!NOT_SCANNED.has(entry.name)) stack.push(
|
|
9726
|
+
if (!NOT_SCANNED.has(entry.name)) stack.push(join40(dir, entry.name));
|
|
9366
9727
|
continue;
|
|
9367
9728
|
}
|
|
9368
9729
|
if (!SOURCE_FILE2.test(entry.name)) continue;
|
|
9369
9730
|
try {
|
|
9370
|
-
if (pattern.test(
|
|
9731
|
+
if (pattern.test(readFileSync34(join40(dir, entry.name), "utf8"))) return true;
|
|
9371
9732
|
} catch {
|
|
9372
9733
|
continue;
|
|
9373
9734
|
}
|
|
@@ -9376,11 +9737,11 @@ function sourceImports(cwd, pattern) {
|
|
|
9376
9737
|
return false;
|
|
9377
9738
|
}
|
|
9378
9739
|
function dependencyNames(cwd) {
|
|
9379
|
-
const path =
|
|
9380
|
-
if (!
|
|
9740
|
+
const path = join40(cwd, "package.json");
|
|
9741
|
+
if (!existsSync33(path)) return [];
|
|
9381
9742
|
try {
|
|
9382
9743
|
const manifest = parseJsonc(
|
|
9383
|
-
|
|
9744
|
+
readFileSync34(path, "utf8"),
|
|
9384
9745
|
path
|
|
9385
9746
|
);
|
|
9386
9747
|
return [
|
|
@@ -9403,7 +9764,7 @@ function declaresJsx(cwd) {
|
|
|
9403
9764
|
for (const name of entries) {
|
|
9404
9765
|
try {
|
|
9405
9766
|
const config = parseJsonc(
|
|
9406
|
-
|
|
9767
|
+
readFileSync34(join40(cwd, name), "utf8"),
|
|
9407
9768
|
name
|
|
9408
9769
|
);
|
|
9409
9770
|
if (config.compilerOptions?.jsx !== void 0) return true;
|
|
@@ -9548,6 +9909,15 @@ async function dispatch(opts) {
|
|
|
9548
9909
|
if (opts.dryRun) {
|
|
9549
9910
|
return previewPlan(opts, plan4);
|
|
9550
9911
|
}
|
|
9912
|
+
if (adapter.beforeInit) {
|
|
9913
|
+
await adapter.beforeInit({
|
|
9914
|
+
module: basename3(opts.cwd),
|
|
9915
|
+
cwd: opts.cwd,
|
|
9916
|
+
preset,
|
|
9917
|
+
ci: false,
|
|
9918
|
+
fix: false
|
|
9919
|
+
});
|
|
9920
|
+
}
|
|
9551
9921
|
for (const change of applyPlan(opts.cwd, plan4)) {
|
|
9552
9922
|
process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
|
|
9553
9923
|
`);
|
|
@@ -9591,4 +9961,4 @@ export {
|
|
|
9591
9961
|
detectFramework,
|
|
9592
9962
|
dispatch
|
|
9593
9963
|
};
|
|
9594
|
-
//# sourceMappingURL=chunk-
|
|
9964
|
+
//# sourceMappingURL=chunk-MUQWAZQN.js.map
|