Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e0476b0
Scanner: measured install size, extension buckets, platform constrain…
JosephMaynard Aug 15, 2026
fe54b47
Graph highlights: "No imports found" and "Duplicate versions"
JosephMaynard Aug 15, 2026
d3d55a7
Removal simulator, dossier cost facts, list filters, fine print, hove…
JosephMaynard Aug 15, 2026
393a6f0
Fix review findings: collector honesty, dup matching, hidden-view crash
JosephMaynard Aug 15, 2026
7acf519
Address Sol review: wire new list filters, partial-scan honesty, filt…
JosephMaynard Aug 15, 2026
bb13063
Hash router, fullscreen mode, list-view simulator, sim panel, toolbar…
JosephMaynard Aug 16, 2026
f2c1d4e
Fullscreen centering, instant sim summaries, shared sim cache, route …
JosephMaynard Aug 16, 2026
662bb23
Root workspace truthfulness, list-view fine print, balloon tip label
JosephMaynard Aug 16, 2026
0e9d57e
Fix install-size row font and fine-print popover stacking in list view
JosephMaynard Aug 16, 2026
f34b8ed
Label the aggregate workspace honestly: "Whole project" vs root packa…
JosephMaynard Aug 16, 2026
3f3b9a6
Address external review: router back-reconciliation, aggregate manife…
JosephMaynard Aug 16, 2026
33d5656
Centre labels name the selected workspace, balloon label balanced
JosephMaynard Aug 16, 2026
3be2074
Classic graph: remove dead popover, route-parity selection, subtle im…
JosephMaynard Aug 16, 2026
5ad3e44
Revert adaptive column gap; soften impact sizing
JosephMaynard Aug 16, 2026
b3c748b
Remove em-dashes from all user-facing copy
JosephMaynard Aug 16, 2026
bbea4d9
Update README and screenshots for the v1.2 feature branch
JosephMaynard Aug 16, 2026
2d3d69c
Fix self-review findings: router integrity, fullscreen gaps, root sco…
JosephMaynard Aug 16, 2026
7b73a3e
Bump version to 1.2.0
JosephMaynard Aug 16, 2026
6d9f4a3
Address CodeRabbit review: route-guard integrity, control semantics, …
JosephMaynard Aug 16, 2026
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
33 changes: 24 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ You can see a [Dependency Radar example report](https://www.dependency-radar.com
---

![Dependency Radar – dependency list view](./docs/screenshot-01.jpg)
*List view: search, filter, and drill into every dependency, licence, vulnerabilities, install risk, depth, origins, and more.*
*List view: search, filter, and drill into every dependency, with licence, vulnerabilities, install risk, sub-dependency and removal counts, and more.*

![Dependency Radar – expanded dependency](./docs/screenshot-03.jpg)
*Expanded dependency view: scan the key risk signals first, then drill into status, scope, origins, install behaviour, licence, vulnerabilities, and upgrade blockers.*
*Expanded dependency view: key risk signals first, then status, origins, measured install size, removal preview, licence, vulnerabilities, and upgrade blockers.*

![Dependency Radar – interactive dependency graph view](./docs/screenshot-02.jpg)
*Graph view: explore the full dependency tree visually, with a docked side panel for search and per-package details — no tooltips covering the graph.*
*Graph view: explore the full dependency tree visually. Selecting a package colours the routes that keep it installed in amber and what it depends on in blue, with a docked side panel for search and per-package details.*

![Dependency Radar – flame view](./docs/screenshot-04.jpg)
*Flame view: a profiler-style icicle of your dependency tree. Bar width is the share of the tree beneath it, so the heaviest direct dependencies are obvious at a glance.*
*Flame view: a profiler-style icicle over the dominator tree. Bar width is the number of packages that would leave node_modules if you deleted it, so the heaviest direct dependencies are obvious at a glance.*

![Dependency Radar – balloon view](./docs/screenshot-05.jpg)
*Balloon view: every direct dependency is a system orbiting your project, its sub-dependencies fanning out behind it, coloured by lineage.*
Expand Down Expand Up @@ -79,6 +79,8 @@ Security issues should be reported privately; see [SECURITY.md](./SECURITY.md).

- **Vulnerability scanning** — runs `npm audit` / `pnpm audit` / `yarn audit` and surfaces advisories with severity, fix availability, installed-version matching, and direct static-import evidence
- **License analysis** — validates SPDX declarations, infers licences from `LICENSE` files, and flags mismatches, unknown licences, and strong copyleft
- **Measured install size** and per-package byte buckets (code, type declarations, source maps, other): exact on-disk numbers, never estimates
- **Removal previews**: for any package, what deleting it would actually free (packages and measured bytes), what survives and who keeps it, and which dependents block a direct dependency's removal
- **Five interactive graph layouts** — the classic dependency graph plus flame (removal-cost icicle over the dominator tree), treemap (disk-usage style, area = what deleting frees), balloon (orbital constellation), and hyperbolic (focus+context with re-rooting) views, switchable from the graph toolbar, with a shared docked side panel and package search
- **Upgrade friction analysis** — identifies upgrade blockers: peer constraints, engine ranges, native bindings, install scripts, deprecated packages
- **Maintenance signals** — flags deprecated, repo-archived, unmaintained, stale, and slowing dependencies from npm registry metadata (plus repository push activity when available), with a local 7-day cache
Expand Down Expand Up @@ -206,21 +208,23 @@ It is recomputed after registry enrichment so a registry-discovered deprecation

The report's Graph View offers five switchable layouts (toolbar buttons), all sharing the same docked side panel and package search:

- **Graph** — the classic layered dependency graph: pan, zoom, and click a node to focus its ancestors and descendants. Focusing zooms to fit the highlighted subtree.
- **Graph** — the classic layered dependency graph: pan, zoom, and click a node to focus its ancestors and descendants. Focusing zooms to fit the highlighted subtree and colours the two halves of the story separately: amber for the routes that keep the package installed, accent for what it depends on, with gentle flow pulses along both.
- **Flame** — a profiler-style icicle plot over the *dominator tree*: every package appears exactly once, and a bar's width is the number of packages that would leave `node_modules` if you deleted it. Packages kept alive by more than one dependency sit in a grey **shared** band — no single dependency gets credit for them. Click a bar to zoom in — every ancestor stays pinned above you; double-click to reset.
- **Treemap** — the disk-usage view of your dependencies: rectangle area is the number of packages deleting it would free, with the shared block greyed out. Click to inspect, double-click to drill in, double-click the zoomed box to climb back out.
- **Balloon** — an orbital constellation: the project at the centre, direct dependencies orbiting it, each one's sub-dependencies fanning out behind it recursively. Bodies are sized by how many unique packages sit beneath them. Drag to pan, scroll to zoom, click a body to fly to it.
- **Hyperbolic** — a Poincaré-disk focus+context view: the whole tree in one finite circle, sub-dependencies compressing toward the rim. Clicking a package *re-roots the layout around it* — its dependencies fan out wide, the route back to your project becomes a spine, and the rest of the graph compresses toward the rim but stays visible. Dragging warps the space; scrolling magnifies the disk.

Shared behaviour:

- **Side panel** — selecting a package in any layout shows its dossier: version, kind, licence, vulnerabilities, how many unique packages sit beneath it, what removing it would actually free, and clickable *depends-on* / *required-by* chips that refocus the current view. For a direct dependency that other packages also pull in, the dossier says so honestly — removing its package.json entry frees nothing while something else still needs it. Search results fly to the package in whichever layout you're using.
- **Side panel** — selecting a package in any layout shows its dossier: version, kind, licence, vulnerabilities, how many unique packages sit beneath it, what removing it would actually free (packages and measured on-disk bytes, with the full freed/retained preview in the list view), measured install size, import evidence, and clickable *depends-on* / *required-by* chips that refocus the current view. Measured numbers carry small (i) notes explaining exactly what was and was not measured. For a direct dependency that other packages also pull in, the dossier says so honestly — removing its package.json entry frees nothing while something else still needs it. Search results fly to the package in whichever layout you're using.
- **Status line** — in the flame, treemap, balloon, and hyperbolic views, hovering shows the full trail (`project › a › b › c`) along the bottom of the canvas instead of a popover covering the visualization. The treemap adds a small cursor name tag, since most of its boxes are too small to carry a label.
- **Colours are lineage** — in Flame and Balloon, each direct dependency's entire subtree keeps one hue, so you can trace which root pulled a package in; red always marks vulnerable packages, and dev-only dependencies render dimmer. The toolbar's **Key** dropdown explains each layout's encoding.
- **Workspaces** — all layouts respect the workspace selector, scoping the tree to that workspace's direct dependencies.
- **Filters** — a toolbar dropdown with two sections. *Show* toggles runtime dependencies, dev dependencies, and sub-dependencies, and a depth selector caps how deep the tree is expanded (Depth ≤ 1 shows just your direct dependencies and their children). *Highlight* spotlights the signals worth acting on — vulnerable packages, maintenance concerns (deprecated/archived/unmaintained/stale), community replacement suggestions, licence issues, and upgrade blockers — by dimming everything that doesn't match, in all five layouts. Filters change what renders, never the numbers: the dossier's impact facts always come from the full workspace graph.
- **Workspaces** — all layouts respect the workspace selector. *Whole project* is the aggregate of every workspace's direct dependencies; the monorepo root's own manifest appears as *name (root package.json)*; and selecting a workspace with nothing to show explains itself with a clickable list of workspaces that do.
- **Filters** — a toolbar dropdown with two sections. *Show* toggles runtime dependencies, dev dependencies, and sub-dependencies, and a depth selector caps how deep the tree is expanded (Depth ≤ 1 shows just your direct dependencies and their children). *Highlight* spotlights the signals worth acting on — vulnerable packages, maintenance concerns (deprecated/archived/unmaintained/stale), community replacement suggestions, licence issues, upgrade blockers, duplicate installed versions, and direct dependencies with no imports found — by dimming everything that doesn't match, in all five layouts. Filters change what renders, never the numbers: the dossier's impact facts always come from the full workspace graph.
- **Search doubles as a highlight filter** — while a search query is active, packages whose names don't match render dimmed in every layout, so matches stand out across the whole tree.
- **Replacement suggestions** — if a selected package has a community replacement suggestion (from the [e18e](https://e18e.dev/) [module-replacements](https://github.com/es-tooling/module-replacements) catalogue), the dossier shows a *swap for …* chip, linking to the migration guidance when the catalogue provides it.
- **Full screen** — a button in the corner of the canvas hides everything except the visualization, for presentations. Escape or the button exits.
- **Back, forward, and deep links** — view switches, layout changes, workspace changes, filters, and selections mirror into the URL hash, so the browser's Back/Forward buttons walk your exploration and a copied link reopens the report exactly where you were.
- **Remembered between sessions** — the chosen layout and filters persist in the browser (like the light/dark theme), so the report reopens the way you left it.

---
Expand Down Expand Up @@ -708,7 +712,7 @@ The JSON schema matches the `AggregatedData` TypeScript interface in `src/types.

```ts
export interface AggregatedData {
schemaVersion: '1.6'; // Report schema version for compatibility checks
schemaVersion: '1.8'; // Report schema version for compatibility checks
generatedAt: string; // ISO timestamp when the scan finished
dependencyRadarVersion: string; // CLI version that produced the report
git: {
Expand Down Expand Up @@ -827,6 +831,17 @@ export interface DependencyRecord {
version: string; // Installed version from npm ls
description?: string; // Description from the installed package.json (if present)
fileCount?: number; // Number of files in the installed package folder (excluding nested node_modules)
installSize?: {
totalBytes: number; // Measured on-disk bytes (uncompressed, excluding nested node_modules)
codeBytes: number; // Bytes in JS/TS source files
typesBytes: number; // Bytes in .d.ts/.d.mts/.d.cts declaration files
mapBytes: number; // Bytes in source maps
otherBytes: number; // Everything else (assets, manifests, docs)
}; // Omitted when any file could not be read, so partial sums are never presented as measurements
platform?: {
os?: string[]; // package.json#os constraints (npm ! negations preserved)
cpu?: string[]; // package.json#cpu constraints
};
hasBin?: true; // True if package.json declares at least one executable in `bin`
deprecated: boolean; // True when deprecated on the npm registry (installed or latest version), or via a local package.json deprecated flag
links: {
Expand Down
88 changes: 78 additions & 10 deletions dist/aggregator.js
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,8 @@ async function aggregateData(input) {
version: node.version,
...(packageInsights.description ? { description: packageInsights.description } : {}),
...(typeof packageInsights.fileCount === 'number' ? { fileCount: packageInsights.fileCount } : {}),
...(packageInsights.installSize ? { installSize: packageInsights.installSize } : {}),
...(packageInsights.platform ? { platform: packageInsights.platform } : {}),
...(packageInsights.hasBin ? { hasBin: true } : {}),
deprecated: packageInsights.deprecated,
links: {
Expand Down Expand Up @@ -625,7 +627,7 @@ async function aggregateData(input) {
const dependencyCount = nodes.length;
const transitiveCount = dependencyCount - directCount;
const aggregated = {
schemaVersion: '1.7',
schemaVersion: '1.8',
generatedAt: new Date().toISOString(),
dependencyRadarVersion,
git: {
Expand Down Expand Up @@ -1589,12 +1591,37 @@ async function gatherPackageInsights(name, version, resolvePaths, metaCache, sta
const links = extractPackageLinks(pkg);
const execution = await deriveExecutionInfo(pkg, scripts, dir, stats);
const packaging = derivePackagingInfo(pkg, stats);
// Platform constraints from the installed manifest (package.json os/cpu):
// string arrays only, non-string entries dropped.
const normalizePlatformList = (value) => {
if (!Array.isArray(value))
return undefined;
const list = value.filter((item) => typeof item === 'string' && item.trim().length > 0);
return list.length > 0 ? list : undefined;
};
const platformOs = normalizePlatformList(pkg.os);
const platformCpu = normalizePlatformList(pkg.cpu);
const platform = platformOs || platformCpu
? { ...(platformOs ? { os: platformOs } : {}), ...(platformCpu ? { cpu: platformCpu } : {}) }
: undefined;
return {
deprecated,
nodeEngine,
requiredPeerDependencies,
description,
...(typeof (stats === null || stats === void 0 ? void 0 : stats.fileCount) === 'number' ? { fileCount: stats.fileCount } : {}),
...((stats === null || stats === void 0 ? void 0 : stats.sizeComplete)
? {
installSize: {
totalBytes: stats.totalBytes,
codeBytes: stats.codeBytes,
typesBytes: stats.typesBytes,
mapBytes: stats.mapBytes,
otherBytes: stats.otherBytes
}
}
: {}),
...(platform ? { platform } : {}),
hasBin,
declaredDependencies,
links,
Expand Down Expand Up @@ -1756,6 +1783,8 @@ async function hasDefinitelyTypedPackage(name, resolvePaths, cache) {
* - `hasShrinkwrap`: `true` when an `npm-shrinkwrap.json` file was found.
* - `fileCount`: total number of regular files encountered under the directory.
*/
const DECLARATION_FILE_RE = /\.d\.(ts|mts|cts)$/;
const CODE_FILE_RE = /\.(js|mjs|cjs|jsx|ts|tsx|mts|cts)$/;
async function calculatePackageStats(dir, cache) {
if (cache.has(dir))
return cache.get(dir);
Expand All @@ -1764,8 +1793,22 @@ async function calculatePackageStats(dir, cache) {
let hasBindingGyp = false;
let hasShrinkwrap = false;
let fileCount = 0;
let codeBytes = 0;
let typesBytes = 0;
let mapBytes = 0;
let otherBytes = 0;
let sizeComplete = true;
async function walk(current) {
const entries = await promises_1.default.readdir(current, { withFileTypes: true });
let entries;
try {
entries = await promises_1.default.readdir(current, { withFileTypes: true });
}
catch {
// Unreadable/removed subdirectory: the sums are now partial. Keep
// walking siblings, but never report the result as a measurement.
sizeComplete = false;
return;
}
for (const entry of entries) {
const full = path_1.default.join(current, entry.name);
if (entry.isSymbolicLink())
Expand All @@ -1778,24 +1821,49 @@ async function calculatePackageStats(dir, cache) {
}
else if (entry.isFile()) {
fileCount += 1;
if (entry.name.endsWith('.d.ts'))
if (DECLARATION_FILE_RE.test(entry.name))
hasDts = true;
if (entry.name.endsWith('.node'))
hasNativeBinary = true;
if (entry.name === 'binding.gyp')
hasBindingGyp = true;
if (entry.name === 'npm-shrinkwrap.json')
hasShrinkwrap = true;
// Measured on-disk size, bucketed by what the bytes are for. Maps
// are matched first so .d.ts.map lands in maps, not types.
try {
const { size } = await promises_1.default.stat(full);
if (entry.name.endsWith('.map'))
mapBytes += size;
else if (DECLARATION_FILE_RE.test(entry.name))
typesBytes += size;
else if (CODE_FILE_RE.test(entry.name))
codeBytes += size;
else
otherBytes += size;
}
catch {
// A file disappearing mid-scan must not abort stats, but its
// bytes are missing from the sums.
sizeComplete = false;
}
}
}
}
try {
await walk(dir);
}
catch (err) {
// best-effort; ignore inaccessible paths
}
const result = { hasDts, hasNativeBinary, hasBindingGyp, hasShrinkwrap, fileCount };
await walk(dir);
const result = {
hasDts,
hasNativeBinary,
hasBindingGyp,
hasShrinkwrap,
fileCount,
totalBytes: codeBytes + typesBytes + mapBytes + otherBytes,
codeBytes,
typesBytes,
mapBytes,
otherBytes,
sizeComplete
};
cache.set(dir, result);
return result;
}
Expand Down
Loading