Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- `devDependencies` are allowed.
- Dependencies used only to build report assets are allowed only if compiled/bundled into shipped JS so users do not install them at runtime.
- Edit `src/`; never edit generated files in `dist/`.
- Key files: `src/cli.ts`, `src/aggregator.ts`, `src/report.ts`, `src/runners/`, `src/types/`, `src/utils.ts`.
- Key files: `src/cli.ts`, `src/aggregator.ts`, `src/report.ts`, `src/runners/`, `src/types.ts`, `src/utils.ts`, `src/httpClient.ts`, `src/maintenanceCache.ts`.
- The report UI lives in `report-ui/`; its header/filter markup is duplicated in `src/report.ts` and the two must be kept in sync. `npm run build:report` regenerates `src/report-assets.ts`.
- Scan temp output is written to `.dependency-radar/` in the target project.
- Default report output is `dependency-radar.html` at the repo root.
71 changes: 52 additions & 19 deletions README.md

Large diffs are not rendered by default.

28 changes: 19 additions & 9 deletions dist/aggregator.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ async function getGitBranch(projectPath) {
return undefined;
}
}
function findRootCauses(node, nodeMap, pkg) {
function findRootCauses(node, nodeMap) {
// If it's a direct dependency, it's its own root cause
if (isDirectDependency(node.name, pkg)) {
if (node.isDirect) {
return [{ name: node.name, version: node.version }];
}
// BFS up the parent chain to find all direct dependencies that lead to this
Expand All @@ -46,7 +46,7 @@ function findRootCauses(node, nodeMap, pkg) {
const parent = nodeMap.get(parentKey);
if (!parent)
continue;
if (isDirectDependency(parent.name, pkg)) {
if (parent.isDirect) {
rootCauses.set(parent.key, { name: parent.name, version: parent.version });
}
else {
Expand Down Expand Up @@ -315,7 +315,10 @@ async function aggregateData(input) {
const project = buildProjectMetadata(input.projectPath, projectPkg, input.projectDependencyPolicy);
// Get git branch
const gitBranch = await getGitBranch(input.projectPath);
const nodeMap = buildNodeMap((_a = input.npmLsResult) === null || _a === void 0 ? void 0 : _a.data);
const nodeMap = buildNodeMap((_a = input.npmLsResult) === null || _a === void 0 ? void 0 : _a.data, Boolean(input.workspaceEnabled));
for (const node of nodeMap.values()) {
node.isDirect = node.importerChild && isDirectDependency(node.name, pkg);
}
const vulnMap = parseVulnerabilities((_b = input.auditResult) === null || _b === void 0 ? void 0 : _b.data);
const importGraph = normalizeImportGraph((_c = input.importGraphResult) === null || _c === void 0 ? void 0 : _c.data);
const usageResult = buildUsageSummary(importGraph, input.projectPath);
Expand All @@ -335,7 +338,7 @@ async function aggregateData(input) {
const MAX_TOP_ROOT_PACKAGES = 10; // cap to keep payload size predictable
const MAX_TOP_PARENT_PACKAGES = 5; // cap for direct parents to keep payload size predictable
for (const node of nodes) {
const direct = isDirectDependency(node.name, pkg);
const direct = node.isDirect;
if (direct)
directCount += 1;
const cacheKey = `${node.name}@${node.version}`;
Expand All @@ -352,7 +355,7 @@ async function aggregateData(input) {
const licenseInfo = buildLicenseInfo(licenseSource.license, licenseSource.licenseText);
const licenseRisk = resolveLicenseRisk(licenseInfo);
// Calculate root causes (direct dependencies that cause this to be installed)
const rootCauses = findRootCauses(node, nodeMap, pkg);
const rootCauses = findRootCauses(node, nodeMap);
const packageInsights = await gatherPackageInsights(node.name, node.version, resolvePaths, packageMetaCache, packageStatCache);
if (packageInsights.nodeEngine) {
nodeEngineRanges.push(packageInsights.nodeEngine);
Expand Down Expand Up @@ -438,7 +441,7 @@ async function aggregateData(input) {
const dependencyCount = nodes.length;
const transitiveCount = dependencyCount - directCount;
const aggregated = {
schemaVersion: '1.4',
schemaVersion: '1.5',
generatedAt: new Date().toISOString(),
dependencyRadarVersion,
git: {
Expand Down Expand Up @@ -555,8 +558,11 @@ function parseMajorFromToken(token) {
const major = Number.parseInt(match[1], 10);
return Number.isNaN(major) ? undefined : major;
}
function buildNodeMap(lsData) {
function buildNodeMap(lsData, workspaceMode = false) {
const map = new Map();
// In combined workspace graphs each workspace package is a synthetic depth-1
// node, so the importer's real direct dependencies sit at traversal depth 2.
const importerDepth = workspaceMode ? 2 : 1;
const traverse = (node, depth, parentKey, providedName) => {
const nodeName = (node === null || node === void 0 ? void 0 : node.name) || providedName;
if (!node || !nodeName)
Expand All @@ -573,12 +579,16 @@ function buildNodeMap(lsData) {
children: new Set(),
childByName: new Map(),
dev: node.dev,
path: typeof node.path === 'string' ? node.path : undefined
path: typeof node.path === 'string' ? node.path : undefined,
importerChild: depth === importerDepth,
isDirect: false
});
}
else {
const existing = map.get(key);
existing.depth = Math.min(existing.depth, depth);
if (depth === importerDepth)
existing.importerChild = true;
if (parentKey)
existing.parents.add(parentKey);
if (existing.dev === undefined && node.dev !== undefined)
Expand Down
71 changes: 56 additions & 15 deletions dist/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const npmAudit_1 = require("./runners/npmAudit");
const npmLs_1 = require("./runners/npmLs");
const npmOutdated_1 = require("./runners/npmOutdated");
const npmRegistryMetadata_1 = require("./runners/npmRegistryMetadata");
const maintenanceSignals_1 = require("./runners/maintenanceSignals");
const lockfileSignals_1 = require("./runners/lockfileSignals");
const report_1 = require("./report");
const compare_1 = require("./compare");
Expand Down Expand Up @@ -934,8 +935,8 @@ function buildCombinedDependencyGraph(rootPath, packageMetas, dependencyGraphs)
*
* Recognizes an optional leading command (scan, explain, compare, why, schema), positional operands for
* commands that require them (package name for explain/why, compare path for compare), and these flags:
* --project, --quiet, --out, --keep-temp, --offline, --json, --format, --sbom, --target-node,
* --audit-signatures, --schema, --timestamp, --open, --no-report, --fail-on, --help / -h.
* --project, --quiet, --out, --keep-temp, --offline, --no-maintenance, --json, --format, --sbom,
* --target-node, --audit-signatures, --schema, --timestamp, --open, --no-report, --fail-on, --help / -h.
*
* The --offline flag disables registry-backed checks. Unknown options or unexpected positional
* arguments cause the process to exit with an error.
Expand All @@ -953,6 +954,7 @@ function parseArgs(argv) {
keepTemp: false,
audit: true,
outdated: true,
maintenance: true,
json: false,
open: false,
noReport: false,
Expand Down Expand Up @@ -1002,7 +1004,10 @@ function parseArgs(argv) {
else if (arg === "--offline") {
opts.audit = false;
opts.outdated = false;
opts.maintenance = false;
}
else if (arg === "--no-maintenance")
opts.maintenance = false;
else if (arg === "--json") {
opts.json = true;
opts.format = "json";
Expand Down Expand Up @@ -1177,16 +1182,19 @@ Options:
--timestamp Add a local timestamp to generated report filenames
--no-report Do not write HTML/JSON report files or temp artifacts to disk
--keep-temp Keep .dependency-radar folder
--offline Skip registry-backed checks (audit, outdated, signatures, targeted registry enrichment)
--offline Skip registry-backed checks (audit, outdated, signatures, maintenance signals, targeted registry enrichment)
--no-maintenance Skip registry maintenance signals (deprecated/unmaintained/archived checks)
--open Open the generated report using the system default application
--fail-on <rules> Fail with exit code 1 when selected rules are violated
Scan rules: reachable-vuln, production-vuln, high-severity-vuln,
licence-mismatch, copyleft-detected, unknown-licence,
supply-chain-source
Compare rules: new-supply-chain-signal, new-install-script,
new-native-binding, new-bin, new-direct-dependency,
new-child-process, new-network-access, new-env-access,
new-home-access, new-ssh-usage, new-obfuscation-signal,
supply-chain-source, deprecated-dependency,
unmaintained-dependency
Compare rules: new-deprecated, new-supply-chain-signal,
new-install-script, new-native-binding, new-bin,
new-direct-dependency, new-child-process,
new-network-access, new-env-access, new-home-access,
new-ssh-usage, new-obfuscation-signal,
new-bundled-dependencies, new-shrinkwrap,
new-recent-package, new-recent-version,
new-low-release-history, new-reactivated-package,
Expand Down Expand Up @@ -1763,6 +1771,7 @@ async function executeAnalysis(opts, options) {
...(toolVersions ? { toolVersions } : {}),
...(typeof opts.targetNodeMajor === "number" ? { targetNodeMajor: opts.targetNodeMajor } : {}),
});
let enrichmentTouchedData = false;
if (opts.outdated) {
let registryEnrichment = { attempted: 0, succeeded: 0 };
try {
Expand All @@ -1779,12 +1788,44 @@ async function executeAnalysis(opts, options) {
if (!opts.quiet && registryEnrichment.succeeded > 0) {
spinner.log(statusLine("✔", `Targeted registry metadata collected for ${registryEnrichment.succeeded} suspicious package${registryEnrichment.succeeded === 1 ? "" : "s"}`));
}
if (registryEnrichment.attempted > 0) {
const findings = (0, findings_1.buildDependencyFindings)(aggregated, { targetNodeMajor: opts.targetNodeMajor });
aggregated.findings = findings;
aggregated.summary.findingCount = findings.length;
if (registryEnrichment.attempted > 0)
enrichmentTouchedData = true;
}
if (opts.maintenance) {
try {
const budgetOverride = Number.parseInt(process.env.DEPENDENCY_RADAR_MAINTENANCE_BUDGET_MS || "", 10);
const maintenance = await (0, maintenanceSignals_1.enrichAggregatedWithMaintenanceSignals)(aggregated, {
projectPath,
...(Number.isFinite(budgetOverride) ? { budgetMs: budgetOverride } : {}),
});
if (maintenance.checkedNames > 0) {
enrichmentTouchedData = true;
if (!opts.quiet) {
const flagged = [
maintenance.deprecatedNames > 0 ? `${maintenance.deprecatedNames} deprecated` : undefined,
maintenance.archivedNames > 0 ? `${maintenance.archivedNames} archived` : undefined,
maintenance.unmaintainedNames > 0 ? `${maintenance.unmaintainedNames} unmaintained` : undefined,
maintenance.fromCache > 0 ? `${maintenance.fromCache} from cache` : undefined,
].filter(Boolean).join(", ");
spinner.log(statusLine("✔", `Maintenance signals: ${maintenance.checkedNames} package${maintenance.checkedNames === 1 ? "" : "s"} checked${flagged ? ` (${flagged})` : ""}`));
if (maintenance.truncatedNames > 0) {
spinner.log(statusLine("⚠", `Maintenance signals skipped ${maintenance.truncatedNames} package name${maintenance.truncatedNames === 1 ? "" : "s"} beyond the lookup cap`));
}
}
}
}
catch (err) {
if (!opts.quiet) {
const message = err instanceof Error ? err.message : String(err);
spinner.log(statusLine("⚠", `Maintenance signals unavailable (${message})`));
}
}
}
if (enrichmentTouchedData) {
const findings = (0, findings_1.buildDependencyFindings)(aggregated, { targetNodeMajor: opts.targetNodeMajor });
aggregated.findings = findings;
aggregated.summary.findingCount = findings.length;
}
dependencyCount = Object.keys(aggregated.dependencies).length;
const importGraphComplete = perPackageImportGraph.every((result) => result.ok);
const summary = buildCliSummary(aggregated, {
Expand Down Expand Up @@ -1889,7 +1930,7 @@ async function runScanCommand(opts) {
printCliSummary(result.summary);
printPolicyViolations(result.policyViolations);
if (!opts.quiet) {
console.log(`Enrich this scan with maintenance signals, upgrade readiness, and risk modelling at ${formatTerminalLink("https://www.dependency-radar.com", "https://www.dependency-radar.com")}`);
console.log(`Docs, examples, and issue reporting: ${formatTerminalLink("https://github.com/JosephMaynard/dependency-radar", "https://github.com/JosephMaynard/dependency-radar")}`);
}
if (result.policyViolations.length > 0) {
process.exit(1);
Expand Down Expand Up @@ -1962,12 +2003,12 @@ async function runCompareCommand(opts) {
const schemaVersion = parsed && typeof parsed === "object" ? parsed.schemaVersion : undefined;
if (!parsed ||
typeof parsed !== "object" ||
schemaVersion !== schema_1.REPORT_SCHEMA_VERSION ||
!schema_1.COMPATIBLE_BASELINE_SCHEMA_VERSIONS.includes(schemaVersion) ||
!parsed.project ||
!parsed.summary ||
!parsed.dependencies ||
typeof parsed.dependencies !== "object") {
console.error(`Previous report schema mismatch: expected schemaVersion ${schema_1.REPORT_SCHEMA_VERSION}, found ${schemaVersion !== null && schemaVersion !== void 0 ? schemaVersion : "missing"}.`);
console.error(`Previous report schema mismatch: expected schemaVersion ${schema_1.COMPATIBLE_BASELINE_SCHEMA_VERSIONS.join(", ")} (found ${schemaVersion !== null && schemaVersion !== void 0 ? schemaVersion : "missing"}).`);
process.exit(1);
return;
}
Expand Down
17 changes: 0 additions & 17 deletions dist/cta.js

This file was deleted.

Loading