dependency-radar 0.3.0 → 0.4.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 CHANGED
@@ -1,22 +1,152 @@
1
1
  # Dependency Radar
2
2
 
3
- Dependency Radar is a local-first CLI tool that inspects a Node.js project’s installed dependencies and generates a single, human-readable HTML report. The report highlights dependency structure, usage, size, licences, vulnerabilities, and other signals that help you understand risk and complexity hidden in your node_modules folder.
3
+ Dependency Radar is a CLI tool that inspects a Node.js project’s installed dependencies and generates a single, human-readable HTML report. The report highlights dependency structure, usage, licences, vulnerabilities, and other signals that help you understand risk and complexity hidden in your node_modules folder.
4
+
5
+ The simplest way to get started is:
6
+
7
+ ```bash
8
+ npx dependency-radar
9
+ ```
10
+
11
+ This runs a scan against the current project and writes a self-contained `dependency-radar.html` report you can open locally, share with teammates, or attach to tickets and documentation.
4
12
 
5
13
  ## What it does
6
14
 
7
- - Analyses installed dependencies using only local data (no SaaS, no uploads by default)
8
- - Combines multiple tools (npm audit, npm ls, import graph analysis) into a single report
9
- - Shows direct vs sub-dependencies, dependency depth, and parent relationships
10
- - Highlights licences, known vulnerabilities, install-time scripts, native modules, and package footprint
11
- - Produces a single self-contained HTML file you can share or archive
15
+ - Analyses installed dependencies by running standard package manager tooling (npm, pnpm, or yarn)
16
+ - Combines multiple signals (audit results, dependency graph data, import usage, and heuristics) into a single report
17
+ - Shows direct vs transitive dependencies, dependency depth, and parent relationships
18
+ - Highlights licences, known vulnerabilities, install-time scripts, native modules, and package footprint (including installed file counts)
19
+ - Produces a single self-contained HTML file with no external assets, which you can easily share
12
20
 
13
21
  ## What it is not
14
22
 
15
- - Not a CI service or hosted platform
23
+ - Not a CI service or hosted scanning platform
16
24
  - Not a replacement for dedicated security scanners
17
25
  - Not a bundler or build tool
18
26
  - Not a dependency updater
19
27
 
