dependency-radar 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +214 -119
- package/dist/cli.js +402 -50
- package/dist/failOn.js +177 -0
- package/dist/report-assets.js +2 -2
- package/dist/report.js +70 -3
- package/dist/runners/importGraphRunner.js +28 -5
- package/dist/runners/lockfileGraph.js +81 -25
- package/dist/runners/lockfileParsers.js +434 -0
- package/dist/runners/npmAudit.js +28 -11
- package/dist/runners/npmLs.js +58 -18
- package/dist/runners/npmOutdated.js +37 -16
- package/dist/utils.js +36 -1
- package/package.json +4 -8
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ const npmAudit_1 = require("./runners/npmAudit");
|
|
|
13
13
|
const npmLs_1 = require("./runners/npmLs");
|
|
14
14
|
const npmOutdated_1 = require("./runners/npmOutdated");
|
|
15
15
|
const report_1 = require("./report");
|
|
16
|
+
const failOn_1 = require("./failOn");
|
|
16
17
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
17
18
|
const utils_1 = require("./utils");
|
|
18
19
|
function normalizeSlashes(p) {
|
|
@@ -908,6 +909,16 @@ function buildCombinedDependencyGraph(rootPath, packageMetas, dependencyGraphs)
|
|
|
908
909
|
}
|
|
909
910
|
return { name: "dependency-radar-workspace", version: "0.0.0", dependencies };
|
|
910
911
|
}
|
|
912
|
+
/**
|
|
913
|
+
* Parse command-line tokens into a populated CliOptions object.
|
|
914
|
+
*
|
|
915
|
+
* Recognizes a leading non-flag token as the command and the following flags:
|
|
916
|
+
* --project, --out, --keep-temp, --offline, --json, --open, --no-report, --fail-on, and --help / -h.
|
|
917
|
+
* The --offline flag disables both audit and outdated checks.
|
|
918
|
+
*
|
|
919
|
+
* @param argv - Array of CLI tokens (typically process.argv.slice(2))
|
|
920
|
+
* @returns The resolved CliOptions with defaults applied and values overridden by argv
|
|
921
|
+
*/
|
|
911
922
|
function parseArgs(argv) {
|
|
912
923
|
const opts = {
|
|
913
924
|
command: "scan",
|
|
@@ -918,6 +929,8 @@ function parseArgs(argv) {
|
|
|
918
929
|
outdated: true,
|
|
919
930
|
json: false,
|
|
920
931
|
open: false,
|
|
932
|
+
noReport: false,
|
|
933
|
+
failOn: new Set(),
|
|
921
934
|
};
|
|
922
935
|
const args = [...argv];
|
|
923
936
|
if (args[0] && !args[0].startsWith("-")) {
|
|
@@ -941,6 +954,27 @@ function parseArgs(argv) {
|
|
|
941
954
|
opts.json = true;
|
|
942
955
|
else if (arg === "--open")
|
|
943
956
|
opts.open = true;
|
|
957
|
+
else if (arg === "--no-report")
|
|
958
|
+
opts.noReport = true;
|
|
959
|
+
else if (arg === "--fail-on") {
|
|
960
|
+
const value = args.shift();
|
|
961
|
+
if (!value) {
|
|
962
|
+
console.error("Missing value for --fail-on. Provide a comma-separated list of rules.");
|
|
963
|
+
process.exit(1);
|
|
964
|
+
}
|
|
965
|
+
let rules;
|
|
966
|
+
try {
|
|
967
|
+
rules = (0, failOn_1.parseFailOnRules)(value);
|
|
968
|
+
}
|
|
969
|
+
catch (err) {
|
|
970
|
+
console.error(err instanceof Error ? err.message : "Invalid --fail-on rules.");
|
|
971
|
+
process.exit(1);
|
|
972
|
+
return opts;
|
|
973
|
+
}
|
|
974
|
+
for (const rule of rules) {
|
|
975
|
+
opts.failOn.add(rule);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
944
978
|
else if (arg === "--help" || arg === "-h") {
|
|
945
979
|
printHelp();
|
|
946
980
|
process.exit(0);
|
|
@@ -948,6 +982,12 @@ function parseArgs(argv) {
|
|
|
948
982
|
}
|
|
949
983
|
return opts;
|
|
950
984
|
}
|
|
985
|
+
/**
|
|
986
|
+
* Print the CLI usage and available options to the console.
|
|
987
|
+
*
|
|
988
|
+
* Displays the command synopsis and descriptions for supported flags including
|
|
989
|
+
* --project, --out, --json, --no-report, --keep-temp, --offline, --open, and --fail-on.
|
|
990
|
+
*/
|
|
951
991
|
function printHelp() {
|
|
952
992
|
console.log(`dependency-radar [scan] [options]
|
|
953
993
|
|
|
@@ -957,11 +997,22 @@ Options:
|
|
|
957
997
|
--project <path> Project folder (default: cwd)
|
|
958
998
|
--out <path> Output HTML file (default: dependency-radar.html)
|
|
959
999
|
--json Write aggregated data to JSON (default filename: dependency-radar.json)
|
|
1000
|
+
--no-report Do not write HTML/JSON report files or temp artifacts to disk
|
|
960
1001
|
--keep-temp Keep .dependency-radar folder
|
|
961
1002
|
--offline Skip npm audit and npm outdated (useful for offline scans)
|
|
962
1003
|
--open Open the generated report using the system default application
|
|
1004
|
+
--fail-on <rules> Fail with exit code 1 when selected rules are violated
|
|
1005
|
+
Supported: reachable-vuln, production-vuln, high-severity-vuln,
|
|
1006
|
+
licence-mismatch, copyleft-detected, unknown-licence
|
|
963
1007
|
`);
|
|
964
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Attempts to open the given file in the system's default application.
|
|
1011
|
+
*
|
|
1012
|
+
* Spawns a detached OS-specific opener process (so the function returns immediately). If the spawn fails, a warning is logged to the console.
|
|
1013
|
+
*
|
|
1014
|
+
* @param filePath - Path (absolute or relative) to the file to open
|
|
1015
|
+
*/
|
|
965
1016
|
function openInBrowser(filePath) {
|
|
966
1017
|
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
967
1018
|
let child;
|
|
@@ -993,14 +1044,267 @@ function openInBrowser(filePath) {
|
|
|
993
1044
|
});
|
|
994
1045
|
child.unref();
|
|
995
1046
|
}
|
|
1047
|
+
const ANSI = {
|
|
1048
|
+
reset: "\x1b[0m",
|
|
1049
|
+
bold: "\x1b[1m",
|
|
1050
|
+
green: "\x1b[32m",
|
|
1051
|
+
red: "\x1b[31m",
|
|
1052
|
+
yellow: "\x1b[33m",
|
|
1053
|
+
cyan: "\x1b[36m",
|
|
1054
|
+
};
|
|
1055
|
+
/**
|
|
1056
|
+
* Determine whether ANSI color output should be enabled for the current process.
|
|
1057
|
+
*
|
|
1058
|
+
* Considers the `NO_COLOR` and `FORCE_COLOR` environment variables and falls back to whether `stdout` is a TTY.
|
|
1059
|
+
*
|
|
1060
|
+
* @returns `true` if ANSI color output should be enabled, `false` otherwise.
|
|
1061
|
+
*/
|
|
1062
|
+
function shouldUseColor() {
|
|
1063
|
+
if (process.env.NO_COLOR !== undefined)
|
|
1064
|
+
return false;
|
|
1065
|
+
const forceColor = process.env.FORCE_COLOR;
|
|
1066
|
+
if (forceColor === "0")
|
|
1067
|
+
return false;
|
|
1068
|
+
if (forceColor !== undefined)
|
|
1069
|
+
return true;
|
|
1070
|
+
return Boolean(process.stdout.isTTY);
|
|
1071
|
+
}
|
|
1072
|
+
const COLOR_ENABLED = shouldUseColor();
|
|
1073
|
+
/**
|
|
1074
|
+
* Wraps text with ANSI color or style escape sequences when terminal coloring is enabled.
|
|
1075
|
+
*
|
|
1076
|
+
* @param value - The text to style
|
|
1077
|
+
* @param color - The style to apply; one of `'bold'`, `'green'`, `'red'`, `'yellow'`, or `'cyan'`
|
|
1078
|
+
* @returns The input string wrapped with the selected ANSI escape codes if colors are enabled, otherwise the original `value`
|
|
1079
|
+
*/
|
|
1080
|
+
function styleText(value, color) {
|
|
1081
|
+
if (!COLOR_ENABLED)
|
|
1082
|
+
return value;
|
|
1083
|
+
return `${ANSI[color]}${value}${ANSI.reset}`;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Extracts the first Unicode character from a string and the remaining substring.
|
|
1087
|
+
*
|
|
1088
|
+
* @param value - The input string to split
|
|
1089
|
+
* @returns An object with `head` set to the first character (empty string if input is empty) and `tail` set to the remainder of the string after `head`
|
|
1090
|
+
*/
|
|
1091
|
+
function splitFirstGlyph(value) {
|
|
1092
|
+
const chars = Array.from(value);
|
|
1093
|
+
const head = chars[0] || "";
|
|
1094
|
+
const tail = value.slice(head.length);
|
|
1095
|
+
return { head, tail };
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Apply ANSI color styling to recognized status glyphs at the start of a string.
|
|
1099
|
+
*
|
|
1100
|
+
* Recognized leading glyphs are colored as follows: "✔" → green, "✖" → red, "⚠" → yellow,
|
|
1101
|
+
* "↗", "ℹ", "📦" → cyan, and "📉" → yellow. If the first grapheme is not one of these,
|
|
1102
|
+
* the input is returned unchanged.
|
|
1103
|
+
*
|
|
1104
|
+
* @param symbol - The string whose leading glyph should be colorized (if recognized)
|
|
1105
|
+
* @returns The input string with the leading glyph wrapped in color styling when recognized, otherwise the original string
|
|
1106
|
+
*/
|
|
1107
|
+
function colorSymbol(symbol) {
|
|
1108
|
+
const { head, tail } = splitFirstGlyph(symbol);
|
|
1109
|
+
if (!head)
|
|
1110
|
+
return symbol;
|
|
1111
|
+
if (head === "✔")
|
|
1112
|
+
return `${styleText(head, "green")}${tail}`;
|
|
1113
|
+
if (head === "✖")
|
|
1114
|
+
return `${styleText(head, "red")}${tail}`;
|
|
1115
|
+
if (head === "⚠")
|
|
1116
|
+
return `${styleText(head, "yellow")}${tail}`;
|
|
1117
|
+
if (head === "↗" || head === "ℹ" || head === "📦") {
|
|
1118
|
+
return `${styleText(head, "cyan")}${tail}`;
|
|
1119
|
+
}
|
|
1120
|
+
if (head === "📉")
|
|
1121
|
+
return `${styleText(head, "yellow")}${tail}`;
|
|
1122
|
+
return symbol;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Applies ANSI color styling to a leading status glyph in a text line when present.
|
|
1126
|
+
*
|
|
1127
|
+
* @param line - The input line; if it starts with a recognized status glyph (e.g., ✔, ✖, ⚠, ↗, ℹ, 📦, 📉), that glyph will be replaced with its colored equivalent.
|
|
1128
|
+
* @returns The line with the leading glyph colorized when applicable, or the original line unchanged.
|
|
1129
|
+
*/
|
|
1130
|
+
function colorLeadingSymbol(line) {
|
|
1131
|
+
const { head } = splitFirstGlyph(line);
|
|
1132
|
+
if (!head)
|
|
1133
|
+
return line;
|
|
1134
|
+
if (head !== "✔" &&
|
|
1135
|
+
head !== "✖" &&
|
|
1136
|
+
head !== "⚠" &&
|
|
1137
|
+
head !== "↗" &&
|
|
1138
|
+
head !== "ℹ" &&
|
|
1139
|
+
head !== "📦" &&
|
|
1140
|
+
head !== "📉") {
|
|
1141
|
+
return line;
|
|
1142
|
+
}
|
|
1143
|
+
return `${colorSymbol(head)}${line.slice(head.length)}`;
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Format a CLI status line with a colored leading symbol and message.
|
|
1147
|
+
*
|
|
1148
|
+
* @param symbol - The single-character or glyph to display as the leading symbol
|
|
1149
|
+
* @param message - The text message that follows the symbol
|
|
1150
|
+
* @returns The formatted status line with the colored symbol, a single separating space, and the message
|
|
1151
|
+
*/
|
|
1152
|
+
function statusLine(symbol, message) {
|
|
1153
|
+
return `${colorSymbol(symbol)} ${message}`;
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Print policy violation messages to stdout as a human-readable list when any exist.
|
|
1157
|
+
*
|
|
1158
|
+
* @param violations - An array of policy violations to display; each violation's `message` will be printed as a list item. If the array is empty, nothing is printed.
|
|
1159
|
+
*/
|
|
1160
|
+
function printPolicyViolations(violations) {
|
|
1161
|
+
if (violations.length === 0)
|
|
1162
|
+
return;
|
|
1163
|
+
console.log("");
|
|
1164
|
+
console.log(colorLeadingSymbol("✖ Policy violations detected:"));
|
|
1165
|
+
for (const violation of violations) {
|
|
1166
|
+
console.log(`- ${violation.message}`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Produce a concise CLI summary from aggregated workspace data.
|
|
1171
|
+
*
|
|
1172
|
+
* @param aggregated - Aggregated workspace data produced by the scan
|
|
1173
|
+
* @param options.importGraphComplete - `true` when import graph collection completed for all packages; affects unused dependency counting
|
|
1174
|
+
* @returns An object with:
|
|
1175
|
+
* - `directDeps`: number of direct dependencies in the workspace
|
|
1176
|
+
* - `transitiveDeps`: number of transitive dependencies in the workspace
|
|
1177
|
+
* - `vulnerablePackages`: count of dependencies with at least one reported vulnerability
|
|
1178
|
+
* - `reachableVulnerablePackages`: count of vulnerable dependencies that are reachable according to import usage
|
|
1179
|
+
* - `unusedInstalledDeps`: count of direct runtime dependencies that appear unused (only when `importGraphComplete` is `true`)
|
|
1180
|
+
* - `licenseMismatches`: count of dependencies whose license status is `mismatch`
|
|
1181
|
+
* - `majorUpgradeBlockers`: count of dependencies that have one or more upgrade blockers
|
|
1182
|
+
* - `majorUpgradeBlockerBreakdown`: object with counts for specific blocker types (`peerDependency`, `nodeEngine`, `deprecated`, `nativeBindings`, `installScripts`)
|
|
1183
|
+
*/
|
|
1184
|
+
function buildCliSummary(aggregated, options) {
|
|
1185
|
+
var _a;
|
|
1186
|
+
let vulnerablePackages = 0;
|
|
1187
|
+
let reachableVulnerablePackages = 0;
|
|
1188
|
+
let unusedInstalledDeps = 0;
|
|
1189
|
+
let licenseMismatches = 0;
|
|
1190
|
+
let majorUpgradeBlockers = 0;
|
|
1191
|
+
const majorUpgradeBlockerBreakdown = {
|
|
1192
|
+
peerDependency: 0,
|
|
1193
|
+
nodeEngine: 0,
|
|
1194
|
+
deprecated: 0,
|
|
1195
|
+
nativeBindings: 0,
|
|
1196
|
+
installScripts: 0,
|
|
1197
|
+
};
|
|
1198
|
+
const deps = Object.values(aggregated.dependencies || {});
|
|
1199
|
+
for (const dep of deps) {
|
|
1200
|
+
const vulnTotal = (dep.security.summary.critical || 0) +
|
|
1201
|
+
(dep.security.summary.high || 0) +
|
|
1202
|
+
(dep.security.summary.moderate || 0) +
|
|
1203
|
+
(dep.security.summary.low || 0);
|
|
1204
|
+
if (vulnTotal > 0) {
|
|
1205
|
+
vulnerablePackages += 1;
|
|
1206
|
+
if ((((_a = dep.usage.importUsage) === null || _a === void 0 ? void 0 : _a.fileCount) || 0) > 0) {
|
|
1207
|
+
reachableVulnerablePackages += 1;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// Count "unused" only when import graph collection succeeded for all packages.
|
|
1211
|
+
// `importUsage` is an optional object (or undefined), not a boolean/string state.
|
|
1212
|
+
// Otherwise, missing importUsage can mean "unknown" rather than "unused".
|
|
1213
|
+
if (options.importGraphComplete &&
|
|
1214
|
+
dep.usage.direct &&
|
|
1215
|
+
dep.usage.scope === "runtime" &&
|
|
1216
|
+
!dep.usage.importUsage) {
|
|
1217
|
+
unusedInstalledDeps += 1;
|
|
1218
|
+
}
|
|
1219
|
+
if (dep.compliance.license.status === "mismatch") {
|
|
1220
|
+
licenseMismatches += 1;
|
|
1221
|
+
}
|
|
1222
|
+
const blockers = dep.upgrade.blockers || [];
|
|
1223
|
+
if (blockers.length > 0) {
|
|
1224
|
+
majorUpgradeBlockers += 1;
|
|
1225
|
+
}
|
|
1226
|
+
if (blockers.includes("peerDependency")) {
|
|
1227
|
+
majorUpgradeBlockerBreakdown.peerDependency += 1;
|
|
1228
|
+
}
|
|
1229
|
+
if (blockers.includes("nodeEngine")) {
|
|
1230
|
+
majorUpgradeBlockerBreakdown.nodeEngine += 1;
|
|
1231
|
+
}
|
|
1232
|
+
if (blockers.includes("deprecated")) {
|
|
1233
|
+
majorUpgradeBlockerBreakdown.deprecated += 1;
|
|
1234
|
+
}
|
|
1235
|
+
if (blockers.includes("nativeBindings")) {
|
|
1236
|
+
majorUpgradeBlockerBreakdown.nativeBindings += 1;
|
|
1237
|
+
}
|
|
1238
|
+
if (blockers.includes("installScripts")) {
|
|
1239
|
+
majorUpgradeBlockerBreakdown.installScripts += 1;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return {
|
|
1243
|
+
directDeps: aggregated.summary.directCount,
|
|
1244
|
+
transitiveDeps: aggregated.summary.transitiveCount,
|
|
1245
|
+
vulnerablePackages,
|
|
1246
|
+
reachableVulnerablePackages,
|
|
1247
|
+
unusedInstalledDeps,
|
|
1248
|
+
licenseMismatches,
|
|
1249
|
+
majorUpgradeBlockers,
|
|
1250
|
+
majorUpgradeBlockerBreakdown,
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Choose the correct singular or plural form based on a numeric count.
|
|
1255
|
+
*
|
|
1256
|
+
* @param value - The numeric count that determines which form to use
|
|
1257
|
+
* @param singular - The singular form to use when `value` equals 1
|
|
1258
|
+
* @param plural - The plural form to use for any other `value`
|
|
1259
|
+
* @returns The `singular` string if `value` is 1, `plural` otherwise
|
|
1260
|
+
*/
|
|
1261
|
+
function pluralize(value, singular, plural) {
|
|
1262
|
+
return value === 1 ? singular : plural;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Print a concise, human-readable CLI summary of scan results to standard output.
|
|
1266
|
+
*
|
|
1267
|
+
* @param summary - Aggregated counts and breakdowns (dependencies, vulnerabilities, unused deps, license mismatches, and major-upgrade blocker details) used to compose the printed summary
|
|
1268
|
+
*/
|
|
1269
|
+
function printCliSummary(summary) {
|
|
1270
|
+
const bullet = "•";
|
|
1271
|
+
console.log("");
|
|
1272
|
+
console.log("Summary:");
|
|
1273
|
+
console.log(`${bullet} Direct deps scanned: ${summary.directDeps}`);
|
|
1274
|
+
console.log(`${bullet} Transitive deps scanned: ${summary.transitiveDeps}`);
|
|
1275
|
+
console.log(`${bullet} Vulnerable packages: ${summary.vulnerablePackages} (${summary.reachableVulnerablePackages} reachable)`);
|
|
1276
|
+
console.log(`${bullet} Unused installed deps: ${summary.unusedInstalledDeps}`);
|
|
1277
|
+
console.log(`${bullet} License mismatches: ${summary.licenseMismatches}`);
|
|
1278
|
+
console.log(`${bullet} Major upgrade blockers: ${summary.majorUpgradeBlockers}`);
|
|
1279
|
+
const blockerDetails = [];
|
|
1280
|
+
if (summary.majorUpgradeBlockerBreakdown.peerDependency > 0) {
|
|
1281
|
+
blockerDetails.push(` - ${summary.majorUpgradeBlockerBreakdown.peerDependency} ${pluralize(summary.majorUpgradeBlockerBreakdown.peerDependency, "strict peer dependency constraint", "strict peer dependency constraints")}`);
|
|
1282
|
+
}
|
|
1283
|
+
if (summary.majorUpgradeBlockerBreakdown.nodeEngine > 0) {
|
|
1284
|
+
blockerDetails.push(` - ${summary.majorUpgradeBlockerBreakdown.nodeEngine} ${pluralize(summary.majorUpgradeBlockerBreakdown.nodeEngine, "narrow engine range", "narrow engine ranges")}`);
|
|
1285
|
+
}
|
|
1286
|
+
if (summary.majorUpgradeBlockerBreakdown.deprecated > 0) {
|
|
1287
|
+
blockerDetails.push(` - ${summary.majorUpgradeBlockerBreakdown.deprecated} ${pluralize(summary.majorUpgradeBlockerBreakdown.deprecated, "deprecated package", "deprecated packages")}`);
|
|
1288
|
+
}
|
|
1289
|
+
if (summary.majorUpgradeBlockerBreakdown.nativeBindings > 0) {
|
|
1290
|
+
blockerDetails.push(` - ${summary.majorUpgradeBlockerBreakdown.nativeBindings} ${pluralize(summary.majorUpgradeBlockerBreakdown.nativeBindings, "native binding", "native bindings")}`);
|
|
1291
|
+
}
|
|
1292
|
+
if (summary.majorUpgradeBlockerBreakdown.installScripts > 0) {
|
|
1293
|
+
blockerDetails.push(` - ${summary.majorUpgradeBlockerBreakdown.installScripts} ${pluralize(summary.majorUpgradeBlockerBreakdown.installScripts, "install lifecycle script", "install lifecycle scripts")}`);
|
|
1294
|
+
}
|
|
1295
|
+
for (const line of blockerDetails) {
|
|
1296
|
+
console.log(line);
|
|
1297
|
+
}
|
|
1298
|
+
console.log("");
|
|
1299
|
+
}
|
|
996
1300
|
/**
|
|
997
|
-
*
|
|
1301
|
+
* Run the CLI "scan" command to collect and aggregate dependency data for a project or workspace.
|
|
998
1302
|
*
|
|
999
1303
|
* Detects workspace type and package manager, runs per-package collectors (audit, dependency tree, import graph, outdated),
|
|
1000
|
-
* merges collected signals into a workspace-level model, and writes a JSON or HTML report to
|
|
1001
|
-
* Manages a temporary working directory
|
|
1002
|
-
*
|
|
1003
|
-
|
|
1304
|
+
* merges collected signals into a workspace-level model, and writes a JSON or HTML report according to CLI options.
|
|
1305
|
+
* Manages a temporary working directory and optionally opens the generated report. Exits the process with a non-zero code
|
|
1306
|
+
* on fatal errors or when configured policy violations are detected.
|
|
1307
|
+
*/
|
|
1004
1308
|
async function run() {
|
|
1005
1309
|
var _a;
|
|
1006
1310
|
const opts = parseArgs(process.argv.slice(2));
|
|
@@ -1009,7 +1313,13 @@ async function run() {
|
|
|
1009
1313
|
process.exit(1);
|
|
1010
1314
|
return;
|
|
1011
1315
|
}
|
|
1316
|
+
const shouldWriteArtifacts = !opts.noReport;
|
|
1012
1317
|
const projectPath = path_1.default.resolve(opts.project);
|
|
1318
|
+
let summary;
|
|
1319
|
+
let policyViolations = [];
|
|
1320
|
+
if (opts.noReport && opts.keepTemp) {
|
|
1321
|
+
console.log(statusLine("⚠", "--keep-temp is ignored when --no-report is enabled."));
|
|
1322
|
+
}
|
|
1013
1323
|
if (opts.json && opts.out === "dependency-radar.html") {
|
|
1014
1324
|
opts.out = "dependency-radar.json";
|
|
1015
1325
|
}
|
|
@@ -1017,18 +1327,20 @@ async function run() {
|
|
|
1017
1327
|
const startTime = Date.now();
|
|
1018
1328
|
let dependencyCount = 0;
|
|
1019
1329
|
let outputCreated = false;
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1330
|
+
if (shouldWriteArtifacts) {
|
|
1331
|
+
try {
|
|
1332
|
+
const stat = await promises_1.default.stat(outputPath).catch(() => undefined);
|
|
1333
|
+
const endsWithSeparator = opts.out.endsWith("/") || opts.out.endsWith("\\");
|
|
1334
|
+
const hasExtension = Boolean(path_1.default.extname(outputPath));
|
|
1335
|
+
if ((stat && stat.isDirectory()) ||
|
|
1336
|
+
endsWithSeparator ||
|
|
1337
|
+
(!stat && !hasExtension)) {
|
|
1338
|
+
outputPath = path_1.default.join(outputPath, opts.json ? "dependency-radar.json" : "dependency-radar.html");
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
catch (e) {
|
|
1342
|
+
// ignore, best-effort path normalization
|
|
1028
1343
|
}
|
|
1029
|
-
}
|
|
1030
|
-
catch (e) {
|
|
1031
|
-
// ignore, best-effort path normalization
|
|
1032
1344
|
}
|
|
1033
1345
|
const tempDir = path_1.default.join(projectPath, ".dependency-radar");
|
|
1034
1346
|
// Stage 1: detect workspace/package-manager context and collect tool versions.
|
|
@@ -1048,7 +1360,7 @@ async function run() {
|
|
|
1048
1360
|
const yarnHint = yarnPnP
|
|
1049
1361
|
? " Yarn Plug'n'Play appears enabled; Dependency Radar currently requires node_modules linker."
|
|
1050
1362
|
: "";
|
|
1051
|
-
console.warn(`⚠ node_modules was not found at ${projectPath}. Scan completeness may be reduced for this ${workspaceHint}. Run your package manager install (npm install, pnpm install, or yarn install) before scanning.${yarnHint}`);
|
|
1363
|
+
console.warn(colorLeadingSymbol(`⚠ node_modules was not found at ${projectPath}. Scan completeness may be reduced for this ${workspaceHint}. Run your package manager install (npm install, pnpm install, or yarn install) before scanning.${yarnHint}`));
|
|
1052
1364
|
}
|
|
1053
1365
|
const rootPkg = await readJsonFile(path_1.default.join(projectPath, "package.json"));
|
|
1054
1366
|
const projectDependencyPolicy = workspace.pnpmWorkspaceOverrides
|
|
@@ -1087,13 +1399,15 @@ async function run() {
|
|
|
1087
1399
|
const workspaceLabel = workspace.type === "none"
|
|
1088
1400
|
? "Single project"
|
|
1089
1401
|
: `${workspace.type.toUpperCase()} workspace`;
|
|
1090
|
-
console.log(
|
|
1402
|
+
console.log(statusLine("✔", `${workspaceLabel} detected`));
|
|
1091
1403
|
if (workspace.type !== "none" && scanManager !== workspace.type) {
|
|
1092
|
-
console.log(
|
|
1404
|
+
console.log(statusLine("✔", `Using ${scanManager.toUpperCase()} for dependency data (lockfile detected)`));
|
|
1093
1405
|
}
|
|
1094
1406
|
const spinner = startSpinner(`Scanning ${workspaceLabel} at ${projectPath}`);
|
|
1095
1407
|
try {
|
|
1096
|
-
|
|
1408
|
+
if (shouldWriteArtifacts) {
|
|
1409
|
+
await (0, utils_1.ensureDir)(tempDir);
|
|
1410
|
+
}
|
|
1097
1411
|
// Stage 2: run per-package collectors and persist raw tool outputs.
|
|
1098
1412
|
const packageMetas = await readWorkspacePackageMeta(projectPath, packagePaths);
|
|
1099
1413
|
const workspaceClassification = buildWorkspaceClassification(projectPath, packageMetas);
|
|
@@ -1104,19 +1418,24 @@ async function run() {
|
|
|
1104
1418
|
for (const meta of packageMetas) {
|
|
1105
1419
|
spinner.update(`Scanning ${workspaceLabel} (${perPackageLs.length + 1}/${packageMetas.length}) at ${projectPath}`);
|
|
1106
1420
|
const pkgTempDir = path_1.default.join(tempDir, meta.name.replace(/[^a-zA-Z0-9._-]/g, "_"));
|
|
1107
|
-
|
|
1421
|
+
if (shouldWriteArtifacts) {
|
|
1422
|
+
await (0, utils_1.ensureDir)(pkgTempDir);
|
|
1423
|
+
}
|
|
1108
1424
|
const [a, l, ig, o] = await Promise.all([
|
|
1109
1425
|
opts.audit
|
|
1110
|
-
? (0, npmAudit_1.runPackageAudit)(meta.path, pkgTempDir, scanManager, yarnVersion).catch((err) => ({ ok: false, error: String(err) }))
|
|
1426
|
+
? (0, npmAudit_1.runPackageAudit)(meta.path, pkgTempDir, scanManager, yarnVersion, { persistToDisk: shouldWriteArtifacts }).catch((err) => ({ ok: false, error: String(err) }))
|
|
1111
1427
|
: Promise.resolve(undefined),
|
|
1112
1428
|
(0, npmLs_1.runNpmLs)(meta.path, pkgTempDir, scanManager, {
|
|
1113
1429
|
contextLabel: meta.name,
|
|
1114
1430
|
lockfileSearchRoot: projectPath,
|
|
1115
1431
|
onProgress: (line) => spinner.log(line),
|
|
1432
|
+
persistToDisk: shouldWriteArtifacts,
|
|
1433
|
+
}).catch((err) => ({ ok: false, error: String(err) })),
|
|
1434
|
+
(0, importGraphRunner_1.runImportGraph)(meta.path, pkgTempDir, {
|
|
1435
|
+
persistToDisk: shouldWriteArtifacts,
|
|
1116
1436
|
}).catch((err) => ({ ok: false, error: String(err) })),
|
|
1117
|
-
(0, importGraphRunner_1.runImportGraph)(meta.path, pkgTempDir).catch((err) => ({ ok: false, error: String(err) })),
|
|
1118
1437
|
opts.outdated
|
|
1119
|
-
? (0, npmOutdated_1.runPackageOutdated)(meta.path, pkgTempDir, scanManager).catch((err) => ({ ok: false, error: String(err) }))
|
|
1438
|
+
? (0, npmOutdated_1.runPackageOutdated)(meta.path, pkgTempDir, scanManager, { persistToDisk: shouldWriteArtifacts }).catch((err) => ({ ok: false, error: String(err) }))
|
|
1120
1439
|
: Promise.resolve(undefined),
|
|
1121
1440
|
]);
|
|
1122
1441
|
perPackageAudit.push(a);
|
|
@@ -1128,19 +1447,19 @@ async function run() {
|
|
|
1128
1447
|
if (opts.audit) {
|
|
1129
1448
|
const auditOk = perPackageAudit.every((r) => r && r.ok);
|
|
1130
1449
|
if (auditOk) {
|
|
1131
|
-
spinner.log(
|
|
1450
|
+
spinner.log(statusLine("✔", `${scanManager.toUpperCase()} audit data collected`));
|
|
1132
1451
|
}
|
|
1133
1452
|
else {
|
|
1134
|
-
spinner.log(
|
|
1453
|
+
spinner.log(statusLine("✖", `${scanManager.toUpperCase()} audit data unavailable`));
|
|
1135
1454
|
}
|
|
1136
1455
|
}
|
|
1137
1456
|
if (opts.outdated) {
|
|
1138
1457
|
const outdatedOk = perPackageOutdated.every((r) => r.result && r.result.ok);
|
|
1139
1458
|
if (outdatedOk) {
|
|
1140
|
-
spinner.log(
|
|
1459
|
+
spinner.log(statusLine("✔", `${scanManager.toUpperCase()} outdated data collected`));
|
|
1141
1460
|
}
|
|
1142
1461
|
else {
|
|
1143
|
-
spinner.log(
|
|
1462
|
+
spinner.log(statusLine("✖", `${scanManager.toUpperCase()} outdated data unavailable`));
|
|
1144
1463
|
}
|
|
1145
1464
|
}
|
|
1146
1465
|
const mergedAuditData = mergeAuditResults(perPackageAudit.map((r) => (r && r.ok ? r.data : undefined)));
|
|
@@ -1213,10 +1532,15 @@ async function run() {
|
|
|
1213
1532
|
...(toolVersions ? { toolVersions } : {}),
|
|
1214
1533
|
});
|
|
1215
1534
|
dependencyCount = Object.keys(aggregated.dependencies).length;
|
|
1535
|
+
const importGraphComplete = perPackageImportGraph.every((result) => result.ok);
|
|
1536
|
+
summary = buildCliSummary(aggregated, {
|
|
1537
|
+
importGraphComplete,
|
|
1538
|
+
});
|
|
1539
|
+
policyViolations = (0, failOn_1.evaluatePolicyViolations)(aggregated, opts.failOn);
|
|
1216
1540
|
if (workspace.type !== "none") {
|
|
1217
1541
|
console.log(`Detected ${workspace.type.toUpperCase()} workspace with ${packagePaths.length} package${packagePaths.length === 1 ? "" : "s"}.`);
|
|
1218
1542
|
}
|
|
1219
|
-
if (dependencyCount > 0) {
|
|
1543
|
+
if (dependencyCount > 0 && shouldWriteArtifacts) {
|
|
1220
1544
|
if (opts.json) {
|
|
1221
1545
|
await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
|
|
1222
1546
|
await promises_1.default.writeFile(outputPath, JSON.stringify(aggregated, null, 2), "utf8");
|
|
@@ -1228,12 +1552,15 @@ async function run() {
|
|
|
1228
1552
|
}
|
|
1229
1553
|
spinner.stop(true);
|
|
1230
1554
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
1231
|
-
console.log(
|
|
1232
|
-
if (
|
|
1233
|
-
console.log(
|
|
1555
|
+
console.log(statusLine("✔", `Scan complete: ${dependencyCount} dependencies analysed in ${elapsed}s`));
|
|
1556
|
+
if (!shouldWriteArtifacts) {
|
|
1557
|
+
console.log(statusLine("ℹ", "Report output disabled (--no-report); no report artifacts written."));
|
|
1558
|
+
}
|
|
1559
|
+
else if (outputCreated) {
|
|
1560
|
+
console.log(statusLine("✔", `${opts.json ? "JSON" : "Report"} written to ${outputPath}`));
|
|
1234
1561
|
}
|
|
1235
1562
|
else {
|
|
1236
|
-
console.log(
|
|
1563
|
+
console.log(statusLine("✖", `No dependencies were found - ${opts.json ? "JSON file" : "Report"} not created`));
|
|
1237
1564
|
}
|
|
1238
1565
|
}
|
|
1239
1566
|
catch (err) {
|
|
@@ -1242,25 +1569,48 @@ async function run() {
|
|
|
1242
1569
|
process.exit(1);
|
|
1243
1570
|
}
|
|
1244
1571
|
finally {
|
|
1245
|
-
if (
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1572
|
+
if (shouldWriteArtifacts) {
|
|
1573
|
+
if (!opts.keepTemp) {
|
|
1574
|
+
await (0, utils_1.removeDir)(tempDir);
|
|
1575
|
+
}
|
|
1576
|
+
else {
|
|
1577
|
+
console.log(statusLine("✔", `Temporary data kept at ${tempDir}`));
|
|
1578
|
+
}
|
|
1250
1579
|
}
|
|
1251
1580
|
}
|
|
1252
|
-
if (opts.open &&
|
|
1253
|
-
console.log(
|
|
1581
|
+
if (opts.open && !shouldWriteArtifacts) {
|
|
1582
|
+
console.log(statusLine("✖", "Skipping auto-open because --no-report is enabled."));
|
|
1583
|
+
}
|
|
1584
|
+
else if (opts.open && outputCreated && !isCI()) {
|
|
1585
|
+
console.log(statusLine("↗", `Opening ${path_1.default.basename(outputPath)} using system default ${opts.json ? "application" : "browser"}.`));
|
|
1254
1586
|
openInBrowser(outputPath);
|
|
1255
1587
|
}
|
|
1256
1588
|
else if (opts.open && outputCreated && isCI()) {
|
|
1257
|
-
console.log("✖ Skipping auto-open in CI environment.");
|
|
1589
|
+
console.log(statusLine("✖", "Skipping auto-open in CI environment."));
|
|
1590
|
+
}
|
|
1591
|
+
if (summary) {
|
|
1592
|
+
printCliSummary(summary);
|
|
1258
1593
|
}
|
|
1594
|
+
else {
|
|
1595
|
+
console.log("");
|
|
1596
|
+
}
|
|
1597
|
+
printPolicyViolations(policyViolations);
|
|
1259
1598
|
// Always show CTA as the last output
|
|
1260
|
-
console.log("");
|
|
1261
|
-
|
|
1599
|
+
console.log("Enrich this scan with maintenance signals, upgrade readiness, and risk modelling at dependency-radar.com");
|
|
1600
|
+
if (policyViolations.length > 0) {
|
|
1601
|
+
process.exit(1);
|
|
1602
|
+
}
|
|
1262
1603
|
}
|
|
1263
1604
|
run();
|
|
1605
|
+
/**
|
|
1606
|
+
* Displays a rotating CLI spinner with a message and returns controls to stop, update, or log lines.
|
|
1607
|
+
*
|
|
1608
|
+
* @param text - Initial message shown next to the spinner.
|
|
1609
|
+
* @returns An object with control methods:
|
|
1610
|
+
* - `stop(success?)` - Stops the spinner and writes a final line using a check mark when `success` is `true` or a cross when `false` (defaults to `true`).
|
|
1611
|
+
* - `update(nextText)` - Replaces the spinner's message with `nextText`.
|
|
1612
|
+
* - `log(line)` - Writes `line` as a new output line above the active spinner without stopping it.
|
|
1613
|
+
*/
|
|
1264
1614
|
function startSpinner(text) {
|
|
1265
1615
|
const frames = ["|", "/", "-", "\\"];
|
|
1266
1616
|
let i = 0;
|
|
@@ -1281,20 +1631,21 @@ function startSpinner(text) {
|
|
|
1281
1631
|
return `${head}…/${tail}`;
|
|
1282
1632
|
};
|
|
1283
1633
|
const formatLine = (prefix, value) => {
|
|
1634
|
+
const coloredPrefix = colorSymbol(prefix);
|
|
1284
1635
|
if (!process.stdout.isTTY)
|
|
1285
|
-
return `${
|
|
1636
|
+
return `${coloredPrefix} ${value}`;
|
|
1286
1637
|
const displayValue = shortenPathInMessage(value);
|
|
1287
1638
|
const columns = process.stdout.columns || 0;
|
|
1288
1639
|
if (columns <= 0)
|
|
1289
|
-
return `${
|
|
1640
|
+
return `${coloredPrefix} ${displayValue}`;
|
|
1290
1641
|
const max = columns - (prefix.length + 1);
|
|
1291
1642
|
if (max <= 0)
|
|
1292
|
-
return
|
|
1643
|
+
return coloredPrefix;
|
|
1293
1644
|
if (displayValue.length <= max)
|
|
1294
|
-
return `${
|
|
1645
|
+
return `${coloredPrefix} ${displayValue}`;
|
|
1295
1646
|
const ellipsis = "…";
|
|
1296
1647
|
const keep = Math.max(0, max - ellipsis.length);
|
|
1297
|
-
return `${
|
|
1648
|
+
return `${coloredPrefix} ${displayValue.slice(0, keep)}${ellipsis}`;
|
|
1298
1649
|
};
|
|
1299
1650
|
process.stdout.write(formatLine(frames[i], currentText));
|
|
1300
1651
|
const timer = setInterval(() => {
|
|
@@ -1316,11 +1667,12 @@ function startSpinner(text) {
|
|
|
1316
1667
|
process.stdout.write(`\r\x1b[K${formatLine(frames[i], currentText)}`);
|
|
1317
1668
|
};
|
|
1318
1669
|
const log = (line) => {
|
|
1670
|
+
const renderedLine = colorLeadingSymbol(line);
|
|
1319
1671
|
if (stopped) {
|
|
1320
|
-
process.stdout.write(`${
|
|
1672
|
+
process.stdout.write(`${renderedLine}\n`);
|
|
1321
1673
|
return;
|
|
1322
1674
|
}
|
|
1323
|
-
process.stdout.write(`\r\x1b[K${
|
|
1675
|
+
process.stdout.write(`\r\x1b[K${renderedLine}\n`);
|
|
1324
1676
|
process.stdout.write(formatLine(frames[i], currentText));
|
|
1325
1677
|
};
|
|
1326
1678
|
return { stop, update, log };
|