hermex 2.0.0-beta.5 → 2.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +212 -46
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +53 -2
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import ora from "ora";
|
|
4
3
|
import chalk from "chalk";
|
|
5
4
|
import Table from "cli-table3";
|
|
6
5
|
import fs, { existsSync, readFileSync } from "node:fs";
|
|
@@ -18,6 +17,7 @@ import lockfile from "@yarnpkg/lockfile";
|
|
|
18
17
|
import { randomUUID } from "node:crypto";
|
|
19
18
|
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
20
19
|
import { homedir } from "node:os";
|
|
20
|
+
import ora from "ora";
|
|
21
21
|
//#region src/utils/format-utils.ts
|
|
22
22
|
/**
|
|
23
23
|
* Format a number with thousand separators
|
|
@@ -302,7 +302,9 @@ function formatRuleType(type) {
|
|
|
302
302
|
case "require_packages": return "require_packages";
|
|
303
303
|
case "require_scripts": return "require_scripts";
|
|
304
304
|
case "require_package_fields": return "pkg_fields";
|
|
305
|
+
case "forbid_package_fields": return "pkg_fields";
|
|
305
306
|
case "engine_version": return "engine_version";
|
|
307
|
+
case "codeowners": return "codeowners";
|
|
306
308
|
}
|
|
307
309
|
}
|
|
308
310
|
function ruleIcon(violation) {
|
|
@@ -321,11 +323,21 @@ function describeViolation(v) {
|
|
|
321
323
|
if (v.type === "require_packages") return `${patterns} not installed${suffix}`;
|
|
322
324
|
if (v.type === "forbid_packages") return `${patterns} is forbidden${suffix}`;
|
|
323
325
|
if (v.type === "require_scripts") return `script ${patterns} missing in package.json${suffix}`;
|
|
324
|
-
if (v.type === "require_package_fields")
|
|
326
|
+
if (v.type === "require_package_fields") {
|
|
327
|
+
if (v.fieldPath && v.actualValue !== void 0) return `field ${v.fieldPath} is ${chalk.yellow(v.actualValue)}, does not match required value${suffix}`;
|
|
328
|
+
return `field ${patterns} missing in package.json${suffix}`;
|
|
329
|
+
}
|
|
330
|
+
if (v.type === "forbid_package_fields") return `field ${v.fieldPath ?? patterns} is forbidden in package.json${suffix}`;
|
|
325
331
|
if (v.type === "engine_version") {
|
|
326
332
|
if (!v.installedRange) return `engines.node not specified (required ${v.requiredRange})${suffix}`;
|
|
327
333
|
return `engines.node is ${chalk.yellow(v.installedRange)}, required ${chalk.cyan(v.requiredRange)}${suffix}`;
|
|
328
334
|
}
|
|
335
|
+
if (v.type === "codeowners") {
|
|
336
|
+
if (v.matchedFiles.length === 0) return `CODEOWNERS not found (looked in ${patterns})${suffix}`;
|
|
337
|
+
const shown = v.matchedFiles.slice(0, 3).join(", ");
|
|
338
|
+
const more = v.matchedFiles.length > 3 ? ` and ${v.matchedFiles.length - 3} more` : "";
|
|
339
|
+
return `${v.matchedFiles.length} scanned file(s) have no owner: ${shown}${more}${suffix}`;
|
|
340
|
+
}
|
|
329
341
|
return `${patterns} not present${suffix}`;
|
|
330
342
|
}
|
|
331
343
|
function printRules(aggregated) {
|
|
@@ -420,11 +432,19 @@ const RuleConfigSchema = z.object({
|
|
|
420
432
|
message: z.string().optional()
|
|
421
433
|
});
|
|
422
434
|
const RuleConfigOrArraySchema = z.union([RuleConfigSchema, z.array(RuleConfigSchema)]);
|
|
435
|
+
const PackageFieldRuleSchema = RuleConfigSchema.extend({
|
|
436
|
+
/** Optional micromatch patterns the field's stringified value must match */
|
|
437
|
+
values: z.array(z.string()).optional() });
|
|
438
|
+
const PackageFieldRuleOrArraySchema = z.union([PackageFieldRuleSchema, z.array(PackageFieldRuleSchema)]);
|
|
423
439
|
const EngineVersionRuleSchema = z.object({
|
|
424
440
|
severity: RuleSeveritySchema,
|
|
425
441
|
range: z.string(),
|
|
426
442
|
message: z.string().optional()
|
|
427
443
|
});
|
|
444
|
+
const CodeownersRuleSchema = z.object({
|
|
445
|
+
severity: RuleSeveritySchema,
|
|
446
|
+
message: z.string().optional()
|
|
447
|
+
});
|
|
428
448
|
const ThresholdSchema = z.union([z.number(), z.literal(false)]);
|
|
429
449
|
const HermexConfigSchema = z.object({
|
|
430
450
|
includes: z.array(z.string()).default(["**/*.{tsx,jsx,ts,js}"]),
|
|
@@ -450,15 +470,18 @@ const HermexConfigSchema = z.object({
|
|
|
450
470
|
forbid_packages: RuleConfigOrArraySchema.default([]),
|
|
451
471
|
require_packages: RuleConfigOrArraySchema.default([]),
|
|
452
472
|
require_scripts: RuleConfigOrArraySchema.default([]),
|
|
453
|
-
require_package_fields:
|
|
454
|
-
|
|
473
|
+
require_package_fields: PackageFieldRuleOrArraySchema.default([]),
|
|
474
|
+
forbid_package_fields: PackageFieldRuleOrArraySchema.default([]),
|
|
475
|
+
engine_version: z.union([EngineVersionRuleSchema, z.array(EngineVersionRuleSchema)]).optional(),
|
|
476
|
+
codeowners: CodeownersRuleSchema.optional()
|
|
455
477
|
}).default(() => ({
|
|
456
478
|
detect_files: [],
|
|
457
479
|
require_files: [],
|
|
458
480
|
forbid_packages: [],
|
|
459
481
|
require_packages: [],
|
|
460
482
|
require_scripts: [],
|
|
461
|
-
require_package_fields: []
|
|
483
|
+
require_package_fields: [],
|
|
484
|
+
forbid_package_fields: []
|
|
462
485
|
})),
|
|
463
486
|
output: z.object({
|
|
464
487
|
summary: z.union([z.literal("log"), z.literal(false)]).default("log"),
|
|
@@ -870,10 +893,10 @@ function analyzeConditionalExpression(node, state) {
|
|
|
870
893
|
*/
|
|
871
894
|
function analyzeArrayExpression(node, state) {
|
|
872
895
|
if (node.elements?.some((elem) => {
|
|
873
|
-
if (elem?.type === "Identifier") return state.componentNames.has(elem.value);
|
|
896
|
+
if (elem?.expression?.type === "Identifier") return state.componentNames.has(elem.expression.value);
|
|
874
897
|
return false;
|
|
875
898
|
})) state.usagePatterns.arrayMappings.add({
|
|
876
|
-
components: node.elements?.map((elem) => elem?.value).filter(Boolean),
|
|
899
|
+
components: node.elements?.map((elem) => elem?.expression?.value).filter(Boolean),
|
|
877
900
|
line: node.span?.start || 0
|
|
878
901
|
});
|
|
879
902
|
}
|
|
@@ -899,11 +922,11 @@ function analyzeObjectExpression(node, state) {
|
|
|
899
922
|
* Analyzes React.lazy() imports
|
|
900
923
|
*/
|
|
901
924
|
function analyzeLazyImport(node, state) {
|
|
902
|
-
const arg = node.arguments?.[0];
|
|
925
|
+
const arg = node.arguments?.[0]?.expression;
|
|
903
926
|
if (arg?.type === "ArrowFunctionExpression" && arg.body?.type === "CallExpression") {
|
|
904
927
|
const importCall = arg.body;
|
|
905
928
|
if (importCall.callee?.type === "Import") {
|
|
906
|
-
const source = importCall.arguments?.[0]?.value;
|
|
929
|
+
const source = importCall.arguments?.[0]?.expression?.value;
|
|
907
930
|
if (source) state.usagePatterns.lazyImports.add({
|
|
908
931
|
source,
|
|
909
932
|
line: node.span?.start || 0
|
|
@@ -915,7 +938,7 @@ function analyzeLazyImport(node, state) {
|
|
|
915
938
|
* Analyzes dynamic import() calls
|
|
916
939
|
*/
|
|
917
940
|
function analyzeDynamicImport(node, state) {
|
|
918
|
-
const source = node.arguments?.[0]?.value;
|
|
941
|
+
const source = node.arguments?.[0]?.expression?.value;
|
|
919
942
|
if (source) state.usagePatterns.dynamicImports.add({
|
|
920
943
|
source,
|
|
921
944
|
line: node.span?.start || 0
|
|
@@ -929,7 +952,7 @@ function analyzeDynamicImport(node, state) {
|
|
|
929
952
|
function analyzeHOCUsage(node, state) {
|
|
930
953
|
state.usagePatterns.hocUsage.add({
|
|
931
954
|
function: node.callee?.value || "[unknown]",
|
|
932
|
-
component: node.arguments?.[0]?.value || "[unknown]",
|
|
955
|
+
component: node.arguments?.[0]?.expression?.value || "[unknown]",
|
|
933
956
|
line: node.span?.start || 0
|
|
934
957
|
});
|
|
935
958
|
}
|
|
@@ -937,7 +960,7 @@ function analyzeHOCUsage(node, state) {
|
|
|
937
960
|
* Analyzes React.memo() usage
|
|
938
961
|
*/
|
|
939
962
|
function analyzeMemoUsage(node, state) {
|
|
940
|
-
const component = node.arguments?.[0];
|
|
963
|
+
const component = node.arguments?.[0]?.expression;
|
|
941
964
|
if (component?.type === "Identifier" && state.componentNames.has(component.value)) state.usagePatterns.memoizedComponents.add({
|
|
942
965
|
component: component.value,
|
|
943
966
|
line: node.span?.start || 0
|
|
@@ -968,7 +991,7 @@ function analyzeMemberExpression(node, state) {
|
|
|
968
991
|
* Checks if a node represents HOC pattern
|
|
969
992
|
*/
|
|
970
993
|
function isHOCPattern(node, state) {
|
|
971
|
-
return node.callee?.type === "Identifier" && node.arguments?.some((arg) => arg.type === "Identifier" && state.componentNames.has(arg.value));
|
|
994
|
+
return node.callee?.type === "Identifier" && node.arguments?.some((arg) => arg.expression?.type === "Identifier" && state.componentNames.has(arg.expression.value));
|
|
972
995
|
}
|
|
973
996
|
//#endregion
|
|
974
997
|
//#region src/swc-parser/core/visitor.ts
|
|
@@ -1075,8 +1098,9 @@ function visitChildren(node, state, context) {
|
|
|
1075
1098
|
/**
|
|
1076
1099
|
* Generates a comprehensive usage report from parser state
|
|
1077
1100
|
*/
|
|
1078
|
-
function generateReport(state) {
|
|
1101
|
+
function generateReport(state, filePath) {
|
|
1079
1102
|
return {
|
|
1103
|
+
filePath,
|
|
1080
1104
|
summary: {
|
|
1081
1105
|
totalImports: state.usagePatterns.defaultImports.size + state.usagePatterns.namedImports.size + state.usagePatterns.namespaceImports.size,
|
|
1082
1106
|
totalComponents: state.componentNames.size,
|
|
@@ -1157,7 +1181,7 @@ function swcOptionsForFile(filePath) {
|
|
|
1157
1181
|
function parseCode(code, filePath = "file.tsx") {
|
|
1158
1182
|
const state = createState();
|
|
1159
1183
|
visitNode(parseSync(code, swcOptionsForFile(filePath)), state);
|
|
1160
|
-
return generateReport(state);
|
|
1184
|
+
return generateReport(state, filePath);
|
|
1161
1185
|
}
|
|
1162
1186
|
function parseFile(filePath) {
|
|
1163
1187
|
return parseCode(fs.readFileSync(filePath, "utf8"), filePath);
|
|
@@ -1296,18 +1320,63 @@ function evaluateScriptRules(repoPath, rulesConfig) {
|
|
|
1296
1320
|
}
|
|
1297
1321
|
//#endregion
|
|
1298
1322
|
//#region src/rules/package-field-rules.ts
|
|
1323
|
+
/** Resolves a dot-path like "engines.node" against the manifest object. */
|
|
1324
|
+
function getFieldAtPath(pkg, path) {
|
|
1325
|
+
let current = pkg;
|
|
1326
|
+
for (const key of path.split(".")) {
|
|
1327
|
+
if (current === null || typeof current !== "object" || !(key in current)) return {
|
|
1328
|
+
exists: false,
|
|
1329
|
+
value: void 0
|
|
1330
|
+
};
|
|
1331
|
+
current = current[key];
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
exists: true,
|
|
1335
|
+
value: current
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
/** Primitives compare by string form; objects/arrays never match a value pattern. */
|
|
1339
|
+
function valueMatches(value, valuePatterns) {
|
|
1340
|
+
if (value === null || typeof value === "object") return false;
|
|
1341
|
+
return micromatch.isMatch(String(value), valuePatterns);
|
|
1342
|
+
}
|
|
1299
1343
|
function evaluatePackageFieldRules(repoPath, rulesConfig) {
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1344
|
+
const requireRules = toArray(rulesConfig.require_package_fields);
|
|
1345
|
+
const forbidRules = toArray(rulesConfig.forbid_package_fields);
|
|
1346
|
+
if (requireRules.length === 0 && forbidRules.length === 0) return [];
|
|
1302
1347
|
const pkg = readPackageJson(repoPath);
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1348
|
+
const violations = [];
|
|
1349
|
+
for (const rule of requireRules) {
|
|
1350
|
+
const lookups = rule.patterns.map((p) => ({
|
|
1351
|
+
path: p,
|
|
1352
|
+
...getFieldAtPath(pkg, p)
|
|
1353
|
+
}));
|
|
1354
|
+
if (!lookups.some((l) => l.exists && (!rule.values || valueMatches(l.value, rule.values)))) {
|
|
1355
|
+
const mismatch = rule.values ? lookups.find((l) => l.exists) : void 0;
|
|
1356
|
+
violations.push({
|
|
1357
|
+
type: "require_package_fields",
|
|
1358
|
+
severity: rule.severity,
|
|
1359
|
+
patterns: rule.patterns,
|
|
1360
|
+
message: rule.message,
|
|
1361
|
+
matchedFiles: [],
|
|
1362
|
+
fieldPath: mismatch?.path,
|
|
1363
|
+
actualValue: mismatch && typeof mismatch.value !== "object" ? String(mismatch.value) : void 0
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
for (const rule of forbidRules) for (const pattern of rule.patterns) {
|
|
1368
|
+
const lookup = getFieldAtPath(pkg, pattern);
|
|
1369
|
+
if (lookup.exists && (!rule.values || valueMatches(lookup.value, rule.values))) violations.push({
|
|
1370
|
+
type: "forbid_package_fields",
|
|
1371
|
+
severity: rule.severity,
|
|
1372
|
+
patterns: rule.patterns,
|
|
1373
|
+
message: rule.message,
|
|
1374
|
+
matchedFiles: [],
|
|
1375
|
+
fieldPath: pattern,
|
|
1376
|
+
actualValue: lookup.value !== null && typeof lookup.value !== "object" ? String(lookup.value) : void 0
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
return violations;
|
|
1311
1380
|
}
|
|
1312
1381
|
//#endregion
|
|
1313
1382
|
//#region src/rules/engine-version.ts
|
|
@@ -1338,13 +1407,102 @@ function evaluateEngineVersion(repoPath, rulesConfig) {
|
|
|
1338
1407
|
});
|
|
1339
1408
|
}
|
|
1340
1409
|
//#endregion
|
|
1410
|
+
//#region src/rules/codeowners.ts
|
|
1411
|
+
const CODEOWNERS_LOCATIONS = [
|
|
1412
|
+
".github/CODEOWNERS",
|
|
1413
|
+
"CODEOWNERS",
|
|
1414
|
+
"docs/CODEOWNERS"
|
|
1415
|
+
];
|
|
1416
|
+
/** First existing location, GitHub search order. Null if none exist. */
|
|
1417
|
+
function findCodeownersFile(repoPath) {
|
|
1418
|
+
for (const location of CODEOWNERS_LOCATIONS) {
|
|
1419
|
+
const full = path$1.join(repoPath, location);
|
|
1420
|
+
if (fs$1.existsSync(full)) return full;
|
|
1421
|
+
}
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
/** Parses content into entries; skips blank lines and # comments. */
|
|
1425
|
+
function parseCodeowners(content) {
|
|
1426
|
+
const entries = [];
|
|
1427
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
1428
|
+
const line = rawLine.trim();
|
|
1429
|
+
if (!line || line.startsWith("#")) continue;
|
|
1430
|
+
const tokens = line.split(/\s+/);
|
|
1431
|
+
const pattern = tokens[0];
|
|
1432
|
+
const owners = tokens.slice(1);
|
|
1433
|
+
entries.push({
|
|
1434
|
+
pattern,
|
|
1435
|
+
globs: codeownersPatternToGlobs(pattern),
|
|
1436
|
+
owners
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
return entries;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Translates a CODEOWNERS (gitignore-style) pattern into micromatch glob(s).
|
|
1443
|
+
*
|
|
1444
|
+
* Rules:
|
|
1445
|
+
* - `*` alone matches everything.
|
|
1446
|
+
* - A leading `/`, or a `/` anywhere in the middle of the pattern, anchors
|
|
1447
|
+
* the match to the repo root (the leading `/` is stripped once anchored).
|
|
1448
|
+
* - A pattern with no anchoring gets a `**\/` prefix so it matches at any depth.
|
|
1449
|
+
* - A trailing `/` restricts the match to a directory, so `/**` is appended.
|
|
1450
|
+
* - A bare name (no glob characters, no slash at all) also matches as a
|
|
1451
|
+
* directory anywhere, so the `/**` directory variant is added alongside
|
|
1452
|
+
* the plain match.
|
|
1453
|
+
*/
|
|
1454
|
+
function codeownersPatternToGlobs(pattern) {
|
|
1455
|
+
if (pattern === "*") return ["**"];
|
|
1456
|
+
let p = pattern;
|
|
1457
|
+
const hadLeadingSlash = p.startsWith("/");
|
|
1458
|
+
if (hadLeadingSlash) p = p.slice(1);
|
|
1459
|
+
const hadTrailingSlash = p.endsWith("/") && p.length > 1;
|
|
1460
|
+
if (hadTrailingSlash) p = p.slice(0, -1);
|
|
1461
|
+
const hasInternalSlash = p.includes("/");
|
|
1462
|
+
const anchored = hadLeadingSlash || hasInternalSlash;
|
|
1463
|
+
const hasGlobChars = /[*?[\]{}!()]/.test(p);
|
|
1464
|
+
const isBareName = !anchored && !hasGlobChars && !hadTrailingSlash;
|
|
1465
|
+
const base = anchored ? p : `**/${p}`;
|
|
1466
|
+
const globs = [hadTrailingSlash ? `${base}/**` : base];
|
|
1467
|
+
if (isBareName) globs.push(`${base}/**`);
|
|
1468
|
+
return globs;
|
|
1469
|
+
}
|
|
1470
|
+
/** Last matching entry wins; owned iff that entry has >= 1 owner. */
|
|
1471
|
+
function fileIsOwned(file, entries) {
|
|
1472
|
+
for (let i = entries.length - 1; i >= 0; i--) if (micromatch.isMatch(file, entries[i].globs, { dot: true })) return entries[i].owners.length > 0;
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
function evaluateCodeowners(repoPath, rulesConfig, scannedFiles) {
|
|
1476
|
+
const rule = rulesConfig.codeowners;
|
|
1477
|
+
if (!rule) return [];
|
|
1478
|
+
const filePath = findCodeownersFile(repoPath);
|
|
1479
|
+
if (!filePath) return [{
|
|
1480
|
+
type: "codeowners",
|
|
1481
|
+
severity: rule.severity,
|
|
1482
|
+
patterns: CODEOWNERS_LOCATIONS,
|
|
1483
|
+
message: rule.message,
|
|
1484
|
+
matchedFiles: []
|
|
1485
|
+
}];
|
|
1486
|
+
const entries = parseCodeowners(fs$1.readFileSync(filePath, "utf-8"));
|
|
1487
|
+
const unowned = scannedFiles.map((f) => (path$1.isAbsolute(f) ? path$1.relative(repoPath, f) : f).replace(/\\/g, "/")).filter((f) => !fileIsOwned(f, entries));
|
|
1488
|
+
if (unowned.length === 0) return [];
|
|
1489
|
+
return [{
|
|
1490
|
+
type: "codeowners",
|
|
1491
|
+
severity: rule.severity,
|
|
1492
|
+
patterns: [path$1.basename(filePath)],
|
|
1493
|
+
message: rule.message,
|
|
1494
|
+
matchedFiles: unowned
|
|
1495
|
+
}];
|
|
1496
|
+
}
|
|
1497
|
+
//#endregion
|
|
1341
1498
|
//#region src/rules/evaluator.ts
|
|
1342
|
-
function evaluateRules(repoPath, rulesConfig, excludes) {
|
|
1499
|
+
function evaluateRules(repoPath, rulesConfig, excludes, scannedFiles = []) {
|
|
1343
1500
|
return [
|
|
1344
1501
|
...evaluateFileRules(repoPath, rulesConfig, excludes),
|
|
1345
1502
|
...evaluateScriptRules(repoPath, rulesConfig),
|
|
1346
1503
|
...evaluatePackageFieldRules(repoPath, rulesConfig),
|
|
1347
|
-
...evaluateEngineVersion(repoPath, rulesConfig)
|
|
1504
|
+
...evaluateEngineVersion(repoPath, rulesConfig),
|
|
1505
|
+
...evaluateCodeowners(repoPath, rulesConfig, scannedFiles)
|
|
1348
1506
|
];
|
|
1349
1507
|
}
|
|
1350
1508
|
//#endregion
|
|
@@ -1462,14 +1620,16 @@ function aggregateReports(reports, versions = {}, config, multiVersions = {}) {
|
|
|
1462
1620
|
for (const jsx of report.patterns.usage.jsx) {
|
|
1463
1621
|
const key = jsx.component;
|
|
1464
1622
|
const existing = componentUsageMap.get(key);
|
|
1465
|
-
if (existing)
|
|
1466
|
-
|
|
1623
|
+
if (existing) {
|
|
1624
|
+
existing.count++;
|
|
1625
|
+
existing.files.add(report.filePath);
|
|
1626
|
+
} else {
|
|
1467
1627
|
const source = findComponentSource(jsx.component, report, availablePackages);
|
|
1468
1628
|
componentUsageMap.set(key, {
|
|
1469
1629
|
name: jsx.component,
|
|
1470
1630
|
source,
|
|
1471
1631
|
count: 1,
|
|
1472
|
-
files: /* @__PURE__ */ new Set()
|
|
1632
|
+
files: /* @__PURE__ */ new Set([report.filePath])
|
|
1473
1633
|
});
|
|
1474
1634
|
}
|
|
1475
1635
|
}
|
|
@@ -1981,7 +2141,7 @@ async function runPipeline(config, spinner) {
|
|
|
1981
2141
|
spinner.succeed(chalk.green(`Analysis complete! Analyzed ${reports.length}/${files.length} files`));
|
|
1982
2142
|
printErrors(parseErrors);
|
|
1983
2143
|
const aggregated = aggregateReports(reports, lockfileResult.versions, config, lockfileResult.multiVersions);
|
|
1984
|
-
const evaluatorViolations = evaluateRules(process.cwd(), config.rules, config.excludes);
|
|
2144
|
+
const evaluatorViolations = evaluateRules(process.cwd(), config.rules, config.excludes, files);
|
|
1985
2145
|
aggregated.ruleViolations = [...aggregated.ruleViolations, ...evaluatorViolations];
|
|
1986
2146
|
if (config.releaseAge.enabled) {
|
|
1987
2147
|
spinner.start("Fetching release age from registry...");
|
|
@@ -1992,6 +2152,24 @@ async function runPipeline(config, spinner) {
|
|
|
1992
2152
|
return aggregated;
|
|
1993
2153
|
}
|
|
1994
2154
|
//#endregion
|
|
2155
|
+
//#region src/commands/command-context.ts
|
|
2156
|
+
/**
|
|
2157
|
+
* Shared command preamble: routes human-readable chrome (version line,
|
|
2158
|
+
* spinner) to stderr when the command emits JSON on stdout.
|
|
2159
|
+
*/
|
|
2160
|
+
function createCommandContext(config) {
|
|
2161
|
+
const isJson = config.output.format === "json";
|
|
2162
|
+
const stream = isJson ? process.stderr : process.stdout;
|
|
2163
|
+
stream.write(chalk.gray(`hermex v${getVersion()}\n`));
|
|
2164
|
+
return {
|
|
2165
|
+
isJson,
|
|
2166
|
+
spinner: ora({
|
|
2167
|
+
text: "Parsing lockfile...",
|
|
2168
|
+
stream
|
|
2169
|
+
}).start()
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
//#endregion
|
|
1995
2173
|
//#region src/commands/scan.ts
|
|
1996
2174
|
function registerScanCommand(program) {
|
|
1997
2175
|
program.command("scan").description("Scan and analyze local files").option("--config <path>", "Path to hermex config file (overrides CWD discovery)").action(async (options) => {
|
|
@@ -1999,12 +2177,7 @@ function registerScanCommand(program) {
|
|
|
1999
2177
|
});
|
|
2000
2178
|
}
|
|
2001
2179
|
async function executeScan(config) {
|
|
2002
|
-
const isJson = config
|
|
2003
|
-
(isJson ? process.stderr : process.stdout).write(chalk.gray(`hermex v${getVersion()}\n`));
|
|
2004
|
-
const spinner = ora({
|
|
2005
|
-
text: "Parsing lockfile...",
|
|
2006
|
-
stream: isJson ? process.stderr : process.stdout
|
|
2007
|
-
}).start();
|
|
2180
|
+
const { isJson, spinner } = createCommandContext(config);
|
|
2008
2181
|
try {
|
|
2009
2182
|
const aggregated = await runPipeline(config, spinner);
|
|
2010
2183
|
if (!aggregated) return;
|
|
@@ -2013,8 +2186,7 @@ async function executeScan(config) {
|
|
|
2013
2186
|
} catch (error) {
|
|
2014
2187
|
const message = error instanceof Error ? error.message : String(error);
|
|
2015
2188
|
spinner.fail(chalk.red("Analysis failed: " + message));
|
|
2016
|
-
|
|
2017
|
-
process.exit(1);
|
|
2189
|
+
process.exitCode = 1;
|
|
2018
2190
|
}
|
|
2019
2191
|
}
|
|
2020
2192
|
function printScanResults(aggregated, config) {
|
|
@@ -2073,12 +2245,7 @@ function registerComplyCommand(program) {
|
|
|
2073
2245
|
});
|
|
2074
2246
|
}
|
|
2075
2247
|
async function executeComply(config) {
|
|
2076
|
-
const isJson = config
|
|
2077
|
-
(isJson ? process.stderr : process.stdout).write(chalk.gray(`hermex v${getVersion()}\n`));
|
|
2078
|
-
const spinner = ora({
|
|
2079
|
-
text: "Parsing lockfile...",
|
|
2080
|
-
stream: isJson ? process.stderr : process.stdout
|
|
2081
|
-
}).start();
|
|
2248
|
+
const { isJson, spinner } = createCommandContext(config);
|
|
2082
2249
|
try {
|
|
2083
2250
|
const aggregated = await runPipeline(config, spinner);
|
|
2084
2251
|
if (!aggregated) {
|
|
@@ -2096,7 +2263,6 @@ async function executeComply(config) {
|
|
|
2096
2263
|
} catch (error) {
|
|
2097
2264
|
const message = error instanceof Error ? error.message : String(error);
|
|
2098
2265
|
spinner.fail(chalk.red("Compliance check failed: " + message));
|
|
2099
|
-
console.error(error);
|
|
2100
2266
|
process.exitCode = 2;
|
|
2101
2267
|
}
|
|
2102
2268
|
}
|