@hublo/sentinel 1.4.3 → 1.5.0-alpha.2
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 +15 -6
- package/dist/{chunk-DYK6UEMO.js → chunk-AHA7LGGA.js} +501 -57
- package/dist/{chunk-TDNPPNGK.js → chunk-E3MYVYEZ.js} +35 -7
- package/dist/chunk-GJGZRXM3.js +92 -0
- package/dist/chunk-GJGZRXM3.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +4 -3
- package/dist/roles/static-analysis/config.d.ts +60 -0
- package/dist/roles/static-analysis/config.js +7 -0
- package/dist/roles/static-analysis/config.js.map +1 -0
- package/dist/roles/test/setup/file-boundary.js +3 -3
- package/dist/{validate-3QMCX2HW.js → validate-STUK2AGG.js} +2 -2
- package/docs/static-analysis-study.md +788 -0
- package/docs/typescript-traces.md +1 -1
- package/package.json +7 -1
|
@@ -53,10 +53,7 @@ import {
|
|
|
53
53
|
selfCommand,
|
|
54
54
|
splitToolArgs,
|
|
55
55
|
walk
|
|
56
|
-
} from "./chunk-
|
|
57
|
-
import {
|
|
58
|
-
parseJsonc
|
|
59
|
-
} from "./chunk-QXFCZON7.js";
|
|
56
|
+
} from "./chunk-E3MYVYEZ.js";
|
|
60
57
|
import {
|
|
61
58
|
DEFAULT_MAX_DIAGNOSTICS,
|
|
62
59
|
findWorkspaceRoot,
|
|
@@ -64,6 +61,14 @@ import {
|
|
|
64
61
|
readOwnPackage,
|
|
65
62
|
readProjectPackageJson
|
|
66
63
|
} from "./chunk-KMKQDGI6.js";
|
|
64
|
+
import {
|
|
65
|
+
REPORT_DIR,
|
|
66
|
+
defineStaticAnalysis,
|
|
67
|
+
isPlainObject
|
|
68
|
+
} from "./chunk-GJGZRXM3.js";
|
|
69
|
+
import {
|
|
70
|
+
parseJsonc
|
|
71
|
+
} from "./chunk-QXFCZON7.js";
|
|
67
72
|
|
|
68
73
|
// src/core/registry.ts
|
|
69
74
|
var adapters = [];
|
|
@@ -760,7 +765,7 @@ function invokesVite(segment) {
|
|
|
760
765
|
}
|
|
761
766
|
function buildScripts(cwd) {
|
|
762
767
|
const own = selfCommand(cwd, "--run --build");
|
|
763
|
-
const
|
|
768
|
+
const scripts2 = {
|
|
764
769
|
[BUILD_SCRIPT_NAME]: composeToolScript(existingCommand(cwd, BUILD_SCRIPT_NAME), {
|
|
765
770
|
command: own ?? SENTINEL_BUILD_COMMAND,
|
|
766
771
|
replaces: invokesVite,
|
|
@@ -772,14 +777,14 @@ function buildScripts(cwd) {
|
|
|
772
777
|
const existingDev = existingCommand(cwd, DEV_SCRIPT_NAME);
|
|
773
778
|
if (existingDev !== void 0) {
|
|
774
779
|
const own2 = moduleName(cwd);
|
|
775
|
-
|
|
780
|
+
scripts2[DEV_SCRIPT_NAME] = composeDevScript(
|
|
776
781
|
existingDev,
|
|
777
782
|
// No trailing `--` here: `composeDevScript` owns the separator, and adding one on both
|
|
778
783
|
// sides produced `-- --`, handing Vite an argument nobody wrote.
|
|
779
784
|
`${selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND} --module ${own2}`
|
|
780
785
|
);
|
|
781
786
|
}
|
|
782
|
-
return
|
|
787
|
+
return scripts2;
|
|
783
788
|
}
|
|
784
789
|
function plan(context) {
|
|
785
790
|
const manifest = readProjectPackageJson(context.cwd);
|
|
@@ -843,8 +848,8 @@ function planReactApp(context, configFile) {
|
|
|
843
848
|
`${configFile} now imports ${rewrite.moved.join(", ")} from \`${BUILD_PRESET_SPECIFIER}\`. Only the import lines changed: every attribute, alias and path is untouched, so this build produces what it produced before.`
|
|
844
849
|
);
|
|
845
850
|
}
|
|
846
|
-
const
|
|
847
|
-
operations.push(manifestOperation(context.cwd,
|
|
851
|
+
const scripts2 = buildScripts(context.cwd);
|
|
852
|
+
operations.push(manifestOperation(context.cwd, scripts2));
|
|
848
853
|
operations.push(
|
|
849
854
|
...nxTargetOperations({
|
|
850
855
|
cwd: context.cwd,
|
|
@@ -854,7 +859,7 @@ function planReactApp(context, configFile) {
|
|
|
854
859
|
})
|
|
855
860
|
})
|
|
856
861
|
);
|
|
857
|
-
const dev = devTargetOperations(context.cwd,
|
|
862
|
+
const dev = devTargetOperations(context.cwd, scripts2[DEV_SCRIPT_NAME]);
|
|
858
863
|
operations.push(...dev.operations);
|
|
859
864
|
if (dev.note !== void 0) notes.push(dev.note);
|
|
860
865
|
const owned = ownedBuildDependencies(context.cwd);
|
|
@@ -1148,8 +1153,8 @@ function insideBuildScript(env = process.env) {
|
|
|
1148
1153
|
return env.npm_lifecycle_event === BUILD_SCRIPT_NAME;
|
|
1149
1154
|
}
|
|
1150
1155
|
function runPrerequisite(cwd, env = process.env) {
|
|
1151
|
-
const
|
|
1152
|
-
const script =
|
|
1156
|
+
const scripts2 = moduleScripts(cwd);
|
|
1157
|
+
const script = scripts2[PREBUILD_SCRIPT_NAME];
|
|
1153
1158
|
if (script === void 0 || script.trim() === "") return { kind: "none" };
|
|
1154
1159
|
if (insideBuildScript(env)) return { kind: "already-run" };
|
|
1155
1160
|
const result = spawnSync2("pnpm", ["run", PREBUILD_SCRIPT_NAME], { cwd, stdio: "inherit" });
|
|
@@ -1861,16 +1866,16 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1861
1866
|
* (`svelte-kit sync && <format> && eslint .`).
|
|
1862
1867
|
*/
|
|
1863
1868
|
formatScripts(cwd) {
|
|
1864
|
-
const
|
|
1869
|
+
const scripts2 = moduleScripts(cwd);
|
|
1865
1870
|
const ownCommand = selfCommand(cwd, "--run --format");
|
|
1866
1871
|
const compose = (existing, keepFix) => composeFormatScript(existing, { keepFix, command: ownCommand });
|
|
1867
1872
|
const rewritten = {
|
|
1868
1873
|
// `existingCommand` for the role's own script: the command can live in an
|
|
1869
1874
|
// `nx:run-commands` target that adoption deletes, taking anything else it ran with it.
|
|
1870
1875
|
[FORMAT_SCRIPT_NAME]: compose(existingCommand(cwd, FORMAT_SCRIPT_NAME), false),
|
|
1871
|
-
[`${FORMAT_SCRIPT_NAME}:fix`]: compose(
|
|
1876
|
+
[`${FORMAT_SCRIPT_NAME}:fix`]: compose(scripts2[`${FORMAT_SCRIPT_NAME}:fix`], true)
|
|
1872
1877
|
};
|
|
1873
|
-
for (const [name, command] of Object.entries(
|
|
1878
|
+
for (const [name, command] of Object.entries(scripts2)) {
|
|
1874
1879
|
if (name in rewritten) continue;
|
|
1875
1880
|
const needsRewrite = /\bprettier\b/.test(command) || // ...and, for a self-adopting package, a script already running sentinel FOR FORMAT.
|
|
1876
1881
|
// The `--format` check is load-bearing: without it this matched the module's `lint`
|
|
@@ -1879,7 +1884,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1879
1884
|
if (!needsRewrite) continue;
|
|
1880
1885
|
rewritten[name] = compose(command, writesWhenRewritten(name, command));
|
|
1881
1886
|
}
|
|
1882
|
-
return this.joinFormatIntoLint(
|
|
1887
|
+
return this.joinFormatIntoLint(scripts2, rewritten, ownCommand);
|
|
1883
1888
|
}
|
|
1884
1889
|
/**
|
|
1885
1890
|
* Make `pnpm run lint` check formatting too, the way it did before the migration.
|
|
@@ -4067,7 +4072,15 @@ function findSuppressionRenames(cwd, renames) {
|
|
|
4067
4072
|
var DEFAULT_IGNORE_PATTERNS = [
|
|
4068
4073
|
"**/dist/**",
|
|
4069
4074
|
"**/node_modules/**",
|
|
4070
|
-
|
|
4075
|
+
// Anchored, unlike the two above: this one names a word a domain can legitimately use.
|
|
4076
|
+
// `**/coverage/**` also swallowed `planning-requirement/src/coverage`, the RequirementResult
|
|
4077
|
+
// coverage feature, whose 48 TypeScript files were then linted by nothing at all — neither
|
|
4078
|
+
// the preset nor the module's own planning layer said a word about them. `dist` and
|
|
4079
|
+
// `node_modules` are tool output wherever they sit, so they stay unanchored; a folder called
|
|
4080
|
+
// `coverage` is only output at the module root. Measured on the monorepo before changing:
|
|
4081
|
+
// of the 55 modules carrying the unanchored entry, planning-requirement is the ONLY one with
|
|
4082
|
+
// a source folder of that name, so this narrows nothing anywhere else.
|
|
4083
|
+
"coverage/**",
|
|
4071
4084
|
// `**/*.mock.ts` used to sit here and has been removed. It was the only entry in this list
|
|
4072
4085
|
// with no reason written next to it, and it does not meet the bar the paragraph above sets:
|
|
4073
4086
|
// a mock is not build output, not vendored and not generated. It is hand-written TypeScript
|
|
@@ -4230,7 +4243,7 @@ function plan2(context) {
|
|
|
4230
4243
|
return { operations, notes };
|
|
4231
4244
|
}
|
|
4232
4245
|
function lintScripts(cwd) {
|
|
4233
|
-
const
|
|
4246
|
+
const scripts2 = moduleScripts(cwd);
|
|
4234
4247
|
const rewritten = {
|
|
4235
4248
|
// `existingCommand`, not `scripts[...]`: the command can live in an `nx:run-commands`
|
|
4236
4249
|
// target instead of a script, and adoption deletes that target. Composing against only
|
|
@@ -4240,7 +4253,7 @@ function lintScripts(cwd) {
|
|
|
4240
4253
|
})
|
|
4241
4254
|
};
|
|
4242
4255
|
const ownCommand = selfCommand(cwd, "--run --lint");
|
|
4243
|
-
for (const [name, command] of Object.entries(
|
|
4256
|
+
for (const [name, command] of Object.entries(scripts2)) {
|
|
4244
4257
|
if (name === LINT_SCRIPT_NAME) continue;
|
|
4245
4258
|
const needsRewrite = /\beslint\b/.test(command) || // ...and, for a self-adopting package, a script already running sentinel FOR LINT.
|
|
4246
4259
|
// The `--lint` check is load-bearing: without it this matched the module's `format`
|
|
@@ -4516,11 +4529,11 @@ ${result.stderr ?? ""}`);
|
|
|
4516
4529
|
const { options: passedOptions, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
|
|
4517
4530
|
valueFlags: OXLINT_VALUE_FLAGS
|
|
4518
4531
|
});
|
|
4519
|
-
const
|
|
4532
|
+
const targets2 = paths.length > 0 ? paths : ["."];
|
|
4520
4533
|
const emptyScope = scopeFlags(paths, passedOptions);
|
|
4521
4534
|
const lint = (extra) => {
|
|
4522
4535
|
const passed = [...extra, ...emptyScope, ...passedOptions];
|
|
4523
|
-
const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...
|
|
4536
|
+
const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets2], {
|
|
4524
4537
|
cwd: ctx.cwd,
|
|
4525
4538
|
stdio: "inherit",
|
|
4526
4539
|
env
|
|
@@ -4706,8 +4719,8 @@ ${result.stderr ?? ""}`);
|
|
|
4706
4719
|
} catch {
|
|
4707
4720
|
return void 0;
|
|
4708
4721
|
}
|
|
4709
|
-
const
|
|
4710
|
-
return
|
|
4722
|
+
const targets2 = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
|
|
4723
|
+
return targets2.find((target) => !existsSync12(join16(cwd, target)));
|
|
4711
4724
|
}
|
|
4712
4725
|
/** Announce what is not enforced, so reduced coverage is never silent. */
|
|
4713
4726
|
announceDisabled(preset) {
|
|
@@ -4737,6 +4750,439 @@ function registerLint() {
|
|
|
4737
4750
|
setDefaultRunner("lint", "oxlint");
|
|
4738
4751
|
}
|
|
4739
4752
|
|
|
4753
|
+
// src/roles/static-analysis/adapters/jscpd/jscpd.adapter.ts
|
|
4754
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
4755
|
+
import { existsSync as existsSync14, mkdirSync, readFileSync as readFileSync14, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4756
|
+
import { join as join19 } from "node:path";
|
|
4757
|
+
|
|
4758
|
+
// src/roles/static-analysis/configs/resolve.ts
|
|
4759
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
4760
|
+
import { join as join17 } from "node:path";
|
|
4761
|
+
import { pathToFileURL } from "node:url";
|
|
4762
|
+
var STATIC_CONFIG_FILE = "static-analysis.config.mjs";
|
|
4763
|
+
async function loadStaticConfig(cwd) {
|
|
4764
|
+
const path = join17(cwd, STATIC_CONFIG_FILE);
|
|
4765
|
+
if (!existsSync13(path)) {
|
|
4766
|
+
return { config: defineStaticAnalysis(), from: null, unreadable: null };
|
|
4767
|
+
}
|
|
4768
|
+
try {
|
|
4769
|
+
const loaded = await import(pathToFileURL(path).href);
|
|
4770
|
+
return { config: adopt(loaded.default), from: STATIC_CONFIG_FILE, unreadable: null };
|
|
4771
|
+
} catch (error) {
|
|
4772
|
+
return {
|
|
4773
|
+
config: defineStaticAnalysis(),
|
|
4774
|
+
from: null,
|
|
4775
|
+
unreadable: `${STATIC_CONFIG_FILE}: ${error instanceof Error ? error.message : String(error)}`
|
|
4776
|
+
};
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
function adopt(exported) {
|
|
4780
|
+
if (exported === null || typeof exported !== "object") return defineStaticAnalysis();
|
|
4781
|
+
if (Array.isArray(exported.moved)) {
|
|
4782
|
+
return exported;
|
|
4783
|
+
}
|
|
4784
|
+
return defineStaticAnalysis(exported);
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
// src/roles/static-analysis/help/metrics.ts
|
|
4788
|
+
var metricsHelp = {
|
|
4789
|
+
duplicatedLinesPercent: {
|
|
4790
|
+
label: "Duplicated lines",
|
|
4791
|
+
what: "The share of lines jscpd found in at least one clone, over the lines it scanned.",
|
|
4792
|
+
read: "Higher is worse. It is computed per module, so two modules compare directly; the repository-wide figure is not the average of theirs, because modules differ in size.",
|
|
4793
|
+
caveat: "A clone is not automatically a defect. Two screens that genuinely do the same thing are supposed to look alike, and extracting them can couple two features that have no reason to change together. The usual reading is the Rule of Three: a second occurrence is tolerable, a third is the signal to refactor. This share cannot apply that rule for you, because it counts lines, not how many times one fragment occurs.",
|
|
4794
|
+
source: "jscpd, over the token stream rather than the text: for JavaScript and TypeScript the tokens come from the oxc parser, which emits neither whitespace nor comments, so reindenting a block is not a clone and `--mode` measured identical on all three values. Rule of Three: Fowler, Refactoring, crediting Don Roberts."
|
|
4795
|
+
},
|
|
4796
|
+
clones: {
|
|
4797
|
+
label: "Clones",
|
|
4798
|
+
what: "How many duplicated fragments jscpd reported, each entry linking two places that match.",
|
|
4799
|
+
read: "A count, so it grows with the size of the module. Read it next to the duplicated-line share rather than on its own, and treat it as the number of places to go and look at.",
|
|
4800
|
+
caveat: "Not a count of duplicated places, and the gap is measured, not theoretical: five identical files produce four entries, all of them naming one file in common, so n copies give n-1 entries, neither n nor the n(n-1)/2 pairs. Worse for reading occurrences, jscpd keeps only the longest match per pair of files, so when a third file shares just part of a block, that block is reported under two different fragments and its real number of occurrences cannot be recovered from the report. Size is invisible too: one clone of sixty lines and one of six both count as one.",
|
|
4801
|
+
source: "jscpd report statistics. Counting behaviour measured on fixtures with jscpd 5.4.0; the shared file is not the same one across major versions, so only the count is relied on."
|
|
4802
|
+
}
|
|
4803
|
+
};
|
|
4804
|
+
function helpFor(key) {
|
|
4805
|
+
return metricsHelp[key];
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
// src/roles/static-analysis/report.ts
|
|
4809
|
+
function hasFinding(report) {
|
|
4810
|
+
return report.rows.some((row) => row.over);
|
|
4811
|
+
}
|
|
4812
|
+
function renderConsole(report) {
|
|
4813
|
+
const lines = [];
|
|
4814
|
+
const width = (pick) => Math.max(...report.rows.map((row) => pick(row).length));
|
|
4815
|
+
const label = (row) => helpFor(row.metric)?.label ?? row.metric;
|
|
4816
|
+
const value = (row) => `${row.value} ${row.unit}`;
|
|
4817
|
+
const limit = (row) => row.threshold === null ? "no threshold" : `max ${row.threshold}${row.moved ? " (module)" : ""}`;
|
|
4818
|
+
const labelWidth = width(label);
|
|
4819
|
+
const valueWidth = width(value);
|
|
4820
|
+
lines.push(`sentinel static-analysis: ${report.module}`);
|
|
4821
|
+
for (const row of report.rows) {
|
|
4822
|
+
lines.push(
|
|
4823
|
+
` ${row.over ? "!" : " "} ${label(row).padEnd(labelWidth)} ${value(row).padStart(valueWidth)} ${limit(row)}`
|
|
4824
|
+
);
|
|
4825
|
+
}
|
|
4826
|
+
if (report.byLanguage && Object.keys(report.byLanguage).length > 0) {
|
|
4827
|
+
lines.push("", " per language, where a clone was found");
|
|
4828
|
+
for (const [language, stats] of Object.entries(report.byLanguage)) {
|
|
4829
|
+
lines.push(
|
|
4830
|
+
` ${language.padEnd(12)} ${stats.percentage.toFixed(2)}% (${stats.duplicatedLines} lines over ${stats.files} files)`
|
|
4831
|
+
);
|
|
4832
|
+
}
|
|
4833
|
+
lines.push(" a language with no clone is not listed");
|
|
4834
|
+
}
|
|
4835
|
+
const caveats = report.rows.map((row) => ({ row, help: helpFor(row.metric) })).filter(
|
|
4836
|
+
(entry) => Boolean(entry.help)
|
|
4837
|
+
);
|
|
4838
|
+
if (caveats.length > 0) {
|
|
4839
|
+
lines.push("", " what these numbers do not say");
|
|
4840
|
+
for (const { row, help } of caveats) {
|
|
4841
|
+
lines.push(` ${help.label}: ${help.caveat}`, ` source: ${help.source}`);
|
|
4842
|
+
void row;
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
return `${lines.join("\n")}
|
|
4846
|
+
`;
|
|
4847
|
+
}
|
|
4848
|
+
function renderJson(report) {
|
|
4849
|
+
const rows = report.rows.map((row) => ({ ...row, help: helpFor(row.metric) ?? null }));
|
|
4850
|
+
return `${JSON.stringify({ ...report, rows }, null, 2)}
|
|
4851
|
+
`;
|
|
4852
|
+
}
|
|
4853
|
+
function escape(text) {
|
|
4854
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
4855
|
+
}
|
|
4856
|
+
function renderHtml(report) {
|
|
4857
|
+
const rows = report.rows.map((row) => {
|
|
4858
|
+
const help = helpFor(row.metric);
|
|
4859
|
+
return ` <tr class="${row.over ? "over" : ""}">
|
|
4860
|
+
<th scope="row">${escape(help?.label ?? row.metric)}</th>
|
|
4861
|
+
<td class="num">${escape(String(row.value))} <span class="unit">${escape(row.unit)}</span></td>
|
|
4862
|
+
<td class="num">${row.threshold === null ? '<span class="unit">none</span>' : escape(String(row.threshold))}${row.moved ? ' <span class="moved">module</span>' : ""}</td>
|
|
4863
|
+
<td class="caveat">${escape(help?.caveat ?? "")}</td>
|
|
4864
|
+
</tr>`;
|
|
4865
|
+
}).join("\n");
|
|
4866
|
+
return `<!doctype html>
|
|
4867
|
+
<html lang="en">
|
|
4868
|
+
<head>
|
|
4869
|
+
<meta charset="utf-8">
|
|
4870
|
+
<title>Static analysis: ${escape(report.module)}</title>
|
|
4871
|
+
<style>
|
|
4872
|
+
:root { color-scheme: light dark; --ink: #1a1a1a; --bg: #fdfdfc; --rule: #e2e0da; --warn: #9a3412; --dim: #6b6b6b; }
|
|
4873
|
+
@media (prefers-color-scheme: dark) {
|
|
4874
|
+
:root { --ink: #e8e6e1; --bg: #1b1b1a; --rule: #35332e; --warn: #fb923c; --dim: #9b9892; }
|
|
4875
|
+
}
|
|
4876
|
+
body { margin: 0; padding: 2rem 1rem; background: var(--bg); color: var(--ink);
|
|
4877
|
+
font: 15px/1.55 ui-sans-serif, system-ui, sans-serif; }
|
|
4878
|
+
main { max-width: 60rem; margin: 0 auto; }
|
|
4879
|
+
h1 { font-size: 1.25rem; margin: 0 0 0.25rem; }
|
|
4880
|
+
p.sub { color: var(--dim); margin: 0 0 1.5rem; }
|
|
4881
|
+
table { width: 100%; border-collapse: collapse; }
|
|
4882
|
+
th, td { text-align: left; padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--rule);
|
|
4883
|
+
vertical-align: top; }
|
|
4884
|
+
thead th { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--dim); }
|
|
4885
|
+
td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
4886
|
+
.unit, .moved { color: var(--dim); font-size: 0.85em; }
|
|
4887
|
+
.caveat { color: var(--dim); font-size: 0.9em; }
|
|
4888
|
+
tr.over th, tr.over td.num { color: var(--warn); font-weight: 600; }
|
|
4889
|
+
</style>
|
|
4890
|
+
</head>
|
|
4891
|
+
<body>
|
|
4892
|
+
<main>
|
|
4893
|
+
<h1>Static analysis: ${escape(report.module)}</h1>
|
|
4894
|
+
<p class="sub">Passes: ${escape(report.passes.join(", "))}</p>
|
|
4895
|
+
<table>
|
|
4896
|
+
<thead>
|
|
4897
|
+
<tr><th scope="col">Metric</th><th scope="col">Value</th><th scope="col">Threshold</th><th scope="col">What it does not say</th></tr>
|
|
4898
|
+
</thead>
|
|
4899
|
+
<tbody>
|
|
4900
|
+
${rows}
|
|
4901
|
+
</tbody>
|
|
4902
|
+
</table>
|
|
4903
|
+
</main>
|
|
4904
|
+
</body>
|
|
4905
|
+
</html>
|
|
4906
|
+
`;
|
|
4907
|
+
}
|
|
4908
|
+
|
|
4909
|
+
// src/roles/static-analysis/adapters/jscpd/config.ts
|
|
4910
|
+
import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4911
|
+
import { tmpdir } from "node:os";
|
|
4912
|
+
import { join as join18 } from "node:path";
|
|
4913
|
+
function fileSettings(config) {
|
|
4914
|
+
return {
|
|
4915
|
+
// The detection window. Stated rather than defaulted, because it decides what is invisible.
|
|
4916
|
+
minTokens: config.duplication.minTokens,
|
|
4917
|
+
minLines: config.duplication.minLines,
|
|
4918
|
+
// `format` is jscpd's name for what we call `languages`. Naming `typescript` alone would
|
|
4919
|
+
// leave out `.tsx`, which is a separate language to it: 300 files of 523 on front/console.
|
|
4920
|
+
format: config.languages,
|
|
4921
|
+
ignore: config.ignore,
|
|
4922
|
+
// A scan that analyses nothing otherwise exits 0 with only a warning, which reads as a pass.
|
|
4923
|
+
failOnEmpty: true,
|
|
4924
|
+
// The dead-code engine keeps its own section, and its own `minLines`: jscpd's documentation is
|
|
4925
|
+
// explicit that the top-level one is about clones and is never carried into a dead-code run.
|
|
4926
|
+
deadCode: {
|
|
4927
|
+
entry: config.entry,
|
|
4928
|
+
minConfidence: config.deadCode.minConfidence,
|
|
4929
|
+
categories: config.deadCode.categories
|
|
4930
|
+
},
|
|
4931
|
+
// The escape hatch, last so it wins, and empty unless a module asked for it.
|
|
4932
|
+
...config.tools.jscpd
|
|
4933
|
+
};
|
|
4934
|
+
}
|
|
4935
|
+
function duplicationRun(config, reportDir, paths) {
|
|
4936
|
+
const dir = mkdtempSync(join18(tmpdir(), "sentinel-static-"));
|
|
4937
|
+
const configPath = join18(dir, "jscpd.json");
|
|
4938
|
+
writeFileSync3(configPath, `${JSON.stringify(fileSettings(config), null, 2)}
|
|
4939
|
+
`, "utf8");
|
|
4940
|
+
const args = [
|
|
4941
|
+
"--config",
|
|
4942
|
+
configPath,
|
|
4943
|
+
// Always json, and only json, whatever shape the caller asked for. The three shapes are
|
|
4944
|
+
// rendered by this role from the figures, not by the tool: one report lives under
|
|
4945
|
+
// `static-analysis`, assembled from several passes, so a per-pass console table from the tool
|
|
4946
|
+
// would be a fourth voice saying the same numbers differently. `--output` below is jscpd's
|
|
4947
|
+
// output DIRECTORY, not our `--output` shape.
|
|
4948
|
+
"--reporters",
|
|
4949
|
+
"json",
|
|
4950
|
+
"--output",
|
|
4951
|
+
reportDir,
|
|
4952
|
+
"--no-tips",
|
|
4953
|
+
...paths
|
|
4954
|
+
];
|
|
4955
|
+
return { args, dispose: () => rmSync3(dir, { force: true, recursive: true }) };
|
|
4956
|
+
}
|
|
4957
|
+
|
|
4958
|
+
// src/roles/static-analysis/adapters/jscpd/resolve.ts
|
|
4959
|
+
function resolveJscpd(cwd) {
|
|
4960
|
+
return resolveBin(cwd, "jscpd") ?? binFromOwnInstall("jscpd", "jscpd");
|
|
4961
|
+
}
|
|
4962
|
+
|
|
4963
|
+
// src/roles/static-analysis/adapters/jscpd/jscpd.adapter.ts
|
|
4964
|
+
var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".css", ".scss"];
|
|
4965
|
+
var SCRIPT = "static-analysis";
|
|
4966
|
+
var SHAPES = ["console", "json", "html"];
|
|
4967
|
+
var scripts = () => ({
|
|
4968
|
+
[SCRIPT]: `sentinel --run --${SCRIPT}`,
|
|
4969
|
+
[`${SCRIPT}:json`]: `sentinel --run --${SCRIPT} --output json`,
|
|
4970
|
+
[`${SCRIPT}:html`]: `sentinel --run --${SCRIPT} --output html`
|
|
4971
|
+
});
|
|
4972
|
+
var targets = () => ({
|
|
4973
|
+
[SCRIPT]: { cache: true, inputs: ["default", `{projectRoot}/${STATIC_CONFIG_FILE}`] },
|
|
4974
|
+
[`${SCRIPT}:json`]: { cache: false },
|
|
4975
|
+
[`${SCRIPT}:html`]: { cache: false }
|
|
4976
|
+
});
|
|
4977
|
+
var CONFIG_STUB = `import { defineStaticAnalysis } from '@hublo/sentinel/static-analysis'
|
|
4978
|
+
|
|
4979
|
+
export default defineStaticAnalysis({
|
|
4980
|
+
root: import.meta.dirname,
|
|
4981
|
+
|
|
4982
|
+
// Files that RUN although nothing imports them: they are where reachability starts. Added to
|
|
4983
|
+
// the patterns sentinel ships, never replacing them.
|
|
4984
|
+
entry: [],
|
|
4985
|
+
|
|
4986
|
+
// Named product journeys, as { 'the name product uses': ['src/some/path/**'] }. Paths are
|
|
4987
|
+
// relative to this module, because one run measures one module.
|
|
4988
|
+
journeys: {},
|
|
4989
|
+
})
|
|
4990
|
+
`;
|
|
4991
|
+
var JscpdAdapter = class extends BaseAdapter {
|
|
4992
|
+
target = "static-analysis";
|
|
4993
|
+
runner = "jscpd";
|
|
4994
|
+
/** Every preset: duplication, dead code and complexity are not stack-specific. */
|
|
4995
|
+
appliesTo(_preset) {
|
|
4996
|
+
return true;
|
|
4997
|
+
}
|
|
4998
|
+
plan(context) {
|
|
4999
|
+
if (!hasSourceFiles(context.cwd, EXTENSIONS)) {
|
|
5000
|
+
return {
|
|
5001
|
+
operations: [],
|
|
5002
|
+
skipped: `no file ${this.runner} can read (looked for ${EXTENSIONS.join(", ")}).`
|
|
5003
|
+
};
|
|
5004
|
+
}
|
|
5005
|
+
return {
|
|
5006
|
+
operations: [
|
|
5007
|
+
// `write` and not `merge-json`: this file is code, and the engine's in-place merge is for
|
|
5008
|
+
// JSON. A re-init therefore RESTORES the stub, so an existing one is left alone below.
|
|
5009
|
+
...existsSync14(join19(context.cwd, STATIC_CONFIG_FILE)) ? [] : [{ kind: "write", path: STATIC_CONFIG_FILE, contents: CONFIG_STUB }],
|
|
5010
|
+
manifestOperation(context.cwd, scripts()),
|
|
5011
|
+
...nxTargetOperations({ cwd: context.cwd, targets: targets() }),
|
|
5012
|
+
// The reports are build output. Without this every adopting module grows an untracked
|
|
5013
|
+
// directory nobody can explain, which is exactly what the format role's `lint/` did.
|
|
5014
|
+
{ kind: "ensure-lines", path: ".gitignore", lines: [`/${REPORT_DIR}/`] }
|
|
5015
|
+
],
|
|
5016
|
+
notes: [
|
|
5017
|
+
`added the three \`${SCRIPT}\` scripts and ${STATIC_CONFIG_FILE}; declare this module's entry points and journeys there, everything else comes from the role`
|
|
5018
|
+
]
|
|
5019
|
+
};
|
|
5020
|
+
}
|
|
5021
|
+
async run(ctx) {
|
|
5022
|
+
const shape = ctx.output ?? "console";
|
|
5023
|
+
if (!SHAPES.includes(shape)) {
|
|
5024
|
+
process.stderr.write(
|
|
5025
|
+
`sentinel static-analysis(jscpd): unknown --output "${ctx.output}" (known: ${SHAPES.join(", ")}).
|
|
5026
|
+
`
|
|
5027
|
+
);
|
|
5028
|
+
return { ok: false, code: 1 };
|
|
5029
|
+
}
|
|
5030
|
+
if (!hasSourceFiles(ctx.cwd, EXTENSIONS)) {
|
|
5031
|
+
process.stderr.write(
|
|
5032
|
+
`sentinel static-analysis(jscpd): no file to measure in this module (looked for ${EXTENSIONS.join(", ")}).
|
|
5033
|
+
`
|
|
5034
|
+
);
|
|
5035
|
+
return { ok: true, code: 0 };
|
|
5036
|
+
}
|
|
5037
|
+
const { config, unreadable } = await loadStaticConfig(ctx.cwd);
|
|
5038
|
+
if (unreadable) {
|
|
5039
|
+
process.stderr.write(`sentinel static-analysis(jscpd): ${unreadable}
|
|
5040
|
+
`);
|
|
5041
|
+
return { ok: false, code: 1 };
|
|
5042
|
+
}
|
|
5043
|
+
const kinds = config.duplication.kinds;
|
|
5044
|
+
if (kinds.length !== 1 || kinds[0] !== "exact") {
|
|
5045
|
+
process.stderr.write(
|
|
5046
|
+
`sentinel static-analysis(jscpd): duplication.kinds is ${JSON.stringify(kinds)}, and only ["exact"] is supported. Any other kind overlaps the first, and the duplicated-line share then counts the same lines twice.
|
|
5047
|
+
`
|
|
5048
|
+
);
|
|
5049
|
+
return { ok: false, code: 1 };
|
|
5050
|
+
}
|
|
5051
|
+
const jscpd = resolveJscpd(ctx.cwd);
|
|
5052
|
+
if (!jscpd) {
|
|
5053
|
+
process.stderr.write(
|
|
5054
|
+
`sentinel static-analysis(jscpd): could not find the jscpd binary (looked in ${binSearchPath(ctx.cwd)}). Run \`pnpm install\` in the module.
|
|
5055
|
+
`
|
|
5056
|
+
);
|
|
5057
|
+
return { ok: false, code: 1 };
|
|
5058
|
+
}
|
|
5059
|
+
const { paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: ["-c", "--config"] });
|
|
5060
|
+
const reportDir = join19(ctx.cwd, REPORT_DIR);
|
|
5061
|
+
mkdirSync(reportDir, { recursive: true });
|
|
5062
|
+
const { args, dispose } = duplicationRun(config, reportDir, paths.length > 0 ? paths : ["."]);
|
|
5063
|
+
try {
|
|
5064
|
+
const result = spawnSync5(jscpd, args, { cwd: ctx.cwd, encoding: "utf8" });
|
|
5065
|
+
if (result.error) {
|
|
5066
|
+
process.stderr.write(
|
|
5067
|
+
`sentinel static-analysis(jscpd): could not run jscpd (${result.error.message})
|
|
5068
|
+
`
|
|
5069
|
+
);
|
|
5070
|
+
return { ok: false, code: 1 };
|
|
5071
|
+
}
|
|
5072
|
+
if (result.status !== 0) {
|
|
5073
|
+
process.stderr.write(
|
|
5074
|
+
`sentinel static-analysis(jscpd): jscpd exited ${result.status}
|
|
5075
|
+
${result.stderr ?? ""}`
|
|
5076
|
+
);
|
|
5077
|
+
return { ok: false, code: result.status ?? 1 };
|
|
5078
|
+
}
|
|
5079
|
+
const report = buildReport(ctx, reportDir, config);
|
|
5080
|
+
if (!report) {
|
|
5081
|
+
process.stderr.write(
|
|
5082
|
+
`sentinel static-analysis(jscpd): jscpd wrote no report in ${REPORT_DIR}
|
|
5083
|
+
`
|
|
5084
|
+
);
|
|
5085
|
+
return { ok: false, code: 1 };
|
|
5086
|
+
}
|
|
5087
|
+
emit(report, shape, reportDir);
|
|
5088
|
+
return { ok: true, code: 0, metrics: metricsOf(report) };
|
|
5089
|
+
} finally {
|
|
5090
|
+
dispose();
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
/**
|
|
5094
|
+
* Adoption, from the COMMITTED manifest only, so a workspace-wide sweep stays cheap and needs no
|
|
5095
|
+
* install. The script is the marker, which is also how the migration page follows this role.
|
|
5096
|
+
*/
|
|
5097
|
+
async status(ctx) {
|
|
5098
|
+
const declared = moduleScripts(ctx.cwd);
|
|
5099
|
+
const missing = Object.keys(scripts()).filter((name) => !(name in declared));
|
|
5100
|
+
const adopted = missing.length < Object.keys(scripts()).length;
|
|
5101
|
+
const hasConfig = existsSync14(join19(ctx.cwd, STATIC_CONFIG_FILE));
|
|
5102
|
+
return {
|
|
5103
|
+
adopted,
|
|
5104
|
+
preset: adopted ? ctx.preset : null,
|
|
5105
|
+
conformant: adopted && missing.length === 0 && hasConfig,
|
|
5106
|
+
drift: [
|
|
5107
|
+
...missing.map((name) => `missing script: ${name}`),
|
|
5108
|
+
...adopted && !hasConfig ? [`missing ${STATIC_CONFIG_FILE}`] : []
|
|
5109
|
+
]
|
|
5110
|
+
};
|
|
5111
|
+
}
|
|
5112
|
+
};
|
|
5113
|
+
function buildReport(ctx, reportDir, config) {
|
|
5114
|
+
const path = join19(reportDir, "jscpd-report.json");
|
|
5115
|
+
if (!existsSync14(path)) return void 0;
|
|
5116
|
+
let parsed;
|
|
5117
|
+
try {
|
|
5118
|
+
parsed = JSON.parse(readFileSync14(path, "utf8"));
|
|
5119
|
+
} catch {
|
|
5120
|
+
return void 0;
|
|
5121
|
+
}
|
|
5122
|
+
const total = parsed.statistics?.total;
|
|
5123
|
+
if (!total) return void 0;
|
|
5124
|
+
const limit = config.thresholds.duplicatedLinesPercent;
|
|
5125
|
+
const moved = config.moved.includes("thresholds.duplicatedLinesPercent");
|
|
5126
|
+
const percent = Number((total.percentage ?? 0).toFixed(2));
|
|
5127
|
+
const rows = [
|
|
5128
|
+
{
|
|
5129
|
+
metric: "duplicatedLinesPercent",
|
|
5130
|
+
value: percent,
|
|
5131
|
+
unit: "%",
|
|
5132
|
+
threshold: limit,
|
|
5133
|
+
over: percent > limit,
|
|
5134
|
+
moved
|
|
5135
|
+
},
|
|
5136
|
+
{
|
|
5137
|
+
metric: "clones",
|
|
5138
|
+
value: total.clones ?? 0,
|
|
5139
|
+
unit: "entries",
|
|
5140
|
+
threshold: null,
|
|
5141
|
+
over: false,
|
|
5142
|
+
moved: false
|
|
5143
|
+
}
|
|
5144
|
+
];
|
|
5145
|
+
const formats = parsed.statistics?.formats ?? {};
|
|
5146
|
+
const byLanguage = Object.fromEntries(
|
|
5147
|
+
Object.entries(formats).map(([language, stats]) => [
|
|
5148
|
+
language,
|
|
5149
|
+
{
|
|
5150
|
+
files: stats.sources ?? 0,
|
|
5151
|
+
duplicatedLines: stats.duplicatedLines ?? 0,
|
|
5152
|
+
percentage: Number((stats.percentage ?? 0).toFixed(2))
|
|
5153
|
+
}
|
|
5154
|
+
])
|
|
5155
|
+
);
|
|
5156
|
+
return { module: ctx.module, passes: ["duplication"], rows, byLanguage };
|
|
5157
|
+
}
|
|
5158
|
+
function emit(report, shape, reportDir) {
|
|
5159
|
+
if (shape === "console") {
|
|
5160
|
+
process.stdout.write(renderConsole(report));
|
|
5161
|
+
return;
|
|
5162
|
+
}
|
|
5163
|
+
const name = shape === "json" ? "static-analysis.json" : "static-analysis.html";
|
|
5164
|
+
const body = shape === "json" ? renderJson(report) : renderHtml(report);
|
|
5165
|
+
const path = join19(reportDir, name);
|
|
5166
|
+
writeFileSync4(path, body, "utf8");
|
|
5167
|
+
process.stdout.write(`${renderConsole(report)}
|
|
5168
|
+
written: ${REPORT_DIR}/${name}
|
|
5169
|
+
`);
|
|
5170
|
+
}
|
|
5171
|
+
function metricsOf(report) {
|
|
5172
|
+
return {
|
|
5173
|
+
passes: report.passes,
|
|
5174
|
+
finding: hasFinding(report),
|
|
5175
|
+
...Object.fromEntries(report.rows.map((row) => [row.metric, row.value])),
|
|
5176
|
+
byLanguage: report.byLanguage
|
|
5177
|
+
};
|
|
5178
|
+
}
|
|
5179
|
+
|
|
5180
|
+
// src/roles/static-analysis/register.ts
|
|
5181
|
+
function registerStaticAnalysis() {
|
|
5182
|
+
register(new JscpdAdapter());
|
|
5183
|
+
setDefaultRunner("static-analysis", "jscpd");
|
|
5184
|
+
}
|
|
5185
|
+
|
|
4740
5186
|
// src/roles/test/register.ts
|
|
4741
5187
|
function registerTest() {
|
|
4742
5188
|
register(new VitestAdapter());
|
|
@@ -4744,10 +5190,10 @@ function registerTest() {
|
|
|
4744
5190
|
}
|
|
4745
5191
|
|
|
4746
5192
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
4747
|
-
import { spawnSync as
|
|
4748
|
-
import { existsSync as
|
|
5193
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
5194
|
+
import { existsSync as existsSync15, readFileSync as readFileSync16 } from "node:fs";
|
|
4749
5195
|
import { createRequire } from "node:module";
|
|
4750
|
-
import { join as
|
|
5196
|
+
import { join as join21 } from "node:path";
|
|
4751
5197
|
|
|
4752
5198
|
// src/roles/typescript/presets/nest.json
|
|
4753
5199
|
var nest_default2 = {
|
|
@@ -4900,7 +5346,11 @@ function localOnlyCompilerOptions(compilerOptions, preset) {
|
|
|
4900
5346
|
|
|
4901
5347
|
// src/roles/typescript/resolve-tsconfig-target.ts
|
|
4902
5348
|
var TSCONFIG_CANDIDATES = ["tsconfig.app.json", "tsconfig.json"];
|
|
4903
|
-
var TSCONFIG_MARKERS = [
|
|
5349
|
+
var TSCONFIG_MARKERS = [
|
|
5350
|
+
"tsconfig.base.json",
|
|
5351
|
+
"@hublo/sentinel/typescript/",
|
|
5352
|
+
"@hublo/sentinel/tsconfig/"
|
|
5353
|
+
];
|
|
4904
5354
|
var TSCONFIG_FALLBACK = "tsconfig.json";
|
|
4905
5355
|
function resolveTsconfigTarget(moduleDir) {
|
|
4906
5356
|
return resolveConfigTarget(moduleDir, {
|
|
@@ -4928,8 +5378,8 @@ function readTsconfigAdoption(cwd) {
|
|
|
4928
5378
|
}
|
|
4929
5379
|
|
|
4930
5380
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
4931
|
-
import { readFileSync as
|
|
4932
|
-
import { join as
|
|
5381
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
5382
|
+
import { join as join20 } from "node:path";
|
|
4933
5383
|
|
|
4934
5384
|
// src/roles/typescript/typecheck-script.ts
|
|
4935
5385
|
var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
|
|
@@ -5022,7 +5472,7 @@ function planAdoption(context) {
|
|
|
5022
5472
|
};
|
|
5023
5473
|
}
|
|
5024
5474
|
const existing = parseJsonc(
|
|
5025
|
-
|
|
5475
|
+
readFileSync15(join20(context.cwd, target.path), "utf8"),
|
|
5026
5476
|
target.path
|
|
5027
5477
|
);
|
|
5028
5478
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -5160,7 +5610,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5160
5610
|
let chain;
|
|
5161
5611
|
try {
|
|
5162
5612
|
const parsed = parseJsonc(
|
|
5163
|
-
|
|
5613
|
+
readFileSync16(join21(cwd, target.path), "utf8"),
|
|
5164
5614
|
target.path
|
|
5165
5615
|
);
|
|
5166
5616
|
chain = parsed.extends;
|
|
@@ -5173,7 +5623,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5173
5623
|
);
|
|
5174
5624
|
if (preset === void 0) return void 0;
|
|
5175
5625
|
try {
|
|
5176
|
-
createRequire(
|
|
5626
|
+
createRequire(join21(cwd, "noop.js")).resolve(preset);
|
|
5177
5627
|
return void 0;
|
|
5178
5628
|
} catch {
|
|
5179
5629
|
return preset;
|
|
@@ -5214,7 +5664,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5214
5664
|
return { ok: false, code: 1 };
|
|
5215
5665
|
}
|
|
5216
5666
|
if (options.length > 0) return this.runWithOptions(ctx, tsc, config);
|
|
5217
|
-
const result =
|
|
5667
|
+
const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
|
|
5218
5668
|
if (result.error) {
|
|
5219
5669
|
process.stderr.write(
|
|
5220
5670
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -5249,7 +5699,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5249
5699
|
let worst = 0;
|
|
5250
5700
|
for (const project of this.referencedProjects(ctx.cwd, config)) {
|
|
5251
5701
|
const args = ["-p", project, "--noEmit", "--composite", "false", ...ctx.toolArgs ?? []];
|
|
5252
|
-
const result =
|
|
5702
|
+
const result = spawnSync6(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
|
|
5253
5703
|
if (result.error) {
|
|
5254
5704
|
process.stderr.write(
|
|
5255
5705
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -5269,7 +5719,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5269
5719
|
referencedProjects(cwd, config) {
|
|
5270
5720
|
try {
|
|
5271
5721
|
const parsed = parseJsonc(
|
|
5272
|
-
|
|
5722
|
+
readFileSync16(join21(cwd, config), "utf8"),
|
|
5273
5723
|
config
|
|
5274
5724
|
);
|
|
5275
5725
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
|
|
@@ -5285,7 +5735,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5285
5735
|
* check.
|
|
5286
5736
|
*/
|
|
5287
5737
|
typecheckTarget(cwd) {
|
|
5288
|
-
if (
|
|
5738
|
+
if (existsSync15(join21(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
5289
5739
|
const target = resolveTsconfigTarget(cwd);
|
|
5290
5740
|
return target.reason === "none" ? null : target.path;
|
|
5291
5741
|
}
|
|
@@ -5338,7 +5788,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5338
5788
|
return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
|
|
5339
5789
|
}
|
|
5340
5790
|
const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
|
|
5341
|
-
const result =
|
|
5791
|
+
const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
|
|
5342
5792
|
if (result.error) {
|
|
5343
5793
|
process.stderr.write(
|
|
5344
5794
|
`sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
|
|
@@ -5381,7 +5831,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
5381
5831
|
* errors in the output mean the rule must be on.
|
|
5382
5832
|
*/
|
|
5383
5833
|
noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
|
|
5384
|
-
const shown =
|
|
5834
|
+
const shown = spawnSync6(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
|
|
5385
5835
|
if (shown.status === 0 && shown.stdout) {
|
|
5386
5836
|
try {
|
|
5387
5837
|
const co = parseJsonc(
|
|
@@ -5409,6 +5859,7 @@ function registerAdapters() {
|
|
|
5409
5859
|
registerFormat();
|
|
5410
5860
|
registerBuild();
|
|
5411
5861
|
registerTest();
|
|
5862
|
+
registerStaticAnalysis();
|
|
5412
5863
|
}
|
|
5413
5864
|
|
|
5414
5865
|
// src/core/detect-framework.ts
|
|
@@ -5490,16 +5941,9 @@ function replaceLines(current, replacements) {
|
|
|
5490
5941
|
}
|
|
5491
5942
|
|
|
5492
5943
|
// src/core/apply-plan.ts
|
|
5493
|
-
import { existsSync as
|
|
5944
|
+
import { existsSync as existsSync16, readFileSync as readFileSync17, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
5494
5945
|
import { resolve as resolve3, sep } from "node:path";
|
|
5495
5946
|
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
5496
|
-
|
|
5497
|
-
// src/shared/deep-merge.ts
|
|
5498
|
-
function isPlainObject(value) {
|
|
5499
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5500
|
-
}
|
|
5501
|
-
|
|
5502
|
-
// src/core/apply-plan.ts
|
|
5503
5947
|
function resolveWithinRoot(cwd, relativePath) {
|
|
5504
5948
|
const root = resolve3(cwd);
|
|
5505
5949
|
const absolutePath = resolve3(root, relativePath);
|
|
@@ -5509,7 +5953,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
5509
5953
|
return absolutePath;
|
|
5510
5954
|
}
|
|
5511
5955
|
function readIfExists(absolutePath) {
|
|
5512
|
-
return
|
|
5956
|
+
return existsSync16(absolutePath) ? readFileSync17(absolutePath, "utf8") : void 0;
|
|
5513
5957
|
}
|
|
5514
5958
|
function* leaves(value, prefix = []) {
|
|
5515
5959
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -5611,14 +6055,14 @@ function preparePlan(cwd, plan3) {
|
|
|
5611
6055
|
}
|
|
5612
6056
|
function writeFileAtomic(absolutePath, contents) {
|
|
5613
6057
|
const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
|
|
5614
|
-
|
|
6058
|
+
writeFileSync5(tempPath, contents);
|
|
5615
6059
|
renameSync(tempPath, absolutePath);
|
|
5616
6060
|
}
|
|
5617
6061
|
function applyPlan(cwd, plan3) {
|
|
5618
6062
|
const changed = preparePlan(cwd, plan3).filter((file) => file.before !== file.after);
|
|
5619
6063
|
for (const file of changed) {
|
|
5620
6064
|
if (file.deleted) {
|
|
5621
|
-
|
|
6065
|
+
rmSync4(file.absolutePath, { force: true });
|
|
5622
6066
|
continue;
|
|
5623
6067
|
}
|
|
5624
6068
|
writeFileAtomic(file.absolutePath, file.after);
|
|
@@ -5627,8 +6071,8 @@ function applyPlan(cwd, plan3) {
|
|
|
5627
6071
|
}
|
|
5628
6072
|
|
|
5629
6073
|
// src/core/config/preset-evidence.ts
|
|
5630
|
-
import { existsSync as
|
|
5631
|
-
import { join as
|
|
6074
|
+
import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as readFileSync18 } from "node:fs";
|
|
6075
|
+
import { join as join22 } from "node:path";
|
|
5632
6076
|
var PATH_SIGNALS = [
|
|
5633
6077
|
{
|
|
5634
6078
|
preset: "nest",
|
|
@@ -5662,12 +6106,12 @@ function sourceImports(cwd, pattern) {
|
|
|
5662
6106
|
}
|
|
5663
6107
|
for (const entry of entries) {
|
|
5664
6108
|
if (entry.isDirectory()) {
|
|
5665
|
-
if (!NOT_SCANNED.has(entry.name)) stack.push(
|
|
6109
|
+
if (!NOT_SCANNED.has(entry.name)) stack.push(join22(dir, entry.name));
|
|
5666
6110
|
continue;
|
|
5667
6111
|
}
|
|
5668
6112
|
if (!SOURCE_FILE.test(entry.name)) continue;
|
|
5669
6113
|
try {
|
|
5670
|
-
if (pattern.test(
|
|
6114
|
+
if (pattern.test(readFileSync18(join22(dir, entry.name), "utf8"))) return true;
|
|
5671
6115
|
} catch {
|
|
5672
6116
|
continue;
|
|
5673
6117
|
}
|
|
@@ -5676,11 +6120,11 @@ function sourceImports(cwd, pattern) {
|
|
|
5676
6120
|
return false;
|
|
5677
6121
|
}
|
|
5678
6122
|
function dependencyNames(cwd) {
|
|
5679
|
-
const path =
|
|
5680
|
-
if (!
|
|
6123
|
+
const path = join22(cwd, "package.json");
|
|
6124
|
+
if (!existsSync17(path)) return [];
|
|
5681
6125
|
try {
|
|
5682
6126
|
const manifest = parseJsonc(
|
|
5683
|
-
|
|
6127
|
+
readFileSync18(path, "utf8"),
|
|
5684
6128
|
path
|
|
5685
6129
|
);
|
|
5686
6130
|
return [
|
|
@@ -5703,7 +6147,7 @@ function reactTsconfigEvidence(cwd) {
|
|
|
5703
6147
|
}
|
|
5704
6148
|
for (const name of entries) {
|
|
5705
6149
|
try {
|
|
5706
|
-
const config = parseJsonc(
|
|
6150
|
+
const config = parseJsonc(readFileSync18(join22(cwd, name), "utf8"), name);
|
|
5707
6151
|
if (config.compilerOptions?.jsx !== void 0) return 'its tsconfig sets "jsx"';
|
|
5708
6152
|
const extended = Array.isArray(config.extends) ? config.extends : config.extends === void 0 ? [] : [config.extends];
|
|
5709
6153
|
if (extended.some((entry) => entry === REACT_TYPESCRIPT_PRESET)) {
|