28
+ ---
29
+
30
+ For teams that want deeper analysis, long-term tracking, and additional enrichment (such as ecosystem and maintenance signals), Dependency Radar also offers an optional premium service.
31
+ See https://dependency-radar.com for details.
32
+
33
+ ## How a scan works
34
+
35
+ When you run `npx dependency-radar` (or `dependency-radar scan`), the CLI executes this pipeline:
36
+
37
+ 1. Parse CLI options (`--project`, `--out`, `--offline`, `--json`, `--keep-temp`, `--open`).
38
+ 2. Detect workspace/package-manager context:
39
+ - Workspace roots from `pnpm-workspace.yaml` or `package.json#workspaces`
40
+ - Dependency policy from `package.json` and `pnpm-workspace.yaml` overrides/resolutions
41
+ - Package manager from `packageManager`, lockfiles, and installed metadata
42
+ - Yarn Plug'n'Play detection (`.pnp.cjs`/`.pnp.js` or `.yarnrc.yml nodeLinker: pnp`)
43
+ 3. Create a temporary `.dependency-radar/` directory inside the scanned project.
44
+ 4. For each workspace package (or just the project root in single-package mode), run collectors:
45
+ - Dependency tree (`npm ls` / `pnpm list` / `yarn list`)
46
+ - Vulnerabilities (`npm audit` / `pnpm audit` / `yarn audit` or `yarn npm audit`)
47
+ - Version drift (`npm outdated` / `pnpm outdated` / `yarn outdated`, where available)
48
+ - Source import graph (static import/require parsing in `src/` or project root)
49
+ 5. Normalize tool outputs into one internal shape and merge workspace package results.
50
+ - PNPM dependency trees are filtered to installed-only packages (non-installed optional/platform variants are dropped)
51
+ 6. Resolve and crawl installed package directories in `node_modules` to collect local metadata:
52
+ - Resolve `package.json` paths via package-manager-aware lookups (including PNPM virtual store layouts)
53
+ - Read local package metadata and license artifacts from installed files
54
+ 7. Aggregate dependency records by enriching each installed package with:
55
+ - License declaration + `LICENSE` file inference/validation
56
+ - Advisory summaries and severity/risk rollups
57
+ - Root-cause/origin and runtime-impact heuristics
58
+ - Install-time execution signals
59
+ - Local package metadata (`description`, links, deprecation, TypeScript type availability, installed file count, CLI `bin` presence)
60
+ 8. Write final output as either:
61
+ - `dependency-radar.html` (self-contained report), or
62
+ - `dependency-radar.json` (raw aggregated model)
63
+ 9. Remove `.dependency-radar/` unless `--keep-temp` is set.
64
+
65
+ The scan is local-first: package metadata is read from `node_modules`; only audit/outdated commands require registry access.
66
+
67
+ ### `node_modules` crawling details
68
+
69
+ - Dependency metadata is read from installed package directories, not from registry documents.
70
+ - Package resolution is workspace-aware and PNPM-aware, including `.pnpm` virtual store paths.
71
+ - License discovery checks common file variants such as `LICENSE`, `LICENCE`, `COPYING`, and `NOTICE` (with or without extensions like `.md`).
72
+
73
+ ### PNPM workspace hardening (problems solved)
74
+
75
+ - 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).
76
+ - Dependency Radar now verifies PNPM entries against installed artifacts (`node_modules/.pnpm` and workspace-linked `node_modules` paths) before including them in the report.
77
+ - Result: reports now reflect only dependencies that actually exist on disk and can be inspected locally.
78
+
79
+ ## Usage Heuristics (`usage.runtimeImpact` and `usage.introduction`)
80
+
81
+ These two fields are inferred from local signals. They are intended as review hints, not strict truth.
82
+
83
+ ### `usage.runtimeImpact`
84
+
85
+ `runtimeImpact` is inferred from the import graph and file-path classification:
86
+
87
+ 1. Dependency imports are collected from source files (`import`, `export ... from`, `require()`, and static `import()`).
88
+ 2. Each importing file is classified into one of: `runtime`, `build`, `testing`, `tooling`.
89
+ 3. Classification is path-pattern based (examples):
90
+ - `testing`: `__tests__`, `test`, `tests`, `e2e`, `cypress`, `playwright`, `*.test.*`, `*.spec.*`
91
+ - `tooling`: eslint/prettier/stylelint/commitlint/lint-staged/husky/renovate/release configs
92
+ - `build`: webpack/rollup/vite/tsconfig/babel/swc/esbuild/parcel/postcss/tailwind/storybook/turbo/nx configs and common `scripts/build*` paths
93
+ 4. Per dependency, category weights are summed from import counts.
94
+ 5. Result selection:
95
+ - Single dominant category => that category
96
+ - Strong majority (for example >= 70%) => that category
97
+ - Otherwise => `mixed`
98
+
99
+ ### `usage.introduction`
100
+
101
+ `introduction` is inferred from dependency graph roots, scope, and runtime impact:
102
+
103
+ 1. If dependency is direct => `direct`
104
+ 2. If `runtimeImpact` is `testing` => `testing`
105
+ 3. If `runtimeImpact` is `tooling` or `build` => `tooling`
106
+ 4. If inferred scope is `dev` => `tooling`
107
+ 5. If inferred scope is `peer` and `runtimeImpact` is not `runtime` => `tooling`
108
+ 6. If all root-cause direct dependencies are in a tooling allowlist => `tooling`
109
+ 7. If any root-cause direct dependency is in a framework allowlist => `framework`
110
+ 8. If root causes exist but none of the above match => `transitive`
111
+ 9. Otherwise => `unknown`
112
+
113
+ ### Validity and limits
114
+
115
+ - Valid as directional metadata for prioritization and triage.
116
+ - Not valid as a definitive runtime/ownership model.
117
+ - Accuracy depends on file naming conventions, static import detectability, and dependency graph quality from package manager output.
118
+
119
+ ## Upgrade Blockers Heuristic (`upgrade.blockers`, `upgrade.blocksNodeMajor`)
120
+
121
+ `upgrade.blockers` is a local, static heuristic for upgrade friction. It does not run package code and does not query external APIs.
122
+
123
+ ### How blockers are collected
124
+
125
+ For each installed dependency, Dependency Radar inspects local package metadata and install-surface signals and may add one or more blockers:
126
+
127
+ - `nodeEngine`: Added when `package.json#engines.node` looks restrictive (for example `>=16`, `^18`, `<20`, ranges with concrete major constraints). Permissive forms such as `*` and `>=0` are not flagged.
128
+ - `peerDependency`: Added when the package declares at least one non-optional peer dependency (`peerDependencies`, excluding peers marked `peerDependenciesMeta.<name>.optional: true`).
129
+ - `nativeBindings`: Added when native build/binary surface is detected (`binding.gyp`, `.node` binaries, or native build tooling in scripts such as `node-gyp`/`prebuild`).
130
+ - `installScripts`: Added when install lifecycle hooks are present (`preinstall`, `install`, or `postinstall`).
131
+ - `deprecated`: Added when the package is marked deprecated in installed metadata.
132
+
133
+ ### `blocksNodeMajor` meaning
134
+
135
+ `upgrade.blocksNodeMajor` is only emitted when local signals suggest Node major upgrades may be risky for that package. It is set from a subset of blockers:
136
+
137
+ - `nodeEngine`
138
+ - `nativeBindings`
139
+ - `installScripts`
140
+
141
+ It is not set from `peerDependency` or `deprecated` alone.
142
+
143
+ ### Accuracy and limits
144
+
145
+ - High signal: `nativeBindings`, `deprecated`, and non-optional `peerDependency` are generally reliable local indicators.
146
+ - Medium signal: `nodeEngine` is heuristic range parsing; unusual semver expressions may be under- or over-classified.
147
+ - Medium signal: `installScripts` indicates lifecycle execution surface, not guaranteed breakage.
148
+ - The field represents friction likelihood, not a guaranteed upgrade failure.
149
+ - Future versions may expand blocker categories; consumers should handle unknown blocker strings defensively.
20
150
 
