@hublo/sentinel 1.2.0-alpha.27 → 1.2.0-alpha.29
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
CHANGED
|
@@ -298,6 +298,19 @@ function manifestOperation(cwd, scripts) {
|
|
|
298
298
|
};
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
// src/core/config/project-json.ts
|
|
302
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
303
|
+
import { join as join4 } from "path";
|
|
304
|
+
function projectTargets(cwd) {
|
|
305
|
+
const path = join4(cwd, "project.json");
|
|
306
|
+
if (!existsSync4(path)) return void 0;
|
|
307
|
+
try {
|
|
308
|
+
return JSON.parse(readFileSync4(path, "utf8")).targets;
|
|
309
|
+
} catch {
|
|
310
|
+
return void 0;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
301
314
|
// src/roles/build/presets/requirements.json
|
|
302
315
|
var requirements_default = {
|
|
303
316
|
svelte: {
|
|
@@ -413,19 +426,6 @@ var SENTINEL_OWNED_BUILD_PACKAGES = OWNED_PACKAGES.map(
|
|
|
413
426
|
(entry) => entry.package
|
|
414
427
|
);
|
|
415
428
|
|
|
416
|
-
// src/roles/build/project-json.ts
|
|
417
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
418
|
-
import { join as join4 } from "path";
|
|
419
|
-
function projectTargets(cwd) {
|
|
420
|
-
const path = join4(cwd, "project.json");
|
|
421
|
-
if (!existsSync4(path)) return void 0;
|
|
422
|
-
try {
|
|
423
|
-
return JSON.parse(readFileSync4(path, "utf8")).targets;
|
|
424
|
-
} catch {
|
|
425
|
-
return void 0;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
429
|
// src/roles/build/adoption-drift.ts
|
|
430
430
|
function buildAdoptionDrift(cwd) {
|
|
431
431
|
const drift = [];
|
|
@@ -5582,12 +5582,356 @@ function registerLint() {
|
|
|
5582
5582
|
setDefaultRunner("lint", "oxlint");
|
|
5583
5583
|
}
|
|
5584
5584
|
|
|
5585
|
-
// src/roles/
|
|
5585
|
+
// src/roles/test/adapters/vitest/vitest.adapter.ts
|
|
5586
5586
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
5587
|
-
import { existsSync as
|
|
5588
|
-
import { createRequire as createRequire4 } from "module";
|
|
5587
|
+
import { existsSync as existsSync26 } from "fs";
|
|
5589
5588
|
import { join as join30 } from "path";
|
|
5590
5589
|
|
|
5590
|
+
// src/roles/test/presets/toolchain.json
|
|
5591
|
+
var toolchain_default2 = {
|
|
5592
|
+
$comment: "The test toolchain sentinel owns. Same shape and same rule as the build one: a single table drives what `--init` removes from a module's package.json, which import specifiers move, and the name each default import is re-exported under, so the three cannot disagree.",
|
|
5593
|
+
owned: [
|
|
5594
|
+
{
|
|
5595
|
+
package: "vitest",
|
|
5596
|
+
specifier: "vitest/config",
|
|
5597
|
+
named: ["defineConfig", "mergeConfig"],
|
|
5598
|
+
types: ["ConfigEnv", "TestUserConfig", "ViteUserConfig"],
|
|
5599
|
+
why: "the runner itself, declared at the workspace ROOT today, which is what stops a module being testable on its own. Note the package is `vitest` and the specifier is `vitest/config`: the same split as nitro on the build side, and the reason one table holds both."
|
|
5600
|
+
},
|
|
5601
|
+
{
|
|
5602
|
+
package: "vite",
|
|
5603
|
+
specifier: "vite",
|
|
5604
|
+
named: ["loadEnv"],
|
|
5605
|
+
why: "`loadEnv` has no equivalent in `vitest/config`, and two of the six configs import it from vite to build their test env. Re-exported here rather than claimed: the BUILD role owns the `vite` package, and two roles owning one package is how a version conflict gets written down twice."
|
|
5606
|
+
}
|
|
5607
|
+
]
|
|
5608
|
+
};
|
|
5609
|
+
|
|
5610
|
+
// src/roles/test/preset-data.ts
|
|
5611
|
+
var OWNED_TEST_PACKAGES = toolchain_default2.owned;
|
|
5612
|
+
|
|
5613
|
+
// src/roles/test/config-policy.ts
|
|
5614
|
+
var TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
|
|
5615
|
+
var TEST_CONFIG_FILES = [
|
|
5616
|
+
"vitest.config.ts",
|
|
5617
|
+
"vitest.config.mts",
|
|
5618
|
+
"vitest.config.js",
|
|
5619
|
+
"vitest.config.mjs"
|
|
5620
|
+
];
|
|
5621
|
+
var TEST_OWNED_PACKAGES = OWNED_TEST_PACKAGES;
|
|
5622
|
+
var SENTINEL_OWNED_TEST_PACKAGES = TEST_OWNED_PACKAGES.filter(
|
|
5623
|
+
(entry) => entry.package !== "vite"
|
|
5624
|
+
).map((entry) => entry.package);
|
|
5625
|
+
var TEST_TOOLCHAIN = {
|
|
5626
|
+
presetSpecifier: TEST_PRESET_SPECIFIER,
|
|
5627
|
+
owned: TEST_OWNED_PACKAGES
|
|
5628
|
+
};
|
|
5629
|
+
var TEST_SCRIPT_NAME = "test";
|
|
5630
|
+
var SENTINEL_TEST_COMMAND = "sentinel --run --test --";
|
|
5631
|
+
var VITEST_INVOCATION = /(^|&&\s*|\|\|\s*|;\s*)(?:npx\s+|pnpm\s+(?:exec\s+|dlx\s+)?)?vitest\b([^&|;]*)/g;
|
|
5632
|
+
function rewriteVitestScript(command) {
|
|
5633
|
+
return command.replace(VITEST_INVOCATION, (_match, lead, tail) => {
|
|
5634
|
+
const args = tail.trim().split(/\s+/).filter(Boolean);
|
|
5635
|
+
const oneShot = args[0] === "run" || args.includes("--run");
|
|
5636
|
+
const rest = args.filter((arg, index) => !(index === 0 && arg === "run") && arg !== "--run");
|
|
5637
|
+
const passthrough = oneShot ? rest : ["--watch", ...rest];
|
|
5638
|
+
const suffix = passthrough.length > 0 ? ` -- ${passthrough.join(" ")}` : "";
|
|
5639
|
+
const trailing = /\s$/.test(tail) ? " " : "";
|
|
5640
|
+
return `${lead}sentinel --run --test${suffix}${trailing}`;
|
|
5641
|
+
});
|
|
5642
|
+
}
|
|
5643
|
+
function testTarget(options) {
|
|
5644
|
+
const { configFiles, outputs } = options;
|
|
5645
|
+
const cached = outputs !== void 0 && outputs.length > 0;
|
|
5646
|
+
return {
|
|
5647
|
+
[TEST_SCRIPT_NAME]: {
|
|
5648
|
+
cache: cached,
|
|
5649
|
+
inputs: ["default", "^default", ...configFiles.map((name) => `{projectRoot}/${name}`)],
|
|
5650
|
+
...cached ? { outputs: [...outputs] } : {}
|
|
5651
|
+
}
|
|
5652
|
+
};
|
|
5653
|
+
}
|
|
5654
|
+
|
|
5655
|
+
// src/roles/test/read-adoption.ts
|
|
5656
|
+
import { existsSync as existsSync25, readFileSync as readFileSync23, readdirSync as readdirSync3 } from "fs";
|
|
5657
|
+
import { join as join29 } from "path";
|
|
5658
|
+
var JEST_CONFIG_FILES = [
|
|
5659
|
+
"jest.config.ts",
|
|
5660
|
+
"jest.config.js",
|
|
5661
|
+
"jest.prisma.config.ts",
|
|
5662
|
+
"jest.functional.config.ts",
|
|
5663
|
+
"jest.integration-config.ts"
|
|
5664
|
+
];
|
|
5665
|
+
function testConfigFile(cwd) {
|
|
5666
|
+
return TEST_CONFIG_FILES.find((name) => existsSync25(join29(cwd, name)));
|
|
5667
|
+
}
|
|
5668
|
+
function jestConfigFiles(cwd) {
|
|
5669
|
+
return JEST_CONFIG_FILES.filter((name) => existsSync25(join29(cwd, name)));
|
|
5670
|
+
}
|
|
5671
|
+
function rootJestConfigFiles(cwd) {
|
|
5672
|
+
const root = findWorkspaceRoot(cwd);
|
|
5673
|
+
if (root === void 0 || root === cwd) return [];
|
|
5674
|
+
return JEST_CONFIG_FILES.filter((name) => existsSync25(join29(root, name)));
|
|
5675
|
+
}
|
|
5676
|
+
var TEST_FILE = /[.-](spec|test)\.[cm]?[jt]sx?$/;
|
|
5677
|
+
var NOT_WALKED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".nx", ".turbo"]);
|
|
5678
|
+
function hasTestFiles(cwd) {
|
|
5679
|
+
const stack = [cwd];
|
|
5680
|
+
while (stack.length > 0) {
|
|
5681
|
+
const dir = stack.pop();
|
|
5682
|
+
let entries;
|
|
5683
|
+
try {
|
|
5684
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
5685
|
+
} catch {
|
|
5686
|
+
continue;
|
|
5687
|
+
}
|
|
5688
|
+
for (const entry of entries) {
|
|
5689
|
+
if (entry.isDirectory()) {
|
|
5690
|
+
if (!NOT_WALKED.has(entry.name)) stack.push(join29(dir, entry.name));
|
|
5691
|
+
} else if (TEST_FILE.test(entry.name)) {
|
|
5692
|
+
return true;
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
return false;
|
|
5697
|
+
}
|
|
5698
|
+
function readTestConfigSource(cwd, fileName) {
|
|
5699
|
+
try {
|
|
5700
|
+
return readFileSync23(join29(cwd, fileName), "utf8");
|
|
5701
|
+
} catch {
|
|
5702
|
+
return "";
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
function importsPreset2(source) {
|
|
5706
|
+
const escaped = TEST_PRESET_SPECIFIER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5707
|
+
return new RegExp(String.raw`(?:from|import)\s*\(?\s*['"]${escaped}['"]`).test(source);
|
|
5708
|
+
}
|
|
5709
|
+
function readTestAdoption(cwd) {
|
|
5710
|
+
const manifest = readProjectPackageJson(cwd);
|
|
5711
|
+
const jestConfigs = jestConfigFiles(cwd);
|
|
5712
|
+
const configFile = testConfigFile(cwd);
|
|
5713
|
+
const declared = { ...manifest.dependencies, ...manifest.devDependencies };
|
|
5714
|
+
const ownDeclarations = ["vitest", "jest", "@nx/jest", "jest-mock-extended", "@swc/jest"].filter((name) => declared[name] !== void 0).map((name) => ({ name, version: declared[name] }));
|
|
5715
|
+
if (configFile === void 0) {
|
|
5716
|
+
const hasTests = hasTestFiles(cwd);
|
|
5717
|
+
const inheritsRootConfig = hasTests && jestConfigs.length === 0 && rootJestConfigFiles(cwd).length > 0;
|
|
5718
|
+
return {
|
|
5719
|
+
configFile: null,
|
|
5720
|
+
preset: null,
|
|
5721
|
+
adopted: false,
|
|
5722
|
+
conformant: false,
|
|
5723
|
+
drift: [],
|
|
5724
|
+
unreadable: null,
|
|
5725
|
+
state: hasTests ? "jest" : "no-tests",
|
|
5726
|
+
runner: hasTests ? "jest" : "none",
|
|
5727
|
+
jestConfigs,
|
|
5728
|
+
inheritsRootConfig,
|
|
5729
|
+
ownDeclarations
|
|
5730
|
+
};
|
|
5731
|
+
}
|
|
5732
|
+
const source = readTestConfigSource(cwd, configFile);
|
|
5733
|
+
if (source === "") {
|
|
5734
|
+
return {
|
|
5735
|
+
configFile,
|
|
5736
|
+
preset: null,
|
|
5737
|
+
adopted: false,
|
|
5738
|
+
conformant: false,
|
|
5739
|
+
drift: [],
|
|
5740
|
+
unreadable: `${configFile} could not be read`,
|
|
5741
|
+
state: "vitest-own-toolchain",
|
|
5742
|
+
runner: "vitest",
|
|
5743
|
+
jestConfigs,
|
|
5744
|
+
inheritsRootConfig: false,
|
|
5745
|
+
// it has a config of its own, readable or not
|
|
5746
|
+
ownDeclarations
|
|
5747
|
+
};
|
|
5748
|
+
}
|
|
5749
|
+
const adopted = importsPreset2(source);
|
|
5750
|
+
const script = moduleScripts(cwd)["test"];
|
|
5751
|
+
return {
|
|
5752
|
+
configFile,
|
|
5753
|
+
preset: adopted ? "react" : null,
|
|
5754
|
+
adopted,
|
|
5755
|
+
conformant: adopted && script !== void 0 && jestConfigs.length === 0,
|
|
5756
|
+
drift: [],
|
|
5757
|
+
unreadable: null,
|
|
5758
|
+
state: adopted ? "adopted" : "vitest-own-toolchain",
|
|
5759
|
+
runner: "vitest",
|
|
5760
|
+
jestConfigs,
|
|
5761
|
+
inheritsRootConfig: false,
|
|
5762
|
+
// local always wins: this module is out of the root's reach
|
|
5763
|
+
ownDeclarations
|
|
5764
|
+
};
|
|
5765
|
+
}
|
|
5766
|
+
|
|
5767
|
+
// src/roles/test/plan.ts
|
|
5768
|
+
function ownedTestDependencies(cwd, declared) {
|
|
5769
|
+
return declared.filter((entry) => SENTINEL_OWNED_TEST_PACKAGES.includes(entry.name)).flatMap((entry) => [
|
|
5770
|
+
["dependencies", entry.name],
|
|
5771
|
+
["devDependencies", entry.name]
|
|
5772
|
+
]).filter(([section]) => section !== void 0);
|
|
5773
|
+
}
|
|
5774
|
+
function plan3(context) {
|
|
5775
|
+
const configFile = testConfigFile(context.cwd);
|
|
5776
|
+
const adoption = readTestAdoption(context.cwd);
|
|
5777
|
+
if (configFile === void 0) {
|
|
5778
|
+
return {
|
|
5779
|
+
operations: [],
|
|
5780
|
+
skipped: adoption.state === "jest" ? `this module tests with jest (${adoption.jestConfigs.join(", ")}). Migrating it moves its test FILES as well as its config, which is the next step of this role and not this one.` : `this module has no test config and no test files, so there is nothing to point at sentinel. What it needs is its jest leftovers removed, which is a cleanup rather than an adoption.`
|
|
5781
|
+
};
|
|
5782
|
+
}
|
|
5783
|
+
const operations = [];
|
|
5784
|
+
const notes = [];
|
|
5785
|
+
const rewrite = rewriteToolchainImports(
|
|
5786
|
+
TEST_TOOLCHAIN,
|
|
5787
|
+
readTestConfigSource(context.cwd, configFile),
|
|
5788
|
+
configFile
|
|
5789
|
+
);
|
|
5790
|
+
if (rewrite.kind === "blocked") {
|
|
5791
|
+
return {
|
|
5792
|
+
operations: [],
|
|
5793
|
+
skipped: `${configFile} could not be pointed at sentinel, so nothing was written: ${rewrite.why}. Nothing here is broken; this needs a look before the role applies.`
|
|
5794
|
+
};
|
|
5795
|
+
}
|
|
5796
|
+
if (rewrite.kind === "rewritten") {
|
|
5797
|
+
operations.push({ kind: "write", path: configFile, contents: rewrite.source });
|
|
5798
|
+
notes.push(
|
|
5799
|
+
`${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.`
|
|
5800
|
+
);
|
|
5801
|
+
}
|
|
5802
|
+
const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
|
|
5803
|
+
const outputs = existing?.outputs ?? [];
|
|
5804
|
+
if (outputs.length === 0) {
|
|
5805
|
+
notes.push(
|
|
5806
|
+
`this target declares no \`outputs\`, so it is left UNCACHED. Caching a run that restores nothing is the defect where nx reports a hit and the coverage directory is empty; if this module writes coverage, declare where and the target can cache.`
|
|
5807
|
+
);
|
|
5808
|
+
}
|
|
5809
|
+
const siblingScripts = {};
|
|
5810
|
+
for (const [name, command] of Object.entries(moduleScripts(context.cwd))) {
|
|
5811
|
+
if (name === TEST_SCRIPT_NAME || typeof command !== "string") continue;
|
|
5812
|
+
const rewritten = rewriteVitestScript(command);
|
|
5813
|
+
if (rewritten !== command) siblingScripts[name] = rewritten;
|
|
5814
|
+
}
|
|
5815
|
+
if (Object.keys(siblingScripts).length > 0) {
|
|
5816
|
+
notes.push(
|
|
5817
|
+
`rewrote ${Object.keys(siblingScripts).join(", ")} to run through sentinel: they called vitest directly, and the dependency they relied on is removed below. A watch script keeps watching, through \`-- --watch\`.`
|
|
5818
|
+
);
|
|
5819
|
+
}
|
|
5820
|
+
operations.push(
|
|
5821
|
+
manifestOperation(context.cwd, {
|
|
5822
|
+
[TEST_SCRIPT_NAME]: SENTINEL_TEST_COMMAND,
|
|
5823
|
+
...siblingScripts
|
|
5824
|
+
})
|
|
5825
|
+
);
|
|
5826
|
+
operations.push(
|
|
5827
|
+
...nxTargetOperations({
|
|
5828
|
+
cwd: context.cwd,
|
|
5829
|
+
targets: testTarget({ configFiles: [configFile], outputs })
|
|
5830
|
+
})
|
|
5831
|
+
);
|
|
5832
|
+
const owned = ownedTestDependencies(context.cwd, adoption.ownDeclarations);
|
|
5833
|
+
if (owned.length > 0) {
|
|
5834
|
+
operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
|
|
5835
|
+
notes.push(
|
|
5836
|
+
`removed ${[...new Set(owned.map(([, name]) => name))].join(", ")} from this module: sentinel owns the runner now, and two copies of Vitest in one run give the plugins a different Vite than the one running them.`
|
|
5837
|
+
);
|
|
5838
|
+
}
|
|
5839
|
+
if (adoption.jestConfigs.length > 0) {
|
|
5840
|
+
notes.push(
|
|
5841
|
+
`${adoption.jestConfigs.join(", ")} is still here beside the Vitest config. It is not this step's to delete, since something may still run it, but the workspace cannot drop jest while it exists.`
|
|
5842
|
+
);
|
|
5843
|
+
}
|
|
5844
|
+
return { operations, notes };
|
|
5845
|
+
}
|
|
5846
|
+
|
|
5847
|
+
// src/roles/test/adapters/vitest/vitest.adapter.ts
|
|
5848
|
+
function resolveVitest(cwd) {
|
|
5849
|
+
const fromSentinel = join30(
|
|
5850
|
+
new URL("../../../../../node_modules/.bin/vitest", import.meta.url).pathname
|
|
5851
|
+
);
|
|
5852
|
+
if (existsSync26(fromSentinel)) return { bin: fromSentinel, origin: "sentinel" };
|
|
5853
|
+
const fromModule = join30(cwd, "node_modules", ".bin", "vitest");
|
|
5854
|
+
return existsSync26(fromModule) ? { bin: fromModule, origin: "module" } : { bin: void 0, origin: "module" };
|
|
5855
|
+
}
|
|
5856
|
+
var VitestAdapter = class extends BaseAdapter {
|
|
5857
|
+
target = "test";
|
|
5858
|
+
runner = "vitest";
|
|
5859
|
+
appliesTo(_preset) {
|
|
5860
|
+
return true;
|
|
5861
|
+
}
|
|
5862
|
+
plan(context) {
|
|
5863
|
+
return plan3(context);
|
|
5864
|
+
}
|
|
5865
|
+
async status(ctx) {
|
|
5866
|
+
return Promise.resolve(readTestAdoption(ctx.cwd));
|
|
5867
|
+
}
|
|
5868
|
+
async inspect(ctx) {
|
|
5869
|
+
const adoption = readTestAdoption(ctx.cwd);
|
|
5870
|
+
return Promise.resolve({
|
|
5871
|
+
state: adoption.state,
|
|
5872
|
+
runner: adoption.runner,
|
|
5873
|
+
configFile: adoption.configFile,
|
|
5874
|
+
jestConfigs: adoption.jestConfigs,
|
|
5875
|
+
adopted: adoption.adopted,
|
|
5876
|
+
conformant: adoption.conformant,
|
|
5877
|
+
drift: adoption.drift,
|
|
5878
|
+
unreadable: adoption.unreadable
|
|
5879
|
+
});
|
|
5880
|
+
}
|
|
5881
|
+
/**
|
|
5882
|
+
* Run the module's tests.
|
|
5883
|
+
*
|
|
5884
|
+
* A module that has not adopted is reported and PASSES, the stance every role takes: `--run`
|
|
5885
|
+
* with no named target sweeps the workspace, so failing here would exit non-zero on the 107
|
|
5886
|
+
* modules still on jest, which says nothing anyone can act on.
|
|
5887
|
+
*/
|
|
5888
|
+
async run(ctx) {
|
|
5889
|
+
const adoption = readTestAdoption(ctx.cwd);
|
|
5890
|
+
if (!adoption.adopted) {
|
|
5891
|
+
this.say(
|
|
5892
|
+
adoption.state === "jest" ? `this module tests with jest (${adoption.jestConfigs.join(", ")}); run \`sentinel --init --test\` to migrate it.` : adoption.state === "no-tests" ? `this module has no tests.` : `no sentinel preset in this module's Vitest config; run \`sentinel --init --test\` to adopt.`
|
|
5893
|
+
);
|
|
5894
|
+
return Promise.resolve({ ok: true, code: 0 });
|
|
5895
|
+
}
|
|
5896
|
+
const { bin, origin } = resolveVitest(ctx.cwd);
|
|
5897
|
+
if (bin === void 0) {
|
|
5898
|
+
this.say(`could not find the vitest binary. Run \`pnpm install\` in the module.`);
|
|
5899
|
+
return Promise.resolve({ ok: false, code: 1 });
|
|
5900
|
+
}
|
|
5901
|
+
if (origin === "module") {
|
|
5902
|
+
this.say(
|
|
5903
|
+
`using the MODULE's vitest, not sentinel's. An adopted config gets its plugins from sentinel, and two copies fail in ways that never mention a version. Remove vitest from this module's package.json.`
|
|
5904
|
+
);
|
|
5905
|
+
}
|
|
5906
|
+
const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
|
|
5907
|
+
const watching = options.includes("--watch") || options.includes("-w");
|
|
5908
|
+
const argv = watching ? [...options, ...paths] : ["run", ...options, ...paths];
|
|
5909
|
+
const result = spawnSync5(bin, argv, { cwd: ctx.cwd, stdio: "inherit" });
|
|
5910
|
+
if (result.error) {
|
|
5911
|
+
this.say(`could not run vitest (${result.error.message})`);
|
|
5912
|
+
return Promise.resolve({ ok: false, code: 1 });
|
|
5913
|
+
}
|
|
5914
|
+
const code = result.status ?? 1;
|
|
5915
|
+
return Promise.resolve({ ok: code === 0, code });
|
|
5916
|
+
}
|
|
5917
|
+
say(message) {
|
|
5918
|
+
process.stderr.write(`sentinel test(vitest): ${message}
|
|
5919
|
+
`);
|
|
5920
|
+
}
|
|
5921
|
+
};
|
|
5922
|
+
|
|
5923
|
+
// src/roles/test/register.ts
|
|
5924
|
+
function registerTest() {
|
|
5925
|
+
register(new VitestAdapter());
|
|
5926
|
+
setDefaultRunner("test", "vitest");
|
|
5927
|
+
}
|
|
5928
|
+
|
|
5929
|
+
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
5930
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
5931
|
+
import { existsSync as existsSync27, readFileSync as readFileSync25 } from "fs";
|
|
5932
|
+
import { createRequire as createRequire4 } from "module";
|
|
5933
|
+
import { join as join32 } from "path";
|
|
5934
|
+
|
|
5591
5935
|
// src/roles/typescript/presets/nest.json
|
|
5592
5936
|
var nest_default2 = {
|
|
5593
5937
|
compilerOptions: {
|
|
@@ -5767,8 +6111,8 @@ function readTsconfigAdoption(cwd) {
|
|
|
5767
6111
|
}
|
|
5768
6112
|
|
|
5769
6113
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
5770
|
-
import { readFileSync as
|
|
5771
|
-
import { join as
|
|
6114
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
6115
|
+
import { join as join31 } from "path";
|
|
5772
6116
|
|
|
5773
6117
|
// src/roles/typescript/typecheck-script.ts
|
|
5774
6118
|
var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
|
|
@@ -5861,7 +6205,7 @@ function planAdoption(context) {
|
|
|
5861
6205
|
};
|
|
5862
6206
|
}
|
|
5863
6207
|
const existing = parseJsonc(
|
|
5864
|
-
|
|
6208
|
+
readFileSync24(join31(context.cwd, target.path), "utf8"),
|
|
5865
6209
|
target.path
|
|
5866
6210
|
);
|
|
5867
6211
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -5999,7 +6343,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5999
6343
|
let chain;
|
|
6000
6344
|
try {
|
|
6001
6345
|
const parsed = parseJsonc(
|
|
6002
|
-
|
|
6346
|
+
readFileSync25(join32(cwd, target.path), "utf8"),
|
|
6003
6347
|
target.path
|
|
6004
6348
|
);
|
|
6005
6349
|
chain = parsed.extends;
|
|
@@ -6012,7 +6356,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6012
6356
|
);
|
|
6013
6357
|
if (preset === void 0) return void 0;
|
|
6014
6358
|
try {
|
|
6015
|
-
createRequire4(
|
|
6359
|
+
createRequire4(join32(cwd, "noop.js")).resolve(preset);
|
|
6016
6360
|
return void 0;
|
|
6017
6361
|
} catch {
|
|
6018
6362
|
return preset;
|
|
@@ -6053,7 +6397,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6053
6397
|
return { ok: false, code: 1 };
|
|
6054
6398
|
}
|
|
6055
6399
|
if (options.length > 0) return this.runWithOptions(ctx, tsc, config);
|
|
6056
|
-
const result =
|
|
6400
|
+
const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
|
|
6057
6401
|
if (result.error) {
|
|
6058
6402
|
process.stderr.write(
|
|
6059
6403
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -6088,7 +6432,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6088
6432
|
let worst = 0;
|
|
6089
6433
|
for (const project of this.referencedProjects(ctx.cwd, config)) {
|
|
6090
6434
|
const args = ["-p", project, "--noEmit", "--composite", "false", ...ctx.toolArgs ?? []];
|
|
6091
|
-
const result =
|
|
6435
|
+
const result = spawnSync6(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
|
|
6092
6436
|
if (result.error) {
|
|
6093
6437
|
process.stderr.write(
|
|
6094
6438
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -6108,7 +6452,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6108
6452
|
referencedProjects(cwd, config) {
|
|
6109
6453
|
try {
|
|
6110
6454
|
const parsed = parseJsonc(
|
|
6111
|
-
|
|
6455
|
+
readFileSync25(join32(cwd, config), "utf8"),
|
|
6112
6456
|
config
|
|
6113
6457
|
);
|
|
6114
6458
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
@@ -6124,7 +6468,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6124
6468
|
* check.
|
|
6125
6469
|
*/
|
|
6126
6470
|
typecheckTarget(cwd) {
|
|
6127
|
-
if (
|
|
6471
|
+
if (existsSync27(join32(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
6128
6472
|
const target = resolveTsconfigTarget(cwd);
|
|
6129
6473
|
return target.reason === "none" ? null : target.path;
|
|
6130
6474
|
}
|
|
@@ -6177,7 +6521,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6177
6521
|
return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
|
|
6178
6522
|
}
|
|
6179
6523
|
const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
|
|
6180
|
-
const result =
|
|
6524
|
+
const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
|
|
6181
6525
|
if (result.error) {
|
|
6182
6526
|
process.stderr.write(
|
|
6183
6527
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -6220,7 +6564,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
6220
6564
|
* errors in the output mean the rule must be on.
|
|
6221
6565
|
*/
|
|
6222
6566
|
noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
|
|
6223
|
-
const shown =
|
|
6567
|
+
const shown = spawnSync6(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
|
|
6224
6568
|
if (shown.status === 0 && shown.stdout) {
|
|
6225
6569
|
try {
|
|
6226
6570
|
const co = parseJsonc(
|
|
@@ -6247,6 +6591,7 @@ function registerAdapters() {
|
|
|
6247
6591
|
registerLint();
|
|
6248
6592
|
registerFormat();
|
|
6249
6593
|
registerBuild();
|
|
6594
|
+
registerTest();
|
|
6250
6595
|
}
|
|
6251
6596
|
|
|
6252
6597
|
// src/core/detect-framework.ts
|
|
@@ -6328,7 +6673,7 @@ function replaceLines(current, replacements) {
|
|
|
6328
6673
|
}
|
|
6329
6674
|
|
|
6330
6675
|
// src/core/apply-plan.ts
|
|
6331
|
-
import { existsSync as
|
|
6676
|
+
import { existsSync as existsSync28, readFileSync as readFileSync26, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
6332
6677
|
import { resolve as resolve4, sep } from "path";
|
|
6333
6678
|
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
6334
6679
|
|
|
@@ -6347,7 +6692,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
6347
6692
|
return absolutePath;
|
|
6348
6693
|
}
|
|
6349
6694
|
function readIfExists(absolutePath) {
|
|
6350
|
-
return
|
|
6695
|
+
return existsSync28(absolutePath) ? readFileSync26(absolutePath, "utf8") : void 0;
|
|
6351
6696
|
}
|
|
6352
6697
|
function* leaves(value, prefix = []) {
|
|
6353
6698
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -6428,9 +6773,9 @@ function applyOperationTo(current, operation) {
|
|
|
6428
6773
|
}
|
|
6429
6774
|
}
|
|
6430
6775
|
}
|
|
6431
|
-
function preparePlan(cwd,
|
|
6776
|
+
function preparePlan(cwd, plan4) {
|
|
6432
6777
|
const prepared = /* @__PURE__ */ new Map();
|
|
6433
|
-
for (const operation of
|
|
6778
|
+
for (const operation of plan4.operations) {
|
|
6434
6779
|
const absolutePath = resolveWithinRoot(cwd, operation.path);
|
|
6435
6780
|
const existing = prepared.get(operation.path);
|
|
6436
6781
|
const before = existing?.before ?? readIfExists(absolutePath) ?? "";
|
|
@@ -6452,8 +6797,8 @@ function writeFileAtomic(absolutePath, contents) {
|
|
|
6452
6797
|
writeFileSync4(tempPath, contents);
|
|
6453
6798
|
renameSync(tempPath, absolutePath);
|
|
6454
6799
|
}
|
|
6455
|
-
function applyPlan(cwd,
|
|
6456
|
-
const changed = preparePlan(cwd,
|
|
6800
|
+
function applyPlan(cwd, plan4) {
|
|
6801
|
+
const changed = preparePlan(cwd, plan4).filter((file) => file.before !== file.after);
|
|
6457
6802
|
for (const file of changed) {
|
|
6458
6803
|
if (file.deleted) {
|
|
6459
6804
|
rmSync3(file.absolutePath, { force: true });
|
|
@@ -6465,8 +6810,8 @@ function applyPlan(cwd, plan3) {
|
|
|
6465
6810
|
}
|
|
6466
6811
|
|
|
6467
6812
|
// src/core/config/preset-evidence.ts
|
|
6468
|
-
import { existsSync as
|
|
6469
|
-
import { join as
|
|
6813
|
+
import { existsSync as existsSync29, readdirSync as readdirSync4, readFileSync as readFileSync27 } from "fs";
|
|
6814
|
+
import { join as join33 } from "path";
|
|
6470
6815
|
var PATH_SIGNALS = [
|
|
6471
6816
|
{
|
|
6472
6817
|
preset: "nest",
|
|
@@ -6480,11 +6825,11 @@ var DEPENDENCY_SIGNALS = [
|
|
|
6480
6825
|
{ preset: "nest", pattern: /^@nestjs\// }
|
|
6481
6826
|
];
|
|
6482
6827
|
function dependencyNames(cwd) {
|
|
6483
|
-
const path =
|
|
6484
|
-
if (!
|
|
6828
|
+
const path = join33(cwd, "package.json");
|
|
6829
|
+
if (!existsSync29(path)) return [];
|
|
6485
6830
|
try {
|
|
6486
6831
|
const manifest = parseJsonc(
|
|
6487
|
-
|
|
6832
|
+
readFileSync27(path, "utf8"),
|
|
6488
6833
|
path
|
|
6489
6834
|
);
|
|
6490
6835
|
return [
|
|
@@ -6498,7 +6843,7 @@ function dependencyNames(cwd) {
|
|
|
6498
6843
|
function declaresJsx(cwd) {
|
|
6499
6844
|
let entries;
|
|
6500
6845
|
try {
|
|
6501
|
-
entries =
|
|
6846
|
+
entries = readdirSync4(cwd).filter(
|
|
6502
6847
|
(name) => name.startsWith("tsconfig") && name.endsWith(".json")
|
|
6503
6848
|
);
|
|
6504
6849
|
} catch {
|
|
@@ -6507,7 +6852,7 @@ function declaresJsx(cwd) {
|
|
|
6507
6852
|
for (const name of entries) {
|
|
6508
6853
|
try {
|
|
6509
6854
|
const config = parseJsonc(
|
|
6510
|
-
|
|
6855
|
+
readFileSync27(join33(cwd, name), "utf8"),
|
|
6511
6856
|
name
|
|
6512
6857
|
);
|
|
6513
6858
|
if (config.compilerOptions?.jsx !== void 0) return true;
|
|
@@ -6558,14 +6903,14 @@ function resolveFlavour(opts) {
|
|
|
6558
6903
|
}
|
|
6559
6904
|
return detection.preset;
|
|
6560
6905
|
}
|
|
6561
|
-
function previewPlan(opts,
|
|
6562
|
-
const changed = preparePlan(opts.cwd,
|
|
6906
|
+
function previewPlan(opts, plan4) {
|
|
6907
|
+
const changed = preparePlan(opts.cwd, plan4).filter((file) => file.before !== file.after);
|
|
6563
6908
|
if (opts.json) {
|
|
6564
6909
|
process.stdout.write(
|
|
6565
6910
|
JSON.stringify(
|
|
6566
6911
|
{
|
|
6567
6912
|
dryRun: true,
|
|
6568
|
-
notes:
|
|
6913
|
+
notes: plan4.notes ?? [],
|
|
6569
6914
|
files: changed.map(({ path, before, after, deleted }) => ({
|
|
6570
6915
|
path,
|
|
6571
6916
|
action: deleted ? "delete" : before.length === 0 ? "create" : "update",
|
|
@@ -6580,7 +6925,7 @@ function previewPlan(opts, plan3) {
|
|
|
6580
6925
|
return 0;
|
|
6581
6926
|
}
|
|
6582
6927
|
process.stderr.write(" dry run: no files written\n");
|
|
6583
|
-
for (const note of
|
|
6928
|
+
for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
|
|
6584
6929
|
`);
|
|
6585
6930
|
if (changed.length === 0) {
|
|
6586
6931
|
process.stderr.write(" nothing to change\n");
|
|
@@ -6624,25 +6969,25 @@ async function dispatch(opts) {
|
|
|
6624
6969
|
}
|
|
6625
6970
|
}
|
|
6626
6971
|
const context = { cwd: opts.cwd, preset: effective };
|
|
6627
|
-
const
|
|
6628
|
-
if (
|
|
6629
|
-
process.stderr.write(`sentinel (${opts.target}): ${
|
|
6972
|
+
const plan4 = await adapter.plan(context);
|
|
6973
|
+
if (plan4.blocked) {
|
|
6974
|
+
process.stderr.write(`sentinel (${opts.target}): ${plan4.blocked}
|
|
6630
6975
|
`);
|
|
6631
6976
|
return 1;
|
|
6632
6977
|
}
|
|
6633
|
-
if (
|
|
6634
|
-
process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${
|
|
6978
|
+
if (plan4.skipped) {
|
|
6979
|
+
process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan4.skipped}`)}
|
|
6635
6980
|
`);
|
|
6636
6981
|
return 0;
|
|
6637
6982
|
}
|
|
6638
6983
|
if (opts.dryRun) {
|
|
6639
|
-
return previewPlan(opts,
|
|
6984
|
+
return previewPlan(opts, plan4);
|
|
6640
6985
|
}
|
|
6641
|
-
for (const change of applyPlan(opts.cwd,
|
|
6986
|
+
for (const change of applyPlan(opts.cwd, plan4)) {
|
|
6642
6987
|
process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
|
|
6643
6988
|
`);
|
|
6644
6989
|
}
|
|
6645
|
-
for (const note of
|
|
6990
|
+
for (const note of plan4.notes ?? []) process.stderr.write(` ${note}
|
|
6646
6991
|
`);
|
|
6647
6992
|
if (adapter.afterInit) {
|
|
6648
6993
|
process.stderr.write(` fixing what ${opts.target} can fix automatically...
|
|
@@ -6687,4 +7032,4 @@ export {
|
|
|
6687
7032
|
detectFramework,
|
|
6688
7033
|
dispatch
|
|
6689
7034
|
};
|
|
6690
|
-
//# sourceMappingURL=chunk-
|
|
7035
|
+
//# sourceMappingURL=chunk-37Y7RALA.js.map
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.29",
|
|
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",
|