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
38 changes: 24 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This runs a scan against the current project and writes a self-contained `depend

## What it does

- Analyses installed dependencies by running standard package manager tooling (npm, pnpm, or yarn)
- Analyses installed dependencies from lockfiles first (`pnpm-lock.yaml`, `package-lock.json`/`npm-shrinkwrap.json`, `yarn.lock`), with package-manager CLI fallback when needed
- Combines multiple signals (audit results, dependency graph data, import usage, and heuristics) into a single report
- Shows direct vs transitive dependencies, dependency depth, and parent relationships
- Highlights licences, known vulnerabilities, install-time scripts, native modules, and package footprint (including installed file counts)
Expand Down Expand Up @@ -41,26 +41,29 @@ When you run `npx dependency-radar` (or `dependency-radar scan`), the CLI execut
- Package manager from `packageManager`, lockfiles, and installed metadata
- Yarn Plug'n'Play detection (`.pnp.cjs`/`.pnp.js` or `.yarnrc.yml nodeLinker: pnp`)
3. Create a temporary `.dependency-radar/` directory inside the scanned project.
4. For each workspace package (or just the project root in single-package mode), run collectors:
- Dependency tree (`npm ls` / `pnpm list` / `yarn list`)
4. For each workspace package (or just the project root in single-package mode), collect dependency graph data:
- Lockfile-first graph parsing (`pnpm-lock.yaml`, `npm-shrinkwrap.json`/`package-lock.json`, `yarn.lock`)
- Fallback to package-manager tree commands (`npm ls` / `pnpm list` / `yarn list`) only when lockfile parsing is unavailable
- PNPM CLI fallback keeps depth retries for very large trees
5. Run additional collectors:
- Vulnerabilities (`npm audit` / `pnpm audit` / `yarn audit` or `yarn npm audit`)
- Version drift (`npm outdated` / `pnpm outdated` / `yarn outdated`, where available)
- Source import graph (static import/require parsing in `src/` or project root)
5. Normalize tool outputs into one internal shape and merge workspace package results.
- PNPM dependency trees are filtered to installed-only packages (non-installed optional/platform variants are dropped)
6. Resolve and crawl installed package directories in `node_modules` to collect local metadata:
6. Normalize outputs into one internal shape and merge workspace package results.
- PNPM lock/CLI dependency trees are filtered to installed-only packages (non-installed optional/platform variants are dropped)
7. Resolve and crawl installed package directories in `node_modules` to collect local metadata:
- Resolve `package.json` paths via package-manager-aware lookups (including PNPM virtual store layouts)
- Read local package metadata and license artifacts from installed files
7. Aggregate dependency records by enriching each installed package with:
8. Aggregate dependency records by enriching each installed package with:
- License declaration + `LICENSE` file inference/validation
- Advisory summaries and severity/risk rollups
- Root-cause/origin and runtime-impact heuristics
- Install-time execution signals
- Local package metadata (`description`, links, deprecation, TypeScript type availability, installed file count, CLI `bin` presence)
8. Write final output as either:
9. Write final output as either:
- `dependency-radar.html` (self-contained report), or
- `dependency-radar.json` (raw aggregated model)
9. Remove `.dependency-radar/` unless `--keep-temp` is set.
10. Remove `.dependency-radar/` unless `--keep-temp` is set.

The scan is local-first: package metadata is read from `node_modules`; only audit/outdated commands require registry access.

Expand All @@ -70,10 +73,17 @@ The scan is local-first: package metadata is read from `node_modules`; only audi
- Package resolution is workspace-aware and PNPM-aware, including `.pnpm` virtual store paths.
- License discovery checks common file variants such as `LICENSE`, `LICENCE`, `COPYING`, and `NOTICE` (with or without extensions like `.md`).

### Lockfile-first dependency graphing

- Dependency graph construction starts from lockfiles so deep transitive packages are captured without relying on large `* ls` JSON payloads.
- Lockfile detection is scoped to the scan root/workspace root (it does not walk outside the scanned project).
- If lockfile parsing cannot be used, Dependency Radar falls back to package-manager tree commands and continues with warnings when partial failures occur.

### PNPM workspace hardening (problems solved)

- In real PNPM workspaces, `pnpm list --json` can include optional platform dependencies that are not installed on the current machine (for example `@esbuild/linux-*` on macOS ARM64).
- Dependency Radar now verifies PNPM entries against installed artifacts (`node_modules/.pnpm` and workspace-linked `node_modules` paths) before including them in the report.
- Dependency Radar verifies PNPM entries against installed artifacts (`node_modules/.pnpm` and workspace-linked `node_modules` paths) before including them in the report.
- Dependency Radar uses `pnpm-lock.yaml` as the primary graph source and only falls back to `pnpm list` when needed, reducing OOM/string-length failures on large workspaces.
- Result: reports now reflect only dependencies that actually exist on disk and can be inspected locally.

## Usage Heuristics (`usage.runtimeImpact` and `usage.introduction`)
Expand Down Expand Up @@ -225,10 +235,10 @@ npx dependency-radar --help

## Package Manager Support

- npm: Supported for dependency tree, audit, outdated, single-package, and workspaces.
- pnpm: Supported for dependency tree, audit, outdated, and workspaces (with ls depth fallbacks for large projects).
- Yarn Classic (v1, node_modules linker): Supported for dependency tree, audit, outdated, and workspaces.
- Yarn Berry (v2+, node-modules linker): Dependency tree and audit work; outdated support depends on available Yarn commands/plugins and may be unavailable.
- npm: Supported for lockfile-first dependency tree (`npm-shrinkwrap.json` or `package-lock.json`), audit, outdated, single-package, and workspaces.
- pnpm: Supported for lockfile-first dependency tree (`pnpm-lock.yaml`), audit, outdated, and workspaces (with `pnpm list` fallback depth retries when required).
- Yarn Classic (v1, node_modules linker): Supported for lockfile-first dependency tree (`yarn.lock`), audit, outdated, and workspaces.
- Yarn Berry (v2+, node-modules linker): Supported for lockfile-first dependency tree (`yarn.lock`) and audit; outdated support depends on available Yarn commands/plugins and may be unavailable.
- Yarn Plug'n'Play (`nodeLinker: pnp`): Not supported yet.

## Scripts
Expand Down
19 changes: 19 additions & 0 deletions dist/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,14 @@ function openInBrowser(filePath) {
});
child.unref();
}
/**
* Orchestrates the CLI "scan" command to collect, merge, and output dependency data for a project or workspace.
*
* Detects workspace type and package manager, runs per-package collectors (audit, dependency tree, import graph, outdated),
* merges collected signals into a workspace-level model, and writes a JSON or HTML report to the configured output path.
* Manages a temporary working directory (created under the project as .dependency-radar), respects CLI options such as
* JSON output, audit/outdated toggles, keeping the temp directory, and optionally opening the generated output with the
* system default application. Exits the process with a non-zero code on fatal errors. */
async function run() {
var _a;
const opts = parseArgs(process.argv.slice(2));
Expand Down Expand Up @@ -1032,6 +1040,16 @@ async function run() {
process.exit(1);
return;
}
const hasProjectNodeModules = await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules"));
if (!hasProjectNodeModules) {
const workspaceHint = workspace.type === "none"
? "single project"
: `${workspace.type.toUpperCase()} workspace`;
const yarnHint = yarnPnP
? " Yarn Plug'n'Play appears enabled; Dependency Radar currently requires node_modules linker."
: "";
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}`);
}
const rootPkg = await readJsonFile(path_1.default.join(projectPath, "package.json"));
const projectDependencyPolicy = workspace.pnpmWorkspaceOverrides
? {
Expand Down Expand Up @@ -1093,6 +1111,7 @@ async function run() {
: Promise.resolve(undefined),
(0, npmLs_1.runNpmLs)(meta.path, pkgTempDir, scanManager, {
contextLabel: meta.name,
lockfileSearchRoot: projectPath,
onProgress: (line) => spinner.log(line),
}).catch((err) => ({ ok: false, error: String(err) })),
(0, importGraphRunner_1.runImportGraph)(meta.path, pkgTempDir).catch((err) => ({ ok: false, error: String(err) })),
Expand Down
3 changes: 3 additions & 0 deletions dist/generated/spdx.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ exports.SPDX_LICENSE_IDS = new Set([
'BlueOak-1.0.0',
'Boehm-GC',
'Boehm-GC-without-fee',
'BOLA-1.1',
'Borceux',
'Brian-Gladman-2-Clause',
'Brian-Gladman-3-Clause',
Expand Down Expand Up @@ -565,6 +566,7 @@ exports.SPDX_LICENSE_IDS = new Set([
'OPL-1.0',
'OPL-UK-3.0',
'OPUBL-1.0',
'OSC-1.0',
'OSET-PL-2.1',
'OSL-1.0',
'OSL-1.1',
Expand Down Expand Up @@ -839,6 +841,7 @@ exports.SPDX_EXCEPTION_IDS = new Set([
'SHL-2.0',
'SHL-2.1',
'Simple-Library-Usage-exception',
'sqlitestudio-OpenSSL-exception',
'stunnel-exception',
'SWI-exception',
'Swift-exception',
Expand Down
42 changes: 38 additions & 4 deletions dist/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,48 @@ const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const cta_1 = require("./cta");
const report_assets_1 = require("./report-assets");
/**
* Escape occurrences of closing `</style` tags in a CSS payload to prevent premature termination when inlined into HTML.
*
* @param value - The CSS text to sanitize
* @returns The sanitized string with each `</style` sequence replaced by `<\/style` (case-insensitive)
*/
function sanitizeInlineStyleTagPayload(value) {
return value.replace(/<\/style/gi, '<\\/style');
}
/**
* Escapes closing `</script` sequences so a string can be embedded safely inside an inline `<script>` tag.
*
* @param value - The script content to sanitize
* @returns The input with every `</script` (case-insensitive) replaced by `<\/script`
*/
function sanitizeInlineScriptTagPayload(value) {
return value.replace(/<\/script/gi, '<\\/script');
}
/**
* Generate the HTML report from aggregated data and write it to the given file path.
*
* @param data - Aggregated radar data used to build the report
* @param outputPath - Filesystem path where the generated HTML report will be written; parent directories are created if missing
*/
async function renderReport(data, outputPath) {
const html = buildHtml(data);
await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
await promises_1.default.writeFile(outputPath, html, 'utf8');
}
/**
* Build a complete HTML report string populated from the provided aggregated data.
*
* The returned document embeds sanitized CSS and JS assets, a JSON-serialized copy of `data` (with `<` characters escaped), a computed CTA URL derived from `data.dependencyRadarVersion`, and a human-friendly formatted `generatedAt` timestamp when parsable. Dynamic interpolations that appear in the HTML (e.g., project path, formatted date, CTA URL) are HTML-escaped.
*
* @param data - Aggregated data used to populate the report (includes project metadata, generatedAt timestamp, dependencyRadarVersion, and dependency list)
* @returns The full HTML document for the dependency radar report as a string
*/
function buildHtml(data) {
const json = JSON.stringify(data).replace(/</g, '\\u003c');
const ctaUrl = (0, cta_1.buildCtaUrl)(data.dependencyRadarVersion);
const safeCssContent = sanitizeInlineStyleTagPayload(report_assets_1.CSS_CONTENT);
const safeJsContent = sanitizeInlineScriptTagPayload(report_assets_1.JS_CONTENT);
// Format the generated date
let formattedDate = data.generatedAt;
try {
Expand Down Expand Up @@ -86,7 +120,7 @@ function buildHtml(data) {
</svg>"
>
<style>
${report_assets_1.CSS_CONTENT}
${safeCssContent}
</style>
</head>
<body>
Expand Down Expand Up @@ -294,9 +328,9 @@ ${report_assets_1.CSS_CONTENT}
</footer>

<script type="application/json" id="radar-data">${json}</script>
<script>
${report_assets_1.JS_CONTENT}
</script>
<script>
${safeJsContent}
</script>
</body>
</html>`;
}
Expand Down
Loading