21
151
  ## License Scanning
22
152
 
@@ -28,6 +158,10 @@ Dependency Radar validates SPDX licenses declared in `package.json` and can infe
28
158
 
29
159
  This logic applies to all dependencies (direct and transitive). Inferred licenses are never treated as authoritative over valid declared SPDX expressions.
30
160
 
161
+ `licenseRisk` is derived from SPDX IDs, with one escalation rule for safety: when status is `mismatch` (declared SPDX differs from inferred LICENSE text), risk is promoted to at least `amber`.
162
+ In the HTML report, the License badge shows a trailing `*` when status is `mismatch`.
163
+ When a dependency repository resolves to GitHub, the expanded License section links to `package.json` and `LICENSE` source files for faster verification.
164
+
31
165
 
32
166
  ## Setup
33
167
 
@@ -89,20 +223,44 @@ Show options:
89
223
  npx dependency-radar --help
90
224
  ```
91
225
 
226
+ ## Package Manager Support
227
+
228
+ - npm: Supported for dependency tree, audit, outdated, single-package, and workspaces.
229
+ - pnpm: Supported for dependency tree, audit, outdated, and workspaces (with ls depth fallbacks for large projects).
230
+ - Yarn Classic (v1, node_modules linker): Supported for dependency tree, audit, outdated, and workspaces.
231
+ - Yarn Berry (v2+, node-modules linker): Dependency tree and audit work; outdated support depends on available Yarn commands/plugins and may be unavailable.
232
+ - Yarn Plug'n'Play (`nodeLinker: pnp`): Not supported yet.
233
+
92
234
  ## Scripts
93
235
 
94
- - `npm run build` – compile TypeScript to `dist/`
95
- - `npm run dev` – run a scan from source (`ts-node`)
96
- - `npm run scan` – run a scan from the built output
236
+ - `npm run build` – generate SPDX/report assets and compile TypeScript to `dist/`
237
+ - `npm run dev` – run a scan from source (`ts-node src/cli.ts scan`)
238
+ - `npm run scan` – run a scan from the built output (`node dist/cli.js scan`)
239
+ - `npm run dev:report` – run the report UI dev server
240
+ - `npm run build:spdx` – rebuild bundled SPDX identifiers
241
+ - `npm run build:report-ui` – build report UI assets
242
+ - `npm run build:report` – rebuild report assets used by the CLI
243
+
244
+ ### Test scripts:
245
+
246
+ - `npm run test:unit` – run Vitest unit tests
247
+ - `npm run test:unit:watch` – watch mode for fast local iteration
248
+ - `npm run test:fixtures` – run curated fixture integration tests (mostly offline scans)
249
+ - `npm run test:fixtures:online` – run online fixture checks (audit/outdated regression coverage)
250
+ - `npm run test:fixtures:all` – run all fixture integration tests
251
+ - `npm run test:release` – full pre-release gate (`build` + unit + fixture + package dry run)
252
+
253
+ Fixture orchestration lives in `/test-fixtures/package.json` with helper scripts under `/test-fixtures/scripts`.
97
254
 
98
255
  ## Notes
99
256
 
100
- - The target project must have node_modules installed (run npm install first).
101
- - The scan is local-first and does not upload your code or dependencies anywhere.
102
- - `npm audit` and `npm outdated` perform registry lookups; use `--offline` for offline-only scans.
257
+ - The target project must have dependencies installed (run `npm install`, `pnpm install`, or `yarn install` first).
258
+ - The scan runs on your machine and does not upload your code or dependencies anywhere.
259
+ - `npm audit`/`pnpm audit`/`yarn npm audit` and `npm outdated`/`pnpm outdated` perform registry lookups; use `--offline` for offline-only scans.
260
+ - On some Yarn Berry setups, `yarn outdated` is not available; the scan continues and marks outdated data as unavailable.
103
261
  - A temporary `.dependency-radar` folder is created during the scan to store intermediate tool output.
104
262
  - Use `--keep-temp` to retain this folder for debugging; otherwise it is deleted automatically.
105
- - If a tool fails, its section is marked as unavailable, but the report is still generated.
263
+ - If some per-package tools fail (common in large workspaces), the scan continues and reports warnings; missing sections are marked unavailable where applicable.
106
264
 
107
265
  ## Output
108
266
 
@@ -117,7 +275,7 @@ The JSON schema matches the `AggregatedData` TypeScript interface in `src/types.
117
275
 
