@hublo/sentinel 1.5.1-alpha.2 → 1.6.0-alpha.1
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 +9 -5
- package/dist/bin/sentinel.js +179 -12
- package/dist/chunk-4EIY7TTR.js +89 -0
- package/dist/chunk-4EIY7TTR.js.map +1 -0
- package/dist/{chunk-JHLNNIZC.js → chunk-5267A6X3.js} +1400 -272
- package/dist/{chunk-WBDBRGLV.js → chunk-7E6YTTAK.js} +8 -1
- package/dist/chunk-7E6YTTAK.js.map +1 -0
- 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 +268 -0
- package/docs/typescript-traces.md +258 -0
- package/docs/using-sentinel.md +11 -10
- package/docs/validating-a-change.md +114 -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-4EIY7TTR.js";
|
|
84
88
|
import {
|
|
85
89
|
globToRegExp
|
|
86
90
|
} from "./chunk-O7REVMOC.js";
|
|
@@ -171,6 +175,75 @@ 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
|
+
".spec.mts",
|
|
197
|
+
".test.mts",
|
|
198
|
+
".spec.cts",
|
|
199
|
+
".test.cts",
|
|
200
|
+
".spec.mjs",
|
|
201
|
+
".test.mjs",
|
|
202
|
+
".spec.cjs",
|
|
203
|
+
".test.cjs"
|
|
204
|
+
],
|
|
205
|
+
excludeDirs: ["node_modules", "dist"]
|
|
206
|
+
},
|
|
207
|
+
references: {
|
|
208
|
+
roots: ["."],
|
|
209
|
+
ext: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".svelte", ".vue"],
|
|
210
|
+
excludeSuffixes: [],
|
|
211
|
+
excludeDirs: ["node_modules", "dist"]
|
|
212
|
+
},
|
|
213
|
+
referencePatterns: [
|
|
214
|
+
"process\\.env\\s*\\.\\s*([A-Za-z0-9_]+)",
|
|
215
|
+
"process\\.env\\[\\s*['\"`]([^'\"`]+)['\"`]\\s*\\]",
|
|
216
|
+
"import\\.meta\\.env\\s*\\.\\s*([A-Za-z0-9_]+)",
|
|
217
|
+
"import\\.meta\\.env\\[\\s*['\"`]([^'\"`]+)['\"`]\\s*\\]",
|
|
218
|
+
`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]\\$env/static/(?:public|private)['"]`,
|
|
219
|
+
"\\benv\\.([A-Z][A-Z0-9_]*)",
|
|
220
|
+
"\\{([^{}]*)\\}\\s*=\\s*env\\b"
|
|
221
|
+
],
|
|
222
|
+
secretNamePatterns: ["SECRET", "TOKEN", "PASSWORD", "PRIVATE_?KEY", "ACCESS_?KEY", "API_?KEY"],
|
|
223
|
+
publicPrefixes: ["VITE_", "PUBLIC_", "NEXT_PUBLIC_", "REACT_APP_"],
|
|
224
|
+
envFiles: ["**/.env", "**/.env.*", "**/*.env"],
|
|
225
|
+
contracts: ["**/*.env*.example"],
|
|
226
|
+
nodeVersionFile: ".node-version"
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// src/roles/env/preset-data.ts
|
|
230
|
+
var data = shared_default;
|
|
231
|
+
var envDefaults = data;
|
|
232
|
+
function referencePatterns() {
|
|
233
|
+
return data.referencePatterns.map((source) => new RegExp(source, "g"));
|
|
234
|
+
}
|
|
235
|
+
function secretNamePatterns() {
|
|
236
|
+
return data.secretNamePatterns.map((source) => new RegExp(source, "i"));
|
|
237
|
+
}
|
|
238
|
+
var TARGET2 = "env";
|
|
239
|
+
var ENV_CONFIG_FILE = "env.config.mjs";
|
|
240
|
+
var REPORT_DIR2 = "env-report";
|
|
241
|
+
var ENV_SHAPES = ["console", "json", "markdown"];
|
|
242
|
+
var SHAPE_FILE = {
|
|
243
|
+
json: "env.json",
|
|
244
|
+
markdown: "env.md"
|
|
245
|
+
};
|
|
246
|
+
|
|
174
247
|
// src/roles/build/nest/nx-target.ts
|
|
175
248
|
import { existsSync, readFileSync } from "node:fs";
|
|
176
249
|
import { join, relative } from "node:path";
|
|
@@ -1289,15 +1362,1068 @@ function registerBuild() {
|
|
|
1289
1362
|
setDefaultRunner("dev", "vite");
|
|
1290
1363
|
}
|
|
1291
1364
|
|
|
1365
|
+
// src/roles/env/adapters/conformance/conformance.adapter.ts
|
|
1366
|
+
import { existsSync as existsSync7, mkdirSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1367
|
+
import { join as join10 } from "node:path";
|
|
1368
|
+
|
|
1369
|
+
// src/roles/env/collect.ts
|
|
1370
|
+
import { dirname, join as join8 } from "node:path/posix";
|
|
1371
|
+
|
|
1372
|
+
// src/roles/env/checks.ts
|
|
1373
|
+
var READER_VAR_NAME = String.raw`[A-Z0-9_]+`;
|
|
1374
|
+
var REFERENCED_NAME = /[A-Za-z_][A-Za-z0-9_]*/g;
|
|
1375
|
+
function lineAt(content, index) {
|
|
1376
|
+
let line = 1;
|
|
1377
|
+
for (let position = content.indexOf("\n"); position !== -1 && position < index; ) {
|
|
1378
|
+
line += 1;
|
|
1379
|
+
position = content.indexOf("\n", position + 1);
|
|
1380
|
+
}
|
|
1381
|
+
return line;
|
|
1382
|
+
}
|
|
1383
|
+
function readerRegex(readerName) {
|
|
1384
|
+
return new RegExp(String.raw`(?<!\w)${readerName}\(\s*[\`'"](${READER_VAR_NAME})[\`'"]`, "g");
|
|
1385
|
+
}
|
|
1386
|
+
function recordReads(reads, matchers, file) {
|
|
1387
|
+
for (const matcher of matchers) {
|
|
1388
|
+
for (const match of file.content.matchAll(matcher)) {
|
|
1389
|
+
const name = match[1];
|
|
1390
|
+
if (name === void 0) continue;
|
|
1391
|
+
const sites = reads.get(name);
|
|
1392
|
+
if (sites?.some((site2) => site2.path === file.path)) continue;
|
|
1393
|
+
const site = { path: file.path, line: lineAt(file.content, match.index) };
|
|
1394
|
+
if (sites) sites.push(site);
|
|
1395
|
+
else reads.set(name, [site]);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
function deriveVars(files, readers) {
|
|
1400
|
+
const requiredMatchers = (readers.required ?? []).map(readerRegex);
|
|
1401
|
+
const optionalMatchers = (readers.optional ?? []).map(readerRegex);
|
|
1402
|
+
const required = /* @__PURE__ */ new Map();
|
|
1403
|
+
const optional = /* @__PURE__ */ new Map();
|
|
1404
|
+
for (const file of files) {
|
|
1405
|
+
recordReads(required, requiredMatchers, file);
|
|
1406
|
+
recordReads(optional, optionalMatchers, file);
|
|
1407
|
+
}
|
|
1408
|
+
return { required, optional };
|
|
1409
|
+
}
|
|
1410
|
+
var ENV_LINE = /^(?:export\s+)?([\w.-]+)\s*=(.*)$/;
|
|
1411
|
+
var QUOTES = /* @__PURE__ */ new Set(['"', "'", "`"]);
|
|
1412
|
+
function closingQuote(text, quote, from) {
|
|
1413
|
+
for (let index = from; index < text.length; index += 1) {
|
|
1414
|
+
if (text[index] === "\\" && text[index + 1] === quote) index += 1;
|
|
1415
|
+
else if (text[index] === quote) return index;
|
|
1416
|
+
}
|
|
1417
|
+
return -1;
|
|
1418
|
+
}
|
|
1419
|
+
function readValue(raw) {
|
|
1420
|
+
const value = raw.trimStart();
|
|
1421
|
+
const quote = value[0];
|
|
1422
|
+
if (quote !== void 0 && QUOTES.has(quote)) {
|
|
1423
|
+
const end = closingQuote(value, quote, 1);
|
|
1424
|
+
if (end === -1) return { blank: false, opens: quote };
|
|
1425
|
+
const rest = value.slice(end + 1).trim();
|
|
1426
|
+
return { blank: end === 1 && (rest === "" || rest.startsWith("#")) };
|
|
1427
|
+
}
|
|
1428
|
+
const comment = value.indexOf("#");
|
|
1429
|
+
return { blank: (comment === -1 ? value : value.slice(0, comment)).trim() === "" };
|
|
1430
|
+
}
|
|
1431
|
+
function parseEnvFile(content) {
|
|
1432
|
+
const entries = [];
|
|
1433
|
+
const lines = content.split("\n");
|
|
1434
|
+
let closing;
|
|
1435
|
+
for (const [index, rawLine] of lines.entries()) {
|
|
1436
|
+
const line = rawLine.trim();
|
|
1437
|
+
if (closing !== void 0) {
|
|
1438
|
+
if (closingQuote(line, closing, 0) !== -1) closing = void 0;
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
1442
|
+
const match = ENV_LINE.exec(line);
|
|
1443
|
+
if (!match) continue;
|
|
1444
|
+
const { blank, opens } = readValue(match[2] ?? "");
|
|
1445
|
+
entries.push({ key: match[1] ?? "", line: index + 1, blank });
|
|
1446
|
+
closing = opens;
|
|
1447
|
+
}
|
|
1448
|
+
return {
|
|
1449
|
+
entries,
|
|
1450
|
+
keys: entries.map((entry) => entry.key),
|
|
1451
|
+
blanked: entries.filter((entry) => entry.blank).map((entry) => entry.key),
|
|
1452
|
+
filled: entries.filter((entry) => !entry.blank).map((entry) => entry.key)
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
function diffContract(derived, declared, externallyProvided = []) {
|
|
1456
|
+
const declaredKeys = new Set(declared);
|
|
1457
|
+
const external = new Set(externallyProvided);
|
|
1458
|
+
const unaccounted = [...derived.required.keys()].filter((name) => !declaredKeys.has(name) && !external.has(name)).sort();
|
|
1459
|
+
const optionalUndeclared = [...derived.optional.keys()].filter((name) => !declaredKeys.has(name)).sort();
|
|
1460
|
+
return { unaccounted, optionalUndeclared };
|
|
1461
|
+
}
|
|
1462
|
+
function findOrphanEnvFiles(envFiles, allow = []) {
|
|
1463
|
+
const allowed = new Set(allow);
|
|
1464
|
+
return envFiles.filter((path2) => !path2.endsWith(".example") && !allowed.has(path2)).sort();
|
|
1465
|
+
}
|
|
1466
|
+
function* referencedNames(content, patterns) {
|
|
1467
|
+
for (const pattern of patterns) {
|
|
1468
|
+
for (const match of content.matchAll(pattern)) {
|
|
1469
|
+
for (const [name] of (match[1] ?? "").matchAll(REFERENCED_NAME)) yield name;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
function findReferencedVars(files, patterns) {
|
|
1474
|
+
const referenced = /* @__PURE__ */ new Map();
|
|
1475
|
+
for (const { path: path2, content } of files) {
|
|
1476
|
+
for (const name of referencedNames(content, patterns)) {
|
|
1477
|
+
const readers = referenced.get(name) ?? /* @__PURE__ */ new Set();
|
|
1478
|
+
readers.add(path2);
|
|
1479
|
+
referenced.set(name, readers);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return referenced;
|
|
1483
|
+
}
|
|
1484
|
+
function findDeadVars(declaredKeys, referenced, allow = [], allowPatterns = []) {
|
|
1485
|
+
const alive = new Set(referenced);
|
|
1486
|
+
const allowed = new Set(allow);
|
|
1487
|
+
const isExternal = (key) => allowed.has(key) || allowPatterns.some((pattern) => pattern.test(key));
|
|
1488
|
+
return [...new Set(declaredKeys)].filter((key) => !alive.has(key) && !isExternal(key)).sort();
|
|
1489
|
+
}
|
|
1490
|
+
function auditEnvFileSecrets(parsed, options) {
|
|
1491
|
+
const secretFilled = parsed.entries.filter(
|
|
1492
|
+
(entry) => !entry.blank && options.secretPatterns.some((pattern) => pattern.test(entry.key))
|
|
1493
|
+
);
|
|
1494
|
+
const isPublic = (entry) => options.publicPrefixes.some((prefix) => entry.key.startsWith(prefix));
|
|
1495
|
+
return {
|
|
1496
|
+
mustBlank: secretFilled.filter((entry) => !isPublic(entry)),
|
|
1497
|
+
publicSecrets: secretFilled.filter(isPublic)
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
function splitAllowlisted(locations, allowlist) {
|
|
1501
|
+
const allow = new Set(allowlist);
|
|
1502
|
+
const allowed = [];
|
|
1503
|
+
const flagged = [];
|
|
1504
|
+
for (const location of locations) (allow.has(location) ? allowed : flagged).push(location);
|
|
1505
|
+
return { allowed, flagged };
|
|
1506
|
+
}
|
|
1507
|
+
function isDockerfilePath(path2) {
|
|
1508
|
+
const name = path2.slice(path2.lastIndexOf("/") + 1);
|
|
1509
|
+
if (name.endsWith(".dockerignore") || name.endsWith(".md")) return false;
|
|
1510
|
+
return name === "dockerfile" || /^Dockerfile([.-][^/]+)?$/.test(name) || /\.[Dd]ockerfile$/.test(name);
|
|
1511
|
+
}
|
|
1512
|
+
function copySources(line) {
|
|
1513
|
+
const words = line.split(/\s+/).slice(1);
|
|
1514
|
+
const flags = words.findIndex((word) => !word.startsWith("--"));
|
|
1515
|
+
const rest = flags === -1 ? "" : words.slice(flags).join(" ");
|
|
1516
|
+
if (rest.startsWith("<<")) return [];
|
|
1517
|
+
if (rest.startsWith("[")) {
|
|
1518
|
+
try {
|
|
1519
|
+
const parsed = JSON.parse(rest);
|
|
1520
|
+
if (Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string")) {
|
|
1521
|
+
return parsed.slice(0, -1);
|
|
1522
|
+
}
|
|
1523
|
+
} catch {
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return words.slice(flags === -1 ? words.length : flags).slice(0, -1).map((word) => /^(['"])(.*)\1$/.exec(word)?.[2] ?? word);
|
|
1527
|
+
}
|
|
1528
|
+
function parseDockerfile(content) {
|
|
1529
|
+
const froms = [];
|
|
1530
|
+
const contextCopySources = [];
|
|
1531
|
+
for (const [index, rawLine] of content.split("\n").entries()) {
|
|
1532
|
+
const line = rawLine.trim();
|
|
1533
|
+
const from = /^FROM\s+(\S+)/i.exec(line);
|
|
1534
|
+
if (from) {
|
|
1535
|
+
froms.push({ image: from[1] ?? "", line: index + 1 });
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
if (/^COPY\s/i.test(line) && !/--from=/i.test(line)) {
|
|
1539
|
+
for (const source of copySources(line)) {
|
|
1540
|
+
if (!/[*?$]/.test(source)) contextCopySources.push({ source, line: index + 1 });
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return { froms, contextCopySources };
|
|
1545
|
+
}
|
|
1546
|
+
function dockerNodeVersion(fromImage) {
|
|
1547
|
+
const match = /(?:^|\/)node:([0-9]+(?:\.[0-9]+){0,2})/.exec(fromImage);
|
|
1548
|
+
return match?.[1] ?? null;
|
|
1549
|
+
}
|
|
1550
|
+
function normalizeNodeVersion(raw) {
|
|
1551
|
+
const version = raw.trim().replace(/^v/i, "");
|
|
1552
|
+
return version === "" ? void 0 : version;
|
|
1553
|
+
}
|
|
1554
|
+
function nodeVersionAligned(dockerVersion, expected) {
|
|
1555
|
+
return expected === dockerVersion || expected.startsWith(`${dockerVersion}.`);
|
|
1556
|
+
}
|
|
1557
|
+
function dockerHardening(content) {
|
|
1558
|
+
const lines = content.split("\n").map((line) => line.trim());
|
|
1559
|
+
let lastFrom = -1;
|
|
1560
|
+
lines.forEach((line, index) => {
|
|
1561
|
+
if (/^FROM\s/i.test(line)) lastFrom = index;
|
|
1562
|
+
});
|
|
1563
|
+
const start = lastFrom === -1 ? 0 : lastFrom;
|
|
1564
|
+
const finalStage = lines.slice(start).map((text, offset) => ({ text, line: start + offset + 1 }));
|
|
1565
|
+
let user = null;
|
|
1566
|
+
let userLine = start + 1;
|
|
1567
|
+
for (const { text, line } of finalStage) {
|
|
1568
|
+
const match = /^USER\s+(\S+)/i.exec(text);
|
|
1569
|
+
if (match) {
|
|
1570
|
+
user = match[1] ?? null;
|
|
1571
|
+
userLine = line;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
const runsAsRoot = !user || /^(root|0)(:|$)/i.test(user);
|
|
1575
|
+
const ownsTree = finalStage.some(({ text }) => /chown\s+-R/i.test(text));
|
|
1576
|
+
const copiesWithoutChown = ownsTree ? [] : finalStage.filter(({ text }) => /^COPY\s/i.test(text) && !/--chown=/i.test(text)).map(({ text, line }) => ({ instruction: text.replace(/\s+/g, " "), line }));
|
|
1577
|
+
return { user, userLine, runsAsRoot, copiesWithoutChown };
|
|
1578
|
+
}
|
|
1579
|
+
function parseNxTargetRefs(content) {
|
|
1580
|
+
return [...content.matchAll(/\bnx run\s+([^\s:$]+):([^\s$]+)/g)].map((match) => ({
|
|
1581
|
+
project: match[1] ?? "",
|
|
1582
|
+
targetSpec: match[2] ?? "",
|
|
1583
|
+
line: lineAt(content, match.index)
|
|
1584
|
+
}));
|
|
1585
|
+
}
|
|
1586
|
+
function nxTargetCandidates(targetSpec) {
|
|
1587
|
+
const parts = targetSpec.split(":");
|
|
1588
|
+
const candidates = [targetSpec];
|
|
1589
|
+
if (parts.length > 1) candidates.push(parts.slice(0, -1).join(":"));
|
|
1590
|
+
return candidates;
|
|
1591
|
+
}
|
|
1592
|
+
function resolveApp(path2, projects) {
|
|
1593
|
+
let best = null;
|
|
1594
|
+
for (const project of projects) {
|
|
1595
|
+
if (path2 === project.root || path2.startsWith(`${project.root}/`)) {
|
|
1596
|
+
if (!best || project.root.length > best.root.length) best = project;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return best ? best.name : null;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// src/roles/env/repository.ts
|
|
1603
|
+
import { execFileSync } from "node:child_process";
|
|
1604
|
+
import { readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
1605
|
+
import { join as join7 } from "node:path";
|
|
1606
|
+
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
1607
|
+
function trackedPaths(root) {
|
|
1608
|
+
const output = execFileSync("git", ["ls-files", "-z"], {
|
|
1609
|
+
cwd: root,
|
|
1610
|
+
encoding: "utf8",
|
|
1611
|
+
maxBuffer: MAX_BUFFER
|
|
1612
|
+
});
|
|
1613
|
+
return output.split("\0").filter(Boolean);
|
|
1614
|
+
}
|
|
1615
|
+
function loadNxTargets(root) {
|
|
1616
|
+
const nx = resolveBin(root, "nx");
|
|
1617
|
+
if (nx === void 0) return void 0;
|
|
1618
|
+
const runNx = (args) => execFileSync(nx, args, {
|
|
1619
|
+
cwd: root,
|
|
1620
|
+
encoding: "utf8",
|
|
1621
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1622
|
+
maxBuffer: MAX_BUFFER,
|
|
1623
|
+
env: { ...process.env, NX_DAEMON: "false" }
|
|
1624
|
+
});
|
|
1625
|
+
try {
|
|
1626
|
+
const projects = new Set(JSON.parse(runNx(["show", "projects", "--json"])));
|
|
1627
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1628
|
+
return (project) => {
|
|
1629
|
+
if (!projects.has(project)) return null;
|
|
1630
|
+
const known = cache.get(project);
|
|
1631
|
+
if (known) return known;
|
|
1632
|
+
const data2 = JSON.parse(runNx(["show", "project", project, "--json"]));
|
|
1633
|
+
const targets2 = new Set(Object.keys(data2.targets ?? {}));
|
|
1634
|
+
cache.set(project, targets2);
|
|
1635
|
+
return targets2;
|
|
1636
|
+
};
|
|
1637
|
+
} catch {
|
|
1638
|
+
return void 0;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
function openRepository(root) {
|
|
1642
|
+
let tracked;
|
|
1643
|
+
try {
|
|
1644
|
+
tracked = trackedPaths(root);
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
throw new Error(
|
|
1647
|
+
`git could not list the tracked files of ${root}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1648
|
+
{ cause: error }
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
let nx = null;
|
|
1652
|
+
const hasNx = tracked.includes("nx.json");
|
|
1653
|
+
return {
|
|
1654
|
+
root,
|
|
1655
|
+
tracked,
|
|
1656
|
+
read(path2) {
|
|
1657
|
+
try {
|
|
1658
|
+
return readFileSync6(join7(root, path2), "utf8");
|
|
1659
|
+
} catch {
|
|
1660
|
+
return void 0;
|
|
1661
|
+
}
|
|
1662
|
+
},
|
|
1663
|
+
exists(path2) {
|
|
1664
|
+
try {
|
|
1665
|
+
statSync2(join7(root, path2));
|
|
1666
|
+
return true;
|
|
1667
|
+
} catch {
|
|
1668
|
+
return false;
|
|
1669
|
+
}
|
|
1670
|
+
},
|
|
1671
|
+
hasNx,
|
|
1672
|
+
nxTargets() {
|
|
1673
|
+
if (nx === null) nx = hasNx ? loadNxTargets(root) : void 0;
|
|
1674
|
+
return nx;
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function loadProjects(view) {
|
|
1679
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
1680
|
+
for (const file of view.tracked) {
|
|
1681
|
+
const manifest = /(?:^|\/)(project|package)\.json$/.exec(file);
|
|
1682
|
+
if (!manifest) continue;
|
|
1683
|
+
const slash = file.lastIndexOf("/");
|
|
1684
|
+
if (slash === -1) continue;
|
|
1685
|
+
const root = file.slice(0, slash);
|
|
1686
|
+
const fromProjectJson = manifest[1] === "project";
|
|
1687
|
+
const existing = byRoot.get(root);
|
|
1688
|
+
if (existing && !(fromProjectJson && !existing.fromProjectJson)) continue;
|
|
1689
|
+
try {
|
|
1690
|
+
const name = JSON.parse(view.read(file) ?? "").name;
|
|
1691
|
+
if (typeof name === "string" && name !== "") byRoot.set(root, { name, fromProjectJson });
|
|
1692
|
+
} catch {
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return [...byRoot].map(([root, { name }]) => ({ root, name }));
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// src/roles/env/collect.ts
|
|
1699
|
+
var RULES = {
|
|
1700
|
+
"required-var-undeclared": {
|
|
1701
|
+
number: 1,
|
|
1702
|
+
severity: "blocking",
|
|
1703
|
+
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"
|
|
1704
|
+
},
|
|
1705
|
+
"optional-var-undeclared": {
|
|
1706
|
+
number: 1,
|
|
1707
|
+
severity: "info",
|
|
1708
|
+
help: "read by an optional reader and declared in no contract; a default applies"
|
|
1709
|
+
},
|
|
1710
|
+
"committed-env-file": {
|
|
1711
|
+
number: 2,
|
|
1712
|
+
severity: "blocking",
|
|
1713
|
+
help: "a real env file is committed: remove it from git (only .example contracts belong there; values come from a secret store)"
|
|
1714
|
+
},
|
|
1715
|
+
"var-not-in-app-contract": {
|
|
1716
|
+
number: 3,
|
|
1717
|
+
severity: "advisory",
|
|
1718
|
+
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"
|
|
1719
|
+
},
|
|
1720
|
+
"dead-var": {
|
|
1721
|
+
number: 4,
|
|
1722
|
+
severity: "advisory",
|
|
1723
|
+
help: "declared but read nowhere in source: a candidate to remove, or add it to knownExternalVars if a tool or CI consumes it"
|
|
1724
|
+
},
|
|
1725
|
+
"public-secret-to-review": {
|
|
1726
|
+
number: 5,
|
|
1727
|
+
severity: "advisory",
|
|
1728
|
+
help: "a public-prefixed, secret-named variable carries a committed value: it ships to the browser by design, confirm it is genuinely public"
|
|
1729
|
+
},
|
|
1730
|
+
"secret-not-blanked": {
|
|
1731
|
+
number: 6,
|
|
1732
|
+
severity: "blocking",
|
|
1733
|
+
help: "a secret-named variable carries a committed value: blank it (leave the value empty) and move the value to a secret store"
|
|
1734
|
+
},
|
|
1735
|
+
"secret-allowlisted": {
|
|
1736
|
+
number: 6,
|
|
1737
|
+
severity: "advisory",
|
|
1738
|
+
help: "a secret-named variable carries a committed value, accepted by allowNonBlankedSecrets: confirm it stays non-secret"
|
|
1739
|
+
},
|
|
1740
|
+
"dead-copy-path": {
|
|
1741
|
+
number: 7,
|
|
1742
|
+
severity: "blocking",
|
|
1743
|
+
help: "this COPY source does not exist: remove the line or fix the path"
|
|
1744
|
+
},
|
|
1745
|
+
"nx-project-missing": {
|
|
1746
|
+
number: 8,
|
|
1747
|
+
severity: "blocking",
|
|
1748
|
+
help: "this Dockerfile runs an nx project that does not exist: fix or remove the reference"
|
|
1749
|
+
},
|
|
1750
|
+
"nx-target-missing": {
|
|
1751
|
+
number: 8,
|
|
1752
|
+
severity: "blocking",
|
|
1753
|
+
help: "this Dockerfile runs an nx target the project does not have: fix or remove the reference"
|
|
1754
|
+
},
|
|
1755
|
+
"node-version-mismatch": {
|
|
1756
|
+
number: 9,
|
|
1757
|
+
severity: "blocking",
|
|
1758
|
+
help: "the FROM node version differs from .node-version: align the tag"
|
|
1759
|
+
},
|
|
1760
|
+
"dockerfile-runs-as-root": {
|
|
1761
|
+
number: 10,
|
|
1762
|
+
severity: "blocking",
|
|
1763
|
+
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"
|
|
1764
|
+
},
|
|
1765
|
+
"copy-not-owned": {
|
|
1766
|
+
number: 10,
|
|
1767
|
+
severity: "blocking",
|
|
1768
|
+
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"
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
var RULE_ORDER = Object.keys(RULES);
|
|
1772
|
+
function inScope(path2, scope) {
|
|
1773
|
+
const underRoot = scope.roots.some((root) => {
|
|
1774
|
+
const cleaned = root.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1775
|
+
return cleaned === "." || cleaned === "" || path2.startsWith(`${cleaned}/`);
|
|
1776
|
+
});
|
|
1777
|
+
if (!underRoot) return false;
|
|
1778
|
+
const segments = path2.split("/");
|
|
1779
|
+
const name = segments.pop() ?? "";
|
|
1780
|
+
if (segments.some((segment) => scope.excludeDirs.includes(segment))) return false;
|
|
1781
|
+
return scope.ext.some((suffix) => name.endsWith(suffix)) && !scope.excludeSuffixes.some((suffix) => name.endsWith(suffix));
|
|
1782
|
+
}
|
|
1783
|
+
function* sourcesOf(view, scope) {
|
|
1784
|
+
for (const path2 of view.tracked) {
|
|
1785
|
+
if (!inScope(path2, scope)) continue;
|
|
1786
|
+
const content = view.read(path2);
|
|
1787
|
+
if (content !== void 0) yield { path: path2, content };
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
function missingRoots(view, settings) {
|
|
1791
|
+
const roots = [...settings.scan.roots, ...settings.references.roots];
|
|
1792
|
+
return [
|
|
1793
|
+
...new Set(
|
|
1794
|
+
roots.filter((root) => {
|
|
1795
|
+
const cleaned = root.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1796
|
+
if (cleaned === "." || cleaned === "") return false;
|
|
1797
|
+
return !view.tracked.some((path2) => path2.startsWith(`${cleaned}/`));
|
|
1798
|
+
})
|
|
1799
|
+
)
|
|
1800
|
+
];
|
|
1801
|
+
}
|
|
1802
|
+
function finding(rule, fields) {
|
|
1803
|
+
const { name, file, line, detail, readIn, app } = fields;
|
|
1804
|
+
return {
|
|
1805
|
+
rule,
|
|
1806
|
+
number: RULES[rule].number,
|
|
1807
|
+
severity: RULES[rule].severity,
|
|
1808
|
+
name,
|
|
1809
|
+
...file === void 0 ? {} : { file },
|
|
1810
|
+
...line === void 0 ? {} : { line },
|
|
1811
|
+
...detail === void 0 ? {} : { detail },
|
|
1812
|
+
...readIn === void 0 ? {} : { readIn },
|
|
1813
|
+
...app === void 0 ? {} : { app }
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
function byReportOrder(left, right) {
|
|
1817
|
+
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);
|
|
1818
|
+
}
|
|
1819
|
+
function readEnvFiles(view, settings) {
|
|
1820
|
+
const envGlobs = settings.envFiles.map(globToRegExp);
|
|
1821
|
+
const contractGlobs = settings.contracts.map(globToRegExp);
|
|
1822
|
+
const envFiles = view.tracked.filter((path2) => envGlobs.some((glob) => glob.test(path2)));
|
|
1823
|
+
const contracts = view.tracked.filter((path2) => contractGlobs.some((glob) => glob.test(path2)));
|
|
1824
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
1825
|
+
for (const path2 of /* @__PURE__ */ new Set([...envFiles, ...contracts])) {
|
|
1826
|
+
const content = view.read(path2);
|
|
1827
|
+
if (content !== void 0) parsed.set(path2, parseEnvFile(content));
|
|
1828
|
+
}
|
|
1829
|
+
return { envFiles, contracts, parsed };
|
|
1830
|
+
}
|
|
1831
|
+
function secretFindings(parsed, settings) {
|
|
1832
|
+
const options = {
|
|
1833
|
+
secretPatterns: settings.secretNamePatterns,
|
|
1834
|
+
publicPrefixes: settings.publicPrefixes
|
|
1835
|
+
};
|
|
1836
|
+
const findings = [];
|
|
1837
|
+
const mustBlank = /* @__PURE__ */ new Map();
|
|
1838
|
+
for (const [file, content] of parsed) {
|
|
1839
|
+
const audit = auditEnvFileSecrets(content, options);
|
|
1840
|
+
for (const entry of audit.publicSecrets) {
|
|
1841
|
+
findings.push(finding("public-secret-to-review", { name: entry.key, file, line: entry.line }));
|
|
1842
|
+
}
|
|
1843
|
+
for (const entry of audit.mustBlank) {
|
|
1844
|
+
mustBlank.set(`${file}:${entry.key}`, { file, line: entry.line, name: entry.key });
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
const { allowed, flagged } = splitAllowlisted(
|
|
1848
|
+
[...mustBlank.keys()],
|
|
1849
|
+
settings.allowNonBlankedSecrets
|
|
1850
|
+
);
|
|
1851
|
+
for (const location of flagged) {
|
|
1852
|
+
const entry = mustBlank.get(location);
|
|
1853
|
+
if (entry) findings.push(finding("secret-not-blanked", entry));
|
|
1854
|
+
}
|
|
1855
|
+
for (const location of allowed) {
|
|
1856
|
+
const entry = mustBlank.get(location);
|
|
1857
|
+
if (entry) findings.push(finding("secret-allowlisted", entry));
|
|
1858
|
+
}
|
|
1859
|
+
return findings;
|
|
1860
|
+
}
|
|
1861
|
+
function allReadNames(derived, referenced) {
|
|
1862
|
+
return /* @__PURE__ */ new Set([...referenced.keys(), ...derived.required.keys(), ...derived.optional.keys()]);
|
|
1863
|
+
}
|
|
1864
|
+
function misplacedFindings(view, settings, derived, referenced, contracts, declared) {
|
|
1865
|
+
const projects = loadProjects(view);
|
|
1866
|
+
const readsByApp = /* @__PURE__ */ new Map();
|
|
1867
|
+
const addRead = (name, path2) => {
|
|
1868
|
+
const app = resolveApp(path2, projects);
|
|
1869
|
+
if (!app) return;
|
|
1870
|
+
const reads = readsByApp.get(app) ?? /* @__PURE__ */ new Map();
|
|
1871
|
+
if (!reads.has(name)) reads.set(name, path2);
|
|
1872
|
+
readsByApp.set(app, reads);
|
|
1873
|
+
};
|
|
1874
|
+
for (const [name, sites] of derived.required) for (const site of sites) addRead(name, site.path);
|
|
1875
|
+
for (const [name, paths] of referenced) for (const path2 of paths) addRead(name, path2);
|
|
1876
|
+
const shared = /* @__PURE__ */ new Set();
|
|
1877
|
+
const keysByApp = /* @__PURE__ */ new Map();
|
|
1878
|
+
for (const { path: path2, parsed } of contracts) {
|
|
1879
|
+
const app = resolveApp(path2, projects);
|
|
1880
|
+
const keys = app === null ? shared : keysByApp.get(app) ?? /* @__PURE__ */ new Set();
|
|
1881
|
+
for (const key of parsed.keys) keys.add(key);
|
|
1882
|
+
if (app !== null) keysByApp.set(app, keys);
|
|
1883
|
+
}
|
|
1884
|
+
const external = new Set(settings.externallyProvided);
|
|
1885
|
+
const findings = [];
|
|
1886
|
+
for (const [app, reads] of readsByApp) {
|
|
1887
|
+
const own = keysByApp.get(app);
|
|
1888
|
+
if (!own) continue;
|
|
1889
|
+
for (const [name, path2] of reads) {
|
|
1890
|
+
if (declared.has(name) && !own.has(name) && !shared.has(name) && !external.has(name)) {
|
|
1891
|
+
findings.push(
|
|
1892
|
+
finding("var-not-in-app-contract", {
|
|
1893
|
+
name,
|
|
1894
|
+
file: path2,
|
|
1895
|
+
app,
|
|
1896
|
+
detail: `read in ${app}`
|
|
1897
|
+
})
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
return findings;
|
|
1903
|
+
}
|
|
1904
|
+
function codeFindings(view, settings, contracts) {
|
|
1905
|
+
const derived = deriveVars(sourcesOf(view, settings.scan), settings.readers);
|
|
1906
|
+
const inventory = {
|
|
1907
|
+
required: [...derived.required].map(([name, sites]) => ({ name, files: sites.length })).sort((left, right) => left.name.localeCompare(right.name)),
|
|
1908
|
+
optional: [...derived.optional.keys()].sort(),
|
|
1909
|
+
contracts: contracts.map(({ path: path2, parsed }) => ({ path: path2, keys: parsed.keys.length }))
|
|
1910
|
+
};
|
|
1911
|
+
const counts = { required: derived.required.size, optional: derived.optional.size };
|
|
1912
|
+
if (contracts.length === 0) return { findings: [], inventory, counts, compared: false };
|
|
1913
|
+
const declaredAt = /* @__PURE__ */ new Map();
|
|
1914
|
+
for (const { path: path2, parsed } of contracts) {
|
|
1915
|
+
for (const entry of parsed.entries) {
|
|
1916
|
+
if (!declaredAt.has(entry.key)) declaredAt.set(entry.key, { file: path2, line: entry.line });
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
const declared = new Set(declaredAt.keys());
|
|
1920
|
+
const { unaccounted, optionalUndeclared } = diffContract(
|
|
1921
|
+
derived,
|
|
1922
|
+
declared,
|
|
1923
|
+
settings.externallyProvided
|
|
1924
|
+
);
|
|
1925
|
+
const referenced = findReferencedVars(
|
|
1926
|
+
sourcesOf(view, settings.references),
|
|
1927
|
+
settings.referencePatterns
|
|
1928
|
+
);
|
|
1929
|
+
const dead = findDeadVars(
|
|
1930
|
+
declared,
|
|
1931
|
+
allReadNames(derived, referenced),
|
|
1932
|
+
settings.knownExternalVars,
|
|
1933
|
+
settings.knownExternalPatterns
|
|
1934
|
+
);
|
|
1935
|
+
const findings = [
|
|
1936
|
+
...unaccounted.map((name) => {
|
|
1937
|
+
const sites = derived.required.get(name) ?? [];
|
|
1938
|
+
return finding("required-var-undeclared", {
|
|
1939
|
+
name,
|
|
1940
|
+
file: sites[0]?.path,
|
|
1941
|
+
line: sites[0]?.line,
|
|
1942
|
+
readIn: sites.map((site) => site.path)
|
|
1943
|
+
});
|
|
1944
|
+
}),
|
|
1945
|
+
...misplacedFindings(view, settings, derived, referenced, contracts, declared),
|
|
1946
|
+
...dead.map((name) => finding("dead-var", { name, ...declaredAt.get(name) })),
|
|
1947
|
+
...optionalUndeclared.map((name) => {
|
|
1948
|
+
const site = derived.optional.get(name)?.[0];
|
|
1949
|
+
return finding("optional-var-undeclared", { name, file: site?.path, line: site?.line });
|
|
1950
|
+
})
|
|
1951
|
+
];
|
|
1952
|
+
return { findings, inventory, counts, compared: true };
|
|
1953
|
+
}
|
|
1954
|
+
function dockerFindings(view, settings) {
|
|
1955
|
+
const excluded = (path2) => settings.docker.excludePaths.some((prefix) => path2.startsWith(prefix));
|
|
1956
|
+
const dockerfiles = view.tracked.filter((path2) => isDockerfilePath(path2) && !excluded(path2));
|
|
1957
|
+
const rawNodeVersion = view.read(settings.nodeVersionFile);
|
|
1958
|
+
const expected = rawNodeVersion === void 0 ? void 0 : normalizeNodeVersion(rawNodeVersion);
|
|
1959
|
+
const findings = [];
|
|
1960
|
+
const notes = [];
|
|
1961
|
+
let uncheckedNxRefs = 0;
|
|
1962
|
+
let nxUnavailable = false;
|
|
1963
|
+
for (const file of dockerfiles) {
|
|
1964
|
+
const content = view.read(file);
|
|
1965
|
+
if (content === void 0) continue;
|
|
1966
|
+
const { froms, contextCopySources } = parseDockerfile(content);
|
|
1967
|
+
for (const { image, line } of froms) {
|
|
1968
|
+
const version = dockerNodeVersion(image);
|
|
1969
|
+
if (version && expected && !nodeVersionAligned(version, expected)) {
|
|
1970
|
+
findings.push(
|
|
1971
|
+
finding("node-version-mismatch", {
|
|
1972
|
+
name: `node:${version}`,
|
|
1973
|
+
file,
|
|
1974
|
+
line,
|
|
1975
|
+
detail: `expected ${expected} (${settings.nodeVersionFile})`
|
|
1976
|
+
})
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
for (const { source, line } of contextCopySources) {
|
|
1981
|
+
if (!view.exists(join8(".", source)) && !view.exists(join8(dirname(file), source))) {
|
|
1982
|
+
findings.push(finding("dead-copy-path", { name: source, file, line }));
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
const { runsAsRoot, user, userLine, copiesWithoutChown } = dockerHardening(content);
|
|
1986
|
+
if (runsAsRoot && !settings.docker.allowRoot.includes(file)) {
|
|
1987
|
+
findings.push(
|
|
1988
|
+
finding("dockerfile-runs-as-root", {
|
|
1989
|
+
name: user ? `USER ${user}` : "no USER set",
|
|
1990
|
+
file,
|
|
1991
|
+
line: userLine
|
|
1992
|
+
})
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
for (const { instruction, line } of copiesWithoutChown) {
|
|
1996
|
+
findings.push(finding("copy-not-owned", { name: instruction, file, line }));
|
|
1997
|
+
}
|
|
1998
|
+
const refs = parseNxTargetRefs(content);
|
|
1999
|
+
if (refs.length === 0) continue;
|
|
2000
|
+
const lookup = view.hasNx ? view.nxTargets() : void 0;
|
|
2001
|
+
if (lookup === void 0) {
|
|
2002
|
+
uncheckedNxRefs += refs.length;
|
|
2003
|
+
nxUnavailable ||= view.hasNx;
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
for (const { project, targetSpec, line } of refs) {
|
|
2007
|
+
const targets2 = lookup(project);
|
|
2008
|
+
if (targets2 === null) {
|
|
2009
|
+
findings.push(finding("nx-project-missing", { name: project, file, line }));
|
|
2010
|
+
} else if (!nxTargetCandidates(targetSpec).some((target) => targets2.has(target))) {
|
|
2011
|
+
findings.push(
|
|
2012
|
+
finding("nx-target-missing", { name: `${project}:${targetSpec}`, file, line })
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
if (dockerfiles.length === 0)
|
|
2018
|
+
notes.push("no Dockerfile is tracked: rules 7 to 10 had nothing to check");
|
|
2019
|
+
if (dockerfiles.length > 0 && expected === void 0) {
|
|
2020
|
+
notes.push(
|
|
2021
|
+
`no ${settings.nodeVersionFile} at the repository root: rule 9 had no version to compare the FROM tags with`
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
if (!view.hasNx) {
|
|
2025
|
+
notes.push(
|
|
2026
|
+
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"
|
|
2027
|
+
);
|
|
2028
|
+
} else if (nxUnavailable) {
|
|
2029
|
+
notes.push(
|
|
2030
|
+
`the repository's own nx could not run (not installed?): rule 8 skipped, ${uncheckedNxRefs} \`nx run\` reference(s) not checked`
|
|
2031
|
+
);
|
|
2032
|
+
}
|
|
2033
|
+
return { findings, notes, dockerfiles: dockerfiles.length, nodeVersion: expected ?? null };
|
|
2034
|
+
}
|
|
2035
|
+
function collectEnvReport(view, settings, level, config) {
|
|
2036
|
+
const missing = missingRoots(view, settings);
|
|
2037
|
+
if (missing.length > 0) {
|
|
2038
|
+
throw new Error(
|
|
2039
|
+
`${config ?? "the env settings"}: scan root(s) holding no tracked file: ${missing.join(", ")}`
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
const { envFiles, contracts, parsed } = readEnvFiles(view, settings);
|
|
2043
|
+
const contractFiles = contracts.flatMap((path2) => {
|
|
2044
|
+
const content = parsed.get(path2);
|
|
2045
|
+
return content ? [{ path: path2, parsed: content }] : [];
|
|
2046
|
+
});
|
|
2047
|
+
const code = codeFindings(view, settings, contractFiles);
|
|
2048
|
+
const docker = dockerFindings(view, settings);
|
|
2049
|
+
const orphans = findOrphanEnvFiles(envFiles, settings.allowedCommittedEnvFiles);
|
|
2050
|
+
const notes = [];
|
|
2051
|
+
if (!code.compared) {
|
|
2052
|
+
notes.push(
|
|
2053
|
+
`no contract is committed (${settings.contracts.join(", ")}): rules 1, 3 and 4 had nothing to compare the code with`
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
if (code.counts.required === 0) {
|
|
2057
|
+
notes.push(
|
|
2058
|
+
`no required reader call found (${settings.readers.required.join(", ")}): rule 1 had nothing to check; name the repository's own helper in readers.required`
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
notes.push(...docker.notes);
|
|
2062
|
+
return {
|
|
2063
|
+
level,
|
|
2064
|
+
config,
|
|
2065
|
+
scanned: settings.scan.roots,
|
|
2066
|
+
counts: {
|
|
2067
|
+
required: code.counts.required,
|
|
2068
|
+
optional: code.counts.optional,
|
|
2069
|
+
contracts: contractFiles.length,
|
|
2070
|
+
envFiles: envFiles.length,
|
|
2071
|
+
dockerfiles: docker.dockerfiles
|
|
2072
|
+
},
|
|
2073
|
+
nodeVersion: docker.nodeVersion,
|
|
2074
|
+
findings: [
|
|
2075
|
+
...code.findings,
|
|
2076
|
+
...orphans.map((file) => finding("committed-env-file", { name: file, file })),
|
|
2077
|
+
...secretFindings(parsed, settings),
|
|
2078
|
+
...docker.findings
|
|
2079
|
+
].sort(byReportOrder),
|
|
2080
|
+
notes,
|
|
2081
|
+
inventory: code.inventory
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
function blockingFindings(report) {
|
|
2085
|
+
return report.findings.filter((entry) => entry.severity === "blocking");
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// src/roles/env/configs/resolve.ts
|
|
2089
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
2090
|
+
import { join as join9 } from "node:path";
|
|
2091
|
+
import { pathToFileURL } from "node:url";
|
|
2092
|
+
function explainUnreadableEnvConfig(error) {
|
|
2093
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2094
|
+
const code = error.code;
|
|
2095
|
+
const missingPackage = (code === "ERR_PACKAGE_PATH_NOT_EXPORTED" || code === "ERR_MODULE_NOT_FOUND") && message.includes("@hublo/sentinel");
|
|
2096
|
+
if (!missingPackage) return message;
|
|
2097
|
+
return `${message}
|
|
2098
|
+
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.`;
|
|
2099
|
+
}
|
|
2100
|
+
function unique(values) {
|
|
2101
|
+
return [...new Set(values)];
|
|
2102
|
+
}
|
|
2103
|
+
function scopeOf(base, input) {
|
|
2104
|
+
return {
|
|
2105
|
+
roots: input?.roots ?? base.roots,
|
|
2106
|
+
ext: input?.ext ?? base.ext,
|
|
2107
|
+
excludeSuffixes: input?.excludeSuffixes ?? base.excludeSuffixes,
|
|
2108
|
+
excludeDirs: input?.excludeDirs ?? base.excludeDirs
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
function asPattern(entry) {
|
|
2112
|
+
return entry instanceof RegExp ? entry : new RegExp(entry);
|
|
2113
|
+
}
|
|
2114
|
+
function resolveEnvSettings(input) {
|
|
2115
|
+
return {
|
|
2116
|
+
readers: {
|
|
2117
|
+
required: unique([...envDefaults.readers.required, ...input.readers?.required ?? []]),
|
|
2118
|
+
optional: unique([...envDefaults.readers.optional, ...input.readers?.optional ?? []])
|
|
2119
|
+
},
|
|
2120
|
+
scan: scopeOf(envDefaults.scan, input.scan),
|
|
2121
|
+
references: scopeOf(envDefaults.references, input.references),
|
|
2122
|
+
referencePatterns: referencePatterns(),
|
|
2123
|
+
secretNamePatterns: secretNamePatterns(),
|
|
2124
|
+
publicPrefixes: [...envDefaults.publicPrefixes],
|
|
2125
|
+
envFiles: unique([...envDefaults.envFiles, ...input.envFiles ?? []]),
|
|
2126
|
+
contracts: unique([...envDefaults.contracts, ...input.contracts ?? []]),
|
|
2127
|
+
nodeVersionFile: envDefaults.nodeVersionFile,
|
|
2128
|
+
externallyProvided: input.externallyProvided ?? [],
|
|
2129
|
+
knownExternalVars: input.knownExternalVars ?? [],
|
|
2130
|
+
knownExternalPatterns: (input.knownExternalPatterns ?? []).map(asPattern),
|
|
2131
|
+
allowNonBlankedSecrets: input.allowNonBlankedSecrets ?? [],
|
|
2132
|
+
allowedCommittedEnvFiles: input.allowedCommittedEnvFiles ?? [],
|
|
2133
|
+
docker: {
|
|
2134
|
+
allowRoot: input.docker?.allowRoot ?? [],
|
|
2135
|
+
excludePaths: input.docker?.excludePaths ?? []
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
async function loadEnvConfig(root) {
|
|
2140
|
+
const path2 = join9(root, ENV_CONFIG_FILE);
|
|
2141
|
+
if (!existsSync6(path2)) {
|
|
2142
|
+
return { settings: resolveEnvSettings({}), from: null, unreadable: null };
|
|
2143
|
+
}
|
|
2144
|
+
try {
|
|
2145
|
+
const loaded = await import(pathToFileURL(path2).href);
|
|
2146
|
+
const declared = defineEnv(loaded.default ?? {});
|
|
2147
|
+
return { settings: resolveEnvSettings(declared), from: ENV_CONFIG_FILE, unreadable: null };
|
|
2148
|
+
} catch (error) {
|
|
2149
|
+
return {
|
|
2150
|
+
settings: resolveEnvSettings({}),
|
|
2151
|
+
from: null,
|
|
2152
|
+
unreadable: `${ENV_CONFIG_FILE}: ${explainUnreadableEnvConfig(error)}`
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// src/roles/env/report.ts
|
|
2158
|
+
var LEVEL_MEANING = {
|
|
2159
|
+
info: "report only",
|
|
2160
|
+
warning: "report, and annotate in GitHub Actions; never fails",
|
|
2161
|
+
error: "fails on a blocking finding (rules 1, 2, 6, 7 to 10)"
|
|
2162
|
+
};
|
|
2163
|
+
function locationOf(entry) {
|
|
2164
|
+
if (entry.file === void 0) return "";
|
|
2165
|
+
return entry.line === void 0 ? entry.file : `${entry.file}:${entry.line}`;
|
|
2166
|
+
}
|
|
2167
|
+
function groupByRule(findings) {
|
|
2168
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2169
|
+
for (const entry of findings) {
|
|
2170
|
+
const group = groups.get(entry.rule) ?? [];
|
|
2171
|
+
group.push(entry);
|
|
2172
|
+
groups.set(entry.rule, group);
|
|
2173
|
+
}
|
|
2174
|
+
return groups;
|
|
2175
|
+
}
|
|
2176
|
+
function describe(entry) {
|
|
2177
|
+
const where = locationOf(entry);
|
|
2178
|
+
const subject = entry.rule === "committed-env-file" ? "" : entry.name;
|
|
2179
|
+
const more = entry.readIn !== void 0 && entry.readIn.length > 1 ? ` (read in ${entry.readIn.length} files)` : entry.detail ? ` (${entry.detail})` : "";
|
|
2180
|
+
return [where, subject].filter(Boolean).join(" ") + more;
|
|
2181
|
+
}
|
|
2182
|
+
function verdictOf(report) {
|
|
2183
|
+
const blocking = blockingFindings(report).length;
|
|
2184
|
+
if (blocking === 0) return "no blocking finding";
|
|
2185
|
+
const counted = `${blocking} blocking finding${blocking === 1 ? "" : "s"}`;
|
|
2186
|
+
return report.level === "error" ? `${counted}: failing` : `${counted}: reported, not failing`;
|
|
2187
|
+
}
|
|
2188
|
+
function headline(report) {
|
|
2189
|
+
const { counts } = report;
|
|
2190
|
+
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}` : "");
|
|
2191
|
+
}
|
|
2192
|
+
function renderConsole(report, palette2) {
|
|
2193
|
+
const lines = [];
|
|
2194
|
+
lines.push(
|
|
2195
|
+
`${palette2.strong("env")} ${palette2.dim(`(level ${report.level}: ${LEVEL_MEANING[report.level]})`)}`
|
|
2196
|
+
);
|
|
2197
|
+
lines.push(` ${headline(report)}`);
|
|
2198
|
+
lines.push(
|
|
2199
|
+
` ${palette2.dim(`settings: ${report.config ?? "sentinel defaults (no env.config.mjs)"}`)}`
|
|
2200
|
+
);
|
|
2201
|
+
const listed = report.findings.filter((entry) => entry.severity !== "info");
|
|
2202
|
+
const informational = report.findings.length - listed.length;
|
|
2203
|
+
for (const [rule, entries] of groupByRule(listed)) {
|
|
2204
|
+
const meta = RULES[rule];
|
|
2205
|
+
const mark = meta.severity === "blocking" ? palette2.fail("\u2717") : palette2.warn("!");
|
|
2206
|
+
lines.push("");
|
|
2207
|
+
lines.push(
|
|
2208
|
+
` ${mark} ${palette2.strong(`[${rule}]`)} ${palette2.dim(`rule ${meta.number}, ${meta.severity}, ${entries.length}`)}`
|
|
2209
|
+
);
|
|
2210
|
+
lines.push(` ${palette2.dim(meta.help)}`);
|
|
2211
|
+
for (const entry of entries) lines.push(` ${describe(entry)}`);
|
|
2212
|
+
}
|
|
2213
|
+
if (informational > 0) {
|
|
2214
|
+
lines.push("");
|
|
2215
|
+
lines.push(
|
|
2216
|
+
` ${palette2.dim(`info: ${informational} optional variable(s) in no contract (a default applies); listed in --json`)}`
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
for (const note of report.notes) lines.push(` ${palette2.dim(`note: ${note}`)}`);
|
|
2220
|
+
lines.push("");
|
|
2221
|
+
const blocking = blockingFindings(report).length;
|
|
2222
|
+
const verdict = verdictOf(report);
|
|
2223
|
+
lines.push(
|
|
2224
|
+
` ${blocking === 0 ? palette2.ok(verdict) : report.level === "error" ? palette2.fail(verdict) : palette2.warn(verdict)}`
|
|
2225
|
+
);
|
|
2226
|
+
return `${lines.join("\n")}
|
|
2227
|
+
`;
|
|
2228
|
+
}
|
|
2229
|
+
function cell(text) {
|
|
2230
|
+
return text.replace(/\|/g, "\\|");
|
|
2231
|
+
}
|
|
2232
|
+
function renderMarkdown(report) {
|
|
2233
|
+
const lines = ["<!-- sentinel-env-report -->", "## Env conformance", ""];
|
|
2234
|
+
lines.push(`**Level:** \`${report.level}\` (${LEVEL_MEANING[report.level]}) `);
|
|
2235
|
+
lines.push(`**Scanned:** ${headline(report)} `);
|
|
2236
|
+
lines.push(`**Settings:** ${report.config ? `\`${report.config}\`` : "sentinel defaults"}`);
|
|
2237
|
+
lines.push("");
|
|
2238
|
+
const listed = report.findings.filter((entry) => entry.severity !== "info");
|
|
2239
|
+
const groups = groupByRule(listed);
|
|
2240
|
+
if (groups.size === 0) {
|
|
2241
|
+
lines.push("No finding.");
|
|
2242
|
+
} else {
|
|
2243
|
+
lines.push("| rule | finding | severity | count |", "| --- | --- | --- | --- |");
|
|
2244
|
+
for (const [rule, entries] of groups) {
|
|
2245
|
+
const meta = RULES[rule];
|
|
2246
|
+
lines.push(`| ${meta.number} | \`${rule}\` | ${meta.severity} | ${entries.length} |`);
|
|
2247
|
+
}
|
|
2248
|
+
for (const [rule, entries] of groups) {
|
|
2249
|
+
lines.push("", `<details><summary><code>${rule}</code>: ${entries.length}</summary>`, "");
|
|
2250
|
+
lines.push(cell(RULES[rule].help), "");
|
|
2251
|
+
for (const entry of entries) lines.push(`- \`${cell(describe(entry))}\``);
|
|
2252
|
+
lines.push("", "</details>");
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
if (report.notes.length > 0) {
|
|
2256
|
+
lines.push("");
|
|
2257
|
+
for (const note of report.notes) lines.push(`> ${note}`);
|
|
2258
|
+
}
|
|
2259
|
+
lines.push("", `**Verdict:** ${verdictOf(report)}`);
|
|
2260
|
+
return `${lines.join("\n")}
|
|
2261
|
+
`;
|
|
2262
|
+
}
|
|
2263
|
+
function reportDocument(report) {
|
|
2264
|
+
return {
|
|
2265
|
+
level: report.level,
|
|
2266
|
+
verdict: verdictOf(report),
|
|
2267
|
+
blocking: blockingFindings(report).length,
|
|
2268
|
+
config: report.config,
|
|
2269
|
+
scanned: report.scanned,
|
|
2270
|
+
counts: report.counts,
|
|
2271
|
+
nodeVersion: report.nodeVersion,
|
|
2272
|
+
notes: report.notes,
|
|
2273
|
+
findings: report.findings,
|
|
2274
|
+
inventory: report.inventory
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
function renderJson(report) {
|
|
2278
|
+
return `${JSON.stringify(reportDocument(report), null, 2)}
|
|
2279
|
+
`;
|
|
2280
|
+
}
|
|
2281
|
+
function escapeData(text) {
|
|
2282
|
+
return text.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
2283
|
+
}
|
|
2284
|
+
function escapeProperty(text) {
|
|
2285
|
+
return escapeData(text).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
2286
|
+
}
|
|
2287
|
+
var GITHUB_ANNOTATION_CAP = 10;
|
|
2288
|
+
function summaryAnnotation(command, count, level) {
|
|
2289
|
+
const what = command === "error" ? "blocking findings" : level === "error" ? "advisory findings" : "findings";
|
|
2290
|
+
const message = `${count} ${what} (GitHub shows at most ${GITHUB_ANNOTATION_CAP} ${command} annotations per step): the full list is in the env report, in this step's log, and in env-report/env.md with --output markdown`;
|
|
2291
|
+
return `::${command} title=${escapeProperty(`env: ${count} ${what}`)}::${escapeData(message)}`;
|
|
2292
|
+
}
|
|
2293
|
+
function renderAnnotations(report) {
|
|
2294
|
+
if (report.level === "info") return [];
|
|
2295
|
+
const annotations = report.findings.filter((entry) => entry.severity !== "info").map((entry) => {
|
|
2296
|
+
const command = report.level === "error" && entry.severity === "blocking" ? "error" : "warning";
|
|
2297
|
+
const properties = [
|
|
2298
|
+
entry.file === void 0 ? void 0 : `file=${escapeProperty(entry.file)}`,
|
|
2299
|
+
entry.line === void 0 ? void 0 : `line=${entry.line}`,
|
|
2300
|
+
`title=${escapeProperty(`env rule ${entry.number}: ${entry.rule}`)}`
|
|
2301
|
+
].filter(Boolean);
|
|
2302
|
+
const message = `${entry.name}${entry.detail ? ` (${entry.detail})` : ""}: ${RULES[entry.rule].help}`;
|
|
2303
|
+
return { command, line: `::${command} ${properties.join(",")}::${escapeData(message)}` };
|
|
2304
|
+
});
|
|
2305
|
+
const summaries = ["error", "warning"].flatMap((command) => {
|
|
2306
|
+
const count = annotations.filter((entry) => entry.command === command).length;
|
|
2307
|
+
return count > GITHUB_ANNOTATION_CAP ? [summaryAnnotation(command, count, report.level)] : [];
|
|
2308
|
+
});
|
|
2309
|
+
return [...summaries, ...annotations.map((entry) => entry.line)];
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
// src/roles/env/adapters/conformance/conformance.adapter.ts
|
|
2313
|
+
function shapesOf(output) {
|
|
2314
|
+
const asked = [
|
|
2315
|
+
...new Set(
|
|
2316
|
+
(output ?? "console").split(",").map((one) => one.trim()).filter((one) => one !== "")
|
|
2317
|
+
)
|
|
2318
|
+
];
|
|
2319
|
+
return {
|
|
2320
|
+
known: asked.filter((one) => ENV_SHAPES.includes(one)),
|
|
2321
|
+
unknown: asked.filter((one) => !ENV_SHAPES.includes(one))
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
function writeShapes(root, report, shapes) {
|
|
2325
|
+
const files = shapes.filter((shape) => shape !== "console");
|
|
2326
|
+
if (files.length === 0) return [];
|
|
2327
|
+
const directory = join10(root, REPORT_DIR2);
|
|
2328
|
+
mkdirSync(directory, { recursive: true });
|
|
2329
|
+
for (const [shape, name] of Object.entries(SHAPE_FILE)) {
|
|
2330
|
+
const path2 = join10(directory, name);
|
|
2331
|
+
if (!files.includes(shape) && existsSync7(path2)) unlinkSync(path2);
|
|
2332
|
+
}
|
|
2333
|
+
return files.map((shape) => {
|
|
2334
|
+
const name = SHAPE_FILE[shape];
|
|
2335
|
+
writeFileSync2(
|
|
2336
|
+
join10(directory, name),
|
|
2337
|
+
shape === "json" ? renderJson(report) : renderMarkdown(report)
|
|
2338
|
+
);
|
|
2339
|
+
return `${REPORT_DIR2}/${name}`;
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
function oneLine(message) {
|
|
2343
|
+
const [head = "", ...rest] = message.split("\n").map((line) => line.trim().replace(/^- /, "")).filter((line) => line !== "");
|
|
2344
|
+
return rest.length === 0 ? head : `${head} ${rest.join("; ")}`;
|
|
2345
|
+
}
|
|
2346
|
+
var ConformanceAdapter = class extends BaseAdapter {
|
|
2347
|
+
target = TARGET2;
|
|
2348
|
+
runner = "conformance";
|
|
2349
|
+
/** A repository has no stack, so every preset applies: the role never reads one. */
|
|
2350
|
+
appliesTo(_preset) {
|
|
2351
|
+
return true;
|
|
2352
|
+
}
|
|
2353
|
+
/** Nothing to adopt: the role reads the repository and writes no config of its own. */
|
|
2354
|
+
plan() {
|
|
2355
|
+
return {
|
|
2356
|
+
operations: [],
|
|
2357
|
+
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`."
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
/** `--run`: check, print the report, write the asked files, annotate. */
|
|
2361
|
+
async run(ctx) {
|
|
2362
|
+
return this.check(ctx, true);
|
|
2363
|
+
}
|
|
2364
|
+
/** `--run --json`: the same check, for a machine. Prints nothing on stdout. */
|
|
2365
|
+
async report(ctx) {
|
|
2366
|
+
return this.check(ctx, false);
|
|
2367
|
+
}
|
|
2368
|
+
say(message) {
|
|
2369
|
+
process.stderr.write(`sentinel env: ${message}
|
|
2370
|
+
`);
|
|
2371
|
+
}
|
|
2372
|
+
async check(ctx, printing) {
|
|
2373
|
+
const level = ctx.envLevel ?? DEFAULT_ENV_LEVEL;
|
|
2374
|
+
const { known, unknown } = shapesOf(ctx.output);
|
|
2375
|
+
if (unknown.length > 0) {
|
|
2376
|
+
this.say(
|
|
2377
|
+
`writes ${ENV_SHAPES.join(", ")}; not ${unknown.join(", ")}, which another role may write.`
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
const refuse = (message) => {
|
|
2381
|
+
this.say(message);
|
|
2382
|
+
return { ok: false, code: 1, metrics: { level, error: oneLine(message) } };
|
|
2383
|
+
};
|
|
2384
|
+
const { settings, from, unreadable } = await loadEnvConfig(ctx.cwd);
|
|
2385
|
+
if (unreadable) return refuse(unreadable);
|
|
2386
|
+
let report;
|
|
2387
|
+
try {
|
|
2388
|
+
report = collectEnvReport(openRepository(ctx.cwd), settings, level, from);
|
|
2389
|
+
} catch (error) {
|
|
2390
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
2391
|
+
}
|
|
2392
|
+
if (printing) {
|
|
2393
|
+
const written = writeShapes(ctx.cwd, report, known);
|
|
2394
|
+
process.stdout.write(renderConsole(report, palette(process.stdout)));
|
|
2395
|
+
if (written.length > 0) process.stdout.write(`
|
|
2396
|
+
written: ${written.join(", ")}
|
|
2397
|
+
`);
|
|
2398
|
+
}
|
|
2399
|
+
if (ctx.githubActions) {
|
|
2400
|
+
const annotations = renderAnnotations(report);
|
|
2401
|
+
if (annotations.length > 0) {
|
|
2402
|
+
const stream = printing ? process.stdout : process.stderr;
|
|
2403
|
+
stream.write(`${annotations.join("\n")}
|
|
2404
|
+
`);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
const failing = level === "error" && blockingFindings(report).length > 0;
|
|
2408
|
+
return { ok: !failing, code: failing ? 1 : 0, metrics: reportDocument(report) };
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
|
|
2412
|
+
// src/roles/env/register.ts
|
|
2413
|
+
function registerEnv() {
|
|
2414
|
+
register(new ConformanceAdapter());
|
|
2415
|
+
setDefaultRunner("env", "conformance");
|
|
2416
|
+
}
|
|
2417
|
+
|
|
1292
2418
|
// src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
|
|
1293
2419
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1294
|
-
import { existsSync as
|
|
1295
|
-
import { join as
|
|
2420
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
|
|
2421
|
+
import { join as join15 } from "node:path";
|
|
1296
2422
|
import { isDeepStrictEqual } from "node:util";
|
|
1297
2423
|
|
|
1298
2424
|
// src/core/config/has-source.ts
|
|
1299
2425
|
import { readdirSync } from "node:fs";
|
|
1300
|
-
import { extname, join as
|
|
2426
|
+
import { extname, join as join11, relative as relative2 } from "node:path";
|
|
1301
2427
|
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
1302
2428
|
"node_modules",
|
|
1303
2429
|
"dist",
|
|
@@ -1348,7 +2474,7 @@ function hasSourceFiles(cwd, extensions) {
|
|
|
1348
2474
|
}
|
|
1349
2475
|
for (const entry of entries) {
|
|
1350
2476
|
if (entry.isDirectory()) {
|
|
1351
|
-
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(
|
|
2477
|
+
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join11(dir, entry.name));
|
|
1352
2478
|
continue;
|
|
1353
2479
|
}
|
|
1354
2480
|
if (wanted.has(extname(entry.name))) return true;
|
|
@@ -1369,7 +2495,7 @@ function listSourceFiles(cwd, extensions) {
|
|
|
1369
2495
|
continue;
|
|
1370
2496
|
}
|
|
1371
2497
|
for (const entry of entries) {
|
|
1372
|
-
const full =
|
|
2498
|
+
const full = join11(dir, entry.name);
|
|
1373
2499
|
if (entry.isDirectory()) {
|
|
1374
2500
|
if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
|
|
1375
2501
|
continue;
|
|
@@ -1388,8 +2514,8 @@ function scopeFlags(paths, options = []) {
|
|
|
1388
2514
|
}
|
|
1389
2515
|
|
|
1390
2516
|
// src/roles/format/deliberate-import-order.ts
|
|
1391
|
-
import { readFileSync as
|
|
1392
|
-
import { join as
|
|
2517
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2518
|
+
import { join as join12 } from "node:path";
|
|
1393
2519
|
import { parseSync } from "oxc-parser";
|
|
1394
2520
|
import { format } from "oxfmt";
|
|
1395
2521
|
|
|
@@ -1526,8 +2652,8 @@ async function pinDeliberateImportOrder(cwd, options) {
|
|
|
1526
2652
|
if (!options.sortImports) return [];
|
|
1527
2653
|
const pinned = [];
|
|
1528
2654
|
for (const file of listSourceFiles(cwd, CODE_EXTENSIONS).sort()) {
|
|
1529
|
-
const path2 =
|
|
1530
|
-
const text =
|
|
2655
|
+
const path2 = join12(cwd, file);
|
|
2656
|
+
const text = readFileSync7(path2, "utf8");
|
|
1531
2657
|
if (!text.includes("eslint-disable") && !MODULE_NAMES.some((name) => text.includes(name)))
|
|
1532
2658
|
continue;
|
|
1533
2659
|
const before = readImports(file, text);
|
|
@@ -1535,7 +2661,7 @@ async function pinDeliberateImportOrder(cwd, options) {
|
|
|
1535
2661
|
const sorted = await format(file, text, options);
|
|
1536
2662
|
if (sorted.errors.length > 0) continue;
|
|
1537
2663
|
if (!wouldMove(before, readImports(file, sorted.code))) continue;
|
|
1538
|
-
|
|
2664
|
+
writeFileSync3(path2, pin(text, before));
|
|
1539
2665
|
pinned.push(file);
|
|
1540
2666
|
}
|
|
1541
2667
|
return pinned;
|
|
@@ -1569,8 +2695,8 @@ function writesWhenRewritten(name, command) {
|
|
|
1569
2695
|
var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
|
|
1570
2696
|
|
|
1571
2697
|
// src/roles/format/inherited-ignores.ts
|
|
1572
|
-
import { existsSync as
|
|
1573
|
-
import { join as
|
|
2698
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
|
|
2699
|
+
import { join as join13 } from "node:path";
|
|
1574
2700
|
var ROOT_IGNORE_FILE = ".prettierignore";
|
|
1575
2701
|
function isPattern(line) {
|
|
1576
2702
|
const trimmed = line.trim();
|
|
@@ -1586,11 +2712,11 @@ function toModulePattern(pattern) {
|
|
|
1586
2712
|
}
|
|
1587
2713
|
function inheritedIgnorePatterns(workspaceRoot) {
|
|
1588
2714
|
if (!workspaceRoot) return [];
|
|
1589
|
-
const path2 =
|
|
1590
|
-
if (!
|
|
2715
|
+
const path2 = join13(workspaceRoot, ROOT_IGNORE_FILE);
|
|
2716
|
+
if (!existsSync8(path2)) return [];
|
|
1591
2717
|
let contents;
|
|
1592
2718
|
try {
|
|
1593
|
-
contents =
|
|
2719
|
+
contents = readFileSync8(path2, "utf8");
|
|
1594
2720
|
} catch {
|
|
1595
2721
|
return [];
|
|
1596
2722
|
}
|
|
@@ -1635,8 +2761,8 @@ function needsSvelteCompiler(output) {
|
|
|
1635
2761
|
}
|
|
1636
2762
|
|
|
1637
2763
|
// src/roles/format/prettier-config.ts
|
|
1638
|
-
import { existsSync as
|
|
1639
|
-
import { join as
|
|
2764
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
|
|
2765
|
+
import { join as join14 } from "node:path";
|
|
1640
2766
|
var UNSUPPORTED = /* @__PURE__ */ new Set(["plugins", "parser", "filepath", "rangeStart", "rangeEnd"]);
|
|
1641
2767
|
function toOxfmtOverrides(value) {
|
|
1642
2768
|
if (!Array.isArray(value)) return { overrides: [], unresolved: [] };
|
|
@@ -1661,14 +2787,14 @@ function toOxfmtOverrides(value) {
|
|
|
1661
2787
|
return { overrides, unresolved };
|
|
1662
2788
|
}
|
|
1663
2789
|
function readPrettierSettings(cwd) {
|
|
1664
|
-
const file = PRETTIER_CONFIG_FILES.find((name) =>
|
|
2790
|
+
const file = PRETTIER_CONFIG_FILES.find((name) => existsSync9(join14(cwd, name)));
|
|
1665
2791
|
if (!file) return { options: {}, unresolved: [] };
|
|
1666
2792
|
if (/\.(js|cjs|mjs)$/.test(file)) {
|
|
1667
2793
|
return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
|
|
1668
2794
|
}
|
|
1669
2795
|
let parsed;
|
|
1670
2796
|
try {
|
|
1671
|
-
parsed = parseJsonc(
|
|
2797
|
+
parsed = parseJsonc(readFileSync9(join14(cwd, file), "utf8"), file);
|
|
1672
2798
|
} catch {
|
|
1673
2799
|
return { options: {}, file, unresolved: [`${file} could not be parsed`] };
|
|
1674
2800
|
}
|
|
@@ -1786,7 +2912,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1786
2912
|
manifestOperation(context2.cwd, this.formatScripts(context2.cwd))
|
|
1787
2913
|
];
|
|
1788
2914
|
const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
|
|
1789
|
-
(name) =>
|
|
2915
|
+
(name) => existsSync10(join15(context2.cwd, name))
|
|
1790
2916
|
);
|
|
1791
2917
|
for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
|
|
1792
2918
|
const removableDeps = this.modulePrettierDependencies(context2.cwd);
|
|
@@ -1979,7 +3105,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1979
3105
|
const adoption = readFormatAdoption(ctx.cwd);
|
|
1980
3106
|
const preset = formatPresetFor(adoption.preset ?? DEFAULT_PRESET);
|
|
1981
3107
|
const options = { ...preset, ...adoption.local };
|
|
1982
|
-
const
|
|
3108
|
+
const describe2 = (value) => typeof value === "string" ? value : JSON.stringify(value);
|
|
1983
3109
|
return {
|
|
1984
3110
|
runner: "oxfmt",
|
|
1985
3111
|
configFile: adoption.configFile,
|
|
@@ -1988,7 +3114,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
1988
3114
|
options,
|
|
1989
3115
|
overrides: Object.entries(adoption.local).filter(([key]) => !ALWAYS_LOCAL_KEYS.includes(key)).map(([key, value]) => ({
|
|
1990
3116
|
rule: key,
|
|
1991
|
-
reason: key in preset ? `${
|
|
3117
|
+
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
3118
|
})),
|
|
1993
3119
|
// Which files this module does not format at all. A long list here is worth a look:
|
|
1994
3120
|
// it is the quiet way a module stops being covered.
|
|
@@ -2063,7 +3189,7 @@ var OxfmtAdapter = class extends BaseAdapter {
|
|
|
2063
3189
|
modulePrettierDependencies(cwd) {
|
|
2064
3190
|
const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
|
|
2065
3191
|
try {
|
|
2066
|
-
const manifest = JSON.parse(
|
|
3192
|
+
const manifest = JSON.parse(readFileSync10(join15(cwd, "package.json"), "utf8"));
|
|
2067
3193
|
return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
|
|
2068
3194
|
} catch {
|
|
2069
3195
|
return [];
|
|
@@ -2145,8 +3271,8 @@ function registerFormat() {
|
|
|
2145
3271
|
|
|
2146
3272
|
// src/roles/lint/adapters/oxlint/oxlint.adapter.ts
|
|
2147
3273
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
2148
|
-
import { existsSync as
|
|
2149
|
-
import { join as
|
|
3274
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3275
|
+
import { join as join21 } from "node:path";
|
|
2150
3276
|
|
|
2151
3277
|
// src/core/config/deferred-rules.ts
|
|
2152
3278
|
function deferredRuleNames(rules) {
|
|
@@ -2154,8 +3280,8 @@ function deferredRuleNames(rules) {
|
|
|
2154
3280
|
}
|
|
2155
3281
|
|
|
2156
3282
|
// src/core/fix-report.ts
|
|
2157
|
-
import { statSync as
|
|
2158
|
-
import { join as
|
|
3283
|
+
import { statSync as statSync3 } from "node:fs";
|
|
3284
|
+
import { join as join16 } from "node:path";
|
|
2159
3285
|
var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
|
|
2160
3286
|
function readFailure(output) {
|
|
2161
3287
|
const lines = output.split("\n").map((line2) => line2.trim());
|
|
@@ -2166,7 +3292,7 @@ function readFailure(output) {
|
|
|
2166
3292
|
}
|
|
2167
3293
|
function fingerprint(path2) {
|
|
2168
3294
|
try {
|
|
2169
|
-
const stat =
|
|
3295
|
+
const stat = statSync3(path2);
|
|
2170
3296
|
return `${stat.size}:${stat.mtimeMs}`;
|
|
2171
3297
|
} catch {
|
|
2172
3298
|
return void 0;
|
|
@@ -2175,7 +3301,7 @@ function fingerprint(path2) {
|
|
|
2175
3301
|
function snapshotFiles(cwd, extensions) {
|
|
2176
3302
|
const snapshot = /* @__PURE__ */ new Map();
|
|
2177
3303
|
for (const file of listSourceFiles(cwd, extensions)) {
|
|
2178
|
-
const mark = fingerprint(
|
|
3304
|
+
const mark = fingerprint(join16(cwd, file));
|
|
2179
3305
|
if (mark !== void 0) snapshot.set(file, mark);
|
|
2180
3306
|
}
|
|
2181
3307
|
return snapshot;
|
|
@@ -2183,7 +3309,7 @@ function snapshotFiles(cwd, extensions) {
|
|
|
2183
3309
|
function changedSince(cwd, before, extensions) {
|
|
2184
3310
|
const changed = [];
|
|
2185
3311
|
for (const file of listSourceFiles(cwd, extensions)) {
|
|
2186
|
-
const now = fingerprint(
|
|
3312
|
+
const now = fingerprint(join16(cwd, file));
|
|
2187
3313
|
if (now === void 0) continue;
|
|
2188
3314
|
if (before.get(file) !== now) changed.push(file);
|
|
2189
3315
|
}
|
|
@@ -4153,7 +5279,7 @@ var react_default = {
|
|
|
4153
5279
|
};
|
|
4154
5280
|
|
|
4155
5281
|
// src/roles/lint/presets/shared.json
|
|
4156
|
-
var
|
|
5282
|
+
var shared_default2 = {
|
|
4157
5283
|
rules: {
|
|
4158
5284
|
"no-var": "error",
|
|
4159
5285
|
"prefer-const": [
|
|
@@ -4530,7 +5656,7 @@ var tools_default = {
|
|
|
4530
5656
|
};
|
|
4531
5657
|
|
|
4532
5658
|
// src/roles/lint/preset-data.ts
|
|
4533
|
-
var SHARED_RULES =
|
|
5659
|
+
var SHARED_RULES = shared_default2.rules;
|
|
4534
5660
|
var PRESET_FILES = {
|
|
4535
5661
|
nest: nest_default,
|
|
4536
5662
|
node: node_default,
|
|
@@ -4643,14 +5769,14 @@ function downgradedRulesFor(preset) {
|
|
|
4643
5769
|
}
|
|
4644
5770
|
|
|
4645
5771
|
// src/roles/lint/extra-layers.ts
|
|
4646
|
-
import { existsSync as
|
|
4647
|
-
import { isAbsolute, join as
|
|
5772
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "node:fs";
|
|
5773
|
+
import { isAbsolute, join as join17, resolve as resolve2 } from "node:path";
|
|
4648
5774
|
function ruleCount(cwd, specifier) {
|
|
4649
5775
|
const path2 = isAbsolute(specifier) ? specifier : resolve2(cwd, specifier);
|
|
4650
|
-
if (!
|
|
5776
|
+
if (!existsSync11(path2)) return void 0;
|
|
4651
5777
|
try {
|
|
4652
5778
|
const parsed = parseJsonc(
|
|
4653
|
-
|
|
5779
|
+
readFileSync11(path2, "utf8"),
|
|
4654
5780
|
specifier
|
|
4655
5781
|
);
|
|
4656
5782
|
return Object.keys(parsed.rules ?? {}).length;
|
|
@@ -4670,7 +5796,7 @@ function extraLayers(cwd, extendsList) {
|
|
|
4670
5796
|
function committedExtendsList(cwd, configFile) {
|
|
4671
5797
|
try {
|
|
4672
5798
|
const parsed = parseJsonc(
|
|
4673
|
-
|
|
5799
|
+
readFileSync11(join17(cwd, configFile), "utf8"),
|
|
4674
5800
|
configFile
|
|
4675
5801
|
);
|
|
4676
5802
|
return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
|
|
@@ -4853,12 +5979,12 @@ function lintPresetFor(preset) {
|
|
|
4853
5979
|
}
|
|
4854
5980
|
|
|
4855
5981
|
// src/roles/lint/adapters/oxlint/plan.ts
|
|
4856
|
-
import { existsSync as
|
|
4857
|
-
import { join as
|
|
5982
|
+
import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
|
|
5983
|
+
import { join as join20 } from "node:path";
|
|
4858
5984
|
|
|
4859
5985
|
// src/roles/lint/eslint-ignores.ts
|
|
4860
|
-
import { existsSync as
|
|
4861
|
-
import { join as
|
|
5986
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
|
|
5987
|
+
import { join as join18 } from "node:path";
|
|
4862
5988
|
import { parseSync as parseSync2 } from "oxc-parser";
|
|
4863
5989
|
var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
|
|
4864
5990
|
function toOxlintPattern(pattern) {
|
|
@@ -4870,11 +5996,11 @@ function readRootEslintIgnores(root) {
|
|
|
4870
5996
|
return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
|
|
4871
5997
|
}
|
|
4872
5998
|
function readEslintIgnores(cwd) {
|
|
4873
|
-
const config = ESLINT_CONFIG_FILES.map((name) =>
|
|
5999
|
+
const config = ESLINT_CONFIG_FILES.map((name) => join18(cwd, name)).find((path2) => existsSync12(path2));
|
|
4874
6000
|
if (!config) return { patterns: [], unresolved: [] };
|
|
4875
6001
|
let source;
|
|
4876
6002
|
try {
|
|
4877
|
-
source =
|
|
6003
|
+
source = readFileSync12(config, "utf8");
|
|
4878
6004
|
} catch {
|
|
4879
6005
|
return { patterns: [], unresolved: [] };
|
|
4880
6006
|
}
|
|
@@ -4945,8 +6071,8 @@ function keepsOtherCommands3(script) {
|
|
|
4945
6071
|
}
|
|
4946
6072
|
|
|
4947
6073
|
// src/roles/lint/rename-suppressions.ts
|
|
4948
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
4949
|
-
import { join as
|
|
6074
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
|
|
6075
|
+
import { join as join19, relative as relative3 } from "node:path";
|
|
4950
6076
|
var SOURCE_EXTENSIONS = [
|
|
4951
6077
|
".ts",
|
|
4952
6078
|
".tsx",
|
|
@@ -4993,10 +6119,10 @@ function* sourceFiles(dir) {
|
|
|
4993
6119
|
return;
|
|
4994
6120
|
}
|
|
4995
6121
|
for (const entry of entries) {
|
|
4996
|
-
const full =
|
|
6122
|
+
const full = join19(dir, entry);
|
|
4997
6123
|
let isDirectory;
|
|
4998
6124
|
try {
|
|
4999
|
-
isDirectory =
|
|
6125
|
+
isDirectory = statSync4(full).isDirectory();
|
|
5000
6126
|
} catch {
|
|
5001
6127
|
continue;
|
|
5002
6128
|
}
|
|
@@ -5013,7 +6139,7 @@ function findSuppressionRenames(cwd, renames) {
|
|
|
5013
6139
|
for (const file of sourceFiles(cwd)) {
|
|
5014
6140
|
let content;
|
|
5015
6141
|
try {
|
|
5016
|
-
content =
|
|
6142
|
+
content = readFileSync13(file, "utf8");
|
|
5017
6143
|
} catch {
|
|
5018
6144
|
continue;
|
|
5019
6145
|
}
|
|
@@ -5159,7 +6285,7 @@ function plan2(context2) {
|
|
|
5159
6285
|
keys: removableDeps
|
|
5160
6286
|
});
|
|
5161
6287
|
}
|
|
5162
|
-
const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) =>
|
|
6288
|
+
const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync13(join20(context2.cwd, name)));
|
|
5163
6289
|
for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
|
|
5164
6290
|
operations.push(
|
|
5165
6291
|
...nxTargetOperations({
|
|
@@ -5273,7 +6399,7 @@ function lintScripts(cwd) {
|
|
|
5273
6399
|
function committedExtends(cwd) {
|
|
5274
6400
|
try {
|
|
5275
6401
|
const parsed = parseJsonc(
|
|
5276
|
-
|
|
6402
|
+
readFileSync14(join20(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5277
6403
|
LINT_CONFIG_FILE
|
|
5278
6404
|
);
|
|
5279
6405
|
return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
|
|
@@ -5285,7 +6411,7 @@ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array
|
|
|
5285
6411
|
function committedConfig(cwd) {
|
|
5286
6412
|
try {
|
|
5287
6413
|
const parsed = parseJsonc(
|
|
5288
|
-
|
|
6414
|
+
readFileSync14(join20(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5289
6415
|
LINT_CONFIG_FILE
|
|
5290
6416
|
);
|
|
5291
6417
|
return isRecord2(parsed) ? parsed : {};
|
|
@@ -5309,7 +6435,7 @@ function describeGoverned(rule, variant) {
|
|
|
5309
6435
|
function committedIgnorePatterns(cwd) {
|
|
5310
6436
|
try {
|
|
5311
6437
|
const parsed = parseJsonc(
|
|
5312
|
-
|
|
6438
|
+
readFileSync14(join20(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5313
6439
|
LINT_CONFIG_FILE
|
|
5314
6440
|
);
|
|
5315
6441
|
return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
|
|
@@ -5321,7 +6447,7 @@ function moduleEslintDependencies(cwd) {
|
|
|
5321
6447
|
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
6448
|
let manifest;
|
|
5323
6449
|
try {
|
|
5324
|
-
manifest = JSON.parse(
|
|
6450
|
+
manifest = JSON.parse(readFileSync14(join20(cwd, "package.json"), "utf8"));
|
|
5325
6451
|
} catch {
|
|
5326
6452
|
return [];
|
|
5327
6453
|
}
|
|
@@ -5502,23 +6628,23 @@ ${result.stderr ?? ""}`);
|
|
|
5502
6628
|
* build the developer can see, rather than a silent half-adoption they cannot.
|
|
5503
6629
|
*/
|
|
5504
6630
|
writeModuleBaseline(ctx, oxlint, env) {
|
|
5505
|
-
const configPath =
|
|
5506
|
-
const baselinePath =
|
|
5507
|
-
if (!
|
|
6631
|
+
const configPath = join21(ctx.cwd, LINT_CONFIG_FILE);
|
|
6632
|
+
const baselinePath = join21(ctx.cwd, LINT_BASELINE_FILE);
|
|
6633
|
+
if (!existsSync14(configPath)) return;
|
|
5508
6634
|
let config;
|
|
5509
6635
|
try {
|
|
5510
6636
|
config = parseJsonc(
|
|
5511
|
-
|
|
6637
|
+
readFileSync15(configPath, "utf8"),
|
|
5512
6638
|
LINT_CONFIG_FILE
|
|
5513
6639
|
);
|
|
5514
6640
|
} catch {
|
|
5515
6641
|
return;
|
|
5516
6642
|
}
|
|
5517
6643
|
const current = Array.isArray(config.extends) ? config.extends : [];
|
|
5518
|
-
const measurePath =
|
|
6644
|
+
const measurePath = join21(ctx.cwd, LINT_MEASURE_FILE);
|
|
5519
6645
|
let measured;
|
|
5520
6646
|
try {
|
|
5521
|
-
|
|
6647
|
+
writeFileSync4(
|
|
5522
6648
|
measurePath,
|
|
5523
6649
|
`${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
|
|
5524
6650
|
`
|
|
@@ -5553,7 +6679,7 @@ ${result.stderr ?? ""}`);
|
|
|
5553
6679
|
);
|
|
5554
6680
|
const next = extendsWithBaseline(current, holds.length > 0);
|
|
5555
6681
|
if (holds.length > 0) {
|
|
5556
|
-
|
|
6682
|
+
writeFileSync4(baselinePath, renderBaseline(holds, readOwnPackage().version));
|
|
5557
6683
|
process.stderr.write(` ${palette(process.stderr).warn(describeHolds(holds))}
|
|
5558
6684
|
`);
|
|
5559
6685
|
} else {
|
|
@@ -5561,12 +6687,12 @@ ${result.stderr ?? ""}`);
|
|
|
5561
6687
|
}
|
|
5562
6688
|
if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
|
|
5563
6689
|
config.extends = next;
|
|
5564
|
-
|
|
6690
|
+
writeFileSync4(configPath, `${JSON.stringify(config, null, 2)}
|
|
5565
6691
|
`);
|
|
5566
6692
|
}
|
|
5567
6693
|
}
|
|
5568
6694
|
async run(ctx) {
|
|
5569
|
-
if (!
|
|
6695
|
+
if (!existsSync14(join21(ctx.cwd, LINT_CONFIG_FILE))) {
|
|
5570
6696
|
process.stderr.write(
|
|
5571
6697
|
`sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
|
|
5572
6698
|
`
|
|
@@ -5715,7 +6841,7 @@ ${result.stderr ?? ""}`);
|
|
|
5715
6841
|
let stub;
|
|
5716
6842
|
try {
|
|
5717
6843
|
stub = parseJsonc(
|
|
5718
|
-
|
|
6844
|
+
readFileSync15(join21(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5719
6845
|
LINT_CONFIG_FILE
|
|
5720
6846
|
);
|
|
5721
6847
|
} catch {
|
|
@@ -5725,7 +6851,7 @@ ${result.stderr ?? ""}`);
|
|
|
5725
6851
|
for (const entry of stub.extends ?? []) {
|
|
5726
6852
|
try {
|
|
5727
6853
|
const preset = parseJsonc(
|
|
5728
|
-
|
|
6854
|
+
readFileSync15(join21(cwd, entry), "utf8"),
|
|
5729
6855
|
entry
|
|
5730
6856
|
);
|
|
5731
6857
|
for (const rule of Object.keys(preset.rules ?? {})) names.add(canonicalRuleName(rule));
|
|
@@ -5795,14 +6921,14 @@ ${result.stderr ?? ""}`);
|
|
|
5795
6921
|
let parsed;
|
|
5796
6922
|
try {
|
|
5797
6923
|
parsed = parseJsonc(
|
|
5798
|
-
|
|
6924
|
+
readFileSync15(join21(cwd, LINT_CONFIG_FILE), "utf8"),
|
|
5799
6925
|
LINT_CONFIG_FILE
|
|
5800
6926
|
);
|
|
5801
6927
|
} catch {
|
|
5802
6928
|
return void 0;
|
|
5803
6929
|
}
|
|
5804
6930
|
const targets2 = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
|
|
5805
|
-
return targets2.find((target) => !
|
|
6931
|
+
return targets2.find((target) => !existsSync14(join21(cwd, target)));
|
|
5806
6932
|
}
|
|
5807
6933
|
/** Announce what is not enforced, so reduced coverage is never silent. */
|
|
5808
6934
|
announceDisabled(preset) {
|
|
@@ -5834,8 +6960,8 @@ function registerLint() {
|
|
|
5834
6960
|
|
|
5835
6961
|
// src/roles/static-analysis/adapters/jscpd/jscpd.adapter.ts
|
|
5836
6962
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
5837
|
-
import { existsSync as
|
|
5838
|
-
import { join as
|
|
6963
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync3, readFileSync as readFileSync21, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "node:fs";
|
|
6964
|
+
import { join as join31 } from "node:path";
|
|
5839
6965
|
import { globSync as globSync5 } from "tinyglobby";
|
|
5840
6966
|
|
|
5841
6967
|
// src/roles/static-analysis/bundle/budgets.ts
|
|
@@ -5864,32 +6990,32 @@ function bandFor(valueKb, referenceKb) {
|
|
|
5864
6990
|
}
|
|
5865
6991
|
|
|
5866
6992
|
// src/roles/static-analysis/bundle/scan.ts
|
|
5867
|
-
import { existsSync as
|
|
5868
|
-
import { basename, join as
|
|
5869
|
-
import { pathToFileURL } from "node:url";
|
|
6993
|
+
import { existsSync as existsSync16, readFileSync as readFileSync17 } from "node:fs";
|
|
6994
|
+
import { basename, join as join23 } from "node:path";
|
|
6995
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
5870
6996
|
import { gzipSync } from "node:zlib";
|
|
5871
6997
|
import { globSync as globSync2 } from "tinyglobby";
|
|
5872
6998
|
|
|
5873
6999
|
// src/roles/static-analysis/bundle/find-build.ts
|
|
5874
|
-
import { existsSync as
|
|
5875
|
-
import { join as
|
|
7000
|
+
import { existsSync as existsSync15, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
|
|
7001
|
+
import { join as join22 } from "node:path";
|
|
5876
7002
|
import { globSync } from "tinyglobby";
|
|
5877
7003
|
var MANIFEST_GLOB = "server/_tanstack-start-manifest*.mjs";
|
|
5878
7004
|
function candidateDirs(cwd) {
|
|
5879
7005
|
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,
|
|
7006
|
+
const fromDeclaration = Array.isArray(declared) ? declared.filter((token) => typeof token === "string").map((token) => join22(cwd, token.replace(/^\{projectRoot\}\//, ""))) : [];
|
|
7007
|
+
return [...fromDeclaration, join22(cwd, ".output"), join22(cwd, "dist"), join22(cwd, "build")];
|
|
5882
7008
|
}
|
|
5883
7009
|
function findBuildOutput(cwd) {
|
|
5884
7010
|
const lookedIn = [];
|
|
5885
7011
|
for (const dir of candidateDirs(cwd)) {
|
|
5886
7012
|
lookedIn.push(dir);
|
|
5887
|
-
if (!
|
|
7013
|
+
if (!existsSync15(dir)) continue;
|
|
5888
7014
|
const found = globSync(MANIFEST_GLOB, { cwd: dir, absolute: true, dot: false });
|
|
5889
7015
|
if (found.length === 0) continue;
|
|
5890
7016
|
const manifestPath = found[0];
|
|
5891
|
-
const publicDir =
|
|
5892
|
-
if (!
|
|
7017
|
+
const publicDir = join22(dir, "public");
|
|
7018
|
+
if (!existsSync15(publicDir)) continue;
|
|
5893
7019
|
return { manifestPath, publicDir, ...dateOf(dir, manifestPath) };
|
|
5894
7020
|
}
|
|
5895
7021
|
const pkg = readProjectPackageJson(cwd);
|
|
@@ -5911,10 +7037,10 @@ function findBuildOutput(cwd) {
|
|
|
5911
7037
|
};
|
|
5912
7038
|
}
|
|
5913
7039
|
function dateOf(dir, manifestPath) {
|
|
5914
|
-
const record =
|
|
5915
|
-
if (
|
|
7040
|
+
const record = join22(dir, "nitro.json");
|
|
7041
|
+
if (existsSync15(record)) {
|
|
5916
7042
|
try {
|
|
5917
|
-
const parsed = JSON.parse(
|
|
7043
|
+
const parsed = JSON.parse(readFileSync16(record, "utf8"));
|
|
5918
7044
|
if (typeof parsed.date === "string") {
|
|
5919
7045
|
const date = new Date(parsed.date);
|
|
5920
7046
|
if (!Number.isNaN(date.getTime())) return { builtAt: date, builtAtFrom: "build record" };
|
|
@@ -5922,7 +7048,7 @@ function dateOf(dir, manifestPath) {
|
|
|
5922
7048
|
} catch {
|
|
5923
7049
|
}
|
|
5924
7050
|
}
|
|
5925
|
-
return { builtAt:
|
|
7051
|
+
return { builtAt: statSync5(manifestPath).mtime, builtAtFrom: "file timestamp" };
|
|
5926
7052
|
}
|
|
5927
7053
|
function isBuildOutput(found) {
|
|
5928
7054
|
return "manifestPath" in found;
|
|
@@ -5940,10 +7066,10 @@ function kindOf(path2) {
|
|
|
5940
7066
|
return "other";
|
|
5941
7067
|
}
|
|
5942
7068
|
function resolveAnnounced(publicDir, announced) {
|
|
5943
|
-
const direct =
|
|
5944
|
-
if (
|
|
5945
|
-
const stripped =
|
|
5946
|
-
return
|
|
7069
|
+
const direct = join23(publicDir, announced);
|
|
7070
|
+
if (existsSync16(direct)) return direct;
|
|
7071
|
+
const stripped = join23(publicDir, announced.replace(/^\/[^/]+\//, "/"));
|
|
7072
|
+
return existsSync16(stripped) ? stripped : void 0;
|
|
5947
7073
|
}
|
|
5948
7074
|
function emptyWeight() {
|
|
5949
7075
|
return { transfer: 0, parsed: 0, files: 0 };
|
|
@@ -5979,7 +7105,7 @@ async function scanBundle(cwd) {
|
|
|
5979
7105
|
unresolved.push(announced);
|
|
5980
7106
|
measured.set(announced, void 0);
|
|
5981
7107
|
} else {
|
|
5982
|
-
const buffer =
|
|
7108
|
+
const buffer = readFileSync17(path2);
|
|
5983
7109
|
measured.set(announced, {
|
|
5984
7110
|
parsed: buffer.length,
|
|
5985
7111
|
transfer: gzipSync(buffer, { level: 9 }).length
|
|
@@ -6030,7 +7156,7 @@ async function scanBundle(cwd) {
|
|
|
6030
7156
|
}
|
|
6031
7157
|
async function loadManifest(path2) {
|
|
6032
7158
|
try {
|
|
6033
|
-
const loaded = await import(
|
|
7159
|
+
const loaded = await import(pathToFileURL2(path2).href);
|
|
6034
7160
|
if (typeof loaded.tsrStartManifest !== "function") return void 0;
|
|
6035
7161
|
const produced = loaded.tsrStartManifest();
|
|
6036
7162
|
return produced !== null && typeof produced === "object" ? produced : void 0;
|
|
@@ -6066,7 +7192,7 @@ function scanDynamicLayer(publicDir, references) {
|
|
|
6066
7192
|
})) {
|
|
6067
7193
|
const name = basename(path2);
|
|
6068
7194
|
if (referenced.has(name)) continue;
|
|
6069
|
-
const buffer =
|
|
7195
|
+
const buffer = readFileSync17(path2);
|
|
6070
7196
|
const size = { parsed: buffer.length, transfer: gzipSync(buffer, { level: 9 }).length };
|
|
6071
7197
|
add(all2, size);
|
|
6072
7198
|
const group = groupOf(name);
|
|
@@ -6094,28 +7220,28 @@ function scanDynamicLayer(publicDir, references) {
|
|
|
6094
7220
|
}
|
|
6095
7221
|
|
|
6096
7222
|
// src/roles/static-analysis/sources.ts
|
|
6097
|
-
import { readFileSync as
|
|
6098
|
-
import { join as
|
|
7223
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
7224
|
+
import { join as join26 } from "node:path";
|
|
6099
7225
|
import { parseSync as parseSync3 } from "oxc-parser";
|
|
6100
7226
|
import { globSync as globSync3 } from "tinyglobby";
|
|
6101
7227
|
|
|
6102
7228
|
// src/roles/static-analysis/adapters/jscpd/tool-config.ts
|
|
6103
|
-
import { existsSync as
|
|
7229
|
+
import { existsSync as existsSync18, mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6104
7230
|
import { tmpdir } from "node:os";
|
|
6105
|
-
import { join as
|
|
7231
|
+
import { join as join25 } from "node:path";
|
|
6106
7232
|
|
|
6107
7233
|
// src/roles/static-analysis/configs/resolve.ts
|
|
6108
|
-
import { existsSync as
|
|
6109
|
-
import { join as
|
|
6110
|
-
import { pathToFileURL as
|
|
7234
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
7235
|
+
import { join as join24 } from "node:path";
|
|
7236
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
6111
7237
|
var STATIC_CONFIG_FILE = "static-analysis.config.mjs";
|
|
6112
7238
|
async function loadStaticConfig(cwd) {
|
|
6113
|
-
const path2 =
|
|
6114
|
-
if (!
|
|
7239
|
+
const path2 = join24(cwd, STATIC_CONFIG_FILE);
|
|
7240
|
+
if (!existsSync17(path2)) {
|
|
6115
7241
|
return { config: defineStaticAnalysis(), from: null, unreadable: null };
|
|
6116
7242
|
}
|
|
6117
7243
|
try {
|
|
6118
|
-
const loaded = await import(
|
|
7244
|
+
const loaded = await import(pathToFileURL3(path2).href);
|
|
6119
7245
|
return { config: adopt(loaded.default), from: STATIC_CONFIG_FILE, unreadable: null };
|
|
6120
7246
|
} catch (error) {
|
|
6121
7247
|
return {
|
|
@@ -6157,7 +7283,7 @@ function exclusions(config, scope = "tests") {
|
|
|
6157
7283
|
return [.../* @__PURE__ */ new Set([...config.ignore, ...OWN_ARTEFACTS, ...tests])];
|
|
6158
7284
|
}
|
|
6159
7285
|
function measuredRoots(cwd) {
|
|
6160
|
-
return
|
|
7286
|
+
return existsSync18(join25(cwd, "src")) ? ["src"] : ["."];
|
|
6161
7287
|
}
|
|
6162
7288
|
function fileSettings(config, scope, extraEntries = []) {
|
|
6163
7289
|
return {
|
|
@@ -6187,9 +7313,9 @@ function fileSettings(config, scope, extraEntries = []) {
|
|
|
6187
7313
|
};
|
|
6188
7314
|
}
|
|
6189
7315
|
function duplicationRun(config, paths, scope) {
|
|
6190
|
-
const dir = mkdtempSync(
|
|
6191
|
-
const configPath =
|
|
6192
|
-
|
|
7316
|
+
const dir = mkdtempSync(join25(tmpdir(), "sentinel-static-"));
|
|
7317
|
+
const configPath = join25(dir, "jscpd.json");
|
|
7318
|
+
writeFileSync5(configPath, `${JSON.stringify(fileSettings(config, scope), null, 2)}
|
|
6193
7319
|
`, "utf8");
|
|
6194
7320
|
const args = [
|
|
6195
7321
|
"--config",
|
|
@@ -6214,9 +7340,9 @@ function duplicationRun(config, paths, scope) {
|
|
|
6214
7340
|
return { args, rawReportDir: dir, dispose: () => rmSync3(dir, { force: true, recursive: true }) };
|
|
6215
7341
|
}
|
|
6216
7342
|
function deadCodeRun(config, paths, extraEntries = []) {
|
|
6217
|
-
const dir = mkdtempSync(
|
|
6218
|
-
const configPath =
|
|
6219
|
-
|
|
7343
|
+
const dir = mkdtempSync(join25(tmpdir(), "sentinel-static-"));
|
|
7344
|
+
const configPath = join25(dir, "jscpd.json");
|
|
7345
|
+
writeFileSync5(
|
|
6220
7346
|
configPath,
|
|
6221
7347
|
`${JSON.stringify(fileSettings(config, "dead-code", extraEntries), null, 2)}
|
|
6222
7348
|
`,
|
|
@@ -6267,7 +7393,7 @@ function readParsed(cwd, files, passes) {
|
|
|
6267
7393
|
function parsedSource(cwd, path2) {
|
|
6268
7394
|
let text;
|
|
6269
7395
|
try {
|
|
6270
|
-
text =
|
|
7396
|
+
text = readFileSync18(join26(cwd, path2), "utf8");
|
|
6271
7397
|
} catch {
|
|
6272
7398
|
return void 0;
|
|
6273
7399
|
}
|
|
@@ -6361,7 +7487,7 @@ function nameOf(fn, parent) {
|
|
|
6361
7487
|
if (key) return key;
|
|
6362
7488
|
return "(anonymous)";
|
|
6363
7489
|
}
|
|
6364
|
-
function
|
|
7490
|
+
function lineAt2(source, offset) {
|
|
6365
7491
|
if (offset === void 0) return 1;
|
|
6366
7492
|
let line = 1;
|
|
6367
7493
|
for (let index = 0; index < offset && index < source.length; index += 1) {
|
|
@@ -6383,7 +7509,7 @@ function complexityOfProgram(program, source) {
|
|
|
6383
7509
|
if (!FUNCTION_NODES.has(node.type ?? "")) return;
|
|
6384
7510
|
found.push({
|
|
6385
7511
|
name: nameOf(node, parents.get(node)),
|
|
6386
|
-
line:
|
|
7512
|
+
line: lineAt2(source, node.start),
|
|
6387
7513
|
complexity: complexityOf(node)
|
|
6388
7514
|
});
|
|
6389
7515
|
},
|
|
@@ -6454,13 +7580,13 @@ function unreadableNote(coverage) {
|
|
|
6454
7580
|
}
|
|
6455
7581
|
|
|
6456
7582
|
// src/roles/static-analysis/excerpt.ts
|
|
6457
|
-
import { readFileSync as
|
|
6458
|
-
import { join as
|
|
7583
|
+
import { readFileSync as readFileSync19 } from "node:fs";
|
|
7584
|
+
import { join as join28 } from "node:path";
|
|
6459
7585
|
|
|
6460
7586
|
// src/roles/static-analysis/maintainability/size-rules.ts
|
|
6461
|
-
import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as
|
|
7587
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
6462
7588
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
6463
|
-
import { join as
|
|
7589
|
+
import { join as join27 } from "node:path";
|
|
6464
7590
|
var GAUGE_THRESHOLDS = {
|
|
6465
7591
|
// McCabe 1976 says 10, our p90 says 11. The only indicator where two independent candidates agree
|
|
6466
7592
|
// within one unit, and 94.8% of our functions already sit in the 1-10 band SEI calls low risk.
|
|
@@ -6489,7 +7615,7 @@ var GAUGE_THRESHOLDS = {
|
|
|
6489
7615
|
"cognitive-complexity": 7
|
|
6490
7616
|
};
|
|
6491
7617
|
var COGNITIVE = "cognitive-complexity";
|
|
6492
|
-
var cognitivePlugin = () =>
|
|
7618
|
+
var cognitivePlugin = () => join27(ownPackageDir(), "plugins", "sonar-cognitive.js");
|
|
6493
7619
|
var CATEGORIES = [
|
|
6494
7620
|
"correctness",
|
|
6495
7621
|
"suspicious",
|
|
@@ -6521,9 +7647,9 @@ function sizeRulesConfig() {
|
|
|
6521
7647
|
};
|
|
6522
7648
|
}
|
|
6523
7649
|
function sizeRulesRun(paths, ignore) {
|
|
6524
|
-
const dir = mkdtempSync2(
|
|
6525
|
-
const configPath =
|
|
6526
|
-
|
|
7650
|
+
const dir = mkdtempSync2(join27(tmpdir2(), "sentinel-size-"));
|
|
7651
|
+
const configPath = join27(dir, "oxlintrc.json");
|
|
7652
|
+
writeFileSync6(configPath, `${JSON.stringify(sizeRulesConfig(), null, 2)}
|
|
6527
7653
|
`, "utf8");
|
|
6528
7654
|
return {
|
|
6529
7655
|
// `--format json` because the console shape is rendered by this role from the figures: a
|
|
@@ -7047,23 +8173,23 @@ function helpDetails(help, withoutReference) {
|
|
|
7047
8173
|
<p class="source">Source: ${prose(help.source)}</p>
|
|
7048
8174
|
</details>`;
|
|
7049
8175
|
}
|
|
7050
|
-
function secondSide(
|
|
7051
|
-
if (!
|
|
8176
|
+
function secondSide(finding2) {
|
|
8177
|
+
if (!finding2.other) return "";
|
|
7052
8178
|
const file = (at) => at.split(":")[0] ?? at;
|
|
7053
|
-
const sameFile = file(
|
|
8179
|
+
const sameFile = file(finding2.other) === file(finding2.path);
|
|
7054
8180
|
const label = sameFile ? "and line" : "and";
|
|
7055
|
-
const shownSide = sameFile ?
|
|
8181
|
+
const shownSide = sameFile ? finding2.other.split(":")[1] ?? finding2.other : finding2.other;
|
|
7056
8182
|
return ` <span class="unit">${label}</span> <code>${escape(shownSide)}</code>`;
|
|
7057
8183
|
}
|
|
7058
8184
|
var HTML_GROUP_LIMIT = 50;
|
|
7059
8185
|
function listedByMetric(findings) {
|
|
7060
8186
|
const groups = /* @__PURE__ */ new Map();
|
|
7061
|
-
for (const
|
|
7062
|
-
const bucket = groups.get(
|
|
7063
|
-
bucket.push(
|
|
7064
|
-
groups.set(
|
|
8187
|
+
for (const finding2 of findings) {
|
|
8188
|
+
const bucket = groups.get(finding2.metric) ?? [];
|
|
8189
|
+
bucket.push(finding2);
|
|
8190
|
+
groups.set(finding2.metric, bucket);
|
|
7065
8191
|
}
|
|
7066
|
-
const weight = (
|
|
8192
|
+
const weight = (finding2) => finding2.value ?? finding2.lines ?? 0;
|
|
7067
8193
|
return new Map(
|
|
7068
8194
|
[...groups.entries()].map(([metric, all2]) => {
|
|
7069
8195
|
const sorted = [...all2].sort((left, right) => weight(right) - weight(left));
|
|
@@ -7093,7 +8219,7 @@ function caveatsOf(report) {
|
|
|
7093
8219
|
}
|
|
7094
8220
|
return found;
|
|
7095
8221
|
}
|
|
7096
|
-
function
|
|
8222
|
+
function renderConsole2(report) {
|
|
7097
8223
|
const lines = [];
|
|
7098
8224
|
const width = (pick) => Math.max(...report.rows.map((row) => pick(row).length));
|
|
7099
8225
|
const label = (row) => helpFor(row.metric)?.label ?? row.metric;
|
|
@@ -7148,7 +8274,7 @@ function renderConsole(report) {
|
|
|
7148
8274
|
`;
|
|
7149
8275
|
}
|
|
7150
8276
|
var JSON_SCHEMA_VERSION = 1;
|
|
7151
|
-
function
|
|
8277
|
+
function renderJson2(report) {
|
|
7152
8278
|
const rows = report.rows.map((row) => {
|
|
7153
8279
|
const help = helpFor(row.metric);
|
|
7154
8280
|
return { ...row, help: help ? { label: help.label, caveat: help.caveat } : null };
|
|
@@ -7165,11 +8291,11 @@ function renderJson(report) {
|
|
|
7165
8291
|
return `${JSON.stringify(envelope, null, 2)}
|
|
7166
8292
|
`;
|
|
7167
8293
|
}
|
|
7168
|
-
function
|
|
8294
|
+
function renderMarkdown2(report) {
|
|
7169
8295
|
const version = report.settings.find((setting) => setting.key === "sentinel")?.value;
|
|
7170
8296
|
const places = /* @__PURE__ */ new Map();
|
|
7171
|
-
for (const
|
|
7172
|
-
places.set(
|
|
8297
|
+
for (const finding2 of report.findings) {
|
|
8298
|
+
places.set(finding2.metric, (places.get(finding2.metric) ?? 0) + 1);
|
|
7173
8299
|
}
|
|
7174
8300
|
const figureLine = (row) => {
|
|
7175
8301
|
const help = helpFor(row.metric);
|
|
@@ -7223,64 +8349,64 @@ function ruleChip(metric) {
|
|
|
7223
8349
|
const threshold = GAUGE_THRESHOLDS[metric];
|
|
7224
8350
|
return threshold === void 0 ? "" : ` <span class="chip"><code>${escape(metric)}</code> > ${threshold}</span>`;
|
|
7225
8351
|
}
|
|
7226
|
-
function measureOf(
|
|
7227
|
-
const metric =
|
|
8352
|
+
function measureOf(finding2) {
|
|
8353
|
+
const metric = finding2.metric;
|
|
7228
8354
|
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(
|
|
8355
|
+
if (finding2.value !== void 0) {
|
|
8356
|
+
if (GAUGE_THRESHOLDS[metric] !== void 0) return number(finding2.value);
|
|
8357
|
+
if (metric.startsWith("complexity")) return number(finding2.value, "complexity");
|
|
8358
|
+
if (finding2.dependsOn !== void 0) return number(finding2.value, "imported by");
|
|
8359
|
+
if (metric === "cycles") return number(finding2.value, "files in the cycle");
|
|
8360
|
+
return number(finding2.value);
|
|
7235
8361
|
}
|
|
7236
|
-
return
|
|
8362
|
+
return finding2.lines === void 0 ? "" : number(finding2.lines, "lines");
|
|
7237
8363
|
}
|
|
7238
|
-
function metaOf(
|
|
8364
|
+
function metaOf(finding2) {
|
|
7239
8365
|
const pairs = [];
|
|
7240
8366
|
const add2 = (label, value) => {
|
|
7241
8367
|
pairs.push(`<span class="pair"><span class="k">${escape(label)}</span>${value}</span>`);
|
|
7242
8368
|
};
|
|
7243
|
-
if (
|
|
8369
|
+
if (finding2.name) {
|
|
7244
8370
|
add2(
|
|
7245
|
-
|
|
7246
|
-
`<code>${escape(
|
|
8371
|
+
finding2.metric.startsWith("complexity") ? "function" : "name",
|
|
8372
|
+
`<code>${escape(finding2.name)}</code>`
|
|
7247
8373
|
);
|
|
7248
8374
|
}
|
|
7249
|
-
if (
|
|
7250
|
-
if (
|
|
7251
|
-
if (
|
|
7252
|
-
if (
|
|
7253
|
-
add2("length", `${
|
|
7254
|
-
if (
|
|
7255
|
-
if (
|
|
8375
|
+
if (finding2.dependsOn !== void 0) add2("depends on", String(finding2.dependsOn));
|
|
8376
|
+
if (finding2.instability !== void 0) add2("instability", finding2.instability.toFixed(2));
|
|
8377
|
+
if (finding2.centrality !== void 0) add2("centrality", finding2.centrality.toFixed(4));
|
|
8378
|
+
if (finding2.lines !== void 0 && finding2.value !== void 0)
|
|
8379
|
+
add2("length", `${finding2.lines} lines`);
|
|
8380
|
+
if (finding2.confidence !== void 0) add2("confidence", `${finding2.confidence}%`);
|
|
8381
|
+
if (finding2.reason) add2("unsure", `<span class="reason">${escape(finding2.reason)}</span>`);
|
|
7256
8382
|
return pairs.length === 0 ? "" : ` <div class="pmeta">${pairs.join("")}</div>
|
|
7257
8383
|
`;
|
|
7258
8384
|
}
|
|
7259
|
-
function codeSummary(
|
|
7260
|
-
const count =
|
|
7261
|
-
if (
|
|
7262
|
-
return `${
|
|
8385
|
+
function codeSummary(finding2) {
|
|
8386
|
+
const count = finding2.excerpt === void 0 ? 0 : finding2.excerpt.split("\n").length;
|
|
8387
|
+
if (finding2.metric === "clones" || finding2.metric === "clonesTests") {
|
|
8388
|
+
return `${finding2.lines ?? count} duplicated lines`;
|
|
7263
8389
|
}
|
|
7264
|
-
if (
|
|
7265
|
-
return `${count} lines from ${
|
|
8390
|
+
if (finding2.excerptFirstLine === 1) return `the first ${count} lines`;
|
|
8391
|
+
return `${count} lines from ${finding2.excerptFirstLine ?? "?"}`;
|
|
7266
8392
|
}
|
|
7267
|
-
function codeBlock(
|
|
7268
|
-
if (
|
|
7269
|
-
const first =
|
|
7270
|
-
const body = first === void 0 ? escape(
|
|
8393
|
+
function codeBlock(finding2) {
|
|
8394
|
+
if (finding2.excerpt === void 0) return "";
|
|
8395
|
+
const first = finding2.excerptFirstLine;
|
|
8396
|
+
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
8397
|
return ` <details class="pcode">
|
|
7272
|
-
<summary>${escape(codeSummary(
|
|
8398
|
+
<summary>${escape(codeSummary(finding2))}</summary>
|
|
7273
8399
|
<pre class="code">${body}</pre>
|
|
7274
8400
|
</details>
|
|
7275
8401
|
`;
|
|
7276
8402
|
}
|
|
7277
|
-
function placeCard(
|
|
8403
|
+
function placeCard(finding2) {
|
|
7278
8404
|
return ` <div class="pcard">
|
|
7279
8405
|
<div class="phead">
|
|
7280
|
-
<code class="path">${escape(
|
|
7281
|
-
<span class="pright">${measureOf(
|
|
8406
|
+
<code class="path">${escape(finding2.path)}</code>${secondSide(finding2)}
|
|
8407
|
+
<span class="pright">${measureOf(finding2)}${ruleChip(finding2.metric)}</span>
|
|
7282
8408
|
</div>
|
|
7283
|
-
${metaOf(
|
|
8409
|
+
${metaOf(finding2)}${codeBlock(finding2)} </div>`;
|
|
7284
8410
|
}
|
|
7285
8411
|
function placesFor(group) {
|
|
7286
8412
|
const total = group.listed.length + group.rest;
|
|
@@ -7288,7 +8414,7 @@ function placesFor(group) {
|
|
|
7288
8414
|
return ` <details class="place">
|
|
7289
8415
|
<summary>where to look <span class="count">${grouped(total)} ${total === 1 ? "place" : "places"}</span>${capped}</summary>
|
|
7290
8416
|
<div class="cards">
|
|
7291
|
-
${group.listed.map((
|
|
8417
|
+
${group.listed.map((finding2) => placeCard(finding2)).join("\n")}
|
|
7292
8418
|
</div>
|
|
7293
8419
|
${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
8420
|
</details>`;
|
|
@@ -7345,17 +8471,17 @@ var KIND_UNITS = {
|
|
|
7345
8471
|
cycles: "files",
|
|
7346
8472
|
mostImported: "imported by"
|
|
7347
8473
|
};
|
|
7348
|
-
function findingRow(
|
|
7349
|
-
const path2 =
|
|
8474
|
+
function findingRow(finding2) {
|
|
8475
|
+
const path2 = finding2.path;
|
|
7350
8476
|
const cut = path2.lastIndexOf("/");
|
|
7351
8477
|
const dir = cut === -1 ? "" : path2.slice(0, cut + 1);
|
|
7352
8478
|
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>${
|
|
8479
|
+
const measure = finding2.value ?? finding2.lines;
|
|
8480
|
+
const unit = KIND_UNITS[finding2.metric];
|
|
8481
|
+
const haystack = [path2, finding2.name, finding2.other].filter(Boolean).join(" ").toLowerCase();
|
|
8482
|
+
return ` <tr data-kind="${escape(kindOf2(finding2.metric))}" data-find="${escape(haystack)}">
|
|
8483
|
+
<td><span class="kind">${escape(kindOf2(finding2.metric))}</span></td>
|
|
8484
|
+
<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
8485
|
<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
8486
|
</tr>`;
|
|
7361
8487
|
}
|
|
@@ -7365,7 +8491,7 @@ function findingsPanel(report) {
|
|
|
7365
8491
|
return ` <p class="read">This run produced no finding to list.</p>`;
|
|
7366
8492
|
}
|
|
7367
8493
|
const counts = /* @__PURE__ */ new Map();
|
|
7368
|
-
for (const
|
|
8494
|
+
for (const finding2 of findings) counts.set(finding2.metric, (counts.get(finding2.metric) ?? 0) + 1);
|
|
7369
8495
|
const chips = [...counts.entries()].map(
|
|
7370
8496
|
([metric, count]) => `<button type="button" class="fchip" data-kind="${escape(kindOf2(metric))}">${escape(kindOf2(metric))} <span>${grouped(count)}</span></button>`
|
|
7371
8497
|
).join("\n ");
|
|
@@ -7388,7 +8514,7 @@ function findingsPanel(report) {
|
|
|
7388
8514
|
<tr><th scope="col">Kind</th><th scope="col">Where</th><th scope="col" class="meas">Measure</th></tr>
|
|
7389
8515
|
</thead>
|
|
7390
8516
|
<tbody>
|
|
7391
|
-
${findings.map((
|
|
8517
|
+
${findings.map((finding2) => findingRow(finding2)).join("\n")}
|
|
7392
8518
|
</tbody>
|
|
7393
8519
|
</table>`;
|
|
7394
8520
|
}
|
|
@@ -7401,7 +8527,7 @@ function summaryParts(report, bold) {
|
|
|
7401
8527
|
const withReference = report.rows.filter((row) => row.status !== void 0);
|
|
7402
8528
|
const above = withReference.filter((row) => row.status === "exceed").length;
|
|
7403
8529
|
const findings = report.findings ?? [];
|
|
7404
|
-
const inTests = findings.filter((
|
|
8530
|
+
const inTests = findings.filter((finding2) => finding2.metric === "clonesTests").length;
|
|
7405
8531
|
const inProduct = findings.length - inTests;
|
|
7406
8532
|
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
8533
|
return [
|
|
@@ -7980,7 +9106,7 @@ function linesOf(cache, cwd, relative5) {
|
|
|
7980
9106
|
if (cache.has(relative5)) return cache.get(relative5);
|
|
7981
9107
|
let lines;
|
|
7982
9108
|
try {
|
|
7983
|
-
lines =
|
|
9109
|
+
lines = readFileSync19(join28(cwd, relative5), "utf8").split("\n");
|
|
7984
9110
|
} catch {
|
|
7985
9111
|
lines = void 0;
|
|
7986
9112
|
}
|
|
@@ -8003,16 +9129,16 @@ function declarationOf(lines, name) {
|
|
|
8003
9129
|
}
|
|
8004
9130
|
return hits.length === 1 ? hits[0] : void 0;
|
|
8005
9131
|
}
|
|
8006
|
-
function windowFor(
|
|
8007
|
-
const { line } = pointOf(
|
|
9132
|
+
function windowFor(finding2, lines) {
|
|
9133
|
+
const { line } = pointOf(finding2.path);
|
|
8008
9134
|
const head = () => ({
|
|
8009
9135
|
firstLine: 1,
|
|
8010
9136
|
text: lines.slice(0, WINDOW.lines).join("\n")
|
|
8011
9137
|
});
|
|
8012
|
-
if (WHOLE_FILE.has(
|
|
8013
|
-
const at = line ?? (
|
|
9138
|
+
if (WHOLE_FILE.has(finding2.metric) || SHOWS_THE_HEAD.has(finding2.metric)) return head();
|
|
9139
|
+
const at = line ?? (finding2.name ? declarationOf(lines, finding2.name) : void 0);
|
|
8014
9140
|
if (at === void 0) return void 0;
|
|
8015
|
-
const nests = NESTS_UPWARD.has(
|
|
9141
|
+
const nests = NESTS_UPWARD.has(finding2.metric);
|
|
8016
9142
|
const from = nests ? Math.max(1, at - WINDOW.nesting.before) : at;
|
|
8017
9143
|
const to = Math.min(lines.length, nests ? at + WINDOW.nesting.after : at + WINDOW.lines - 1);
|
|
8018
9144
|
if (from > lines.length) return void 0;
|
|
@@ -8021,18 +9147,18 @@ function windowFor(finding, lines) {
|
|
|
8021
9147
|
function withExcerpts(report, cwd) {
|
|
8022
9148
|
const wanted = /* @__PURE__ */ new Set();
|
|
8023
9149
|
for (const group of listedByMetric(report.findings).values()) {
|
|
8024
|
-
for (const
|
|
8025
|
-
if (
|
|
9150
|
+
for (const finding2 of group.listed) {
|
|
9151
|
+
if (finding2.excerpt === void 0) wanted.add(finding2);
|
|
8026
9152
|
}
|
|
8027
9153
|
}
|
|
8028
9154
|
if (wanted.size === 0) return report;
|
|
8029
9155
|
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 ?
|
|
9156
|
+
const findings = report.findings.map((finding2) => {
|
|
9157
|
+
if (!wanted.has(finding2)) return finding2;
|
|
9158
|
+
const lines = linesOf(cache, cwd, pointOf(finding2.path).file);
|
|
9159
|
+
if (lines === void 0) return finding2;
|
|
9160
|
+
const found = windowFor(finding2, lines);
|
|
9161
|
+
return found === void 0 ? finding2 : { ...finding2, excerpt: found.text, excerptFirstLine: found.firstLine };
|
|
8036
9162
|
});
|
|
8037
9163
|
return { ...report, findings };
|
|
8038
9164
|
}
|
|
@@ -8212,7 +9338,7 @@ var SCRIPT = `(function(){
|
|
|
8212
9338
|
})();`;
|
|
8213
9339
|
|
|
8214
9340
|
// src/roles/static-analysis/graph/imports.ts
|
|
8215
|
-
import { dirname, join as
|
|
9341
|
+
import { dirname as dirname2, join as join29 } from "node:path";
|
|
8216
9342
|
var TS_BEHIND_JS = {
|
|
8217
9343
|
".js": [".ts", ".tsx"],
|
|
8218
9344
|
".mjs": [".mts"],
|
|
@@ -8265,7 +9391,7 @@ function specifiersOf(program) {
|
|
|
8265
9391
|
}
|
|
8266
9392
|
function resolveSpecifier(from, specifier, known) {
|
|
8267
9393
|
if (!specifier.startsWith(".")) return void 0;
|
|
8268
|
-
const base =
|
|
9394
|
+
const base = join29(dirname2(from), specifier);
|
|
8269
9395
|
if (known.has(base)) return base;
|
|
8270
9396
|
for (const extension of RESOLVABLE_EXTENSIONS) {
|
|
8271
9397
|
if (known.has(base + extension)) return base + extension;
|
|
@@ -8276,7 +9402,7 @@ function resolveSpecifier(from, specifier, known) {
|
|
|
8276
9402
|
if (known.has(swapped)) return swapped;
|
|
8277
9403
|
}
|
|
8278
9404
|
for (const extension of RESOLVABLE_EXTENSIONS) {
|
|
8279
|
-
const index =
|
|
9405
|
+
const index = join29(base, `index${extension}`);
|
|
8280
9406
|
if (known.has(index)) return index;
|
|
8281
9407
|
}
|
|
8282
9408
|
return void 0;
|
|
@@ -8389,26 +9515,26 @@ function scanSyntax(cwd, config) {
|
|
|
8389
9515
|
|
|
8390
9516
|
// src/roles/static-analysis/workspace/consumers.ts
|
|
8391
9517
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
8392
|
-
import { existsSync as
|
|
8393
|
-
import { dirname as
|
|
8394
|
-
var CACHE_DIR =
|
|
9518
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync2, readFileSync as readFileSync20, statSync as statSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
9519
|
+
import { dirname as dirname3, join as join30, relative as relative4, resolve as resolve3, sep } from "node:path";
|
|
9520
|
+
var CACHE_DIR = join30("node_modules", ".cache", "sentinel");
|
|
8395
9521
|
var CACHE_FILE = "consumer-index.json";
|
|
8396
9522
|
var SHAPE = 4;
|
|
8397
9523
|
function workspaceRootFrom(cwd) {
|
|
8398
9524
|
let dir = resolve3(cwd);
|
|
8399
9525
|
for (; ; ) {
|
|
8400
|
-
if (
|
|
8401
|
-
const parent =
|
|
9526
|
+
if (existsSync19(join30(dir, WORKSPACE_ROOT_MARKER))) return dir;
|
|
9527
|
+
const parent = dirname3(dir);
|
|
8402
9528
|
if (parent === dir) return void 0;
|
|
8403
9529
|
dir = parent;
|
|
8404
9530
|
}
|
|
8405
9531
|
}
|
|
8406
9532
|
function aliasTable(workspaceRoot) {
|
|
8407
|
-
const file =
|
|
8408
|
-
if (!
|
|
9533
|
+
const file = join30(workspaceRoot, "tsconfig.base.json");
|
|
9534
|
+
if (!existsSync19(file)) return /* @__PURE__ */ new Map();
|
|
8409
9535
|
let paths;
|
|
8410
9536
|
try {
|
|
8411
|
-
const parsed = JSON.parse(
|
|
9537
|
+
const parsed = JSON.parse(readFileSync20(file, "utf8"));
|
|
8412
9538
|
paths = parsed.compilerOptions?.paths ?? {};
|
|
8413
9539
|
} catch {
|
|
8414
9540
|
return /* @__PURE__ */ new Map();
|
|
@@ -8426,7 +9552,7 @@ function aliasTable(workspaceRoot) {
|
|
|
8426
9552
|
function resolveSpecifier2(specifier, table, workspaceRoot) {
|
|
8427
9553
|
const isFile = (candidate) => {
|
|
8428
9554
|
try {
|
|
8429
|
-
return
|
|
9555
|
+
return statSync6(join30(workspaceRoot, candidate)).isFile();
|
|
8430
9556
|
} catch {
|
|
8431
9557
|
return false;
|
|
8432
9558
|
}
|
|
@@ -8463,11 +9589,11 @@ function resolveSpecifier2(specifier, table, workspaceRoot) {
|
|
|
8463
9589
|
return void 0;
|
|
8464
9590
|
}
|
|
8465
9591
|
function resolveRelative(importer, specifier, workspaceRoot) {
|
|
8466
|
-
const base =
|
|
9592
|
+
const base = join30(dirname3(importer), specifier).split(sep).join("/");
|
|
8467
9593
|
if (base.startsWith("..")) return void 0;
|
|
8468
9594
|
const isFile = (candidate) => {
|
|
8469
9595
|
try {
|
|
8470
|
-
return
|
|
9596
|
+
return statSync6(join30(workspaceRoot, candidate)).isFile();
|
|
8471
9597
|
} catch {
|
|
8472
9598
|
return false;
|
|
8473
9599
|
}
|
|
@@ -8565,10 +9691,10 @@ function hash(text) {
|
|
|
8565
9691
|
}
|
|
8566
9692
|
function consumerIndex(workspaceRoot, moduleRoots) {
|
|
8567
9693
|
const key = cacheKey(workspaceRoot);
|
|
8568
|
-
const file =
|
|
8569
|
-
if (key &&
|
|
9694
|
+
const file = join30(workspaceRoot, CACHE_DIR, CACHE_FILE);
|
|
9695
|
+
if (key && existsSync19(file)) {
|
|
8570
9696
|
try {
|
|
8571
|
-
const cached = JSON.parse(
|
|
9697
|
+
const cached = JSON.parse(readFileSync20(file, "utf8"));
|
|
8572
9698
|
if (cached.key === key) {
|
|
8573
9699
|
return {
|
|
8574
9700
|
imported: new Set(cached.imported),
|
|
@@ -8582,14 +9708,14 @@ function consumerIndex(workspaceRoot, moduleRoots) {
|
|
|
8582
9708
|
const index = buildConsumerIndex(workspaceRoot, moduleRoots());
|
|
8583
9709
|
if (key) {
|
|
8584
9710
|
try {
|
|
8585
|
-
|
|
9711
|
+
mkdirSync2(join30(workspaceRoot, CACHE_DIR), { recursive: true });
|
|
8586
9712
|
const payload = {
|
|
8587
9713
|
key,
|
|
8588
9714
|
imported: [...index.imported],
|
|
8589
9715
|
testOnly: [...index.testOnly],
|
|
8590
9716
|
builtFrom: index.builtFrom
|
|
8591
9717
|
};
|
|
8592
|
-
|
|
9718
|
+
writeFileSync7(file, JSON.stringify(payload), "utf8");
|
|
8593
9719
|
} catch {
|
|
8594
9720
|
}
|
|
8595
9721
|
}
|
|
@@ -8686,7 +9812,7 @@ var JscpdAdapter = class extends BaseAdapter {
|
|
|
8686
9812
|
operations: [
|
|
8687
9813
|
// `write` and not `merge-json`: this file is code, and the engine's in-place merge is for
|
|
8688
9814
|
// JSON. A re-init therefore RESTORES the stub, so an existing one is left alone below.
|
|
8689
|
-
...
|
|
9815
|
+
...existsSync20(join31(context2.cwd, STATIC_CONFIG_FILE)) ? [] : [{ kind: "write", path: STATIC_CONFIG_FILE, contents: configStub() }],
|
|
8690
9816
|
manifestOperation(context2.cwd, scripts()),
|
|
8691
9817
|
...nxTargetOperations({ cwd: context2.cwd, targets: targets() })
|
|
8692
9818
|
// The report directory is ignored ONCE at the workspace root, by `--init`'s workspace prep,
|
|
@@ -8803,7 +9929,7 @@ var JscpdAdapter = class extends BaseAdapter {
|
|
|
8803
9929
|
const written = [];
|
|
8804
9930
|
for (const shape of shapes)
|
|
8805
9931
|
written.push(...await emit(report, shape, ctx.cwd, graph, coverage));
|
|
8806
|
-
process.stdout.write(
|
|
9932
|
+
process.stdout.write(renderConsole2(report));
|
|
8807
9933
|
if (written.length > 0) process.stdout.write(`
|
|
8808
9934
|
written: ${written.join(", ")}
|
|
8809
9935
|
`);
|
|
@@ -8936,7 +10062,7 @@ var JscpdAdapter = class extends BaseAdapter {
|
|
|
8936
10062
|
const declared = moduleScripts(ctx.cwd);
|
|
8937
10063
|
const missing = Object.keys(scripts()).filter((name) => !(name in declared));
|
|
8938
10064
|
const adopted = missing.length < Object.keys(scripts()).length;
|
|
8939
|
-
const hasConfig =
|
|
10065
|
+
const hasConfig = existsSync20(join31(ctx.cwd, STATIC_CONFIG_FILE));
|
|
8940
10066
|
return {
|
|
8941
10067
|
adopted,
|
|
8942
10068
|
preset: adopted ? ctx.preset : null,
|
|
@@ -9028,15 +10154,15 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
|
|
|
9028
10154
|
try {
|
|
9029
10155
|
const ran = runTool(jscpd, args, cwd, "dead-code");
|
|
9030
10156
|
if (ran.kind !== "ran") return ran;
|
|
9031
|
-
const path2 =
|
|
9032
|
-
if (!
|
|
10157
|
+
const path2 = join31(rawReportDir, RAW_REPORT["dead-code"]);
|
|
10158
|
+
if (!existsSync20(path2)) {
|
|
9033
10159
|
process.stderr.write(
|
|
9034
10160
|
`sentinel static-analysis(jscpd): the dead-code pass wrote no ${RAW_REPORT["dead-code"]}
|
|
9035
10161
|
`
|
|
9036
10162
|
);
|
|
9037
10163
|
return { kind: "failed", code: 1 };
|
|
9038
10164
|
}
|
|
9039
|
-
const parsed = safeParseDeadCode(
|
|
10165
|
+
const parsed = safeParseDeadCode(readFileSync21(path2, "utf8"));
|
|
9040
10166
|
const statistics = parsed?.statistics;
|
|
9041
10167
|
if (!statistics) {
|
|
9042
10168
|
process.stderr.write(
|
|
@@ -9049,8 +10175,8 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
|
|
|
9049
10175
|
return { kind: "no-entry-point", files: statistics.files ?? 0 };
|
|
9050
10176
|
}
|
|
9051
10177
|
const byCategory = {};
|
|
9052
|
-
for (const
|
|
9053
|
-
const category =
|
|
10178
|
+
for (const finding2 of parsed?.findings ?? []) {
|
|
10179
|
+
const category = finding2.category ?? "unknown";
|
|
9054
10180
|
byCategory[category] = (byCategory[category] ?? 0) + 1;
|
|
9055
10181
|
}
|
|
9056
10182
|
return { kind: "figures", statistics, byCategory, findings: parsed?.findings ?? [] };
|
|
@@ -9059,11 +10185,11 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
|
|
|
9059
10185
|
}
|
|
9060
10186
|
}
|
|
9061
10187
|
function moduleRelative(name, roots, cwd) {
|
|
9062
|
-
if (
|
|
10188
|
+
if (existsSync20(join31(cwd, name))) return name;
|
|
9063
10189
|
for (const root of roots) {
|
|
9064
10190
|
if (root === ".") continue;
|
|
9065
10191
|
const candidate = `${root}/${name}`;
|
|
9066
|
-
if (
|
|
10192
|
+
if (existsSync20(join31(cwd, candidate))) return candidate;
|
|
9067
10193
|
}
|
|
9068
10194
|
return name;
|
|
9069
10195
|
}
|
|
@@ -9072,8 +10198,8 @@ function runDuplication(jscpd, config, paths, scope, cwd) {
|
|
|
9072
10198
|
try {
|
|
9073
10199
|
const ran = runTool(jscpd, args, cwd, scope);
|
|
9074
10200
|
if (ran.kind !== "ran") return ran;
|
|
9075
|
-
const path2 =
|
|
9076
|
-
const parsed =
|
|
10201
|
+
const path2 = join31(rawReportDir, RAW_REPORT.duplication);
|
|
10202
|
+
const parsed = existsSync20(path2) ? safeParse(readFileSync21(path2, "utf8")) : void 0;
|
|
9077
10203
|
const total = parsed?.statistics?.total;
|
|
9078
10204
|
if (!total) {
|
|
9079
10205
|
process.stderr.write(
|
|
@@ -9137,7 +10263,7 @@ function placesToLook(product, tests, dead, complexity, graph, size, outsideTest
|
|
|
9137
10263
|
const toolTestOnlyPaths = new Set(
|
|
9138
10264
|
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
10265
|
);
|
|
9140
|
-
const metricOf = (
|
|
10266
|
+
const metricOf = (finding2) => finding2.category === "unused-file" && finding2.reasons?.includes("used-only-by-tests") === true ? "usedOnlyByTests" : deadMetric[finding2.category ?? ""] ?? "deadLinesPercent";
|
|
9141
10267
|
const weight = (f) => f.value ?? f.lines ?? 0;
|
|
9142
10268
|
const inOrder = (all2) => [...all2].sort(
|
|
9143
10269
|
(left, right) => left.metric.localeCompare(right.metric) || weight(right) - weight(left) || left.path.localeCompare(right.path) || (left.other ?? "").localeCompare(right.other ?? "")
|
|
@@ -9152,14 +10278,14 @@ function placesToLook(product, tests, dead, complexity, graph, size, outsideTest
|
|
|
9152
10278
|
name: fn.name,
|
|
9153
10279
|
value: fn.complexity
|
|
9154
10280
|
})),
|
|
9155
|
-
...dead.kind === "figures" && !unreadable ? dead.findings.map((
|
|
9156
|
-
metric: metricOf(
|
|
9157
|
-
path:
|
|
9158
|
-
...
|
|
9159
|
-
...
|
|
9160
|
-
...
|
|
9161
|
-
...
|
|
9162
|
-
...
|
|
10281
|
+
...dead.kind === "figures" && !unreadable ? dead.findings.map((finding2) => ({
|
|
10282
|
+
metric: metricOf(finding2),
|
|
10283
|
+
path: finding2.path ?? "(unknown)",
|
|
10284
|
+
...finding2.name ? { name: finding2.name } : {},
|
|
10285
|
+
...finding2.lines === void 0 ? {} : { lines: finding2.lines },
|
|
10286
|
+
...finding2.confidence === void 0 ? {} : { confidence: finding2.confidence },
|
|
10287
|
+
...finding2.reasons?.length ? { reason: finding2.reasons.join(", ") } : {},
|
|
10288
|
+
...finding2.message ? { message: finding2.message } : {}
|
|
9163
10289
|
})) : [],
|
|
9164
10290
|
/*
|
|
9165
10291
|
* The files the workspace index found, which the tool cannot see.
|
|
@@ -9242,8 +10368,8 @@ function outsideOf(cwd) {
|
|
|
9242
10368
|
function moduleRootsOf(workspaceRoot) {
|
|
9243
10369
|
const roots = [];
|
|
9244
10370
|
for (const area of ["apps", "libs"]) {
|
|
9245
|
-
const base =
|
|
9246
|
-
if (!
|
|
10371
|
+
const base = join31(workspaceRoot, area);
|
|
10372
|
+
if (!existsSync20(base)) continue;
|
|
9247
10373
|
for (const entry of globSync5([`${area}/**/package.json`, `${area}/**/project.json`], {
|
|
9248
10374
|
cwd: workspaceRoot,
|
|
9249
10375
|
ignore: ["**/node_modules/**"],
|
|
@@ -9400,8 +10526,8 @@ function assembleReport({
|
|
|
9400
10526
|
}
|
|
9401
10527
|
const toolTestOnly = new Set(
|
|
9402
10528
|
dead.kind === "figures" ? dead.findings.filter(
|
|
9403
|
-
(
|
|
9404
|
-
).map((
|
|
10529
|
+
(finding2) => finding2.category === "unused-file" && finding2.reasons?.includes("used-only-by-tests") === true
|
|
10530
|
+
).map((finding2) => finding2.path ?? "").filter(Boolean) : []
|
|
9405
10531
|
);
|
|
9406
10532
|
if (dead.kind === "figures" && !unreadable) {
|
|
9407
10533
|
const stats = dead.statistics;
|
|
@@ -9576,23 +10702,23 @@ function assembleReport({
|
|
|
9576
10702
|
byLanguage
|
|
9577
10703
|
};
|
|
9578
10704
|
}
|
|
9579
|
-
var
|
|
10705
|
+
var SHAPE_FILE2 = {
|
|
9580
10706
|
json: `${TARGET}.json`,
|
|
9581
10707
|
html: `${TARGET}.html`,
|
|
9582
10708
|
markdown: `${TARGET}.md`
|
|
9583
10709
|
};
|
|
9584
10710
|
var PICTURE_FILE = `${TARGET}-graph.svg`;
|
|
9585
10711
|
function clearStaleOutputs(cwd, keeping) {
|
|
9586
|
-
for (const name of [...Object.values(
|
|
10712
|
+
for (const name of [...Object.values(SHAPE_FILE2), PICTURE_FILE]) {
|
|
9587
10713
|
if (keeping.has(name)) continue;
|
|
9588
|
-
const path2 =
|
|
9589
|
-
if (
|
|
10714
|
+
const path2 = join31(cwd, REPORT_DIR, name);
|
|
10715
|
+
if (existsSync20(path2)) unlinkSync2(path2);
|
|
9590
10716
|
}
|
|
9591
10717
|
}
|
|
9592
10718
|
function outputsOf(shapes, graph, coverage) {
|
|
9593
10719
|
const keeping = /* @__PURE__ */ new Set();
|
|
9594
10720
|
for (const shape of shapes) {
|
|
9595
|
-
const name =
|
|
10721
|
+
const name = SHAPE_FILE2[shape];
|
|
9596
10722
|
if (name !== void 0) keeping.add(name);
|
|
9597
10723
|
}
|
|
9598
10724
|
if (shapes.includes("html") && drawsPicture(graph, coverage)) keeping.add(PICTURE_FILE);
|
|
@@ -9604,19 +10730,19 @@ function drawsPicture(graph, coverage) {
|
|
|
9604
10730
|
async function emit(report, shape, cwd, graph, coverage) {
|
|
9605
10731
|
if (shape === "console") return [];
|
|
9606
10732
|
const detailed = shape === "markdown" ? report : withExcerpts(report, cwd);
|
|
9607
|
-
|
|
10733
|
+
mkdirSync3(join31(cwd, REPORT_DIR), { recursive: true });
|
|
9608
10734
|
const written = [];
|
|
9609
10735
|
if (shape === "html" && drawsPicture(graph, coverage)) {
|
|
9610
10736
|
const drawn = layOut(graph.graph, graph.communityOf);
|
|
9611
|
-
const path2 =
|
|
10737
|
+
const path2 = join31(cwd, REPORT_DIR, PICTURE_FILE);
|
|
9612
10738
|
await writeGraphPicture(drawn, path2);
|
|
9613
10739
|
written.push(`${REPORT_DIR}/${PICTURE_FILE}`);
|
|
9614
10740
|
detailed.picture = buildExplorer(drawn, graph.communityOf).markup;
|
|
9615
10741
|
}
|
|
9616
|
-
const RENDER = { json:
|
|
9617
|
-
const name =
|
|
10742
|
+
const RENDER = { json: renderJson2, html: renderHtml, markdown: renderMarkdown2 };
|
|
10743
|
+
const name = SHAPE_FILE2[shape] ?? `${TARGET}.${shape}`;
|
|
9618
10744
|
const body = RENDER[shape](detailed);
|
|
9619
|
-
|
|
10745
|
+
writeFileSync8(join31(cwd, REPORT_DIR, name), body, "utf8");
|
|
9620
10746
|
written.unshift(`${REPORT_DIR}/${name}`);
|
|
9621
10747
|
return written;
|
|
9622
10748
|
}
|
|
@@ -9650,9 +10776,9 @@ function registerTest() {
|
|
|
9650
10776
|
|
|
9651
10777
|
// src/roles/typescript/adapters/tsc/tsc.adapter.ts
|
|
9652
10778
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
9653
|
-
import { existsSync as
|
|
10779
|
+
import { existsSync as existsSync21, readFileSync as readFileSync23 } from "node:fs";
|
|
9654
10780
|
import { createRequire } from "node:module";
|
|
9655
|
-
import { join as
|
|
10781
|
+
import { join as join33 } from "node:path";
|
|
9656
10782
|
|
|
9657
10783
|
// src/roles/typescript/presets/nest.json
|
|
9658
10784
|
var nest_default2 = {
|
|
@@ -9704,7 +10830,7 @@ var react_default2 = {
|
|
|
9704
10830
|
};
|
|
9705
10831
|
|
|
9706
10832
|
// src/roles/typescript/presets/shared.json
|
|
9707
|
-
var
|
|
10833
|
+
var shared_default3 = {
|
|
9708
10834
|
$schema: "https://json.schemastore.org/tsconfig",
|
|
9709
10835
|
compilerOptions: {
|
|
9710
10836
|
strict: true,
|
|
@@ -9730,7 +10856,7 @@ var shared_default2 = {
|
|
|
9730
10856
|
};
|
|
9731
10857
|
|
|
9732
10858
|
// src/roles/typescript/preset-data.ts
|
|
9733
|
-
var SHARED =
|
|
10859
|
+
var SHARED = shared_default3;
|
|
9734
10860
|
var LAYERS = {
|
|
9735
10861
|
nest: nest_default2,
|
|
9736
10862
|
node: node_default2,
|
|
@@ -9837,8 +10963,8 @@ function readTsconfigAdoption(cwd) {
|
|
|
9837
10963
|
}
|
|
9838
10964
|
|
|
9839
10965
|
// src/roles/typescript/adapters/tsc/plan.ts
|
|
9840
|
-
import { readFileSync as
|
|
9841
|
-
import { join as
|
|
10966
|
+
import { readFileSync as readFileSync22 } from "node:fs";
|
|
10967
|
+
import { join as join32 } from "node:path";
|
|
9842
10968
|
|
|
9843
10969
|
// src/roles/typescript/typecheck-script.ts
|
|
9844
10970
|
var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
|
|
@@ -9931,7 +11057,7 @@ function planAdoption(context2) {
|
|
|
9931
11057
|
};
|
|
9932
11058
|
}
|
|
9933
11059
|
const existing = parseJsonc(
|
|
9934
|
-
|
|
11060
|
+
readFileSync22(join32(context2.cwd, target.path), "utf8"),
|
|
9935
11061
|
target.path
|
|
9936
11062
|
);
|
|
9937
11063
|
const extendsChain = composeExtends(existing.extends, preset);
|
|
@@ -10069,7 +11195,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
10069
11195
|
let chain;
|
|
10070
11196
|
try {
|
|
10071
11197
|
const parsed = parseJsonc(
|
|
10072
|
-
|
|
11198
|
+
readFileSync23(join33(cwd, target.path), "utf8"),
|
|
10073
11199
|
target.path
|
|
10074
11200
|
);
|
|
10075
11201
|
chain = parsed.extends;
|
|
@@ -10082,7 +11208,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
10082
11208
|
);
|
|
10083
11209
|
if (preset === void 0) return void 0;
|
|
10084
11210
|
try {
|
|
10085
|
-
createRequire(
|
|
11211
|
+
createRequire(join33(cwd, "noop.js")).resolve(preset);
|
|
10086
11212
|
return void 0;
|
|
10087
11213
|
} catch {
|
|
10088
11214
|
return preset;
|
|
@@ -10178,7 +11304,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
10178
11304
|
referencedProjects(cwd, config) {
|
|
10179
11305
|
try {
|
|
10180
11306
|
const parsed = parseJsonc(
|
|
10181
|
-
|
|
11307
|
+
readFileSync23(join33(cwd, config), "utf8"),
|
|
10182
11308
|
config
|
|
10183
11309
|
);
|
|
10184
11310
|
const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path2) => typeof path2 === "string" && path2.length > 0);
|
|
@@ -10194,7 +11320,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
|
|
|
10194
11320
|
* check.
|
|
10195
11321
|
*/
|
|
10196
11322
|
typecheckTarget(cwd) {
|
|
10197
|
-
if (
|
|
11323
|
+
if (existsSync21(join33(cwd, "tsconfig.json"))) return "tsconfig.json";
|
|
10198
11324
|
const target = resolveTsconfigTarget(cwd);
|
|
10199
11325
|
return target.reason === "none" ? null : target.path;
|
|
10200
11326
|
}
|
|
@@ -10319,6 +11445,7 @@ function registerAdapters() {
|
|
|
10319
11445
|
registerBuild();
|
|
10320
11446
|
registerTest();
|
|
10321
11447
|
registerStaticAnalysis();
|
|
11448
|
+
registerEnv();
|
|
10322
11449
|
}
|
|
10323
11450
|
|
|
10324
11451
|
// src/core/detect-framework.ts
|
|
@@ -10365,12 +11492,12 @@ function diffLines(before, after) {
|
|
|
10365
11492
|
{ length: from.length + 1 },
|
|
10366
11493
|
() => new Array(to.length + 1).fill(0)
|
|
10367
11494
|
);
|
|
10368
|
-
const
|
|
11495
|
+
const cell2 = (i2, j2) => lcs[i2]?.[j2] ?? 0;
|
|
10369
11496
|
for (let i2 = from.length - 1; i2 >= 0; i2--) {
|
|
10370
11497
|
const row = lcs[i2];
|
|
10371
11498
|
if (!row) continue;
|
|
10372
11499
|
for (let j2 = to.length - 1; j2 >= 0; j2--) {
|
|
10373
|
-
row[j2] = from[i2] === to[j2] ?
|
|
11500
|
+
row[j2] = from[i2] === to[j2] ? cell2(i2 + 1, j2 + 1) + 1 : Math.max(cell2(i2 + 1, j2), cell2(i2, j2 + 1));
|
|
10374
11501
|
}
|
|
10375
11502
|
}
|
|
10376
11503
|
const out = [];
|
|
@@ -10381,7 +11508,7 @@ function diffLines(before, after) {
|
|
|
10381
11508
|
out.push(` ${from[i] ?? ""}`);
|
|
10382
11509
|
i++;
|
|
10383
11510
|
j++;
|
|
10384
|
-
} else if (
|
|
11511
|
+
} else if (cell2(i + 1, j) >= cell2(i, j + 1)) {
|
|
10385
11512
|
out.push(`- ${from[i] ?? ""}`);
|
|
10386
11513
|
i++;
|
|
10387
11514
|
} else {
|
|
@@ -10400,7 +11527,7 @@ function replaceLines(current, replacements) {
|
|
|
10400
11527
|
}
|
|
10401
11528
|
|
|
10402
11529
|
// src/core/apply-plan.ts
|
|
10403
|
-
import { existsSync as
|
|
11530
|
+
import { existsSync as existsSync22, readFileSync as readFileSync24, renameSync, rmSync as rmSync5, writeFileSync as writeFileSync9 } from "node:fs";
|
|
10404
11531
|
import { resolve as resolve4, sep as sep2 } from "node:path";
|
|
10405
11532
|
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
|
|
10406
11533
|
function resolveWithinRoot(cwd, relativePath) {
|
|
@@ -10412,7 +11539,7 @@ function resolveWithinRoot(cwd, relativePath) {
|
|
|
10412
11539
|
return absolutePath;
|
|
10413
11540
|
}
|
|
10414
11541
|
function readIfExists(absolutePath) {
|
|
10415
|
-
return
|
|
11542
|
+
return existsSync22(absolutePath) ? readFileSync24(absolutePath, "utf8") : void 0;
|
|
10416
11543
|
}
|
|
10417
11544
|
function* leaves(value, prefix = []) {
|
|
10418
11545
|
for (const [key, keyValue] of Object.entries(value)) {
|
|
@@ -10514,7 +11641,7 @@ function preparePlan(cwd, plan3) {
|
|
|
10514
11641
|
}
|
|
10515
11642
|
function writeFileAtomic(absolutePath, contents) {
|
|
10516
11643
|
const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
|
|
10517
|
-
|
|
11644
|
+
writeFileSync9(tempPath, contents);
|
|
10518
11645
|
renameSync(tempPath, absolutePath);
|
|
10519
11646
|
}
|
|
10520
11647
|
function applyPlan(cwd, plan3) {
|
|
@@ -10530,8 +11657,8 @@ function applyPlan(cwd, plan3) {
|
|
|
10530
11657
|
}
|
|
10531
11658
|
|
|
10532
11659
|
// src/core/config/preset-evidence.ts
|
|
10533
|
-
import { existsSync as
|
|
10534
|
-
import { join as
|
|
11660
|
+
import { existsSync as existsSync23, readdirSync as readdirSync3, readFileSync as readFileSync25 } from "node:fs";
|
|
11661
|
+
import { join as join34 } from "node:path";
|
|
10535
11662
|
var PATH_SIGNALS = [
|
|
10536
11663
|
{
|
|
10537
11664
|
preset: "nest",
|
|
@@ -10565,12 +11692,12 @@ function sourceImports(cwd, pattern) {
|
|
|
10565
11692
|
}
|
|
10566
11693
|
for (const entry of entries) {
|
|
10567
11694
|
if (entry.isDirectory()) {
|
|
10568
|
-
if (!NOT_SCANNED.has(entry.name)) stack.push(
|
|
11695
|
+
if (!NOT_SCANNED.has(entry.name)) stack.push(join34(dir, entry.name));
|
|
10569
11696
|
continue;
|
|
10570
11697
|
}
|
|
10571
11698
|
if (!SOURCE_FILE.test(entry.name)) continue;
|
|
10572
11699
|
try {
|
|
10573
|
-
if (pattern.test(
|
|
11700
|
+
if (pattern.test(readFileSync25(join34(dir, entry.name), "utf8"))) return true;
|
|
10574
11701
|
} catch {
|
|
10575
11702
|
continue;
|
|
10576
11703
|
}
|
|
@@ -10579,11 +11706,11 @@ function sourceImports(cwd, pattern) {
|
|
|
10579
11706
|
return false;
|
|
10580
11707
|
}
|
|
10581
11708
|
function dependencyNames(cwd) {
|
|
10582
|
-
const path2 =
|
|
10583
|
-
if (!
|
|
11709
|
+
const path2 = join34(cwd, "package.json");
|
|
11710
|
+
if (!existsSync23(path2)) return [];
|
|
10584
11711
|
try {
|
|
10585
11712
|
const manifest = parseJsonc(
|
|
10586
|
-
|
|
11713
|
+
readFileSync25(path2, "utf8"),
|
|
10587
11714
|
path2
|
|
10588
11715
|
);
|
|
10589
11716
|
return [
|
|
@@ -10606,7 +11733,7 @@ function reactTsconfigEvidence(cwd) {
|
|
|
10606
11733
|
}
|
|
10607
11734
|
for (const name of entries) {
|
|
10608
11735
|
try {
|
|
10609
|
-
const config = parseJsonc(
|
|
11736
|
+
const config = parseJsonc(readFileSync25(join34(cwd, name), "utf8"), name);
|
|
10610
11737
|
if (config.compilerOptions?.jsx !== void 0) return 'its tsconfig sets "jsx"';
|
|
10611
11738
|
const extended = Array.isArray(config.extends) ? config.extends : config.extends === void 0 ? [] : [config.extends];
|
|
10612
11739
|
if (extended.some((entry) => entry === REACT_TYPESCRIPT_PRESET)) {
|
|
@@ -10813,6 +11940,7 @@ export {
|
|
|
10813
11940
|
resolve,
|
|
10814
11941
|
buildConfigFile,
|
|
10815
11942
|
palette,
|
|
11943
|
+
ENV_SHAPES,
|
|
10816
11944
|
presetNotInstalledYet,
|
|
10817
11945
|
registerAdapters,
|
|
10818
11946
|
correctingSignal,
|