@hublo/sentinel 1.5.1-alpha.2 → 1.6.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/dist/bin/sentinel.js +172 -12
- package/dist/{chunk-WBDBRGLV.js → chunk-7E6YTTAK.js} +8 -1
- package/dist/chunk-7E6YTTAK.js.map +1 -0
- package/dist/chunk-NAFI7OLI.js +75 -0
- package/dist/chunk-NAFI7OLI.js.map +1 -0
- package/dist/{chunk-JHLNNIZC.js → chunk-PC5R4DX5.js} +1368 -272
- package/dist/index.d.ts +31 -1
- package/dist/index.js +3 -2
- package/dist/roles/env/config.d.ts +90 -0
- package/dist/roles/env/config.js +7 -0
- package/dist/roles/env/config.js.map +1 -0
- package/dist/roles/static-analysis/config.js +1 -1
- package/dist/roles/test/setup/jest-parity.js +4 -4
- package/docs/env-adoption.md +229 -0
- package/docs/typescript-traces.md +258 -0
- package/docs/using-sentinel.md +11 -10
- package/docs/validating-a-change.md +103 -0
- package/package.json +5 -1
- package/dist/chunk-WBDBRGLV.js.map +0 -1
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
readProjectPackageJson
|
|
71
71
|
} from "./chunk-JRZI5IIN.js";
|
|
72
72
|
import {
|
|
73
|
+
DEFAULT_ENV_LEVEL,
|
|
73
74
|
PRESET_NAMES,
|
|
74
75
|
REPORT_DIR,
|
|
75
76
|
SCRIPTS,
|
|
@@ -80,7 +81,10 @@ import {
|
|
|
80
81
|
extensionsOf,
|
|
81
82
|
isPlainObject,
|
|
82
83
|
overridableSettings
|
|
83
|
-
} from "./chunk-
|
|
84
|
+
} from "./chunk-7E6YTTAK.js";
|
|
85
|
+
import {
|
|
86
|
+
defineEnv
|
|
87
|
+
} from "./chunk-NAFI7OLI.js";
|
|
84
88
|
import {
|
|
85
89
|
globToRegExp
|
|
86
90
|
} from "./chunk-O7REVMOC.js";
|
|
@@ -171,6 +175,67 @@ function palette(stream) {
|
|
|
171
175
|
};
|
|
172
176
|
}
|
|
173
177
|
|
|
178
|
+
// src/roles/env/presets/shared.json
|
|
179
|
+
var shared_default = {
|
|
180
|
+
readers: {
|
|
181
|
+
required: ["getOrFail", "requireEnv", "requireEnvVariable"],
|
|
182
|
+
optional: ["getOrDefault", "getEnvVariableOrDefault"]
|
|
183
|
+
},
|
|
184
|
+
scan: {
|
|
185
|
+
roots: ["."],
|
|
186
|
+
ext: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
|
|
187
|
+
excludeSuffixes: [
|
|
188
|
+
".spec.ts",
|
|
189
|
+
".test.ts",
|
|
190
|
+
".spec.tsx",
|
|
191
|
+
".test.tsx",
|
|
192
|
+
".spec.js",
|
|
193
|
+
".test.js",
|
|
194
|
+
".spec.jsx",
|
|
195
|
+
".test.jsx"
|
|
196
|
+
],
|
|
197
|
+
excludeDirs: ["node_modules", "dist"]
|
|
198
|
+
},
|
|
199
|
+
references: {
|
|
200
|
+
roots: ["."],
|
|
201
|
+
ext: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".svelte", ".vue"],
|
|
202
|
+
excludeSuffixes: [],
|
|
203
|
+
excludeDirs: ["node_modules", "dist"]
|
|
204
|
+
},
|
|
205
|
+
referencePatterns: [
|
|
206
|
+
"process\\.env\\s*\\.\\s*([A-Za-z0-9_]+)",
|
|
207
|
+
"process\\.env\\[\\s*['\"`]([^'\"`]+)['\"`]\\s*\\]",
|
|
208
|
+
"import\\.meta\\.env\\s*\\.\\s*([A-Za-z0-9_]+)",
|
|
209
|
+
"import\\.meta\\.env\\[\\s*['\"`]([^'\"`]+)['\"`]\\s*\\]",
|
|
210
|
+
`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]\\$env/static/(?:public|private)['"]`,
|
|
211
|
+
"\\benv\\.([A-Z][A-Z0-9_]*)",
|
|
212
|
+
"\\{([^{}]*)\\}\\s*=\\s*env\\b"
|
|
213
|
+
],
|
|
214
|
+
secretNamePatterns: ["SECRET", "TOKEN", "PASSWORD", "PRIVATE_?KEY", "ACCESS_?KEY", "API_?KEY"],
|
|
215
|
+
publicPrefixes: ["VITE_", "PUBLIC_", "NEXT_PUBLIC_", "REACT_APP_"],
|
|
216
|
+
envFiles: ["**/.env", "**/.env.*", "**/*.env"],
|
|
217
|
+
contracts: ["**/*.env*.example"],
|
|
218
|
+
nodeVersionFile: ".node-version"
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// src/roles/env/preset-data.ts
|
|
222
|
+
var data = shared_default;
|
|
223
|
+
var envDefaults = data;
|
|
224
|
+
function referencePatterns() {
|
|
225
|
+
return data.referencePatterns.map((source) => new RegExp(source, "g"));
|
|
226
|
+
}
|
|
227
|
+
function secretNamePatterns() {
|
|
228
|
+
return data.secretNamePatterns.map((source) => new RegExp(source, "i"));
|
|
229
|
+
}
|
|
230
|
+
var TARGET2 = "env";
|
|
231
|
+
var ENV_CONFIG_FILE = "env.config.mjs";
|
|
232
|
+
var REPORT_DIR2 = "env-report";
|
|
233
|
+
var ENV_SHAPES = ["console", "json", "markdown"];
|
|
234
|
+
var SHAPE_FILE = {
|
|
235
|
+
json: "env.json",
|
|
236
|
+
markdown: "env.md"
|
|
237
|
+
};
|
|
238
|
+
|
|
174
239
|
// src/roles/build/nest/nx-target.ts
|
|
175
240
|
import { existsSync, readFileSync } from "node:fs";
|
|
176
241
|
import { join, relative } from "node:path";
|
|
@@ -1289,15 +1354,1044 @@ function registerBuild() {
|
|
|
1289
1354
|
setDefaultRunner("dev", "vite");
|
|
1290
1355
|
}
|
|
1291
1356
|
|
|
1357
|
+
// src/roles/env/adapters/conformance/conformance.adapter.ts
|
|
1358
|
+
import { existsSync as existsSync7, mkdirSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1359
|
+
import { join as join10 } from "node:path";
|
|
1360
|
+
|
|
1361
|
+
// src/roles/env/collect.ts
|
|
1362
|
+
import { dirname, join as join8 } from "node:path/posix";
|
|
1363
|
+
|
|
1364
|
+
// src/roles/env/checks.ts
|
|
1365
|
+
var READER_VAR_NAME = String.raw`[A-Z0-9_]+`;
|
|
1366
|
+
var REFERENCED_NAME = /[A-Za-z_][A-Za-z0-9_]*/g;
|
|
1367
|
+
function lineAt(content, index) {
|
|
1368
|
+
let line = 1;
|
|
1369
|
+
for (let position = content.indexOf("\n"); position !== -1 && position < index; ) {
|
|
1370
|
+
line += 1;
|
|
1371
|
+
position = content.indexOf("\n", position + 1);
|
|
1372
|
+
}
|
|
1373
|
+
return line;
|
|
1374
|
+
}
|
|
1375
|
+
function readerRegex(readerName) {
|
|
1376
|
+
return new RegExp(String.raw`(?<!\w)${readerName}\(\s*[\`'"](${READER_VAR_NAME})[\`'"]`, "g");
|
|
1377
|
+
}
|
|
1378
|
+
function recordReads(reads, matchers, file) {
|
|
1379
|
+
for (const matcher of matchers) {
|
|
1380
|
+
for (const match of file.content.matchAll(matcher)) {
|
|
1381
|
+
const name = match[1];
|
|
1382
|
+
if (name === void 0) continue;
|
|
1383
|
+
const sites = reads.get(name);
|
|
1384
|
+
if (sites?.some((site2) => site2.path === file.path)) continue;
|
|
1385
|
+
const site = { path: file.path, line: lineAt(file.content, match.index) };
|
|
1386
|
+
if (sites) sites.push(site);
|
|
1387
|
+
else reads.set(name, [site]);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
function deriveVars(files, readers) {
|
|
1392
|
+
const requiredMatchers = (readers.required ?? []).map(readerRegex);
|
|
1393
|
+
const optionalMatchers = (readers.optional ?? []).map(readerRegex);
|
|
1394
|
+
const required = /* @__PURE__ */ new Map();
|
|
1395
|
+
const optional = /* @__PURE__ */ new Map();
|
|
1396
|
+
for (const file of files) {
|
|
1397
|
+
recordReads(required, requiredMatchers, file);
|
|
1398
|
+
recordReads(optional, optionalMatchers, file);
|
|
1399
|
+
}
|
|
1400
|
+
return { required, optional };
|
|
1401
|
+
}
|
|
1402
|
+
var ENV_LINE = /^(?:export\s+)?([\w.-]+)\s*=(.*)$/;
|
|
1403
|
+
function isBlankValue(value) {
|
|
1404
|
+
const trimmed = value.trim();
|
|
1405
|
+
const quoted = /^(['"])(.*)\1$/.exec(trimmed);
|
|
1406
|
+
return (quoted ? quoted[2] : trimmed) === "";
|
|
1407
|
+
}
|
|
1408
|
+
function openQuote(value) {
|
|
1409
|
+
const trimmed = value.trim();
|
|
1410
|
+
const quote = trimmed[0];
|
|
1411
|
+
if (quote !== '"' && quote !== "'" && quote !== "`") return void 0;
|
|
1412
|
+
return trimmed.length > 1 && trimmed.endsWith(quote) ? void 0 : quote;
|
|
1413
|
+
}
|
|
1414
|
+
function parseEnvFile(content) {
|
|
1415
|
+
const entries = [];
|
|
1416
|
+
const lines = content.split("\n");
|
|
1417
|
+
let closing;
|
|
1418
|
+
for (const [index, rawLine] of lines.entries()) {
|
|
1419
|
+
const line = rawLine.trim();
|
|
1420
|
+
if (closing !== void 0) {
|
|
1421
|
+
if (line.endsWith(closing)) closing = void 0;
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
1425
|
+
const match = ENV_LINE.exec(line);
|
|
1426
|
+
if (!match) continue;
|
|
1427
|
+
const value = match[2] ?? "";
|
|
1428
|
+
entries.push({ key: match[1] ?? "", line: index + 1, blank: isBlankValue(value) });
|
|
1429
|
+
closing = openQuote(value);
|
|
1430
|
+
}
|
|
1431
|
+
return {
|
|
1432
|
+
entries,
|
|
1433
|
+
keys: entries.map((entry) => entry.key),
|
|
1434
|
+
blanked: entries.filter((entry) => entry.blank).map((entry) => entry.key),
|
|
1435
|
+
filled: entries.filter((entry) => !entry.blank).map((entry) => entry.key)
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
function diffContract(derived, declared, externallyProvided = []) {
|
|
1439
|
+
const declaredKeys = new Set(declared);
|
|
1440
|
+
const external = new Set(externallyProvided);
|
|
1441
|
+
const unaccounted = [...derived.required.keys()].filter((name) => !declaredKeys.has(name) && !external.has(name)).sort();
|
|
1442
|
+
const optionalUndeclared = [...derived.optional.keys()].filter((name) => !declaredKeys.has(name)).sort();
|
|
1443
|
+
return { unaccounted, optionalUndeclared };
|
|
1444
|
+
}
|
|
1445
|
+
function findOrphanEnvFiles(envFiles, allow = []) {
|
|
1446
|
+
const allowed = new Set(allow);
|
|
1447
|
+
return envFiles.filter((path2) => !path2.endsWith(".example") && !allowed.has(path2)).sort();
|
|
1448
|
+
}
|
|
1449
|
+
function* referencedNames(content, patterns) {
|
|
1450
|
+
for (const pattern of patterns) {
|
|
1451
|
+
for (const match of content.matchAll(pattern)) {
|
|
1452
|
+
for (const [name] of (match[1] ?? "").matchAll(REFERENCED_NAME)) yield name;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
function findReferencedVars(files, patterns) {
|
|
1457
|
+
const referenced = /* @__PURE__ */ new Map();
|
|
1458
|
+
for (const { path: path2, content } of files) {
|
|
1459
|
+
for (const name of referencedNames(content, patterns)) {
|
|
1460
|
+
const readers = referenced.get(name) ?? /* @__PURE__ */ new Set();
|
|
1461
|
+
readers.add(path2);
|
|
1462
|
+
referenced.set(name, readers);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return referenced;
|
|
1466
|
+
}
|
|
1467
|
+
function findDeadVars(declaredKeys, referenced, allow = [], allowPatterns = []) {
|
|
1468
|
+
const alive = new Set(referenced);
|
|
1469
|
+
const allowed = new Set(allow);
|
|
1470
|
+
const isExternal = (key) => allowed.has(key) || allowPatterns.some((pattern) => pattern.test(key));
|
|
1471
|
+
return [...new Set(declaredKeys)].filter((key) => !alive.has(key) && !isExternal(key)).sort();
|
|
1472
|
+
}
|
|
1473
|
+
function auditEnvFileSecrets(parsed, options) {
|
|
1474
|
+
const secretFilled = parsed.entries.filter(
|
|
1475
|
+
(entry) => !entry.blank && options.secretPatterns.some((pattern) => pattern.test(entry.key))
|
|
1476
|
+
);
|
|
1477
|
+
const isPublic = (entry) => options.publicPrefixes.some((prefix) => entry.key.startsWith(prefix));
|
|
1478
|
+
return {
|
|
1479
|
+
mustBlank: secretFilled.filter((entry) => !isPublic(entry)),
|
|
1480
|
+
publicSecrets: secretFilled.filter(isPublic)
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function splitAllowlisted(locations, allowlist) {
|
|
1484
|
+
const allow = new Set(allowlist);
|
|
1485
|
+
const allowed = [];
|
|
1486
|
+
const flagged = [];
|
|
1487
|
+
for (const location of locations) (allow.has(location) ? allowed : flagged).push(location);
|
|
1488
|
+
return { allowed, flagged };
|
|
1489
|
+
}
|
|
1490
|
+
function isDockerfilePath(path2) {
|
|
1491
|
+
const name = path2.slice(path2.lastIndexOf("/") + 1);
|
|
1492
|
+
if (name.endsWith(".dockerignore") || name.endsWith(".md")) return false;
|
|
1493
|
+
return name === "dockerfile" || /^Dockerfile([.-][^/]+)?$/.test(name) || /\.[Dd]ockerfile$/.test(name);
|
|
1494
|
+
}
|
|
1495
|
+
function copySources(line) {
|
|
1496
|
+
const words = line.split(/\s+/).slice(1);
|
|
1497
|
+
const flags = words.findIndex((word) => !word.startsWith("--"));
|
|
1498
|
+
const rest = flags === -1 ? "" : words.slice(flags).join(" ");
|
|
1499
|
+
if (rest.startsWith("<<")) return [];
|
|
1500
|
+
if (rest.startsWith("[")) {
|
|
1501
|
+
try {
|
|
1502
|
+
const parsed = JSON.parse(rest);
|
|
1503
|
+
if (Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string")) {
|
|
1504
|
+
return parsed.slice(0, -1);
|
|
1505
|
+
}
|
|
1506
|
+
} catch {
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return words.slice(flags === -1 ? words.length : flags).slice(0, -1).map((word) => /^(['"])(.*)\1$/.exec(word)?.[2] ?? word);
|
|
1510
|
+
}
|
|
1511
|
+
function parseDockerfile(content) {
|
|
1512
|
+
const froms = [];
|
|
1513
|
+
const contextCopySources = [];
|
|
1514
|
+
for (const [index, rawLine] of content.split("\n").entries()) {
|
|
1515
|
+
const line = rawLine.trim();
|
|
1516
|
+
const from = /^FROM\s+(\S+)/i.exec(line);
|
|
1517
|
+
if (from) {
|
|
1518
|
+
froms.push({ image: from[1] ?? "", line: index + 1 });
|
|
1519
|
+
continue;
|
|
1520
|
+
}
|
|
1521
|
+
if (/^COPY\s/i.test(line) && !/--from=/i.test(line)) {
|
|
1522
|
+
for (const source of copySources(line)) {
|
|
1523
|
+
if (!/[*?$]/.test(source)) contextCopySources.push({ source, line: index + 1 });
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return { froms, contextCopySources };
|
|
1528
|
+
}
|
|
1529
|
+
function dockerNodeVersion(fromImage) {
|
|
1530
|
+
const match = /(?:^|\/)node:([0-9]+(?:\.[0-9]+){0,2})/.exec(fromImage);
|
|
1531
|
+
return match?.[1] ?? null;
|
|
1532
|
+
}
|
|
1533
|
+
function normalizeNodeVersion(raw) {
|
|
1534
|
+
const version = raw.trim().replace(/^v/i, "");
|
|
1535
|
+
return version === "" ? void 0 : version;
|
|
1536
|
+
}
|
|
1537
|
+
function nodeVersionAligned(dockerVersion, expected) {
|
|
1538
|
+
return expected === dockerVersion || expected.startsWith(`${dockerVersion}.`);
|
|
1539
|
+
}
|
|
1540
|
+
function dockerHardening(content) {
|
|
1541
|
+
const lines = content.split("\n").map((line) => line.trim());
|
|
1542
|
+
let lastFrom = -1;
|
|
1543
|
+
lines.forEach((line, index) => {
|
|
1544
|
+
if (/^FROM\s/i.test(line)) lastFrom = index;
|
|
1545
|
+
});
|
|
1546
|
+
const start = lastFrom === -1 ? 0 : lastFrom;
|
|
1547
|
+
const finalStage = lines.slice(start).map((text, offset) => ({ text, line: start + offset + 1 }));
|
|
1548
|
+
let user = null;
|
|
1549
|
+
let userLine = start + 1;
|
|
1550
|
+
for (const { text, line } of finalStage) {
|
|
1551
|
+
const match = /^USER\s+(\S+)/i.exec(text);
|
|
1552
|
+
if (match) {
|
|
1553
|
+
user = match[1] ?? null;
|
|
1554
|
+
userLine = line;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
const runsAsRoot = !user || /^(root|0)(:|$)/i.test(user);
|
|
1558
|
+
const ownsTree = finalStage.some(({ text }) => /chown\s+-R/i.test(text));
|
|
1559
|
+
const copiesWithoutChown = ownsTree ? [] : finalStage.filter(({ text }) => /^COPY\s/i.test(text) && !/--chown=/i.test(text)).map(({ text, line }) => ({ instruction: text.replace(/\s+/g, " "), line }));
|
|
1560
|
+
return { user, userLine, runsAsRoot, copiesWithoutChown };
|
|
1561
|
+
}
|
|
1562
|
+
function parseNxTargetRefs(content) {
|
|
1563
|
+
return [...content.matchAll(/\bnx run\s+([^\s:$]+):([^\s$]+)/g)].map((match) => ({
|
|
1564
|
+
project: match[1] ?? "",
|
|
1565
|
+
targetSpec: match[2] ?? "",
|
|
1566
|
+
line: lineAt(content, match.index)
|
|
1567
|
+
}));
|
|
1568
|
+
}
|
|
1569
|
+
function nxTargetCandidates(targetSpec) {
|
|
1570
|
+
const parts = targetSpec.split(":");
|
|
1571
|
+
const candidates = [targetSpec];
|
|
1572
|
+
if (parts.length > 1) candidates.push(parts.slice(0, -1).join(":"));
|
|
1573
|
+
return candidates;
|
|
1574
|
+
}
|
|
1575
|
+
function resolveApp(path2, projects) {
|
|
1576
|
+
let best = null;
|
|
1577
|
+
for (const project of projects) {
|
|
1578
|
+
if (path2 === project.root || path2.startsWith(`${project.root}/`)) {
|
|
1579
|
+
if (!best || project.root.length > best.root.length) best = project;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return best ? best.name : null;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// src/roles/env/repository.ts
|
|
1586
|
+
import { execFileSync } from "node:child_process";
|
|
1587
|
+
import { readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
1588
|
+
import { join as join7 } from "node:path";
|
|
1589
|
+
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
1590
|
+
function trackedPaths(root) {
|
|
1591
|
+
const output = execFileSync("git", ["ls-files", "-z"], {
|
|
1592
|
+
cwd: root,
|
|
1593
|
+
encoding: "utf8",
|
|
1594
|
+
maxBuffer: MAX_BUFFER
|
|
1595
|
+
});
|
|
1596
|
+
return output.split("\0").filter(Boolean);
|
|
1597
|
+
}
|
|
1598
|
+
function loadNxTargets(root) {
|
|
1599
|
+
const nx = resolveBin(root, "nx");
|
|
1600
|
+
if (nx === void 0) return void 0;
|
|
1601
|
+
const runNx = (args) => execFileSync(nx, args, {
|
|
1602
|
+
cwd: root,
|
|
1603
|
+
encoding: "utf8",
|
|
1604
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1605
|
+
maxBuffer: MAX_BUFFER,
|
|
1606
|
+
env: { ...process.env, NX_DAEMON: "false" }
|
|
1607
|
+
});
|
|
1608
|
+
try {
|
|
1609
|
+
const projects = new Set(JSON.parse(runNx(["show", "projects", "--json"])));
|
|
1610
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1611
|
+
return (project) => {
|
|
1612
|
+
if (!projects.has(project)) return null;
|
|
1613
|
+
const known = cache.get(project);
|
|
1614
|
+
if (known) return known;
|
|
1615
|
+
const data2 = JSON.parse(runNx(["show", "project", project, "--json"]));
|
|
1616
|
+
const targets2 = new Set(Object.keys(data2.targets ?? {}));
|
|
1617
|
+
cache.set(project, targets2);
|
|
1618
|
+
return targets2;
|
|
1619
|
+
};
|
|
1620
|
+
} catch {
|
|
1621
|
+
return void 0;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
function openRepository(root) {
|
|
1625
|
+
let tracked;
|
|
1626
|
+
try {
|
|
1627
|
+
tracked = trackedPaths(root);
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
throw new Error(
|
|
1630
|
+
`git could not list the tracked files of ${root}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1631
|
+
{ cause: error }
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
let nx = null;
|
|
1635
|
+
const hasNx = tracked.includes("nx.json");
|
|
1636
|
+
return {
|
|
1637
|
+
root,
|
|
1638
|
+
tracked,
|
|
1639
|
+
read(path2) {
|
|
1640
|
+
try {
|
|
1641
|
+
return readFileSync6(join7(root, path2), "utf8");
|
|
1642
|
+
} catch {
|
|
1643
|
+
return void 0;
|
|
1644
|
+
}
|
|
1645
|
+
},
|
|
1646
|
+
exists(path2) {
|
|
1647
|
+
try {
|
|
1648
|
+
statSync2(join7(root, path2));
|
|
1649
|
+
return true;
|
|
1650
|
+
} catch {
|
|
1651
|
+
return false;
|
|
1652
|
+
}
|
|
1653
|
+
},
|
|
1654
|
+
hasNx,
|
|
1655
|
+
nxTargets() {
|
|
1656
|
+
if (nx === null) nx = hasNx ? loadNxTargets(root) : void 0;
|
|
1657
|
+
return nx;
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
function loadProjects(view) {
|
|
1662
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
1663
|
+
for (const file of view.tracked) {
|
|
1664
|
+
const manifest = /(?:^|\/)(project|package)\.json$/.exec(file);
|
|
1665
|
+
if (!manifest) continue;
|
|
1666
|
+
const slash = file.lastIndexOf("/");
|
|
1667
|
+
if (slash === -1) continue;
|
|
1668
|
+
const root = file.slice(0, slash);
|
|
1669
|
+
const fromProjectJson = manifest[1] === "project";
|
|
1670
|
+
const existing = byRoot.get(root);
|
|
1671
|
+
if (existing && !(fromProjectJson && !existing.fromProjectJson)) continue;
|
|
1672
|
+
try {
|
|
1673
|
+
const name = JSON.parse(view.read(file) ?? "").name;
|
|
1674
|
+
if (typeof name === "string" && name !== "") byRoot.set(root, { name, fromProjectJson });
|
|
1675
|
+
} catch {
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
return [...byRoot].map(([root, { name }]) => ({ root, name }));
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/roles/env/collect.ts
|
|
1682
|
+
var RULES = {
|
|
1683
|
+
"required-var-undeclared": {
|
|
1684
|
+
number: 1,
|
|
1685
|
+
severity: "blocking",
|
|
1686
|
+
help: "read by a required reader but declared in no contract: add it to the right .env*.example, or to externallyProvided if CI or a secret store provides it"
|
|
1687
|
+
},
|
|
1688
|
+
"optional-var-undeclared": {
|
|
1689
|
+
number: 1,
|
|
1690
|
+
severity: "info",
|
|
1691
|
+
help: "read by an optional reader and declared in no contract; a default applies"
|
|
1692
|
+
},
|
|
1693
|
+
"committed-env-file": {
|
|
1694
|
+
number: 2,
|
|
1695
|
+
severity: "blocking",
|
|
1696
|
+
help: "a real env file is committed: remove it from git (only .example contracts belong there; values come from a secret store)"
|
|
1697
|
+
},
|
|
1698
|
+
"var-not-in-app-contract": {
|
|
1699
|
+
number: 3,
|
|
1700
|
+
severity: "advisory",
|
|
1701
|
+
help: "read in this app but declared only in another app's contract: declare it in this app's own contract, or in a shared root one"
|
|
1702
|
+
},
|
|
1703
|
+
"dead-var": {
|
|
1704
|
+
number: 4,
|
|
1705
|
+
severity: "advisory",
|
|
1706
|
+
help: "declared but read nowhere in source: a candidate to remove, or add it to knownExternalVars if a tool or CI consumes it"
|
|
1707
|
+
},
|
|
1708
|
+
"public-secret-to-review": {
|
|
1709
|
+
number: 5,
|
|
1710
|
+
severity: "advisory",
|
|
1711
|
+
help: "a public-prefixed, secret-named variable carries a committed value: it ships to the browser by design, confirm it is genuinely public"
|
|
1712
|
+
},
|
|
1713
|
+
"secret-not-blanked": {
|
|
1714
|
+
number: 6,
|
|
1715
|
+
severity: "blocking",
|
|
1716
|
+
help: "a secret-named variable carries a committed value: blank it (leave the value empty) and move the value to a secret store"
|
|
1717
|
+
},
|
|
1718
|
+
"secret-allowlisted": {
|
|
1719
|
+
number: 6,
|
|
1720
|
+
severity: "advisory",
|
|
1721
|
+
help: "a secret-named variable carries a committed value, accepted by allowNonBlankedSecrets: confirm it stays non-secret"
|
|
1722
|
+
},
|
|
1723
|
+
"dead-copy-path": {
|
|
1724
|
+
number: 7,
|
|
1725
|
+
severity: "blocking",
|
|
1726
|
+
help: "this COPY source does not exist: remove the line or fix the path"
|
|
1727
|
+
},
|
|
1728
|
+
"nx-project-missing": {
|
|
1729
|
+
number: 8,
|
|
1730
|
+
severity: "blocking",
|
|
1731
|
+
help: "this Dockerfile runs an nx project that does not exist: fix or remove the reference"
|
|
1732
|
+
},
|
|
1733
|
+
"nx-target-missing": {
|
|
1734
|
+
number: 8,
|
|
1735
|
+
severity: "blocking",
|
|
1736
|
+
help: "this Dockerfile runs an nx target the project does not have: fix or remove the reference"
|
|
1737
|
+
},
|
|
1738
|
+
"node-version-mismatch": {
|
|
1739
|
+
number: 9,
|
|
1740
|
+
severity: "blocking",
|
|
1741
|
+
help: "the FROM node version differs from .node-version: align the tag"
|
|
1742
|
+
},
|
|
1743
|
+
"dockerfile-runs-as-root": {
|
|
1744
|
+
number: 10,
|
|
1745
|
+
severity: "blocking",
|
|
1746
|
+
help: "the final stage runs as root: set a non-root USER (USER node, or USER 1000:1000) so a compromise cannot tamper with the running image"
|
|
1747
|
+
},
|
|
1748
|
+
"copy-not-owned": {
|
|
1749
|
+
number: 10,
|
|
1750
|
+
severity: "blocking",
|
|
1751
|
+
help: "this COPY into the runtime stage has no --chown, so it lands root-owned under a non-root USER: add --chown (node:node, or 1000:1000 with --link) or a RUN chown -R"
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
var RULE_ORDER = Object.keys(RULES);
|
|
1755
|
+
function inScope(path2, scope) {
|
|
1756
|
+
const underRoot = scope.roots.some((root) => {
|
|
1757
|
+
const cleaned = root.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1758
|
+
return cleaned === "." || cleaned === "" || path2.startsWith(`${cleaned}/`);
|
|
1759
|
+
});
|
|
1760
|
+
if (!underRoot) return false;
|
|
1761
|
+
const segments = path2.split("/");
|
|
1762
|
+
const name = segments.pop() ?? "";
|
|
1763
|
+
if (segments.some((segment) => scope.excludeDirs.includes(segment))) return false;
|
|
1764
|
+
return scope.ext.some((suffix) => name.endsWith(suffix)) && !scope.excludeSuffixes.some((suffix) => name.endsWith(suffix));
|
|
1765
|
+
}
|
|
1766
|
+
function* sourcesOf(view, scope) {
|
|
1767
|
+
for (const path2 of view.tracked) {
|
|
1768
|
+
if (!inScope(path2, scope)) continue;
|
|
1769
|
+
const content = view.read(path2);
|
|
1770
|
+
if (content !== void 0) yield { path: path2, content };
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function missingRoots(view, settings) {
|
|
1774
|
+
const roots = [...settings.scan.roots, ...settings.references.roots];
|
|
1775
|
+
return [
|
|
1776
|
+
...new Set(
|
|
1777
|
+
roots.filter((root) => {
|
|
1778
|
+
const cleaned = root.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1779
|
+
if (cleaned === "." || cleaned === "") return false;
|
|
1780
|
+
return !view.tracked.some((path2) => path2.startsWith(`${cleaned}/`));
|
|
1781
|
+
})
|
|
1782
|
+
)
|
|
1783
|
+
];
|
|
1784
|
+
}
|
|
1785
|
+
function finding(rule, fields) {
|
|
1786
|
+
const { name, file, line, detail, readIn, app } = fields;
|
|
1787
|
+
return {
|
|
1788
|
+
rule,
|
|
1789
|
+
number: RULES[rule].number,
|
|
1790
|
+
severity: RULES[rule].severity,
|
|
1791
|
+
name,
|
|
1792
|
+
...file === void 0 ? {} : { file },
|
|
1793
|
+
...line === void 0 ? {} : { line },
|
|
1794
|
+
...detail === void 0 ? {} : { detail },
|
|
1795
|
+
...readIn === void 0 ? {} : { readIn },
|
|
1796
|
+
...app === void 0 ? {} : { app }
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
function byReportOrder(left, right) {
|
|
1800
|
+
return RULE_ORDER.indexOf(left.rule) - RULE_ORDER.indexOf(right.rule) || (left.file ?? "").localeCompare(right.file ?? "") || (left.line ?? 0) - (right.line ?? 0) || left.name.localeCompare(right.name);
|
|
1801
|
+
}
|
|
1802
|
+
function readEnvFiles(view, settings) {
|
|
1803
|
+
const envGlobs = settings.envFiles.map(globToRegExp);
|
|
1804
|
+
const contractGlobs = settings.contracts.map(globToRegExp);
|
|
1805
|
+
const envFiles = view.tracked.filter((path2) => envGlobs.some((glob) => glob.test(path2)));
|
|
1806
|
+
const contracts = view.tracked.filter((path2) => contractGlobs.some((glob) => glob.test(path2)));
|
|
1807
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
1808
|
+
for (const path2 of /* @__PURE__ */ new Set([...envFiles, ...contracts])) {
|
|
1809
|
+
const content = view.read(path2);
|
|
1810
|
+
if (content !== void 0) parsed.set(path2, parseEnvFile(content));
|
|
1811
|
+
}
|
|
1812
|
+
return { envFiles, contracts, parsed };
|
|
1813
|
+
}
|
|
1814
|
+
function secretFindings(parsed, settings) {
|
|
1815
|
+
const options = {
|
|
1816
|
+
secretPatterns: settings.secretNamePatterns,
|
|
1817
|
+
publicPrefixes: settings.publicPrefixes
|
|
1818
|
+
};
|
|
1819
|
+
const findings = [];
|
|
1820
|
+
const mustBlank = /* @__PURE__ */ new Map();
|
|
1821
|
+
for (const [file, content] of parsed) {
|
|
1822
|
+
const audit = auditEnvFileSecrets(content, options);
|
|
1823
|
+
for (const entry of audit.publicSecrets) {
|
|
1824
|
+
findings.push(finding("public-secret-to-review", { name: entry.key, file, line: entry.line }));
|
|
1825
|
+
}
|
|
1826
|
+
for (const entry of audit.mustBlank) {
|
|
1827
|
+
mustBlank.set(`${file}:${entry.key}`, { file, line: entry.line, name: entry.key });
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
const { allowed, flagged } = splitAllowlisted(
|
|
1831
|
+
[...mustBlank.keys()],
|
|
1832
|
+
settings.allowNonBlankedSecrets
|
|
1833
|
+
);
|
|
1834
|
+
for (const location of flagged) {
|
|
1835
|
+
const entry = mustBlank.get(location);
|
|
1836
|
+
if (entry) findings.push(finding("secret-not-blanked", entry));
|
|
1837
|
+
}
|
|
1838
|
+
for (const location of allowed) {
|
|
1839
|
+
const entry = mustBlank.get(location);
|
|
1840
|
+
if (entry) findings.push(finding("secret-allowlisted", entry));
|
|
1841
|
+
}
|
|
1842
|
+
return findings;
|
|
1843
|
+
}
|
|
1844
|
+
function allReadNames(derived, referenced) {
|
|
1845
|
+
return /* @__PURE__ */ new Set([...referenced.keys(), ...derived.required.keys(), ...derived.optional.keys()]);
|
|
1846
|
+
}
|
|
1847
|
+
function misplacedFindings(view, settings, derived, referenced, contracts, declared) {
|
|
1848
|
+
const projects = loadProjects(view);
|
|
1849
|
+
const readsByApp = /* @__PURE__ */ new Map();
|
|
1850
|
+
const addRead = (name, path2) => {
|
|
1851
|
+
const app = resolveApp(path2, projects);
|
|
1852
|
+
if (!app) return;
|
|
1853
|
+
const reads = readsByApp.get(app) ?? /* @__PURE__ */ new Map();
|
|
1854
|
+
if (!reads.has(name)) reads.set(name, path2);
|
|
1855
|
+
readsByApp.set(app, reads);
|
|
1856
|
+
};
|
|
1857
|
+
for (const [name, sites] of derived.required) for (const site of sites) addRead(name, site.path);
|
|
1858
|
+
for (const [name, paths] of referenced) for (const path2 of paths) addRead(name, path2);
|
|
1859
|
+
const shared = /* @__PURE__ */ new Set();
|
|
1860
|
+
const keysByApp = /* @__PURE__ */ new Map();
|
|
1861
|
+
for (const { path: path2, parsed } of contracts) {
|
|
1862
|
+
const app = resolveApp(path2, projects);
|
|
1863
|
+
const keys = app === null ? shared : keysByApp.get(app) ?? /* @__PURE__ */ new Set();
|
|
1864
|
+
for (const key of parsed.keys) keys.add(key);
|
|
1865
|
+
if (app !== null) keysByApp.set(app, keys);
|
|
1866
|
+
}
|
|
1867
|
+
const external = new Set(settings.externallyProvided);
|
|
1868
|
+
const findings = [];
|
|
1869
|
+
for (const [app, reads] of readsByApp) {
|
|
1870
|
+
const own = keysByApp.get(app);
|
|
1871
|
+
if (!own) continue;
|
|
1872
|
+
for (const [name, path2] of reads) {
|
|
1873
|
+
if (declared.has(name) && !own.has(name) && !shared.has(name) && !external.has(name)) {
|
|
1874
|
+
findings.push(
|
|
1875
|
+
finding("var-not-in-app-contract", {
|
|
1876
|
+
name,
|
|
1877
|
+
file: path2,
|
|
1878
|
+
app,
|
|
1879
|
+
detail: `read in ${app}`
|
|
1880
|
+
})
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
return findings;
|
|
1886
|
+
}
|
|
1887
|
+
function codeFindings(view, settings, contracts) {
|
|
1888
|
+
const derived = deriveVars(sourcesOf(view, settings.scan), settings.readers);
|
|
1889
|
+
const inventory = {
|
|
1890
|
+
required: [...derived.required].map(([name, sites]) => ({ name, files: sites.length })).sort((left, right) => left.name.localeCompare(right.name)),
|
|
1891
|
+
optional: [...derived.optional.keys()].sort(),
|
|
1892
|
+
contracts: contracts.map(({ path: path2, parsed }) => ({ path: path2, keys: parsed.keys.length }))
|
|
1893
|
+
};
|
|
1894
|
+
const counts = { required: derived.required.size, optional: derived.optional.size };
|
|
1895
|
+
if (contracts.length === 0) return { findings: [], inventory, counts, compared: false };
|
|
1896
|
+
const declaredAt = /* @__PURE__ */ new Map();
|
|
1897
|
+
for (const { path: path2, parsed } of contracts) {
|
|
1898
|
+
for (const entry of parsed.entries) {
|
|
1899
|
+
if (!declaredAt.has(entry.key)) declaredAt.set(entry.key, { file: path2, line: entry.line });
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
const declared = new Set(declaredAt.keys());
|
|
1903
|
+
const { unaccounted, optionalUndeclared } = diffContract(
|
|
1904
|
+
derived,
|
|
1905
|
+
declared,
|
|
1906
|
+
settings.externallyProvided
|
|
1907
|
+
);
|
|
1908
|
+
const referenced = findReferencedVars(
|
|
1909
|
+
sourcesOf(view, settings.references),
|
|
1910
|
+
settings.referencePatterns
|
|
1911
|
+
);
|
|
1912
|
+
const dead = findDeadVars(
|
|
1913
|
+
declared,
|
|
1914
|
+
allReadNames(derived, referenced),
|
|
1915
|
+
settings.knownExternalVars,
|
|
1916
|
+
settings.knownExternalPatterns
|
|
1917
|
+
);
|
|
1918
|
+
const findings = [
|
|
1919
|
+
...unaccounted.map((name) => {
|
|
1920
|
+
const sites = derived.required.get(name) ?? [];
|
|
1921
|
+
return finding("required-var-undeclared", {
|
|
1922
|
+
name,
|
|
1923
|
+
file: sites[0]?.path,
|
|
1924
|
+
line: sites[0]?.line,
|
|
1925
|
+
readIn: sites.map((site) => site.path)
|
|
1926
|
+
});
|
|
1927
|
+
}),
|
|
1928
|
+
...misplacedFindings(view, settings, derived, referenced, contracts, declared),
|
|
1929
|
+
...dead.map((name) => finding("dead-var", { name, ...declaredAt.get(name) })),
|
|
1930
|
+
...optionalUndeclared.map((name) => {
|
|
1931
|
+
const site = derived.optional.get(name)?.[0];
|
|
1932
|
+
return finding("optional-var-undeclared", { name, file: site?.path, line: site?.line });
|
|
1933
|
+
})
|
|
1934
|
+
];
|
|
1935
|
+
return { findings, inventory, counts, compared: true };
|
|
1936
|
+
}
|
|
1937
|
+
function dockerFindings(view, settings) {
|
|
1938
|
+
const excluded = (path2) => settings.docker.excludePaths.some((prefix) => path2.startsWith(prefix));
|
|
1939
|
+
const dockerfiles = view.tracked.filter((path2) => isDockerfilePath(path2) && !excluded(path2));
|
|
1940
|
+
const rawNodeVersion = view.read(settings.nodeVersionFile);
|
|
1941
|
+
const expected = rawNodeVersion === void 0 ? void 0 : normalizeNodeVersion(rawNodeVersion);
|
|
1942
|
+
const findings = [];
|
|
1943
|
+
const notes = [];
|
|
1944
|
+
let uncheckedNxRefs = 0;
|
|
1945
|
+
let nxUnavailable = false;
|
|
1946
|
+
for (const file of dockerfiles) {
|
|
1947
|
+
const content = view.read(file);
|
|
1948
|
+
if (content === void 0) continue;
|
|
1949
|
+
const { froms, contextCopySources } = parseDockerfile(content);
|
|
1950
|
+
for (const { image, line } of froms) {
|
|
1951
|
+
const version = dockerNodeVersion(image);
|
|
1952
|
+
if (version && expected && !nodeVersionAligned(version, expected)) {
|
|
1953
|
+
findings.push(
|
|
1954
|
+
finding("node-version-mismatch", {
|
|
1955
|
+
name: `node:${version}`,
|
|
1956
|
+
file,
|
|
1957
|
+
line,
|
|
1958
|
+
detail: `expected ${expected} (${settings.nodeVersionFile})`
|
|
1959
|
+
})
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
for (const { source, line } of contextCopySources) {
|
|
1964
|
+
if (!view.exists(join8(".", source)) && !view.exists(join8(dirname(file), source))) {
|
|
1965
|
+
findings.push(finding("dead-copy-path", { name: source, file, line }));
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
const { runsAsRoot, user, userLine, copiesWithoutChown } = dockerHardening(content);
|
|
1969
|
+
if (runsAsRoot && !settings.docker.allowRoot.includes(file)) {
|
|
1970
|
+
findings.push(
|
|
1971
|
+
finding("dockerfile-runs-as-root", {
|
|
1972
|
+
name: user ? `USER ${user}` : "no USER set",
|
|
1973
|
+
file,
|
|
1974
|
+
line: userLine
|
|
1975
|
+
})
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
for (const { instruction, line } of copiesWithoutChown) {
|
|
1979
|
+
findings.push(finding("copy-not-owned", { name: instruction, file, line }));
|
|
1980
|
+
}
|
|
1981
|
+
const refs = parseNxTargetRefs(content);
|
|
1982
|
+
if (refs.length === 0) continue;
|
|
1983
|
+
const lookup = view.hasNx ? view.nxTargets() : void 0;
|
|
1984
|
+
if (lookup === void 0) {
|
|
1985
|
+
uncheckedNxRefs += refs.length;
|
|
1986
|
+
nxUnavailable ||= view.hasNx;
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
for (const { project, targetSpec, line } of refs) {
|
|
1990
|
+
const targets2 = lookup(project);
|
|
1991
|
+
if (targets2 === null) {
|
|
1992
|
+
findings.push(finding("nx-project-missing", { name: project, file, line }));
|
|
1993
|
+
} else if (!nxTargetCandidates(targetSpec).some((target) => targets2.has(target))) {
|
|
1994
|
+
findings.push(
|
|
1995
|
+
finding("nx-target-missing", { name: `${project}:${targetSpec}`, file, line })
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
if (dockerfiles.length === 0)
|
|
2001
|
+
notes.push("no Dockerfile is tracked: rules 7 to 10 had nothing to check");
|
|
2002
|
+
if (dockerfiles.length > 0 && expected === void 0) {
|
|
2003
|
+
notes.push(
|
|
2004
|
+
`no ${settings.nodeVersionFile} at the repository root: rule 9 had no version to compare the FROM tags with`
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
if (!view.hasNx) {
|
|
2008
|
+
notes.push(
|
|
2009
|
+
uncheckedNxRefs > 0 ? `not an nx workspace (no nx.json): rule 8 skipped, ${uncheckedNxRefs} \`nx run\` reference(s) not checked` : "not an nx workspace (no nx.json): rule 8 skipped"
|
|
2010
|
+
);
|
|
2011
|
+
} else if (nxUnavailable) {
|
|
2012
|
+
notes.push(
|
|
2013
|
+
`the repository's own nx could not run (not installed?): rule 8 skipped, ${uncheckedNxRefs} \`nx run\` reference(s) not checked`
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
return { findings, notes, dockerfiles: dockerfiles.length, nodeVersion: expected ?? null };
|
|
2017
|
+
}
|
|
2018
|
+
function collectEnvReport(view, settings, level, config) {
|
|
2019
|
+
const missing = missingRoots(view, settings);
|
|
2020
|
+
if (missing.length > 0) {
|
|
2021
|
+
throw new Error(
|
|
2022
|
+
`${config ?? "the env settings"}: scan root(s) holding no tracked file: ${missing.join(", ")}`
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
const { envFiles, contracts, parsed } = readEnvFiles(view, settings);
|
|
2026
|
+
const contractFiles = contracts.flatMap((path2) => {
|
|
2027
|
+
const content = parsed.get(path2);
|
|
2028
|
+
return content ? [{ path: path2, parsed: content }] : [];
|
|
2029
|
+
});
|
|
2030
|
+
const code = codeFindings(view, settings, contractFiles);
|
|
2031
|
+
const docker = dockerFindings(view, settings);
|
|
2032
|
+
const orphans = findOrphanEnvFiles(envFiles, settings.allowedCommittedEnvFiles);
|
|
2033
|
+
const notes = [];
|
|
2034
|
+
if (!code.compared) {
|
|
2035
|
+
notes.push(
|
|
2036
|
+
`no contract is committed (${settings.contracts.join(", ")}): rules 1, 3 and 4 had nothing to compare the code with`
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
if (code.counts.required === 0) {
|
|
2040
|
+
notes.push(
|
|
2041
|
+
`no required reader call found (${settings.readers.required.join(", ")}): rule 1 had nothing to check; name the repository's own helper in readers.required`
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
notes.push(...docker.notes);
|
|
2045
|
+
return {
|
|
2046
|
+
level,
|
|
2047
|
+
config,
|
|
2048
|
+
scanned: settings.scan.roots,
|
|
2049
|
+
counts: {
|
|
2050
|
+
required: code.counts.required,
|
|
2051
|
+
optional: code.counts.optional,
|
|
2052
|
+
contracts: contractFiles.length,
|
|
2053
|
+
envFiles: envFiles.length,
|
|
2054
|
+
dockerfiles: docker.dockerfiles
|
|
2055
|
+
},
|
|
2056
|
+
nodeVersion: docker.nodeVersion,
|
|
2057
|
+
findings: [
|
|
2058
|
+
...code.findings,
|
|
2059
|
+
...orphans.map((file) => finding("committed-env-file", { name: file, file })),
|
|
2060
|
+
...secretFindings(parsed, settings),
|
|
2061
|
+
...docker.findings
|
|
2062
|
+
].sort(byReportOrder),
|
|
2063
|
+
notes,
|
|
2064
|
+
inventory: code.inventory
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
function blockingFindings(report) {
|
|
2068
|
+
return report.findings.filter((entry) => entry.severity === "blocking");
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// src/roles/env/configs/resolve.ts
|
|
2072
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
2073
|
+
import { join as join9 } from "node:path";
|
|
2074
|
+
import { pathToFileURL } from "node:url";
|
|
2075
|
+
function explainUnreadableEnvConfig(error) {
|
|
2076
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2077
|
+
const code = error.code;
|
|
2078
|
+
const missingPackage = (code === "ERR_PACKAGE_PATH_NOT_EXPORTED" || code === "ERR_MODULE_NOT_FOUND") && message.includes("@hublo/sentinel");
|
|
2079
|
+
if (!missingPackage) return message;
|
|
2080
|
+
return `${message}
|
|
2081
|
+
This repository cannot import @hublo/sentinel/env: it does not install sentinel, or installs a release without the env role. Export the same object without defineEnv (export default { ... }), which sentinel validates the same way.`;
|
|
2082
|
+
}
|
|
2083
|
+
function unique(values) {
|
|
2084
|
+
return [...new Set(values)];
|
|
2085
|
+
}
|
|
2086
|
+
function scopeOf(base, input) {
|
|
2087
|
+
return {
|
|
2088
|
+
roots: input?.roots ?? base.roots,
|
|
2089
|
+
ext: input?.ext ?? base.ext,
|
|
2090
|
+
excludeSuffixes: input?.excludeSuffixes ?? base.excludeSuffixes,
|
|
2091
|
+
excludeDirs: input?.excludeDirs ?? base.excludeDirs
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
function asPattern(entry) {
|
|
2095
|
+
return entry instanceof RegExp ? entry : new RegExp(entry);
|
|
2096
|
+
}
|
|
2097
|
+
function resolveEnvSettings(input) {
|
|
2098
|
+
return {
|
|
2099
|
+
readers: {
|
|
2100
|
+
required: unique([...envDefaults.readers.required, ...input.readers?.required ?? []]),
|
|
2101
|
+
optional: unique([...envDefaults.readers.optional, ...input.readers?.optional ?? []])
|
|
2102
|
+
},
|
|
2103
|
+
scan: scopeOf(envDefaults.scan, input.scan),
|
|
2104
|
+
references: scopeOf(envDefaults.references, input.references),
|
|
2105
|
+
referencePatterns: referencePatterns(),
|
|
2106
|
+
secretNamePatterns: secretNamePatterns(),
|
|
2107
|
+
publicPrefixes: [...envDefaults.publicPrefixes],
|
|
2108
|
+
envFiles: unique([...envDefaults.envFiles, ...input.envFiles ?? []]),
|
|
2109
|
+
contracts: unique([...envDefaults.contracts, ...input.contracts ?? []]),
|
|
2110
|
+
nodeVersionFile: envDefaults.nodeVersionFile,
|
|
2111
|
+
externallyProvided: input.externallyProvided ?? [],
|
|
2112
|
+
knownExternalVars: input.knownExternalVars ?? [],
|
|
2113
|
+
knownExternalPatterns: (input.knownExternalPatterns ?? []).map(asPattern),
|
|
2114
|
+
allowNonBlankedSecrets: input.allowNonBlankedSecrets ?? [],
|
|
2115
|
+
allowedCommittedEnvFiles: input.allowedCommittedEnvFiles ?? [],
|
|
2116
|
+
docker: {
|
|
2117
|
+
allowRoot: input.docker?.allowRoot ?? [],
|
|
2118
|
+
excludePaths: input.docker?.excludePaths ?? []
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
async function loadEnvConfig(root) {
|
|
2123
|
+
const path2 = join9(root, ENV_CONFIG_FILE);
|
|
2124
|
+
if (!existsSync6(path2)) {
|
|
2125
|
+
return { settings: resolveEnvSettings({}), from: null, unreadable: null };
|
|
2126
|
+
}
|
|
2127
|
+
try {
|
|
2128
|
+
const loaded = await import(pathToFileURL(path2).href);
|
|
2129
|
+
const declared = defineEnv(loaded.default ?? {});
|
|
2130
|
+
return { settings: resolveEnvSettings(declared), from: ENV_CONFIG_FILE, unreadable: null };
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
return {
|
|
2133
|
+
settings: resolveEnvSettings({}),
|
|
2134
|
+
from: null,
|
|
2135
|
+
unreadable: `${ENV_CONFIG_FILE}: ${explainUnreadableEnvConfig(error)}`
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// src/roles/env/report.ts
|
|
2141
|
+
var LEVEL_MEANING = {
|
|
2142
|
+
info: "report only",
|
|
2143
|
+
warning: "report, and annotate in GitHub Actions; never fails",
|
|
2144
|
+
error: "fails on a blocking finding (rules 1, 2, 6, 7 to 10)"
|
|
2145
|
+
};
|
|
2146
|
+
function locationOf(entry) {
|
|
2147
|
+
if (entry.file === void 0) return "";
|
|
2148
|
+
return entry.line === void 0 ? entry.file : `${entry.file}:${entry.line}`;
|
|
2149
|
+
}
|
|
2150
|
+
function groupByRule(findings) {
|
|
2151
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2152
|
+
for (const entry of findings) {
|
|
2153
|
+
const group = groups.get(entry.rule) ?? [];
|
|
2154
|
+
group.push(entry);
|
|
2155
|
+
groups.set(entry.rule, group);
|
|
2156
|
+
}
|
|
2157
|
+
return groups;
|
|
2158
|
+
}
|
|
2159
|
+
function describe(entry) {
|
|
2160
|
+
const where = locationOf(entry);
|
|
2161
|
+
const subject = entry.rule === "committed-env-file" ? "" : entry.name;
|
|
2162
|
+
const more = entry.readIn !== void 0 && entry.readIn.length > 1 ? ` (read in ${entry.readIn.length} files)` : entry.detail ? ` (${entry.detail})` : "";
|
|
2163
|
+
return [where, subject].filter(Boolean).join(" ") + more;
|
|
2164
|
+
}
|
|
2165
|
+
function verdictOf(report) {
|
|
2166
|
+
const blocking = blockingFindings(report).length;
|
|
2167
|
+
if (blocking === 0) return "no blocking finding";
|
|
2168
|
+
const counted = `${blocking} blocking finding${blocking === 1 ? "" : "s"}`;
|
|
2169
|
+
return report.level === "error" ? `${counted}: failing` : `${counted}: reported, not failing`;
|
|
2170
|
+
}
|
|
2171
|
+
function headline(report) {
|
|
2172
|
+
const { counts } = report;
|
|
2173
|
+
return `${counts.envFiles} env file(s), ${counts.contracts} contract(s), ${counts.required} required and ${counts.optional} optional variable(s) read, ${counts.dockerfiles} Dockerfile(s)` + (report.nodeVersion ? `, node ${report.nodeVersion}` : "");
|
|
2174
|
+
}
|
|
2175
|
+
function renderConsole(report, palette2) {
|
|
2176
|
+
const lines = [];
|
|
2177
|
+
lines.push(
|
|
2178
|
+
`${palette2.strong("env")} ${palette2.dim(`(level ${report.level}: ${LEVEL_MEANING[report.level]})`)}`
|
|
2179
|
+
);
|
|
2180
|
+
lines.push(` ${headline(report)}`);
|
|
2181
|
+
lines.push(
|
|
2182
|
+
` ${palette2.dim(`settings: ${report.config ?? "sentinel defaults (no env.config.mjs)"}`)}`
|
|
2183
|
+
);
|
|
2184
|
+
const listed = report.findings.filter((entry) => entry.severity !== "info");
|
|
2185
|
+
const informational = report.findings.length - listed.length;
|
|
2186
|
+
for (const [rule, entries] of groupByRule(listed)) {
|
|
2187
|
+
const meta = RULES[rule];
|
|
2188
|
+
const mark = meta.severity === "blocking" ? palette2.fail("\u2717") : palette2.warn("!");
|
|
2189
|
+
lines.push("");
|
|
2190
|
+
lines.push(
|
|
2191
|
+
` ${mark} ${palette2.strong(`[${rule}]`)} ${palette2.dim(`rule ${meta.number}, ${meta.severity}, ${entries.length}`)}`
|
|
2192
|
+
);
|
|
2193
|
+
lines.push(` ${palette2.dim(meta.help)}`);
|
|
2194
|
+
for (const entry of entries) lines.push(` ${describe(entry)}`);
|
|
2195
|
+
}
|
|
2196
|
+
if (informational > 0) {
|
|
2197
|
+
lines.push("");
|
|
2198
|
+
lines.push(
|
|
2199
|
+
` ${palette2.dim(`info: ${informational} optional variable(s) in no contract (a default applies); listed in --json`)}`
|
|
2200
|
+
);
|
|
2201
|
+
}
|
|
2202
|
+
for (const note of report.notes) lines.push(` ${palette2.dim(`note: ${note}`)}`);
|
|
2203
|
+
lines.push("");
|
|
2204
|
+
const blocking = blockingFindings(report).length;
|
|
2205
|
+
const verdict = verdictOf(report);
|
|
2206
|
+
lines.push(
|
|
2207
|
+
` ${blocking === 0 ? palette2.ok(verdict) : report.level === "error" ? palette2.fail(verdict) : palette2.warn(verdict)}`
|
|
2208
|
+
);
|
|
2209
|
+
return `${lines.join("\n")}
|
|
2210
|
+
`;
|
|
2211
|
+
}
|
|
2212
|
+
function cell(text) {
|
|
2213
|
+
return text.replace(/\|/g, "\\|");
|
|
2214
|
+
}
|
|
2215
|
+
function renderMarkdown(report) {
|
|
2216
|
+
const lines = ["<!-- sentinel-env-report -->", "## Env conformance", ""];
|
|
2217
|
+
lines.push(`**Level:** \`${report.level}\` (${LEVEL_MEANING[report.level]}) `);
|
|
2218
|
+
lines.push(`**Scanned:** ${headline(report)} `);
|
|
2219
|
+
lines.push(`**Settings:** ${report.config ? `\`${report.config}\`` : "sentinel defaults"}`);
|
|
2220
|
+
lines.push("");
|
|
2221
|
+
const listed = report.findings.filter((entry) => entry.severity !== "info");
|
|
2222
|
+
const groups = groupByRule(listed);
|
|
2223
|
+
if (groups.size === 0) {
|
|
2224
|
+
lines.push("No finding.");
|
|
2225
|
+
} else {
|
|
2226
|
+
lines.push("| rule | finding | severity | count |", "| --- | --- | --- | --- |");
|
|
2227
|
+
for (const [rule, entries] of groups) {
|
|
2228
|
+
const meta = RULES[rule];
|
|
2229
|
+
lines.push(`| ${meta.number} | \`${rule}\` | ${meta.severity} | ${entries.length} |`);
|
|
2230
|
+
}
|
|
2231
|
+
for (const [rule, entries] of groups) {
|
|
2232
|
+
lines.push("", `<details><summary><code>${rule}</code>: ${entries.length}</summary>`, "");
|
|
2233
|
+
lines.push(cell(RULES[rule].help), "");
|
|
2234
|
+
for (const entry of entries) lines.push(`- \`${cell(describe(entry))}\``);
|
|
2235
|
+
lines.push("", "</details>");
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
if (report.notes.length > 0) {
|
|
2239
|
+
lines.push("");
|
|
2240
|
+
for (const note of report.notes) lines.push(`> ${note}`);
|
|
2241
|
+
}
|
|
2242
|
+
lines.push("", `**Verdict:** ${verdictOf(report)}`);
|
|
2243
|
+
return `${lines.join("\n")}
|
|
2244
|
+
`;
|
|
2245
|
+
}
|
|
2246
|
+
function reportDocument(report) {
|
|
2247
|
+
return {
|
|
2248
|
+
level: report.level,
|
|
2249
|
+
verdict: verdictOf(report),
|
|
2250
|
+
blocking: blockingFindings(report).length,
|
|
2251
|
+
config: report.config,
|
|
2252
|
+
scanned: report.scanned,
|
|
2253
|
+
counts: report.counts,
|
|
2254
|
+
nodeVersion: report.nodeVersion,
|
|
2255
|
+
notes: report.notes,
|
|
2256
|
+
findings: report.findings,
|
|
2257
|
+
inventory: report.inventory
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
function renderJson(report) {
|
|
2261
|
+
return `${JSON.stringify(reportDocument(report), null, 2)}
|
|
2262
|
+
`;
|
|
2263
|
+
}
|
|
2264
|
+
function escapeData(text) {
|
|
2265
|
+
return text.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
2266
|
+
}
|
|
2267
|
+
function escapeProperty(text) {
|
|
2268
|
+
return escapeData(text).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
2269
|
+
}
|
|
2270
|
+
function renderAnnotations(report) {
|
|
2271
|
+
if (report.level === "info") return [];
|
|
2272
|
+
return report.findings.filter((entry) => entry.severity !== "info").map((entry) => {
|
|
2273
|
+
const command = report.level === "error" && entry.severity === "blocking" ? "error" : "warning";
|
|
2274
|
+
const properties = [
|
|
2275
|
+
entry.file === void 0 ? void 0 : `file=${escapeProperty(entry.file)}`,
|
|
2276
|
+
entry.line === void 0 ? void 0 : `line=${entry.line}`,
|
|
2277
|
+
`title=${escapeProperty(`env rule ${entry.number}: ${entry.rule}`)}`
|
|
2278
|
+
].filter(Boolean);
|
|
2279
|
+
const message = `${entry.name}${entry.detail ? ` (${entry.detail})` : ""}: ${RULES[entry.rule].help}`;
|
|
2280
|
+
return `::${command} ${properties.join(",")}::${escapeData(message)}`;
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// src/roles/env/adapters/conformance/conformance.adapter.ts
|
|
2285
|
+
function shapesOf(output) {
|
|
2286
|
+
const asked = [
|
|
2287
|
+
...new Set(
|
|
2288
|
+
(output ?? "console").split(",").map((one) => one.trim()).filter((one) => one !== "")
|
|
2289
|
+
)
|
|
2290
|
+
];
|
|
2291
|
+
return {
|
|
2292
|
+
known: asked.filter((one) => ENV_SHAPES.includes(one)),
|
|
2293
|
+
unknown: asked.filter((one) => !ENV_SHAPES.includes(one))
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
function writeShapes(root, report, shapes) {
|
|
2297
|
+
const files = shapes.filter((shape) => shape !== "console");
|
|
2298
|
+
if (files.length === 0) return [];
|
|
2299
|
+
const directory = join10(root, REPORT_DIR2);
|
|
2300
|
+
mkdirSync(directory, { recursive: true });
|
|
2301
|
+
for (const [shape, name] of Object.entries(SHAPE_FILE)) {
|
|
2302
|
+
const path2 = join10(directory, name);
|
|
2303
|
+
if (!files.includes(shape) && existsSync7(path2)) unlinkSync(path2);
|
|
2304
|
+
}
|
|
2305
|
+
return files.map((shape) => {
|
|
2306
|
+
const name = SHAPE_FILE[shape];
|
|
2307
|
+
writeFileSync2(
|
|
2308
|
+
join10(directory, name),
|
|
2309
|
+
shape === "json" ? renderJson(report) : renderMarkdown(report)
|
|
2310
|
+
);
|
|
2311
|
+
return `${REPORT_DIR2}/${name}`;
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
var ConformanceAdapter = class extends BaseAdapter {
|
|
2315
|
+
target = TARGET2;
|
|
2316
|
+
runner = "conformance";
|
|
2317
|
+
/** A repository has no stack, so every preset applies: the role never reads one. */
|
|
2318
|
+
appliesTo(_preset) {
|
|
2319
|
+
return true;
|
|
2320
|
+
}
|
|
2321
|
+
/** Nothing to adopt: the role reads the repository and writes no config of its own. */
|
|
2322
|
+
plan() {
|
|
2323
|
+
return {
|
|
2324
|
+
operations: [],
|
|
2325
|
+
blocked: "env has no --init: it writes no config. An optional env.config.mjs at the repository root declares exceptions; run it with `sentinel --run --env`."
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
/** `--run`: check, print the report, write the asked files, annotate. */
|
|
2329
|
+
async run(ctx) {
|
|
2330
|
+
return this.check(ctx, true);
|
|
2331
|
+
}
|
|
2332
|
+
/** `--run --json`: the same check, for a machine. Prints nothing on stdout. */
|
|
2333
|
+
async report(ctx) {
|
|
2334
|
+
return this.check(ctx, false);
|
|
2335
|
+
}
|
|
2336
|
+
say(message) {
|
|
2337
|
+
process.stderr.write(`sentinel env: ${message}
|
|
2338
|
+
`);
|
|
2339
|
+
}
|
|
2340
|
+
async check(ctx, printing) {
|
|
2341
|
+
const level = ctx.envLevel ?? DEFAULT_ENV_LEVEL;
|
|
2342
|
+
const { known, unknown } = shapesOf(ctx.output);
|
|
2343
|
+
if (unknown.length > 0) {
|
|
2344
|
+
this.say(
|
|
2345
|
+
`writes ${ENV_SHAPES.join(", ")}; not ${unknown.join(", ")}, which another role may write.`
|
|
2346
|
+
);
|
|
2347
|
+
}
|
|
2348
|
+
const { settings, from, unreadable } = await loadEnvConfig(ctx.cwd);
|
|
2349
|
+
if (unreadable) {
|
|
2350
|
+
this.say(unreadable);
|
|
2351
|
+
return { ok: false, code: 1 };
|
|
2352
|
+
}
|
|
2353
|
+
let report;
|
|
2354
|
+
try {
|
|
2355
|
+
report = collectEnvReport(openRepository(ctx.cwd), settings, level, from);
|
|
2356
|
+
} catch (error) {
|
|
2357
|
+
this.say(error instanceof Error ? error.message : String(error));
|
|
2358
|
+
return { ok: false, code: 1 };
|
|
2359
|
+
}
|
|
2360
|
+
if (printing) {
|
|
2361
|
+
const written = writeShapes(ctx.cwd, report, known);
|
|
2362
|
+
process.stdout.write(renderConsole(report, palette(process.stdout)));
|
|
2363
|
+
if (written.length > 0) process.stdout.write(`
|
|
2364
|
+
written: ${written.join(", ")}
|
|
2365
|
+
`);
|
|
2366
|
+
}
|
|
2367
|
+
if (ctx.githubActions) {
|
|
2368
|
+
const annotations = renderAnnotations(report);
|
|
2369
|
+
if (annotations.length > 0) {
|
|
2370
|
+
const stream = printing ? process.stdout : process.stderr;
|
|
2371
|
+
stream.write(`${annotations.join("\n")}
|
|
2372
|
+
`);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
const failing = level === "error" && blockingFindings(report).length > 0;
|
|
2376
|
+
return { ok: !failing, code: failing ? 1 : 0, metrics: reportDocument(report) };
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
|
|
2380
|
+
// src/roles/env/register.ts
|
|
2381
|
+
function registerEnv() {
|
|
2382
|
+
register(new ConformanceAdapter());
|
|
2383
|
+
setDefaultRunner("env", "conformance");
|
|
2384
|
+
}
|
|
2385
|
+
|
|
1292
2386
|
// src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
|
|
1293
2387
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1294
|
-
import { existsSync as
|
|
1295
|
-
import { join as
|
|
2388
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
|
|
2389
|
+
import { join as join15 } from "node:path";
|
|
1296
2390
|
import { isDeepStrictEqual } from "node:util";
|
|
1297
2391
|
|
|
1298
2392
|
// src/core/config/has-source.ts
|
|
1299
2393
|
import { readdirSync } from "node:fs";
|
|
1300
|
-
import { extname, join as
|
|
2394
|
+
import { extname, join as join11, relative as relative2 } from "node:path";
|
|
1301
2395
|
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
1302
2396
|
"node_modules",
|
|
1303
2397
|
"dist",
|
|
@@ -1348,7 +2442,7 @@ function hasSourceFiles(cwd, extensions) {
|
|
|
1348
2442
|
}
|
|
1349
2443
|
for (const entry of entries) {
|
|
1350
2444
|
if (entry.isDirectory()) {
|
|
1351
|
-
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(
|
|
2445
|
+
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join11(dir, entry.name));
|
|
1352
2446
|
continue;
|
|
1353
2447
|
}
|
|
1354
2448
|
if (wanted.has(extname(entry.name))) return true;
|
|
@@ -1369,7 +2463,7 @@ function listSourceFiles(cwd, extensions) {
|
|
|
1369
2463
|
continue;
|
|
1370
2464
|
}
|
|
1371
2465
|
for (const entry of entries) {
|
|
1372
|
-
const full =
|
|
2466
|
+
const full = join11(dir, entry.name);
|
|
1373
2467
|
if (entry.isDirectory()) {
|
|
1374
2468
|
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
|
|
1375
2469
|
continue;
|
|
@@ -1388,8 +2482,8 @@ function scopeFlags(paths, options = []) {
|
|
|
1388
2482
|
}
|
|
1389
2483
|
|
|
1390
2484
|
// src/roles/format/deliberate-import-order.ts
|
|
1391
|
-
import { readFileSync as
|
|
1392
|
-
import { join as
|
|
2485
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2486
|
+
import { join as join12 } from "node:path";
|
|
1393
2487
|
import { parseSync } from "oxc-parser";
|
|
1394
2488
|
import { format } from "oxfmt";
|
|
1395
2489
|
|
|
@@ -1526,8 +2620,8 @@ async function pinDeliberateImportOrder(cwd, options) {
|
|
|
1526
2620
|
if (!options.sortImports) return [];
|
|
1527
2621
|
const pinned = [];
|
|
1528
2622
|
for (const file of listSourceFiles(cwd, CODE_EXTENSIONS).sort()) {
|
|
1529
|
-
const path2 =
|
|
1530
|
-
const text =
|
|
2623
|
+
const path2 = join12(cwd, file);
|
|
2624
|
+
const text = readFileSync7(path2, "utf8");
|
|
1531
2625
|
if (!text.includes("eslint-disable") && !MODULE_NAMES.some((name) => text.includes(name)))
|
|
1532
2626
|
continue;
|
|
1533
2627
|
const before = readImports(file, text);
|
|
@@ -1535,7 +2629,7 @@ async function pinDeliberateImportOrder(cwd, options) {
|
|
|
1535
2629
|
const sorted = await format(file, text, options);
|
|
1536
2630
|
if (sorted.errors.length > 0) continue;
|
|
1537
2631
|
if (!wouldMove(before, readImports(file, sorted.code))) continue;
|
|
1538
|
-
|
|
2632
|
+
writeFileSync3(path2, pin(text, before));
|
|
1539
2633
|
pinned.push(file);
|
|
1540
2634
|
}
|
|
1541
2635
|
return pinned;
|
|
@@ -1569,8 +2663,8 @@ function writesWhenRewritten(name, command) {
|
|
|
1569
2663
|
var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
|
|
1570
2664
|
|
|
1571
2665
|
// src/roles/format/inherited-ignores.ts
|
|
1572
|
-
import { existsSync as
|
|
1573
|
-
import { join as
|
|
2666
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
|
|
2667
|
+
import { join as join13 } from "node:path";
|
|
1574
2668
|
var ROOT_IGNORE_FILE = ".prettierignore";
|
|
1575
2669
|
function isPattern(line) {
|
|
1576
2670
|
const trimmed = line.trim();
|
|
@@ -1586,11 +2680,11 @@ function toModulePattern(pattern) {
|
|
|
1586
2680
|
}
|
|
1587
2681
|
function inheritedIgnorePatterns(workspaceRoot) {
|
|
1588
2682
|
if (!workspaceRoot) return [];
|
|
1589
|
-
const path2 =
|
|
1590
|
-
if (!
|
|
2683
|
+
const path2 = join13(workspaceRoot, ROOT_IGNORE_FILE);
|
|
2684
|
+
if (!existsSync8(path2)) return [];
|
|
1591
2685
|
let contents;
|
|
1592
2686
|
try {
|
|
1593
|
-
contents =
|
|
2687
|
+
contents = readFileSync8(path2, "utf8");
|
|
1594
2688
|
} catch {
|
|
1595
2689
|
return [];
|
|
1596
2690
|
}
|
|
@@ -1635,8 +2729,8 @@ function needsSvelteCompiler(output) {
|
|
|
1635
2729
|
}
|
|
1636
2730
|
|
|
1637
2731
|
// src/roles/format/prettier-config.ts
|
|
1638
|
-
import { existsSync as
|
|
1639
|
-
import { join as
|
|
2732
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
|
|
2733
|
+
import { join as join14 } from "node:path";
|
|
1640
2734
|
var UNSUPPORTED = /* @__PURE__ */ new Set(["plugins", "parser", "filepath", "rangeStart", "rangeEnd"]);
|
|
1641
2735
|
function toOxfmtOverrides(value) {
|
|
1642
2736
|
if (!Array.isArray(value)) return { overrides: [], unresolved: [] };
|
|
@@ -1661,14 +2755,14 @@ function toOxfmtOverrides(value) {
|
|
|
1661
2755
|
return { overrides, unresolved };
|
|
1662
2756
|
}
|
|
1663
2757
|
function readPrettierSettings(cwd) {
|
|
1664
|
-
const file = PRETTIER_CONFIG_FILES.find((name) =>
|
|
2758
|
+
const file = PRETTIER_CONFIG_FILES.find((name) => existsSync9(join14(cwd, name)));
|
|
1665
2759
|
if (!file) return { options: {}, unresolved: [] };
|
|
1666
2760
|
if (/\.(js|cjs|mjs)$/.test(file)) {
|
|
1667
2761
|
return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
|
|
1668
2762
|
}
|
|
1669
2763
|
let parsed;
|
|
1670
2764
|
try {
|
|
1671
|
-
parsed = parseJsonc(
|
|
2765
|
+
parsed = parseJsonc(readFileSync9(join14(cwd, file), "utf8"), file);
|
|
1672
2766
|
} catch {
|
|
1673
2767
|
return { options: {}, file, unresolved: [`${file} could not be parsed`] };
|
|
1674
2768
|
}
|
|
@@ -1786,7 +2880,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1786
2880
|
manifestOperation(context2.cwd, this.formatScripts(context2.cwd))
|
|
1787
2881
|
];
|
|
1788
2882
|
const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
|
|
1789
|
-
(name) =>
|
|
2883
|
+
(name) => existsSync10(join15(context2.cwd, name))
|
|
1790
2884
|
);
|
|
1791
2885
|
for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
|
|
1792
2886
|
const removableDeps = this.modulePrettierDependencies(context2.cwd);
|
|
@@ -1979,7 +3073,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1979
3073
|
const adoption = readFormatAdoption(ctx.cwd);
|
|
1980
3074
|
const preset = formatPresetFor(adoption.preset ?? DEFAULT_PRESET);
|
|
1981
3075
|
const options = { ...preset, ...adoption.local };
|
|
1982
|
-
const
|
|
3076
|
+
const describe2 = (value) => typeof value === "string" ? value : JSON.stringify(value);
|
|
1983
3077
|
return {
|
|
1984
3078
|
runner: "oxfmt",
|
|
1985
3079
|
configFile: adoption.configFile,
|
|
@@ -1988,7 +3082,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1988
3082
|
options,
|
|
1989
3083
|
overrides: Object.entries(adoption.local).filter(([key]) => !ALWAYS_LOCAL_KEYS.includes(key)).map(([key, value]) => ({
|
|
1990
3084
|
rule: key,
|
|
1991
|
-
reason: key in preset ? `${
|
|
3085
|
+
reason: key in preset ? `${describe2(value)}, where the ${adoption.preset ?? DEFAULT_PRESET} preset says ${describe2(preset[key])}` : `${describe2(value)}, which the preset does not set`
|
|
1992
3086
|
})),
|
|
1993
3087
|
// Which files this module does not format at all. A long list here is worth a look:
|
|
1994
3088
|
// it is the quiet way a module stops being covered.
|
|
@@ -2063,7 +3157,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
2063
3157
|
modulePrettierDependencies(cwd) {
|
|
2064
3158
|
const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
|
|
2065
3159
|
try {
|
|
2066
|
-
const manifest = JSON.parse(
|
|
3160
|
+
const manifest = JSON.parse(readFileSync10(join15(cwd, "package.json"), "utf8"));
|
|
2067
3161
|
return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
|
|
2068
3162
|
} catch {
|
|
2069
3163
|
return [];
|
|
@@ -2145,8 +3239,8 @@ function registerFormat() {
|
|
|
2145
3239
|
|
|
2146
3240
|
// src/roles/lint/adapters/oxlint/oxlint.adapter.ts
|
|
2147
3241
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
2148
|
-
import { existsSync as
|
|
2149
|
-
import { join as
|
|
3242
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3243
|
+
import { join as join21 } from "node:path";
|
|
2150
3244
|
|
|
2151
3245
|
// src/core/config/deferred-rules.ts
|
|
2152
3246
|
function deferredRuleNames(rules) {
|
|
@@ -2154,8 +3248,8 @@ function deferredRuleNames(rules) {
|
|
|
2154
3248
|
}
|
|
2155
3249
|
|
|
2156
3250
|
// src/core/fix-report.ts
|
|
2157
|
-
import { statSync as
|
|
2158
|
-
import { join as
|
|
3251
|
+
import { statSync as statSync3 } from "node:fs";
|
|
3252
|
+
import { join as join16 } from "node:path";
|
|
2159
3253
|
var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
|
|
2160
3254
|
function readFailure(output) {
|
|
2161
3255
|
const lines = output.split("\n").map((line2) => line2.trim());
|
|
@@ -2166,7 +3260,7 @@ function readFailure(output) {
|
|
|
2166
3260
|
}
|
|
2167
3261
|
function fingerprint(path2) {
|
|
2168
3262
|
try {
|
|
2169
|
-
const stat =
|
|
3263
|
+
const stat = statSync3(path2);
|
|
2170
3264
|
return `${stat.size}:${stat.mtimeMs}`;
|
|
2171
3265
|
} catch {
|
|
2172
3266
|
return void 0;
|
|
@@ -2175,7 +3269,7 @@ function fingerprint(path2) {
|
|
|
2175
3269
|
function snapshotFiles(cwd, extensions) {
|
|
2176
3270
|
const snapshot = /* @__PURE__ */ new Map();
|
|
2177
3271
|
for (const file of listSourceFiles(cwd, extensions)) {
|
|
2178
|
-
const mark = fingerprint(
|
|
3272
|
+
const mark = fingerprint(join16(cwd, file));
|
|
2179
3273
|
if (mark !== void 0) snapshot.set(file, mark);
|
|
2180
3274
|
}
|
|
2181
3275
|
return snapshot;
|
|
@@ -2183,7 +3277,7 @@ function snapshotFiles(cwd, extensions) {
|
|
|
2183
3277
|
function changedSince(cwd, before, extensions) {
|
|
2184
3278
|
const changed = [];
|
|
2185
3279
|
for (const file of listSourceFiles(cwd, extensions)) {
|
|
2186
|
-
const now = fingerprint(
|
|
3280
|
+
const now = fingerprint(join16(cwd, file));
|
|
2187
3281
|
if (now === void 0) continue;
|
|
2188
3282
|
if (before.get(file) !== now) changed.push(file);
|
|
2189
3283
|
}
|
|
@@ -4153,7 +5247,7 @@ var react_default = {
|
|
|
4153
5247
|
};
|
|
4154
5248
|
|
|
4155
5249
|
// src/roles/lint/presets/shared.json
|
|
4156
|
-
var
|
|
5250
|
+
var shared_default2 = {
|
|
4157
5251
|
rules: {
|
|
4158
5252
|
"no-var": "error",
|
|
4159
5253
|
"prefer-const": [
|
|
@@ -4530,7 +5624,7 @@ var tools_default = {
|
|
|
4530
5624
|
};
|
|
4531
5625
|
|
|
4532
5626
|
// src/roles/lint/preset-data.ts
|
|
4533
|
-
var SHARED_RULES =
|
|
5627
|
+
var SHARED_RULES = shared_default2.rules;
|
|
4534
5628
|
var PRESET_FILES = {
|
|
4535
5629
|
nest: nest_default,
|
|
4536
5630
|
node: node_default,
|
|
@@ -4643,14 +5737,14 @@ function downgradedRulesFor(preset) {
|
|
|
4643
5737
|
}
|
|
4644
5738
|
|
|
4645
5739
|
// src/roles/lint/extra-layers.ts
|
|
4646
|
-
import { existsSync as
|
|
4647
|
-
import { isAbsolute, join as
|
|
5740
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "node:fs";
|
|
5741
|
+
import { isAbsolute, join as join17, resolve as resolve2 } from "node:path";
|
|
4648
5742
|
function ruleCount(cwd, specifier) {
|
|
4649
5743
|
const path2 = isAbsolute(specifier) ? specifier : resolve2(cwd, specifier);
|
|
4650
|
-
if (!
|
|
5744
|
+
if (!existsSync11(path2)) return void 0;
|
|
4651
5745
|
try {
|
|
4652
5746
|
const parsed = parseJsonc(
|
|
4653
|
-
|
|
5747
|
+
readFileSync11(path2, "utf8"),
|
|
4654
5748
|
specifier
|
|
4655
5749
|
);
|
|
4656
5750
|
return Object.keys(parsed.rules ?? {}).length;
|
|
@@ -4670,7 +5764,7 @@ function extraLayers(cwd, extendsList) {
|
|
|
4670
5764
|
function committedExtendsList(cwd, configFile) {
|
|
4671
5765
|
try {
|
|
4672
5766
|
const parsed = parseJsonc(
|
|
4673
|
-
|
|
5767
|
+
readFileSync11(join17(cwd, configFile), "utf8"),
|
|
4674
5768
|
configFile
|
|
4675
5769
|
);
|
|
4676
5770
|
return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
|
|
@@ -4853,12 +5947,12 @@ function lintPresetFor(preset) {
|
|
|
4853
5947
|
}
|
|
4854
5948
|
|
|
4855
5949
|
// src/roles/lint/adapters/oxlint/plan.ts
|
|
4856
|
-
import { existsSync as
|
|
4857
|
-
import { join as
|
|
5950
|
+
import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
|
|
5951
|
+
import { join as join20 } from "node:path";
|
|
4858
5952
|
|
|
4859
5953
|
// src/roles/lint/eslint-ignores.ts
|
|
4860
|
-
import { existsSync as
|
|
4861
|
-
import { join as
|
|
5954
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
|
|
5955
|
+
import { join as join18 } from "node:path";
|
|
4862
5956
|
import { parseSync as parseSync2 } from "oxc-parser";
|
|
4863
5957
|
var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
|
|
4864
5958
|
function toOxlintPattern(pattern) {
|
|
@@ -4870,11 +5964,11 @@ function readRootEslintIgnores(root) {
|
|
|
4870
5964
|
return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
|
|
4871
5965
|
}
|
|
4872
5966
|
function readEslintIgnores(cwd) {
|
|
4873
|
-
const config = ESLINT_CONFIG_FILES.map((name) =>
|
|
5967
|
+
const config = ESLINT_CONFIG_FILES.map((name) => join18(cwd, name)).find((path2) => existsSync12(path2));
|
|
4874
5968
|
if (!config) return { patterns: [], unresolved: [] };
|
|
4875
5969
|
let source;
|
|
4876
5970
|
try {
|
|
4877
|
-
source =
|
|
5971
|
+
source = readFileSync12(config, "utf8");
|
|
4878
5972
|
} catch {
|
|
4879
5973
|
return { patterns: [], unresolved: [] };
|
|
4880
5974
|
}
|
|
@@ -4945,8 +6039,8 @@ function keepsOtherCommands3(script) {
|
|
|
4945
6039
|
}
|
|
4946
6040
|
|
|
4947
6041
|
// src/roles/lint/rename-suppressions.ts
|
|
4948
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
4949
|
-
import { join as
|
|
6042
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
|
|
6043
|
+
import { join as join19, relative as relative3 } from "node:path";
|
|
4950
6044
|
var SOURCE_EXTENSIONS = [
|
|
4951
6045
|
".ts",
|
|
4952
6046
|
".tsx",
|
|
@@ -4993,10 +6087,10 @@ function* sourceFiles(dir) {
|
|
|
4993
6087
|
return;
|
|
4994
6088
|
}
|
|
4995
6089
|
for (const entry of entries) {
|
|
4996
|
-
const full =
|
|
6090
|
+
const full = join19(dir, entry);
|
|
4997
6091
|
let isDirectory;
|
|
4998
6092
|
try {
|
|
4999
|
-
isDirectory =
|
|
6093
|
+
isDirectory = statSync4(full).isDirectory();
|
|
5000
6094
|
} catch {
|
|
5001
6095
|
continue;
|
|
5002
6096
|
}
|
|
@@ -5013,7 +6107,7 @@ function findSuppressionRenames(cwd, renames) {
|
|
|
5013
6107
|
for (const file of sourceFiles(cwd)) {
|
|
5014
6108
|
let content;
|
|
5015
6109
|
try {
|
|
5016
|
-
content =
|
|
6110
|
+
content = readFileSync13(file, "utf8");
|
|
5017
6111
|
} catch {
|
|
5018
6112
|
continue;
|
|
5019
6113
|
}
|
|
@@ -5159,7 +6253,7 @@ function plan2(context2) {
|
|
|
5159
6253
|
keys: removableDeps
|
|
5160
6254
|
});
|
|
5161
6255
|
}
|
|
5162
|
-
const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) =>
|
|
6256
|
+
const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync13(join20(context2.cwd, name)));
|
|
5163
6257
|
for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
|
|
5164
6258
|
operations.push(
|
|
5165
6259
|
...nxTargetOperations({
|
|
@@ -5273,7 +6367,7 @@ function lintScripts(cwd) {
|
|
|
5273
6367
|
function committedExtends(cwd) {
|
|
5274
6368
|
try {
|
|
5275
6369
|
const parsed = parseJsonc(
|
|
5276
|
-
|
|
6370
|
+
readFileSync14(join20(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5277
6371
|
LINT_CONFIG_FILE
|
|
5278
6372
|
);
|
|
5279
6373
|
return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
|
|
@@ -5285,7 +6379,7 @@ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array
|
|
|
5285
6379
|
function committedConfig(cwd) {
|
|
5286
6380
|
try {
|
|
5287
6381
|
const parsed = parseJsonc(
|
|
5288
|
-
|
|
6382
|
+
readFileSync14(join20(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5289
6383
|
LINT_CONFIG_FILE
|
|
5290
6384
|
);
|
|
5291
6385
|
return isRecord2(parsed) ? parsed : {};
|
|
@@ -5309,7 +6403,7 @@ function describeGoverned(rule, variant) {
|
|
|
5309
6403
|
function committedIgnorePatterns(cwd) {
|
|
5310
6404
|
try {
|
|
5311
6405
|
const parsed = parseJsonc(
|
|
5312
|
-
|
|
6406
|
+
readFileSync14(join20(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5313
6407
|
LINT_CONFIG_FILE
|
|
5314
6408
|
);
|
|
5315
6409
|
return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
|
|
@@ -5321,7 +6415,7 @@ function moduleEslintDependencies(cwd) {
|
|
|
5321
6415
|
const isEslintPackage = (name) => name === "eslint" || name === "@types/eslint" || name === "typescript-eslint" || name.startsWith("@typescript-eslint/") || name.startsWith("eslint-plugin-") || name.startsWith("eslint-config-") || name.startsWith("@eslint/");
|
|
5322
6416
|
let manifest;
|
|
5323
6417
|
try {
|
|
5324
|
-
manifest = JSON.parse(
|
|
6418
|
+
manifest = JSON.parse(readFileSync14(join20(cwd, "package.json"), "utf8"));
|
|
5325
6419
|
} catch {
|
|
5326
6420
|
return [];
|
|
5327
6421
|
}
|
|
@@ -5502,23 +6596,23 @@ ${result.stderr ?? ""}`);
|
|
|
5502
6596
|
* build the developer can see, rather than a silent half-adoption they cannot.
|
|
5503
6597
|
*/
|
|
5504
6598
|
writeModuleBaseline(ctx, oxlint, env) {
|
|
5505
|
-
const configPath =
|
|
5506
|
-
const baselinePath =
|
|
5507
|
-
if (!
|
|
6599
|
+
const configPath = join21(ctx.cwd, LINT_CONFIG_FILE);
|
|
6600
|
+
const baselinePath = join21(ctx.cwd, LINT_BASELINE_FILE);
|
|
6601
|
+
if (!existsSync14(configPath)) return;
|
|
5508
6602
|
let config;
|
|
5509
6603
|
try {
|
|
5510
6604
|
config = parseJsonc(
|
|
5511
|
-
|
|
6605
|
+
readFileSync15(configPath, "utf8"),
|
|
5512
6606
|
LINT_CONFIG_FILE
|
|
5513
6607
|
);
|
|
5514
6608
|
} catch {
|
|
5515
6609
|
return;
|
|
5516
6610
|
}
|
|
5517
6611
|
const current = Array.isArray(config.extends) ? config.extends : [];
|
|
5518
|
-
const measurePath =
|
|
6612
|
+
const measurePath = join21(ctx.cwd, LINT_MEASURE_FILE);
|
|
5519
6613
|
let measured;
|
|
5520
6614
|
try {
|
|
5521
|
-
|
|
6615
|
+
writeFileSync4(
|
|
5522
6616
|
measurePath,
|
|
5523
6617
|
`${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
|
|
5524
6618
|
`
|
|
@@ -5553,7 +6647,7 @@ ${result.stderr ?? ""}`);
|
|
|
5553
6647
|
);
|
|
5554
6648
|
const next = extendsWithBaseline(current, holds.length > 0);
|
|
5555
6649
|
if (holds.length > 0) {
|
|
5556
|
-
|
|
6650
|
+
writeFileSync4(baselinePath, renderBaseline(holds, readOwnPackage().version));
|
|
5557
6651
|
process.stderr.write(` ${palette(process.stderr).warn(describeHolds(holds))}
|
|
5558
6652
|
`);
|
|
5559
6653
|
} else {
|
|
@@ -5561,12 +6655,12 @@ ${result.stderr ?? ""}`);
|
|
|
5561
6655
|
}
|
|
5562
6656
|
if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
|
|
5563
6657
|
config.extends = next;
|
|
5564
|
-
|
|
6658
|
+
writeFileSync4(configPath, `${JSON.stringify(config, null, 2)}
|
|
5565
6659
|
`);
|
|
5566
6660
|
}
|
|
5567
6661
|
}
|
|
5568
6662
|
async run(ctx) {
|
|
5569
|
-
if (!
|
|
6663
|
+
if (!existsSync14(join21(ctx.cwd, LINT_CONFIG_FILE))) {
|
|
5570
6664
|
process.stderr.write(
|
|
5571
6665
|
`sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
|
|
5572
6666
|
`
|
|
@@ -5715,7 +6809,7 @@ ${result.stderr ?? ""}`);
|
|
|
5715
6809
|
let stub;
|
|
5716
6810
|
try {
|
|
5717
6811
|
stub = parseJsonc(
|
|
5718
|
-
|
|
6812
|
+
readFileSync15(join21(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5719
6813
|
LINT_CONFIG_FILE
|
|
5720
6814
|
);
|
|
5721
6815
|
} catch {
|
|
@@ -5725,7 +6819,7 @@ ${result.stderr ?? ""}`);
|
|
|
5725
6819
|
for (const entry of stub.extends ?? []) {
|
|
5726
6820
|
try {
|
|
5727
6821
|
const preset = parseJsonc(
|
|
5728
|
-
|
|
6822
|
+
readFileSync15(join21(cwd, entry), "utf8"),
|
|
5729
6823
|
entry
|
|
5730
6824
|
);
|
|
5731
6825
|
for (const rule of Object.keys(preset.rules ?? {})) names.add(canonicalRuleName(rule));
|
|
@@ -5795,14 +6889,14 @@ ${result.stderr ?? ""}`);
|
|
|
5795
6889
|
let parsed;
|
|
5796
6890
|
try {
|
|
5797
6891
|
parsed = parseJsonc(
|
|
5798
|
-
|
|
6892
|
+
readFileSync15(join21(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5799
6893
|
LINT_CONFIG_FILE
|
|
5800
6894
|
);
|
|
5801
6895
|
} catch {
|
|
5802
6896
|
return void 0;
|
|
5803
6897
|
}
|
|
5804
6898
|
const targets2 = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
|
|
5805
|
-
return targets2.find((target) => !
|
|
6899
|
+
return targets2.find((target) => !existsSync14(join21(cwd, target)));
|
|
5806
6900
|
}
|
|
5807
6901
|
/** Announce what is not enforced, so reduced coverage is never silent. */
|
|
5808
6902
|
announceDisabled(preset) {
|
|
@@ -5834,8 +6928,8 @@ function registerLint() {
|
|
|
5834
6928
|
|
|
5835
6929
|
// src/roles/static-analysis/adapters/jscpd/jscpd.adapter.ts
|
|
5836
6930
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
5837
|
-
import { existsSync as
|
|
5838
|
-
import { join as
|
|
6931
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync3, readFileSync as readFileSync21, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "node:fs";
|
|
6932
|
+
import { join as join31 } from "node:path";
|
|
5839
6933
|
import { globSync as globSync5 } from "tinyglobby";
|
|
5840
6934
|
|
|
5841
6935
|
// src/roles/static-analysis/bundle/budgets.ts
|
|
@@ -5864,32 +6958,32 @@ function bandFor(valueKb, referenceKb) {
|
|
|
5864
6958
|
}
|
|
5865
6959
|
|
|
5866
6960
|
// src/roles/static-analysis/bundle/scan.ts
|
|
5867
|
-
import { existsSync as
|
|
5868
|
-
import { basename, join as
|
|
5869
|
-
import { pathToFileURL } from "node:url";
|
|
6961
|
+
import { existsSync as existsSync16, readFileSync as readFileSync17 } from "node:fs";
|
|
6962
|
+
import { basename, join as join23 } from "node:path";
|
|
6963
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
5870
6964
|
import { gzipSync } from "node:zlib";
|
|
5871
6965
|
import { globSync as globSync2 } from "tinyglobby";
|
|
5872
6966
|
|
|
5873
6967
|
// src/roles/static-analysis/bundle/find-build.ts
|
|
5874
|
-
import { existsSync as
|
|
5875
|
-
import { join as
|
|
6968
|
+
import { existsSync as existsSync15, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
|
|
6969
|
+
import { join as join22 } from "node:path";
|
|
5876
6970
|
import { globSync } from "tinyglobby";
|
|
5877
6971
|
var MANIFEST_GLOB = "server/_tanstack-start-manifest*.mjs";
|
|
5878
6972
|
function candidateDirs(cwd) {
|
|
5879
6973
|
const declared = readProjectPackageJson(cwd).nx?.targets?.build?.["outputs"];
|
|
5880
|
-
const fromDeclaration = Array.isArray(declared) ? declared.filter((token) => typeof token === "string").map((token) =>
|
|
5881
|
-
return [...fromDeclaration,
|
|
6974
|
+
const fromDeclaration = Array.isArray(declared) ? declared.filter((token) => typeof token === "string").map((token) => join22(cwd, token.replace(/^\{projectRoot\}\//, ""))) : [];
|
|
6975
|
+
return [...fromDeclaration, join22(cwd, ".output"), join22(cwd, "dist"), join22(cwd, "build")];
|
|
5882
6976
|
}
|
|
5883
6977
|
function findBuildOutput(cwd) {
|
|
5884
6978
|
const lookedIn = [];
|
|
5885
6979
|
for (const dir of candidateDirs(cwd)) {
|
|
5886
6980
|
lookedIn.push(dir);
|
|
5887
|
-
if (!
|
|
6981
|
+
if (!existsSync15(dir)) continue;
|
|
5888
6982
|
const found = globSync(MANIFEST_GLOB, { cwd: dir, absolute: true, dot: false });
|
|
5889
6983
|
if (found.length === 0) continue;
|
|
5890
6984
|
const manifestPath = found[0];
|
|
5891
|
-
const publicDir =
|
|
5892
|
-
if (!
|
|
6985
|
+
const publicDir = join22(dir, "public");
|
|
6986
|
+
if (!existsSync15(publicDir)) continue;
|
|
5893
6987
|
return { manifestPath, publicDir, ...dateOf(dir, manifestPath) };
|
|
5894
6988
|
}
|
|
5895
6989
|
const pkg = readProjectPackageJson(cwd);
|
|
@@ -5911,10 +7005,10 @@ function findBuildOutput(cwd) {
|
|
|
5911
7005
|
};
|
|
5912
7006
|
}
|
|
5913
7007
|
function dateOf(dir, manifestPath) {
|
|
5914
|
-
const record =
|
|
5915
|
-
if (
|
|
7008
|
+
const record = join22(dir, "nitro.json");
|
|
7009
|
+
if (existsSync15(record)) {
|
|
5916
7010
|
try {
|
|
5917
|
-
const parsed = JSON.parse(
|
|
7011
|
+
const parsed = JSON.parse(readFileSync16(record, "utf8"));
|
|
5918
7012
|
if (typeof parsed.date === "string") {
|
|
5919
7013
|
const date = new Date(parsed.date);
|
|
5920
7014
|
if (!Number.isNaN(date.getTime())) return { builtAt: date, builtAtFrom: "build record" };
|
|
@@ -5922,7 +7016,7 @@ function dateOf(dir, manifestPath) {
|
|
|
5922
7016
|
} catch {
|
|
5923
7017
|
}
|
|
5924
7018
|
}
|
|
5925
|
-
return { builtAt:
|
|
7019
|
+
return { builtAt: statSync5(manifestPath).mtime, builtAtFrom: "file timestamp" };
|
|
5926
7020
|
}
|
|
5927
7021
|
function isBuildOutput(found) {
|
|
5928
7022
|
return "manifestPath" in found;
|
|
@@ -5940,10 +7034,10 @@ function kindOf(path2) {
|
|
|
5940
7034
|
return "other";
|
|
5941
7035
|
}
|
|
5942
7036
|
function resolveAnnounced(publicDir, announced) {
|
|
5943
|
-
const direct =
|
|
5944
|
-
if (
|
|
5945
|
-
const stripped =
|
|
5946
|
-
return
|
|
7037
|
+
const direct = join23(publicDir, announced);
|
|
7038
|
+
if (existsSync16(direct)) return direct;
|
|
7039
|
+
const stripped = join23(publicDir, announced.replace(/^\/[^/]+\//, "/"));
|
|
7040
|
+
return existsSync16(stripped) ? stripped : void 0;
|
|
5947
7041
|
}
|
|
5948
7042
|
function emptyWeight() {
|
|
5949
7043
|
return { transfer: 0, parsed: 0, files: 0 };
|
|
@@ -5979,7 +7073,7 @@ async function scanBundle(cwd) {
|
|
|
5979
7073
|
unresolved.push(announced);
|
|
5980
7074
|
measured.set(announced, void 0);
|
|
5981
7075
|
} else {
|
|
5982
|
-
const buffer =
|
|
7076
|
+
const buffer = readFileSync17(path2);
|
|
5983
7077
|
measured.set(announced, {
|
|
5984
7078
|
parsed: buffer.length,
|
|
5985
7079
|
transfer: gzipSync(buffer, { level: 9 }).length
|
|
@@ -6030,7 +7124,7 @@ async function scanBundle(cwd) {
|
|
|
6030
7124
|
}
|
|
6031
7125
|
async function loadManifest(path2) {
|
|
6032
7126
|
try {
|
|
6033
|
-
const loaded = await import(
|
|
7127
|
+
const loaded = await import(pathToFileURL2(path2).href);
|
|
6034
7128
|
if (typeof loaded.tsrStartManifest !== "function") return void 0;
|
|
6035
7129
|
const produced = loaded.tsrStartManifest();
|
|
6036
7130
|
return produced !== null && typeof produced === "object" ? produced : void 0;
|
|
@@ -6066,7 +7160,7 @@ function scanDynamicLayer(publicDir, references) {
|
|
|
6066
7160
|
})) {
|
|
6067
7161
|
const name = basename(path2);
|
|
6068
7162
|
if (referenced.has(name)) continue;
|
|
6069
|
-
const buffer =
|
|
7163
|
+
const buffer = readFileSync17(path2);
|
|
6070
7164
|
const size = { parsed: buffer.length, transfer: gzipSync(buffer, { level: 9 }).length };
|
|
6071
7165
|
add(all2, size);
|
|
6072
7166
|
const group = groupOf(name);
|
|
@@ -6094,28 +7188,28 @@ function scanDynamicLayer(publicDir, references) {
|
|
|
6094
7188
|
}
|
|
6095
7189
|
|
|
6096
7190
|
// src/roles/static-analysis/sources.ts
|
|
6097
|
-
import { readFileSync as
|
|
6098
|
-
import { join as
|
|
7191
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
7192
|
+
import { join as join26 } from "node:path";
|
|
6099
7193
|
import { parseSync as parseSync3 } from "oxc-parser";
|
|
6100
7194
|
import { globSync as globSync3 } from "tinyglobby";
|
|
6101
7195
|
|
|
6102
7196
|
// src/roles/static-analysis/adapters/jscpd/tool-config.ts
|
|
6103
|
-
import { existsSync as
|
|
7197
|
+
import { existsSync as existsSync18, mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6104
7198
|
import { tmpdir } from "node:os";
|
|
6105
|
-
import { join as
|
|
7199
|
+
import { join as join25 } from "node:path";
|
|
6106
7200
|
|
|
6107
7201
|
// src/roles/static-analysis/configs/resolve.ts
|
|
6108
|
-
import { existsSync as
|
|
6109
|
-
import { join as
|
|
6110
|
-
import { pathToFileURL as
|
|
7202
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
7203
|
+
import { join as join24 } from "node:path";
|
|
7204
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
6111
7205
|
var STATIC_CONFIG_FILE = "static-analysis.config.mjs";
|
|
6112
7206
|
async function loadStaticConfig(cwd) {
|
|
6113
|
-
const path2 =
|
|
6114
|
-
if (!
|
|
7207
|
+
const path2 = join24(cwd, STATIC_CONFIG_FILE);
|
|
7208
|
+
if (!existsSync17(path2)) {
|
|
6115
7209
|
return { config: defineStaticAnalysis(), from: null, unreadable: null };
|
|
6116
7210
|
}
|
|
6117
7211
|
try {
|
|
6118
|
-
const loaded = await import(
|
|
7212
|
+
const loaded = await import(pathToFileURL3(path2).href);
|
|
6119
7213
|
return { config: adopt(loaded.default), from: STATIC_CONFIG_FILE, unreadable: null };
|
|
6120
7214
|
} catch (error) {
|
|
6121
7215
|
return {
|
|
@@ -6157,7 +7251,7 @@ function exclusions(config, scope = "tests") {
|
|
|
6157
7251
|
return [.../* @__PURE__ */ new Set([...config.ignore, ...OWN_ARTEFACTS, ...tests])];
|
|
6158
7252
|
}
|
|
6159
7253
|
function measuredRoots(cwd) {
|
|
6160
|
-
return
|
|
7254
|
+
return existsSync18(join25(cwd, "src")) ? ["src"] : ["."];
|
|
6161
7255
|
}
|
|
6162
7256
|
function fileSettings(config, scope, extraEntries = []) {
|
|
6163
7257
|
return {
|
|
@@ -6187,9 +7281,9 @@ function fileSettings(config, scope, extraEntries = []) {
|
|
|
6187
7281
|
};
|
|
6188
7282
|
}
|
|
6189
7283
|
function duplicationRun(config, paths, scope) {
|
|
6190
|
-
const dir = mkdtempSync(
|
|
6191
|
-
const configPath =
|
|
6192
|
-
|
|
7284
|
+
const dir = mkdtempSync(join25(tmpdir(), "sentinel-static-"));
|
|
7285
|
+
const configPath = join25(dir, "jscpd.json");
|
|
7286
|
+
writeFileSync5(configPath, `${JSON.stringify(fileSettings(config, scope), null, 2)}
|
|
6193
7287
|
`, "utf8");
|
|
6194
7288
|
const args = [
|
|
6195
7289
|
"--config",
|
|
@@ -6214,9 +7308,9 @@ function duplicationRun(config, paths, scope) {
|
|
|
6214
7308
|
return { args, rawReportDir: dir, dispose: () => rmSync3(dir, { force: true, recursive: true }) };
|
|
6215
7309
|
}
|
|
6216
7310
|
function deadCodeRun(config, paths, extraEntries = []) {
|
|
6217
|
-
const dir = mkdtempSync(
|
|
6218
|
-
const configPath =
|
|
6219
|
-
|
|
7311
|
+
const dir = mkdtempSync(join25(tmpdir(), "sentinel-static-"));
|
|
7312
|
+
const configPath = join25(dir, "jscpd.json");
|
|
7313
|
+
writeFileSync5(
|
|
6220
7314
|
configPath,
|
|
6221
7315
|
`${JSON.stringify(fileSettings(config, "dead-code", extraEntries), null, 2)}
|
|
6222
7316
|
`,
|
|
@@ -6267,7 +7361,7 @@ function readParsed(cwd, files, passes) {
|
|
|
6267
7361
|
function parsedSource(cwd, path2) {
|
|
6268
7362
|
let text;
|
|
6269
7363
|
try {
|
|
6270
|
-
text =
|
|
7364
|
+
text = readFileSync18(join26(cwd, path2), "utf8");
|
|
6271
7365
|
} catch {
|
|
6272
7366
|
return void 0;
|
|
6273
7367
|
}
|
|
@@ -6361,7 +7455,7 @@ function nameOf(fn, parent) {
|
|
|
6361
7455
|
if (key) return key;
|
|
6362
7456
|
return "(anonymous)";
|
|
6363
7457
|
}
|
|
6364
|
-
function
|
|
7458
|
+
function lineAt2(source, offset) {
|
|
6365
7459
|
if (offset === void 0) return 1;
|
|
6366
7460
|
let line = 1;
|
|
6367
7461
|
for (let index = 0; index < offset && index < source.length; index += 1) {
|
|
@@ -6383,7 +7477,7 @@ function complexityOfProgram(program, source) {
|
|
|
6383
7477
|
if (!FUNCTION_NODES.has(node.type ?? "")) return;
|
|
6384
7478
|
found.push({
|
|
6385
7479
|
name: nameOf(node, parents.get(node)),
|
|
6386
|
-
line:
|
|
7480
|
+
line: lineAt2(source, node.start),
|
|
6387
7481
|
complexity: complexityOf(node)
|
|
6388
7482
|
});
|
|
6389
7483
|
},
|
|
@@ -6454,13 +7548,13 @@ function unreadableNote(coverage) {
|
|
|
6454
7548
|
}
|
|
6455
7549
|
|
|
6456
7550
|
// src/roles/static-analysis/excerpt.ts
|
|
6457
|
-
import { readFileSync as
|
|
6458
|
-
import { join as
|
|
7551
|
+
import { readFileSync as readFileSync19 } from "node:fs";
|
|
7552
|
+
import { join as join28 } from "node:path";
|
|
6459
7553
|
|
|
6460
7554
|
// src/roles/static-analysis/maintainability/size-rules.ts
|
|
6461
|
-
import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as
|
|
7555
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
6462
7556
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
6463
|
-
import { join as
|
|
7557
|
+
import { join as join27 } from "node:path";
|
|
6464
7558
|
var GAUGE_THRESHOLDS = {
|
|
6465
7559
|
// McCabe 1976 says 10, our p90 says 11. The only indicator where two independent candidates agree
|
|
6466
7560
|
// within one unit, and 94.8% of our functions already sit in the 1-10 band SEI calls low risk.
|
|
@@ -6489,7 +7583,7 @@ var GAUGE_THRESHOLDS = {
|
|
|
6489
7583
|
"cognitive-complexity": 7
|
|
6490
7584
|
};
|
|
6491
7585
|
var COGNITIVE = "cognitive-complexity";
|
|
6492
|
-
var cognitivePlugin = () =>
|
|
7586
|
+
var cognitivePlugin = () => join27(ownPackageDir(), "plugins", "sonar-cognitive.js");
|
|
6493
7587
|
var CATEGORIES = [
|
|
6494
7588
|
"correctness",
|
|
6495
7589
|
"suspicious",
|
|
@@ -6521,9 +7615,9 @@ function sizeRulesConfig() {
|
|
|
6521
7615
|
};
|
|
6522
7616
|
}
|
|
6523
7617
|
function sizeRulesRun(paths, ignore) {
|
|
6524
|
-
const dir = mkdtempSync2(
|
|
6525
|
-
const configPath =
|
|
6526
|
-
|
|
7618
|
+
const dir = mkdtempSync2(join27(tmpdir2(), "sentinel-size-"));
|
|
7619
|
+
const configPath = join27(dir, "oxlintrc.json");
|
|
7620
|
+
writeFileSync6(configPath, `${JSON.stringify(sizeRulesConfig(), null, 2)}
|
|
6527
7621
|
`, "utf8");
|
|
6528
7622
|
return {
|
|
6529
7623
|
// `--format json` because the console shape is rendered by this role from the figures: a
|
|
@@ -7047,23 +8141,23 @@ function helpDetails(help, withoutReference) {
|
|
|
7047
8141
|
<p class="source">Source: ${prose(help.source)}</p>
|
|
7048
8142
|
</details>`;
|
|
7049
8143
|
}
|
|
7050
|
-
function secondSide(
|
|
7051
|
-
if (!
|
|
8144
|
+
function secondSide(finding2) {
|
|
8145
|
+
if (!finding2.other) return "";
|
|
7052
8146
|
const file = (at) => at.split(":")[0] ?? at;
|
|
7053
|
-
const sameFile = file(
|
|
8147
|
+
const sameFile = file(finding2.other) === file(finding2.path);
|
|
7054
8148
|
const label = sameFile ? "and line" : "and";
|
|
7055
|
-
const shownSide = sameFile ?
|
|
8149
|
+
const shownSide = sameFile ? finding2.other.split(":")[1] ?? finding2.other : finding2.other;
|
|
7056
8150
|
return ` <span class="unit">${label}</span> <code>${escape(shownSide)}</code>`;
|
|
7057
8151
|
}
|
|
7058
8152
|
var HTML_GROUP_LIMIT = 50;
|
|
7059
8153
|
function listedByMetric(findings) {
|
|
7060
8154
|
const groups = /* @__PURE__ */ new Map();
|
|
7061
|
-
for (const
|
|
7062
|
-
const bucket = groups.get(
|
|
7063
|
-
bucket.push(
|
|
7064
|
-
groups.set(
|
|
8155
|
+
for (const finding2 of findings) {
|
|
8156
|
+
const bucket = groups.get(finding2.metric) ?? [];
|
|
8157
|
+
bucket.push(finding2);
|
|
8158
|
+
groups.set(finding2.metric, bucket);
|
|
7065
8159
|
}
|
|
7066
|
-
const weight = (
|
|
8160
|
+
const weight = (finding2) => finding2.value ?? finding2.lines ?? 0;
|
|
7067
8161
|
return new Map(
|
|
7068
8162
|
[...groups.entries()].map(([metric, all2]) => {
|
|
7069
8163
|
const sorted = [...all2].sort((left, right) => weight(right) - weight(left));
|
|
@@ -7093,7 +8187,7 @@ function caveatsOf(report) {
|
|
|
7093
8187
|
}
|
|
7094
8188
|
return found;
|
|
7095
8189
|
}
|
|
7096
|
-
function
|
|
8190
|
+
function renderConsole2(report) {
|
|
7097
8191
|
const lines = [];
|
|
7098
8192
|
const width = (pick) => Math.max(...report.rows.map((row) => pick(row).length));
|
|
7099
8193
|
const label = (row) => helpFor(row.metric)?.label ?? row.metric;
|
|
@@ -7148,7 +8242,7 @@ function renderConsole(report) {
|
|
|
7148
8242
|
`;
|
|
7149
8243
|
}
|
|
7150
8244
|
var JSON_SCHEMA_VERSION = 1;
|
|
7151
|
-
function
|
|
8245
|
+
function renderJson2(report) {
|
|
7152
8246
|
const rows = report.rows.map((row) => {
|
|
7153
8247
|
const help = helpFor(row.metric);
|
|
7154
8248
|
return { ...row, help: help ? { label: help.label, caveat: help.caveat } : null };
|
|
@@ -7165,11 +8259,11 @@ function renderJson(report) {
|
|
|
7165
8259
|
return `${JSON.stringify(envelope, null, 2)}
|
|
7166
8260
|
`;
|
|
7167
8261
|
}
|
|
7168
|
-
function
|
|
8262
|
+
function renderMarkdown2(report) {
|
|
7169
8263
|
const version = report.settings.find((setting) => setting.key === "sentinel")?.value;
|
|
7170
8264
|
const places = /* @__PURE__ */ new Map();
|
|
7171
|
-
for (const
|
|
7172
|
-
places.set(
|
|
8265
|
+
for (const finding2 of report.findings) {
|
|
8266
|
+
places.set(finding2.metric, (places.get(finding2.metric) ?? 0) + 1);
|
|
7173
8267
|
}
|
|
7174
8268
|
const figureLine = (row) => {
|
|
7175
8269
|
const help = helpFor(row.metric);
|
|
@@ -7223,64 +8317,64 @@ function ruleChip(metric) {
|
|
|
7223
8317
|
const threshold = GAUGE_THRESHOLDS[metric];
|
|
7224
8318
|
return threshold === void 0 ? "" : ` <span class="chip"><code>${escape(metric)}</code> > ${threshold}</span>`;
|
|
7225
8319
|
}
|
|
7226
|
-
function measureOf(
|
|
7227
|
-
const metric =
|
|
8320
|
+
function measureOf(finding2) {
|
|
8321
|
+
const metric = finding2.metric;
|
|
7228
8322
|
const number = (value, unit = "") => `<span class="pmeas">${grouped(value)}${unit === "" ? "" : `<span class="unit">${escape(unit)}</span>`}</span>`;
|
|
7229
|
-
if (
|
|
7230
|
-
if (GAUGE_THRESHOLDS[metric] !== void 0) return number(
|
|
7231
|
-
if (metric.startsWith("complexity")) return number(
|
|
7232
|
-
if (
|
|
7233
|
-
if (metric === "cycles") return number(
|
|
7234
|
-
return number(
|
|
8323
|
+
if (finding2.value !== void 0) {
|
|
8324
|
+
if (GAUGE_THRESHOLDS[metric] !== void 0) return number(finding2.value);
|
|
8325
|
+
if (metric.startsWith("complexity")) return number(finding2.value, "complexity");
|
|
8326
|
+
if (finding2.dependsOn !== void 0) return number(finding2.value, "imported by");
|
|
8327
|
+
if (metric === "cycles") return number(finding2.value, "files in the cycle");
|
|
8328
|
+
return number(finding2.value);
|
|
7235
8329
|
}
|
|
7236
|
-
return
|
|
8330
|
+
return finding2.lines === void 0 ? "" : number(finding2.lines, "lines");
|
|
7237
8331
|
}
|
|
7238
|
-
function metaOf(
|
|
8332
|
+
function metaOf(finding2) {
|
|
7239
8333
|
const pairs = [];
|
|
7240
8334
|
const add2 = (label, value) => {
|
|
7241
8335
|
pairs.push(`<span class="pair"><span class="k">${escape(label)}</span>${value}</span>`);
|
|
7242
8336
|
};
|
|
7243
|
-
if (
|
|
8337
|
+
if (finding2.name) {
|
|
7244
8338
|
add2(
|
|
7245
|
-
|
|
7246
|
-
`<code>${escape(
|
|
8339
|
+
finding2.metric.startsWith("complexity") ? "function" : "name",
|
|
8340
|
+
`<code>${escape(finding2.name)}</code>`
|
|
7247
8341
|
);
|
|
7248
8342
|
}
|
|
7249
|
-
if (
|
|
7250
|
-
if (
|
|
7251
|
-
if (
|
|
7252
|
-
if (
|
|
7253
|
-
add2("length", `${
|
|
7254
|
-
if (
|
|
7255
|
-
if (
|
|
8343
|
+
if (finding2.dependsOn !== void 0) add2("depends on", String(finding2.dependsOn));
|
|
8344
|
+
if (finding2.instability !== void 0) add2("instability", finding2.instability.toFixed(2));
|
|
8345
|
+
if (finding2.centrality !== void 0) add2("centrality", finding2.centrality.toFixed(4));
|
|
8346
|
+
if (finding2.lines !== void 0 && finding2.value !== void 0)
|
|
8347
|
+
add2("length", `${finding2.lines} lines`);
|
|
8348
|
+
if (finding2.confidence !== void 0) add2("confidence", `${finding2.confidence}%`);
|
|
8349
|
+
if (finding2.reason) add2("unsure", `<span class="reason">${escape(finding2.reason)}</span>`);
|
|
7256
8350
|
return pairs.length === 0 ? "" : ` <div class="pmeta">${pairs.join("")}</div>
|
|
7257
8351
|
`;
|
|
7258
8352
|
}
|
|
7259
|
-
function codeSummary(
|
|
7260
|
-
const count =
|
|
7261
|
-
if (
|
|
7262
|
-
return `${
|
|
8353
|
+
function codeSummary(finding2) {
|
|
8354
|
+
const count = finding2.excerpt === void 0 ? 0 : finding2.excerpt.split("\n").length;
|
|
8355
|
+
if (finding2.metric === "clones" || finding2.metric === "clonesTests") {
|
|
8356
|
+
return `${finding2.lines ?? count} duplicated lines`;
|
|
7263
8357
|
}
|
|
7264
|
-
if (
|
|
7265
|
-
return `${count} lines from ${
|
|
8358
|
+
if (finding2.excerptFirstLine === 1) return `the first ${count} lines`;
|
|
8359
|
+
return `${count} lines from ${finding2.excerptFirstLine ?? "?"}`;
|
|
7266
8360
|
}
|
|
7267
|
-
function codeBlock(
|
|
7268
|
-
if (
|
|
7269
|
-
const first =
|
|
7270
|
-
const body = first === void 0 ? escape(
|
|
8361
|
+
function codeBlock(finding2) {
|
|
8362
|
+
if (finding2.excerpt === void 0) return "";
|
|
8363
|
+
const first = finding2.excerptFirstLine;
|
|
8364
|
+
const body = first === void 0 ? escape(finding2.excerpt) : finding2.excerpt.split("\n").map((line, offset) => `<span class="n">${first + offset}</span>${escape(line)}`).join("\n");
|
|
7271
8365
|
return ` <details class="pcode">
|
|
7272
|
-
<summary>${escape(codeSummary(
|
|
8366
|
+
<summary>${escape(codeSummary(finding2))}</summary>
|
|
7273
8367
|
<pre class="code">${body}</pre>
|
|
7274
8368
|
</details>
|
|
7275
8369
|
`;
|
|
7276
8370
|
}
|
|
7277
|
-
function placeCard(
|
|
8371
|
+
function placeCard(finding2) {
|
|
7278
8372
|
return ` <div class="pcard">
|
|
7279
8373
|
<div class="phead">
|
|
7280
|
-
<code class="path">${escape(
|
|
7281
|
-
<span class="pright">${measureOf(
|
|
8374
|
+
<code class="path">${escape(finding2.path)}</code>${secondSide(finding2)}
|
|
8375
|
+
<span class="pright">${measureOf(finding2)}${ruleChip(finding2.metric)}</span>
|
|
7282
8376
|
</div>
|
|
7283
|
-
${metaOf(
|
|
8377
|
+
${metaOf(finding2)}${codeBlock(finding2)} </div>`;
|
|
7284
8378
|
}
|
|
7285
8379
|
function placesFor(group) {
|
|
7286
8380
|
const total = group.listed.length + group.rest;
|
|
@@ -7288,7 +8382,7 @@ function placesFor(group) {
|
|
|
7288
8382
|
return ` <details class="place">
|
|
7289
8383
|
<summary>where to look <span class="count">${grouped(total)} ${total === 1 ? "place" : "places"}</span>${capped}</summary>
|
|
7290
8384
|
<div class="cards">
|
|
7291
|
-
${group.listed.map((
|
|
8385
|
+
${group.listed.map((finding2) => placeCard(finding2)).join("\n")}
|
|
7292
8386
|
</div>
|
|
7293
8387
|
${group.rest > 0 ? ` <p class="sub">The other ${group.rest} are not on this page. <code>${SCRIPTS.json}</code> writes all of them, from the same run as this one.</p>` : ""}
|
|
7294
8388
|
</details>`;
|
|
@@ -7345,17 +8439,17 @@ var KIND_UNITS = {
|
|
|
7345
8439
|
cycles: "files",
|
|
7346
8440
|
mostImported: "imported by"
|
|
7347
8441
|
};
|
|
7348
|
-
function findingRow(
|
|
7349
|
-
const path2 =
|
|
8442
|
+
function findingRow(finding2) {
|
|
8443
|
+
const path2 = finding2.path;
|
|
7350
8444
|
const cut = path2.lastIndexOf("/");
|
|
7351
8445
|
const dir = cut === -1 ? "" : path2.slice(0, cut + 1);
|
|
7352
8446
|
const file = cut === -1 ? path2 : path2.slice(cut + 1);
|
|
7353
|
-
const measure =
|
|
7354
|
-
const unit = KIND_UNITS[
|
|
7355
|
-
const haystack = [path2,
|
|
7356
|
-
return ` <tr data-kind="${escape(kindOf2(
|
|
7357
|
-
<td><span class="kind">${escape(kindOf2(
|
|
7358
|
-
<td class="where"><span class="dir">${escape(dir)}</span><b>${escape(file)}</b>${
|
|
8447
|
+
const measure = finding2.value ?? finding2.lines;
|
|
8448
|
+
const unit = KIND_UNITS[finding2.metric];
|
|
8449
|
+
const haystack = [path2, finding2.name, finding2.other].filter(Boolean).join(" ").toLowerCase();
|
|
8450
|
+
return ` <tr data-kind="${escape(kindOf2(finding2.metric))}" data-find="${escape(haystack)}">
|
|
8451
|
+
<td><span class="kind">${escape(kindOf2(finding2.metric))}</span></td>
|
|
8452
|
+
<td class="where"><span class="dir">${escape(dir)}</span><b>${escape(file)}</b>${finding2.name === void 0 ? "" : `<span class="sym">${escape(finding2.name)}</span>`}${finding2.other === void 0 ? "" : `<span class="sym">↔ ${escape(finding2.other)}</span>`}</td>
|
|
7359
8453
|
<td class="meas">${measure === void 0 ? "" : `<b>${escape(typeof measure === "number" ? grouped(measure) : String(measure))}</b>${unit === void 0 ? "" : `<span>${escape(unit)}</span>`}`}</td>
|
|
7360
8454
|
</tr>`;
|
|
7361
8455
|
}
|
|
@@ -7365,7 +8459,7 @@ function findingsPanel(report) {
|
|
|
7365
8459
|
return ` <p class="read">This run produced no finding to list.</p>`;
|
|
7366
8460
|
}
|
|
7367
8461
|
const counts = /* @__PURE__ */ new Map();
|
|
7368
|
-
for (const
|
|
8462
|
+
for (const finding2 of findings) counts.set(finding2.metric, (counts.get(finding2.metric) ?? 0) + 1);
|
|
7369
8463
|
const chips = [...counts.entries()].map(
|
|
7370
8464
|
([metric, count]) => `<button type="button" class="fchip" data-kind="${escape(kindOf2(metric))}">${escape(kindOf2(metric))} <span>${grouped(count)}</span></button>`
|
|
7371
8465
|
).join("\n ");
|
|
@@ -7388,7 +8482,7 @@ function findingsPanel(report) {
|
|
|
7388
8482
|
<tr><th scope="col">Kind</th><th scope="col">Where</th><th scope="col" class="meas">Measure</th></tr>
|
|
7389
8483
|
</thead>
|
|
7390
8484
|
<tbody>
|
|
7391
|
-
${findings.map((
|
|
8485
|
+
${findings.map((finding2) => findingRow(finding2)).join("\n")}
|
|
7392
8486
|
</tbody>
|
|
7393
8487
|
</table>`;
|
|
7394
8488
|
}
|
|
@@ -7401,7 +8495,7 @@ function summaryParts(report, bold) {
|
|
|
7401
8495
|
const withReference = report.rows.filter((row) => row.status !== void 0);
|
|
7402
8496
|
const above = withReference.filter((row) => row.status === "exceed").length;
|
|
7403
8497
|
const findings = report.findings ?? [];
|
|
7404
|
-
const inTests = findings.filter((
|
|
8498
|
+
const inTests = findings.filter((finding2) => finding2.metric === "clonesTests").length;
|
|
7405
8499
|
const inProduct = findings.length - inTests;
|
|
7406
8500
|
const thresholds = withReference.length === 0 ? [] : above === 0 ? [`${bold("No threshold exceeded")}: the measured levels are within the recommended norm`] : [`${bold(`${above} threshold${above === 1 ? "" : "s"} exceeded`)}`];
|
|
7407
8501
|
return [
|
|
@@ -7980,7 +9074,7 @@ function linesOf(cache, cwd, relative5) {
|
|
|
7980
9074
|
if (cache.has(relative5)) return cache.get(relative5);
|
|
7981
9075
|
let lines;
|
|
7982
9076
|
try {
|
|
7983
|
-
lines =
|
|
9077
|
+
lines = readFileSync19(join28(cwd, relative5), "utf8").split("\n");
|
|
7984
9078
|
} catch {
|
|
7985
9079
|
lines = void 0;
|
|
7986
9080
|
}
|
|
@@ -8003,16 +9097,16 @@ function declarationOf(lines, name) {
|
|
|
8003
9097
|
}
|
|
8004
9098
|
return hits.length === 1 ? hits[0] : void 0;
|
|
8005
9099
|
}
|
|
8006
|
-
function windowFor(
|
|
8007
|
-
const { line } = pointOf(
|
|
9100
|
+
function windowFor(finding2, lines) {
|
|
9101
|
+
const { line } = pointOf(finding2.path);
|
|
8008
9102
|
const head = () => ({
|
|
8009
9103
|
firstLine: 1,
|
|
8010
9104
|
text: lines.slice(0, WINDOW.lines).join("\n")
|
|
8011
9105
|
});
|
|
8012
|
-
if (WHOLE_FILE.has(
|
|
8013
|
-
const at = line ?? (
|
|
9106
|
+
if (WHOLE_FILE.has(finding2.metric) || SHOWS_THE_HEAD.has(finding2.metric)) return head();
|
|
9107
|
+
const at = line ?? (finding2.name ? declarationOf(lines, finding2.name) : void 0);
|
|
8014
9108
|
if (at === void 0) return void 0;
|
|
8015
|
-
const nests = NESTS_UPWARD.has(
|
|
9109
|
+
const nests = NESTS_UPWARD.has(finding2.metric);
|
|
8016
9110
|
const from = nests ? Math.max(1, at - WINDOW.nesting.before) : at;
|
|
8017
9111
|
const to = Math.min(lines.length, nests ? at + WINDOW.nesting.after : at + WINDOW.lines - 1);
|
|
8018
9112
|
if (from > lines.length) return void 0;
|
|
@@ -8021,18 +9115,18 @@ function windowFor(finding, lines) {
|
|
|
8021
9115
|
function withExcerpts(report, cwd) {
|
|
8022
9116
|
const wanted = /* @__PURE__ */ new Set();
|
|
8023
9117
|
for (const group of listedByMetric(report.findings).values()) {
|
|
8024
|
-
for (const
|
|
8025
|
-
if (
|
|
9118
|
+
for (const finding2 of group.listed) {
|
|
9119
|
+
if (finding2.excerpt === void 0) wanted.add(finding2);
|
|
8026
9120
|
}
|
|
8027
9121
|
}
|
|
8028
9122
|
if (wanted.size === 0) return report;
|
|
8029
9123
|
const cache = /* @__PURE__ */ new Map();
|
|
8030
|
-
const findings = report.findings.map((
|
|
8031
|
-
if (!wanted.has(
|
|
8032
|
-
const lines = linesOf(cache, cwd, pointOf(
|
|
8033
|
-
if (lines === void 0) return
|
|
8034
|
-
const found = windowFor(
|
|
8035
|
-
return found === void 0 ?
|
|
9124
|
+
const findings = report.findings.map((finding2) => {
|
|
9125
|
+
if (!wanted.has(finding2)) return finding2;
|
|
9126
|
+
const lines = linesOf(cache, cwd, pointOf(finding2.path).file);
|
|
9127
|
+
if (lines === void 0) return finding2;
|
|
9128
|
+
const found = windowFor(finding2, lines);
|
|
9129
|
+
return found === void 0 ? finding2 : { ...finding2, excerpt: found.text, excerptFirstLine: found.firstLine };
|
|
8036
9130
|
});
|
|
8037
9131
|
return { ...report, findings };
|
|
8038
9132
|
}
|
|
@@ -8212,7 +9306,7 @@ var SCRIPT = `(function(){
|
|
|
8212
9306
|
})();`;
|
|
8213
9307
|
|
|
8214
9308
|
// src/roles/static-analysis/graph/imports.ts
|
|
8215
|
-
import { dirname, join as
|
|
9309
|
+
import { dirname as dirname2, join as join29 } from "node:path";
|
|
8216
9310
|
var TS_BEHIND_JS = {
|
|
8217
9311
|
".js": [".ts", ".tsx"],
|
|
8218
9312
|
".mjs": [".mts"],
|
|
@@ -8265,7 +9359,7 @@ function specifiersOf(program) {
|
|
|
8265
9359
|
}
|
|
8266
9360
|
function resolveSpecifier(from, specifier, known) {
|
|
8267
9361
|
if (!specifier.startsWith(".")) return void 0;
|
|
8268
|
-
const base =
|
|
9362
|
+
const base = join29(dirname2(from), specifier);
|
|
8269
9363
|
if (known.has(base)) return base;
|
|
8270
9364
|
for (const extension of RESOLVABLE_EXTENSIONS) {
|
|
8271
9365
|
if (known.has(base + extension)) return base + extension;
|
|
@@ -8276,7 +9370,7 @@ function resolveSpecifier(from, specifier, known) {
|
|
|
8276
9370
|
if (known.has(swapped)) return swapped;
|
|
8277
9371
|
}
|
|
8278
9372
|
for (const extension of RESOLVABLE_EXTENSIONS) {
|
|
8279
|
-
const index =
|
|
9373
|
+
const index = join29(base, `index${extension}`);
|
|
8280
9374
|
if (known.has(index)) return index;
|
|
8281
9375
|
}
|
|
8282
9376
|
return void 0;
|
|
@@ -8389,26 +9483,26 @@ function scanSyntax(cwd, config) {
|
|
|
8389
9483
|
|
|
8390
9484
|
// src/roles/static-analysis/workspace/consumers.ts
|
|
8391
9485
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
8392
|
-
import { existsSync as
|
|
8393
|
-
import { dirname as
|
|
8394
|
-
var CACHE_DIR =
|
|
9486
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync2, readFileSync as readFileSync20, statSync as statSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
9487
|
+
import { dirname as dirname3, join as join30, relative as relative4, resolve as resolve3, sep } from "node:path";
|
|
9488
|
+
var CACHE_DIR = join30("node_modules", ".cache", "sentinel");
|
|
8395
9489
|
var CACHE_FILE = "consumer-index.json";
|
|
8396
9490
|
var SHAPE = 4;
|
|
8397
9491
|
function workspaceRootFrom(cwd) {
|
|
8398
9492
|
let dir = resolve3(cwd);
|
|
8399
9493
|
for (; ; ) {
|
|
8400
|
-
if (
|
|
8401
|
-
const parent =
|
|
9494
|
+
if (existsSync19(join30(dir, WORKSPACE_ROOT_MARKER))) return dir;
|
|
9495
|
+
const parent = dirname3(dir);
|
|
8402
9496
|
if (parent === dir) return void 0;
|
|
8403
9497
|
dir = parent;
|
|
8404
9498
|
}
|
|
8405
9499
|
}
|
|
8406
9500
|
function aliasTable(workspaceRoot) {
|
|
8407
|
-
const file =
|
|
8408
|
-
if (!
|
|
9501
|
+
const file = join30(workspaceRoot, "tsconfig.base.json");
|
|
9502
|
+
if (!existsSync19(file)) return /* @__PURE__ */ new Map();
|
|
8409
9503
|
let paths;
|
|
8410
9504
|
try {
|
|
8411
|
-
const parsed = JSON.parse(
|
|
9505
|
+
const parsed = JSON.parse(readFileSync20(file, "utf8"));
|
|
8412
9506
|
paths = parsed.compilerOptions?.paths ?? {};
|
|
8413
9507
|
} catch {
|
|
8414
9508
|
return /* @__PURE__ */ new Map();
|
|
@@ -8426,7 +9520,7 @@ function aliasTable(workspaceRoot) {
|
|
|
8426
9520
|
function resolveSpecifier2(specifier, table, workspaceRoot) {
|
|
8427
9521
|
const isFile = (candidate) => {
|
|
8428
9522
|
try {
|
|
8429
|
-
return
|
|
9523
|
+
return statSync6(join30(workspaceRoot, candidate)).isFile();
|
|
8430
9524
|
} catch {
|
|
8431
9525
|
return false;
|
|
8432
9526
|
}
|
|
@@ -8463,11 +9557,11 @@ function resolveSpecifier2(specifier, table, workspaceRoot) {
|
|
|
8463
9557
|
return void 0;
|
|
8464
9558
|
}
|
|
8465
9559
|
function resolveRelative(importer, specifier, workspaceRoot) {
|
|
8466
|
-
const base =
|
|
9560
|
+
const base = join30(dirname3(importer), specifier).split(sep).join("/");
|
|
8467
9561
|
if (base.startsWith("..")) return void 0;
|
|
8468
9562
|
const isFile = (candidate) => {
|
|
8469
9563
|
try {
|
|
8470
|
-
return
|
|
9564
|
+
return statSync6(join30(workspaceRoot, candidate)).isFile();
|
|
8471
9565
|
} catch {
|
|
8472
9566
|
return false;
|
|
8473
9567
|
}
|
|
@@ -8565,10 +9659,10 @@ function hash(text) {
|
|
|
8565
9659
|
}
|
|
8566
9660
|
function consumerIndex(workspaceRoot, moduleRoots) {
|
|
8567
9661
|
const key = cacheKey(workspaceRoot);
|
|
8568
|
-
const file =
|
|
8569
|
-
if (key &&
|
|
9662
|
+
const file = join30(workspaceRoot, CACHE_DIR, CACHE_FILE);
|
|
9663
|
+
if (key && existsSync19(file)) {
|
|
8570
9664
|
try {
|
|
8571
|
-
const cached = JSON.parse(
|
|
9665
|
+
const cached = JSON.parse(readFileSync20(file, "utf8"));
|
|
8572
9666
|
if (cached.key === key) {
|
|
8573
9667
|
return {
|
|
8574
9668
|
imported: new Set(cached.imported),
|
|
@@ -8582,14 +9676,14 @@ function consumerIndex(workspaceRoot, moduleRoots) {
|
|
|
8582
9676
|
const index = buildConsumerIndex(workspaceRoot, moduleRoots());
|
|
8583
9677
|
if (key) {
|
|
8584
9678
|
try {
|
|
8585
|
-
|
|
9679
|
+
mkdirSync2(join30(workspaceRoot, CACHE_DIR), { recursive: true });
|
|
8586
9680
|
const payload = {
|
|
8587
9681
|
key,
|
|
8588
9682
|
imported: [...index.imported],
|
|
8589
9683
|
testOnly: [...index.testOnly],
|
|
8590
9684
|
builtFrom: index.builtFrom
|
|
8591
9685
|
};
|
|
8592
|
-
|
|
9686
|
+
writeFileSync7(file, JSON.stringify(payload), "utf8");
|
|
8593
9687
|
} catch {
|
|
8594
9688
|
}
|
|
8595
9689
|
}
|
|
@@ -8686,7 +9780,7 @@ var JscpdAdapter = class extends BaseAdapter {
|
|
|
8686
9780
|
operations: [
|
|
8687
9781
|
// `write` and not `merge-json`: this file is code, and the engine's in-place merge is for
|
|
8688
9782
|
// JSON. A re-init therefore RESTORES the stub, so an existing one is left alone below.
|
|
8689
|
-
...
|
|
9783
|
+
...existsSync20(join31(context2.cwd, STATIC_CONFIG_FILE)) ? [] : [{ kind: "write", path: STATIC_CONFIG_FILE, contents: configStub() }],
|
|
8690
9784
|
manifestOperation(context2.cwd, scripts()),
|
|
8691
9785
|
...nxTargetOperations({ cwd: context2.cwd, targets: targets() })
|
|
8692
9786
|
// The report directory is ignored ONCE at the workspace root, by `--init`'s workspace prep,
|
|
@@ -8803,7 +9897,7 @@ var JscpdAdapter = class extends BaseAdapter {
|
|
|
8803
9897
|
const written = [];
|
|
8804
9898
|
for (const shape of shapes)
|
|
8805
9899
|
written.push(...await emit(report, shape, ctx.cwd, graph, coverage));
|
|
8806
|
-
process.stdout.write(
|
|
9900
|
+
process.stdout.write(renderConsole2(report));
|
|
8807
9901
|
if (written.length > 0) process.stdout.write(`
|
|
8808
9902
|
written: ${written.join(", ")}
|
|
8809
9903
|
`);
|
|
@@ -8936,7 +10030,7 @@ var JscpdAdapter = class extends BaseAdapter {
|
|
|
8936
10030
|
const declared = moduleScripts(ctx.cwd);
|
|
8937
10031
|
const missing = Object.keys(scripts()).filter((name) => !(name in declared));
|
|
8938
10032
|
const adopted = missing.length < Object.keys(scripts()).length;
|
|
8939
|
-
const hasConfig =
|
|
10033
|
+
const hasConfig = existsSync20(join31(ctx.cwd, STATIC_CONFIG_FILE));
|
|
8940
10034
|
return {
|
|
8941
10035
|
adopted,
|
|
8942
10036
|
preset: adopted ? ctx.preset : null,
|
|
@@ -9028,15 +10122,15 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
|
|
|
9028
10122
|
try {
|
|
9029
10123
|
const ran = runTool(jscpd, args, cwd, "dead-code");
|
|
9030
10124
|
if (ran.kind !== "ran") return ran;
|
|
9031
|
-
const path2 =
|
|
9032
|
-
if (!
|
|
10125
|
+
const path2 = join31(rawReportDir, RAW_REPORT["dead-code"]);
|
|
10126
|
+
if (!existsSync20(path2)) {
|
|
9033
10127
|
process.stderr.write(
|
|
9034
10128
|
`sentinel static-analysis(jscpd): the dead-code pass wrote no ${RAW_REPORT["dead-code"]}
|
|
9035
10129
|
`
|
|
9036
10130
|
);
|
|
9037
10131
|
return { kind: "failed", code: 1 };
|
|
9038
10132
|
}
|
|
9039
|
-
const parsed = safeParseDeadCode(
|
|
10133
|
+
const parsed = safeParseDeadCode(readFileSync21(path2, "utf8"));
|
|
9040
10134
|
const statistics = parsed?.statistics;
|
|
9041
10135
|
if (!statistics) {
|
|
9042
10136
|
process.stderr.write(
|
|
@@ -9049,8 +10143,8 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
|
|
|
9049
10143
|
return { kind: "no-entry-point", files: statistics.files ?? 0 };
|
|
9050
10144
|
}
|
|
9051
10145
|
const byCategory = {};
|
|
9052
|
-
for (const
|
|
9053
|
-
const category =
|
|
10146
|
+
for (const finding2 of parsed?.findings ?? []) {
|
|
10147
|
+
const category = finding2.category ?? "unknown";
|
|
9054
10148
|
byCategory[category] = (byCategory[category] ?? 0) + 1;
|
|
9055
10149
|
}
|
|
9056
10150
|
return { kind: "figures", statistics, byCategory, findings: parsed?.findings ?? [] };
|
|
@@ -9059,11 +10153,11 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
|
|
|
9059
10153
|
}
|
|
9060
10154
|
}
|
|
9061
10155
|
function moduleRelative(name, roots, cwd) {
|
|
9062
|
-
if (
|
|
10156
|
+
if (existsSync20(join31(cwd, name))) return name;
|
|
9063
10157
|
for (const root of roots) {
|
|
9064
10158
|
if (root === ".") continue;
|
|
9065
10159
|
const candidate = `${root}/${name}`;
|
|
9066
|
-
if (
|
|
10160
|
+
if (existsSync20(join31(cwd, candidate))) return candidate;
|
|
9067
10161
|
}
|
|
9068
10162
|
return name;
|
|
9069
10163
|
}
|
|
@@ -9072,8 +10166,8 @@ function runDuplication(jscpd, config, paths, scope, cwd) {
|
|
|
9072
10166
|
try {
|
|
9073
10167
|
const ran = runTool(jscpd, args, cwd, scope);
|
|
9074
10168
|
if (ran.kind !== "ran") return ran;
|
|
9075
|
-
const path2 =
|
|
9076
|
-
const parsed =
|
|
10169
|
+
const path2 = join31(rawReportDir, RAW_REPORT.duplication);
|
|
10170
|
+
const parsed = existsSync20(path2) ? safeParse(readFileSync21(path2, "utf8")) : void 0;
|
|
9077
10171
|
const total = parsed?.statistics?.total;
|
|
9078
10172
|
if (!total) {
|
|
9079
10173
|
process.stderr.write(
|
|
@@ -9137,7 +10231,7 @@ function placesToLook(product, tests, dead, complexity, graph, size, outsideTest
|
|
|
9137
10231
|
const toolTestOnlyPaths = new Set(
|
|
9138
10232
|
dead.kind === "figures" ? dead.findings.filter((f) => f.category === "unused-file" && f.reasons?.includes("used-only-by-tests")).map((f) => f.path ?? "").filter(Boolean) : []
|
|
9139
10233
|
);
|
|
9140
|
-
const metricOf = (
|
|
10234
|
+
const metricOf = (finding2) => finding2.category === "unused-file" && finding2.reasons?.includes("used-only-by-tests") === true ? "usedOnlyByTests" : deadMetric[finding2.category ?? ""] ?? "deadLinesPercent";
|
|
9141
10235
|
const weight = (f) => f.value ?? f.lines ?? 0;
|
|
9142
10236
|
const inOrder = (all2) => [...all2].sort(
|
|
9143
10237
|
(left, right) => left.metric.localeCompare(right.metric) || weight(right) - weight(left) || left.path.localeCompare(right.path) || (left.other ?? "").localeCompare(right.other ?? "")
|
|
@@ -9152,14 +10246,14 @@ function placesToLook(product, tests, dead, complexity, graph, size, outsideTest
|
|
|
9152
10246
|
name: fn.name,
|
|
9153
10247
|
value: fn.complexity
|
|
9154
10248
|
})),
|
|
9155
|
-
...dead.kind === "figures" && !unreadable ? dead.findings.map((
|
|
9156
|
-
metric: metricOf(
|
|
9157
|
-
path:
|
|
9158
|
-
...
|
|
9159
|
-
...
|
|
9160
|
-
...
|
|
9161
|
-
...
|
|
9162
|
-
...
|
|
10249
|
+
...dead.kind === "figures" && !unreadable ? dead.findings.map((finding2) => ({
|
|
10250
|
+
metric: metricOf(finding2),
|
|
10251
|
+
path: finding2.path ?? "(unknown)",
|
|
10252
|
+
...finding2.name ? { name: finding2.name } : {},
|
|
10253
|
+
...finding2.lines === void 0 ? {} : { lines: finding2.lines },
|
|
10254
|
+
...finding2.confidence === void 0 ? {} : { confidence: finding2.confidence },
|
|
10255
|
+
...finding2.reasons?.length ? { reason: finding2.reasons.join(", ") } : {},
|
|
10256
|
+
...finding2.message ? { message: finding2.message } : {}
|
|
9163
10257
|
})) : [],
|
|
9164
10258
|
/*
|
|
9165
10259
|
* The files the workspace index found, which the tool cannot see.
|
|
@@ -9242,8 +10336,8 @@ function outsideOf(cwd) {
|
|
|
9242
10336
|
function moduleRootsOf(workspaceRoot) {
|
|
9243
10337
|
const roots = [];
|
|
9244
10338
|
for (const area of ["apps", "libs"]) {
|
|
9245
|
-
const base =
|
|
9246
|
-
if (!
|
|
10339
|
+
const base = join31(workspaceRoot, area);
|
|
10340
|
+
if (!existsSync20(base)) continue;
|
|
9247
10341
|
for (const entry of globSync5([`${area}/**/package.json`, `${area}/**/project.json`], {
|
|
9248
10342
|
cwd: workspaceRoot,
|
|
9249
10343
|
ignore: ["**/node_modules/**"],
|
|
@@ -9400,8 +10494,8 @@ function assembleReport({
|
|
|
9400
10494
|
}
|
|
9401
10495
|
const toolTestOnly = new Set(
|
|
9402
10496
|
dead.kind === "figures" ? dead.findings.filter(
|
|
9403
|
-
(
|
|
9404
|
-
).map((
|
|
10497
|
+
(finding2) => finding2.category === "unused-file" && finding2.reasons?.includes("used-only-by-tests") === true
|
|
10498
|
+
).map((finding2) => finding2.path ?? "").filter(Boolean) : []
|
|
9405
10499
|
);
|
|
9406
10500
|
if (dead.kind === "figures" && !unreadable) {
|
|
9407
10501
|
const stats = dead.statistics;
|
|
@@ -9576,23 +10670,23 @@ function assembleReport({
|
|
|
9576
10670
|
byLanguage
|
|
9577
10671
|
};
|
|
9578
10672
|
}
|
|
9579
|
-
var
|
|
10673
|
+
var SHAPE_FILE2 = {
|
|
9580
10674
|
json: `${TARGET}.json`,
|
|
9581
10675
|
html: `${TARGET}.html`,
|
|
9582
10676
|
markdown: `${TARGET}.md`
|
|
9583
10677
|
};
|
|
9584
10678
|
var PICTURE_FILE = `${TARGET}-graph.svg`;
|
|
9585
10679
|
function clearStaleOutputs(cwd, keeping) {
|
|
9586
|
-
for (const name of [...Object.values(
|
|
10680
|
+
for (const name of [...Object.values(SHAPE_FILE2), PICTURE_FILE]) {
|
|
9587
10681
|
if (keeping.has(name)) continue;
|
|
9588
|
-
const path2 =
|
|
9589
|
-
if (
|
|
10682
|
+
const path2 = join31(cwd, REPORT_DIR, name);
|
|
10683
|
+
if (existsSync20(path2)) unlinkSync2(path2);
|
|
9590
10684
|
}
|
|
9591
10685
|
}
|
|
9592
10686
|
function outputsOf(shapes, graph, coverage) {
|
|
9593
10687
|
const keeping = /* @__PURE__ */ new Set();
|
|
9594
10688
|
for (const shape of shapes) {
|
|
9595
|
-
const name =
|
|
10689
|
+
const name = SHAPE_FILE2[shape];
|
|
9596
10690
|
if (name !== void 0) keeping.add(name);
|
|
9597
10691
|
}
|
|
9598
10692
|
if (shapes.includes("html") && drawsPicture(graph, coverage)) keeping.add(PICTURE_FILE);
|
|
@@ -9604,19 +10698,19 @@ function drawsPicture(graph, coverage) {
|
|
|
9604
10698
|
async function emit(report, shape, cwd, graph, coverage) {
|
|
9605
10699
|
if (shape === "console") return [];
|
|
9606
10700
|
const detailed = shape === "markdown" ? report : withExcerpts(report, cwd);
|
|
9607
|
-
|
|
10701
|
+
mkdirSync3(join31(cwd, REPORT_DIR), { recursive: true });
|
|
9608
10702
|
const written = [];
|
|
9609
10703
|
if (shape === "html" && drawsPicture(graph, coverage)) {
|
|
9610
10704
|
const drawn = layOut(graph.graph, graph.communityOf);
|
|
9611
|
-
const path2 =
|
|
10705
|
+
const path2 = join31(cwd, REPORT_DIR, PICTURE_FILE);
|
|
9612
10706
|
await writeGraphPicture(drawn, path2);
|
|
9613
10707
|
written.push(`${REPORT_DIR}/${PICTURE_FILE}`);
|
|
9614
10708
|
detailed.picture = buildExplorer(drawn, graph.communityOf).markup;
|
|
9615
10709
|
}
|
|
9616
|
-
const RENDER = { json:
|
|
9617
|
-
const name =
|
|
10710
|
+
const RENDER = { json: renderJson2, html: renderHtml, markdown: renderMarkdown2 };
|
|
10711
|
+
const name = SHAPE_FILE2[shape] ?? `${TARGET}.${shape}`;
|
|
9618
10712
|
const body = RENDER[shape](detailed);
|
|
9619
|
-
|
|
10713
|
+
writeFileSync8(join31(cwd, REPORT_DIR, name), body, "utf8");
|
|
9620
10714
|
written.unshift(`${REPORT_DIR}/${name}`);
|
|
9621
10715
|
return written;
|
|
9622
10716
|
}
|
|
@@ -9650,9 +10744,9 @@ function registerTest() {
|
|
|
9650
10744
|
|
|
9651
10745
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
9652
10746
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
9653
|
-
import { existsSync as
|
|
10747
|
+
import { existsSync as existsSync21, readFileSync as readFileSync23 } from "node:fs";
|
|
9654
10748
|
import { createRequire } from "node:module";
|
|
9655
|
-
import { join as
|
|
10749
|
+
import { join as join33 } from "node:path";
|
|
9656
10750
|
|
|
9657
10751
|
// src/roles/typescript/presets/nest.json
|
|
9658
10752
|
var nest_default2 = {
|
|
@@ -9704,7 +10798,7 @@ var react_default2 = {
|
|
|
9704
10798
|
};
|
|
9705
10799
|
|
|
9706
10800
|
// src/roles/typescript/presets/shared.json
|
|
9707
|
-
var
|
|
10801
|
+
var shared_default3 = {
|
|
9708
10802
|
$schema: "https://json.schemastore.org/tsconfig",
|
|
9709
10803
|
compilerOptions: {
|
|
9710
10804
|
strict: true,
|
|
@@ -9730,7 +10824,7 @@ var shared_default2 = {
|
|
|
9730
10824
|
};
|
|
9731
10825
|
|
|
9732
10826
|
// src/roles/typescript/preset-data.ts
|
|
9733
|
-
var SHARED =
|
|
10827
|
+
var SHARED = shared_default3;
|
|
9734
10828
|
var LAYERS = {
|
|
9735
10829
|
nest: nest_default2,
|
|
9736
10830
|
node: node_default2,
|
|
@@ -9837,8 +10931,8 @@ function readTsconfigAdoption(cwd) {
|
|
|
9837
10931
|
}
|
|
9838
10932
|
|
|
9839
10933
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
9840
|
-
import { readFileSync as
|
|
9841
|
-
import { join as
|
|
10934
|
+
import { readFileSync as readFileSync22 } from "node:fs";
|
|
10935
|
+
import { join as join32 } from "node:path";
|
|
9842
10936
|
|
|
9843
10937
|
// src/roles/typescript/typecheck-script.ts
|
|
9844
10938
|
var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
|
|
@@ -9931,7 +11025,7 @@ function planAdoption(context2) {
|
|
|
9931
11025
|
};
|
|
9932
11026
|
}
|
|
9933
11027
|
const existing = parseJsonc(
|
|
9934
|
-
|
|
11028
|
+
readFileSync22(join32(context2.cwd, target.path), "utf8"),
|
|
9935
11029
|
target.path
|
|
9936
11030
|
);
|
|
9937
11031
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -10069,7 +11163,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
10069
11163
|
let chain;
|
|
10070
11164
|
try {
|
|
10071
11165
|
const parsed = parseJsonc(
|
|
10072
|
-
|
|
11166
|
+
readFileSync23(join33(cwd, target.path), "utf8"),
|
|
10073
11167
|
target.path
|
|
10074
11168
|
);
|
|
10075
11169
|
chain = parsed.extends;
|
|
@@ -10082,7 +11176,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
10082
11176
|
);
|
|
10083
11177
|
if (preset === void 0) return void 0;
|
|
10084
11178
|
try {
|
|
10085
|
-
createRequire(
|
|
11179
|
+
createRequire(join33(cwd, "noop.js")).resolve(preset);
|
|
10086
11180
|
return void 0;
|
|
10087
11181
|
} catch {
|
|
10088
11182
|
return preset;
|
|
@@ -10178,7 +11272,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
10178
11272
|
referencedProjects(cwd, config) {
|
|
10179
11273
|
try {
|
|
10180
11274
|
const parsed = parseJsonc(
|
|
10181
|
-
|
|
11275
|
+
readFileSync23(join33(cwd, config), "utf8"),
|
|
10182
11276
|
config
|
|
10183
11277
|
);
|
|
10184
11278
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path2) => typeof path2 === "string" && path2.length > 0);
|
|
@@ -10194,7 +11288,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
10194
11288
|
* check.
|
|
10195
11289
|
*/
|
|
10196
11290
|
typecheckTarget(cwd) {
|
|
10197
|
-
if (
|
|
11291
|
+
if (existsSync21(join33(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
10198
11292
|
const target = resolveTsconfigTarget(cwd);
|
|
10199
11293
|
return target.reason === "none" ? null : target.path;
|
|
10200
11294
|
}
|
|
@@ -10319,6 +11413,7 @@ function registerAdapters() {
|
|
|
10319
11413
|
registerBuild();
|
|
10320
11414
|
registerTest();
|
|
10321
11415
|
registerStaticAnalysis();
|
|
11416
|
+
registerEnv();
|
|
10322
11417
|
}
|
|
10323
11418
|
|
|
10324
11419
|
// src/core/detect-framework.ts
|
|
@@ -10365,12 +11460,12 @@ function diffLines(before, after) {
|
|
|
10365
11460
|
{ length: from.length + 1 },
|
|
10366
11461
|
() => new Array(to.length + 1).fill(0)
|
|
10367
11462
|
);
|
|
10368
|
-
const
|
|
11463
|
+
const cell2 = (i2, j2) => lcs[i2]?.[j2] ?? 0;
|
|
10369
11464
|
for (let i2 = from.length - 1; i2 >= 0; i2--) {
|
|
10370
11465
|
const row = lcs[i2];
|
|
10371
11466
|
if (!row) continue;
|
|
10372
11467
|
for (let j2 = to.length - 1; j2 >= 0; j2--) {
|
|
10373
|
-
row[j2] = from[i2] === to[j2] ?
|
|
11468
|
+
row[j2] = from[i2] === to[j2] ? cell2(i2 + 1, j2 + 1) + 1 : Math.max(cell2(i2 + 1, j2), cell2(i2, j2 + 1));
|
|
10374
11469
|
}
|
|
10375
11470
|
}
|
|
10376
11471
|
const out = [];
|
|
@@ -10381,7 +11476,7 @@ function diffLines(before, after) {
|
|
|
10381
11476
|
out.push(` ${from[i] ?? ""}`);
|
|
10382
11477
|
i++;
|
|
10383
11478
|
j++;
|
|
10384
|
-
} else if (
|
|
11479
|
+
} else if (cell2(i + 1, j) >= cell2(i, j + 1)) {
|
|
10385
11480
|
out.push(`- ${from[i] ?? ""}`);
|
|
10386
11481
|
i++;
|
|
10387
11482
|
} else {
|
|
@@ -10400,7 +11495,7 @@ function replaceLines(current, replacements) {
|
|
|
10400
11495
|
}
|
|
10401
11496
|
|
|
10402
11497
|
// src/core/apply-plan.ts
|
|
10403
|
-
import { existsSync as
|
|
11498
|
+
import { existsSync as existsSync22, readFileSync as readFileSync24, renameSync, rmSync as rmSync5, writeFileSync as writeFileSync9 } from "node:fs";
|
|
10404
11499
|
import { resolve as resolve4, sep as sep2 } from "node:path";
|
|
10405
11500
|
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
10406
11501
|
function resolveWithinRoot(cwd, relativePath) {
|
|
@@ -10412,7 +11507,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
10412
11507
|
return absolutePath;
|
|
10413
11508
|
}
|
|
10414
11509
|
function readIfExists(absolutePath) {
|
|
10415
|
-
return
|
|
11510
|
+
return existsSync22(absolutePath) ? readFileSync24(absolutePath, "utf8") : void 0;
|
|
10416
11511
|
}
|
|
10417
11512
|
function* leaves(value, prefix = []) {
|
|
10418
11513
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -10514,7 +11609,7 @@ function preparePlan(cwd, plan3) {
|
|
|
10514
11609
|
}
|
|
10515
11610
|
function writeFileAtomic(absolutePath, contents) {
|
|
10516
11611
|
const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
|
|
10517
|
-
|
|
11612
|
+
writeFileSync9(tempPath, contents);
|
|
10518
11613
|
renameSync(tempPath, absolutePath);
|
|
10519
11614
|
}
|
|
10520
11615
|
function applyPlan(cwd, plan3) {
|
|
@@ -10530,8 +11625,8 @@ function applyPlan(cwd, plan3) {
|
|
|
10530
11625
|
}
|
|
10531
11626
|
|
|
10532
11627
|
// src/core/config/preset-evidence.ts
|
|
10533
|
-
import { existsSync as
|
|
10534
|
-
import { join as
|
|
11628
|
+
import { existsSync as existsSync23, readdirSync as readdirSync3, readFileSync as readFileSync25 } from "node:fs";
|
|
11629
|
+
import { join as join34 } from "node:path";
|
|
10535
11630
|
var PATH_SIGNALS = [
|
|
10536
11631
|
{
|
|
10537
11632
|
preset: "nest",
|
|
@@ -10565,12 +11660,12 @@ function sourceImports(cwd, pattern) {
|
|
|
10565
11660
|
}
|
|
10566
11661
|
for (const entry of entries) {
|
|
10567
11662
|
if (entry.isDirectory()) {
|
|
10568
|
-
if (!NOT_SCANNED.has(entry.name)) stack.push(
|
|
11663
|
+
if (!NOT_SCANNED.has(entry.name)) stack.push(join34(dir, entry.name));
|
|
10569
11664
|
continue;
|
|
10570
11665
|
}
|
|
10571
11666
|
if (!SOURCE_FILE.test(entry.name)) continue;
|
|
10572
11667
|
try {
|
|
10573
|
-
if (pattern.test(
|
|
11668
|
+
if (pattern.test(readFileSync25(join34(dir, entry.name), "utf8"))) return true;
|
|
10574
11669
|
} catch {
|
|
10575
11670
|
continue;
|
|
10576
11671
|
}
|
|
@@ -10579,11 +11674,11 @@ function sourceImports(cwd, pattern) {
|
|
|
10579
11674
|
return false;
|
|
10580
11675
|
}
|
|
10581
11676
|
function dependencyNames(cwd) {
|
|
10582
|
-
const path2 =
|
|
10583
|
-
if (!
|
|
11677
|
+
const path2 = join34(cwd, "package.json");
|
|
11678
|
+
if (!existsSync23(path2)) return [];
|
|
10584
11679
|
try {
|
|
10585
11680
|
const manifest = parseJsonc(
|
|
10586
|
-
|
|
11681
|
+
readFileSync25(path2, "utf8"),
|
|
10587
11682
|
path2
|
|
10588
11683
|
);
|
|
10589
11684
|
return [
|
|
@@ -10606,7 +11701,7 @@ function reactTsconfigEvidence(cwd) {
|
|
|
10606
11701
|
}
|
|
10607
11702
|
for (const name of entries) {
|
|
10608
11703
|
try {
|
|
10609
|
-
const config = parseJsonc(
|
|
11704
|
+
const config = parseJsonc(readFileSync25(join34(cwd, name), "utf8"), name);
|
|
10610
11705
|
if (config.compilerOptions?.jsx !== void 0) return 'its tsconfig sets "jsx"';
|
|
10611
11706
|
const extended = Array.isArray(config.extends) ? config.extends : config.extends === void 0 ? [] : [config.extends];
|
|
10612
11707
|
if (extended.some((entry) => entry === REACT_TYPESCRIPT_PRESET)) {
|
|
@@ -10813,6 +11908,7 @@ export {
|
|
|
10813
11908
|
resolve,
|
|
10814
11909
|
buildConfigFile,
|
|
10815
11910
|
palette,
|
|
11911
|
+
ENV_SHAPES,
|
|
10816
11912
|
presetNotInstalledYet,
|
|
10817
11913
|
registerAdapters,
|
|
10818
11914
|
correctingSignal,
|