118
276
  ```ts
119
277
  export interface AggregatedData {
120
- schemaVersion: '1.2'; // Report schema version for compatibility checks
278
+ schemaVersion: '1.3'; // Report schema version for compatibility checks
121
279
  generatedAt: string; // ISO timestamp when the scan finished
122
280
  dependencyRadarVersion: string; // CLI version that produced the report
123
281
  git: {
@@ -125,6 +283,31 @@ export interface AggregatedData {
125
283
  };
126
284
  project: {
127
285
  projectDir: string; // Project path relative to the user's home directory (e.g. /Developer/app)
286
+ name?: string; // package.json#name from the scanned project root
287
+ version?: string; // package.json#version from the scanned project root
288
+ description?: string; // package.json#description
289
+ license?: string; // package.json#license
290
+ keywords?: string[]; // package.json#keywords
291
+ homepage?: string; // package.json#homepage
292
+ repository?: string; // repository URL (string or repository.url)
293
+ constraints?: {
294
+ os?: string[]; // package.json#os constraints
295
+ cpu?: string[]; // package.json#cpu constraints
296
+ enginesNode?: string; // package.json#engines.node
297
+ };
298
+ dependencyPolicy?: {
299
+ overrides?: Record<string, unknown>; // package.json overrides plus pnpm workspace overrides
300
+ resolutions?: Record<string, unknown>; // package.json#resolutions
301
+ };
302
+ dependencyPolicySummary?: {
303
+ hasOverrides: boolean;
304
+ overrideCount: number; // Top-level override entries
305
+ overriddenPackageNames?: string[]; // Package names parsed from override selectors
306
+ hasResolutions: boolean;
307
+ resolutionCount: number; // Top-level resolution entries
308
+ resolvedPackageNames?: string[]; // Package names parsed from resolution selectors
309
+ sources?: string[]; // Where policy came from (e.g. package.json#overrides, pnpm-workspace.yaml#overrides)
310
+ };
128
311
  };
129
312
  environment: {
130
313
  nodeVersion: string; // Node.js version from process.versions.node
@@ -132,10 +315,10 @@ export interface AggregatedData {
132
315
  minRequiredMajor: number; // Strictest Node major required by dependency engines (0 if unknown)
133
316
  platform?: string; // OS platform (process.platform)
134
317
  arch?: string; // CPU architecture (process.arch)
135
- ci?: boolean; // True when running in CI (process.env.CI === 'true')
318
+ ci?: boolean; // True when CI indicators are detected
136
319
  packageManagerField?: string; // package.json packageManager field (e.g. pnpm@9.1.0)
137
- packageManager?: 'npm' | 'pnpm' | 'yarn'; // Package manager used to scan
138
- packageManagerVersion?: string; // Version of the package manager used to scan
320
+ packageManager?: 'npm' | 'pnpm' | 'yarn'; // Package manager used for dependency/audit/outdated collection
321
+ packageManagerVersion?: string; // Version of the selected package manager (when available)
139
322
  toolVersions?: {
140
323
  npm?: string;
141
324
  pnpm?: string;
@@ -144,15 +327,25 @@ export interface AggregatedData {
144
327
  };
145
328
  workspaces: {
146
329
  enabled: boolean; // True when the scan used workspace aggregation
147
- type?: 'npm' | 'pnpm' | 'yarn' | 'none'; // Workspace type if detected
148
- packageCount?: number; // Number of workspace packages scanned
330
+ type?: 'npm' | 'pnpm' | 'yarn' | 'none'; // Workspace mode (CLI currently always emits this)
331
+ packageCount?: number; // Number of workspace packages scanned (CLI currently always emits this)
332
+ workspacePackages?: WorkspacePackage[]; // Lightweight first-party workspace metadata
149
333
  };
150
334
  summary: {
151
- dependencyCount: number; // Total dependencies in the graph
152
- directCount: number; // Dependencies listed in package.json
153
- transitiveCount: number; // Dependencies pulled in by other dependencies
335
+ dependencyCount: number; // Total EXTERNAL dependencies in the graph
336
+ directCount: number; // External dependencies listed in package.json
337
+ transitiveCount: number; // External dependencies pulled in by other dependencies
338
+ };
339
+ dependencies: Record<string, DependencyRecord>; // External third-party packages keyed by name@version
340
+ }
341
+
342
+ export interface WorkspacePackage {
343
+ name: string; // Workspace package name from package.json
344
+ relativePath: string; // Workspace-relative path (e.g. apps/web)
345
+ directExternal: {
346
+ runtime: number; // Unique direct external deps from dependencies + optionalDependencies
347
+ dev: number; // Unique direct external deps from devDependencies
154
348
  };
155
- dependencies: Record<string, DependencyRecord>; // Keyed by name@version
156
349
  }
157
350
 
158
351
  export interface DependencyRecord {
@@ -161,6 +354,8 @@ export interface DependencyRecord {
161
354
  name: string; // Package name from npm metadata
162
355
  version: string; // Installed version from npm ls
163
356
  description?: string; // Description from the installed package.json (if present)
357
+ fileCount?: number; // Number of files in the installed package folder (excluding nested node_modules)
358
+ hasBin?: true; // True if package.json declares at least one executable in `bin`
164
359
  deprecated: boolean; // True if the package.json has a deprecated flag
165
360
  links: {
166
361
  npm: string; // npm package page URL
@@ -194,7 +389,7 @@ export interface DependencyRecord {
194
389
  | 'invalid-spdx'
195
390
  | 'unknown';
196
391
  };
197
- licenseRisk: 'green' | 'amber' | 'red'; // Risk classification derived from declared/inferred SPDX ids
392
+ licenseRisk: 'green' | 'amber' | 'red'; // Risk classification derived from declared/inferred SPDX ids (mismatch is escalated to at least amber)
198
393
  };
199
394
  security: {
200
395
  summary: {
@@ -206,20 +401,20 @@ export interface DependencyRecord {
206
401
  risk: 'green' | 'amber' | 'red'; // Risk classification derived from audit counts
207
402
  };
208
403
  advisories?: Array<{
209
- id: string; // GHSA identifier
404
+ id: string; // Advisory identifier (GHSA, npm advisory ID, or source-specific fallback)
210
405
  title: string; // Human-readable advisory title
211
406
  severity: 'low' | 'moderate' | 'high' | 'critical';
212
407
  vulnerableRange: string; // Semver range
213
408
  fixAvailable: boolean; // True if npm audit indicates a fix exists
214
- url: string; // Advisory URL
409
+ url: string; // Advisory URL (may be empty when unavailable)
215
410
  }>;
216
411
  };
217
412
  upgrade: {
218
413
  nodeEngine: string | null; // engines.node from the package.json (if present)
219
- outdatedStatus?: 'current' | 'patch' | 'minor' | 'major' | 'unknown'; // Derived from npm outdated (if present)
220
- latestVersion?: string; // npm latest version (present only when status is not current)
221
- blockers?: Array<'nodeEngine' | 'peerDependency' | 'nativeBindings' | 'deprecated'>; // Reasons for upgrade friction
222
- blocksNodeMajor?: boolean; // True if local signals indicate a node major bump is risky
414
+ outdatedStatus?: 'current' | 'patch' | 'minor' | 'major' | 'unknown'; // Derived from npm outdated (field is omitted rather than set to 'current')
415
+ latestVersion?: string; // Latest version from outdated data (present for patch/minor/major when known)
416
+ blockers?: Array<'nodeEngine' | 'peerDependency' | 'nativeBindings' | 'installScripts' | 'deprecated'>; // Reasons for upgrade friction
417
+ blocksNodeMajor?: boolean; // Present when local signals indicate a Node major bump is risky
223
418
  };
224
419
  usage: {
225
420
  direct: boolean; // True if declared in package.json (dependencies/devDependencies/etc.)
@@ -249,8 +444,8 @@ export interface DependencyRecord {
249
444
  // Only installed dependencies have full dependency records in the top-level list.
250
445
  dep?: Record<string, [string, string | null]>; // Declared runtime deps
251
446
  dev?: Record<string, [string, string | null]>; // Declared dev deps
252
- peer?: Record<string, [string, string | null]>; // Declared peer deps
253
447
  opt?: Record<string, [string, string | null]>; // Declared optional deps
448
+ peer?: Record<string, [string, string | null]>; // Declared peer deps
254
449
  };
255
450
  };
256
451
  execution?: {