dependency-radar 0.3.1 → 0.5.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
@@ -12,10 +12,10 @@ This runs a scan against the current project and writes a self-contained `depend
12
12
 
13
13
  ## What it does
14
14
 
15
- - Analyses installed dependencies by running standard package manager tooling (npm, pnpm, or yarn)
15
+ - Analyses installed dependencies from lockfiles first (`pnpm-lock.yaml`, `package-lock.json`/`npm-shrinkwrap.json`, `yarn.lock`), with package-manager CLI fallback when needed
16
16
  - Combines multiple signals (audit results, dependency graph data, import usage, and heuristics) into a single report
17
17
  - Shows direct vs transitive dependencies, dependency depth, and parent relationships
18
- - Highlights licences, known vulnerabilities, install-time scripts, native modules, and package footprint
18
+ - Highlights licences, known vulnerabilities, install-time scripts, native modules, and package footprint (including installed file counts)
19
19
  - Produces a single self-contained HTML file with no external assets, which you can easily share
20
20
 
21
21
  ## What it is not
@@ -30,6 +30,134 @@ This runs a scan against the current project and writes a self-contained `depend
30
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
31
  See https://dependency-radar.com for details.
32
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), collect dependency graph data:
45
+ - Lockfile-first graph parsing (`pnpm-lock.yaml`, `npm-shrinkwrap.json`/`package-lock.json`, `yarn.lock`)
46
+ - Fallback to package-manager tree commands (`npm ls` / `pnpm list` / `yarn list`) only when lockfile parsing is unavailable
47
+ - PNPM CLI fallback keeps depth retries for very large trees
48
+ 5. Run additional collectors:
49
+ - Vulnerabilities (`npm audit` / `pnpm audit` / `yarn audit` or `yarn npm audit`)
50
+ - Version drift (`npm outdated` / `pnpm outdated` / `yarn outdated`, where available)
51
+ - Source import graph (static import/require parsing in `src/` or project root)
52
+ 6. Normalize outputs into one internal shape and merge workspace package results.
53
+ - PNPM lock/CLI dependency trees are filtered to installed-only packages (non-installed optional/platform variants are dropped)
54
+ 7. Resolve and crawl installed package directories in `node_modules` to collect local metadata:
55
+ - Resolve `package.json` paths via package-manager-aware lookups (including PNPM virtual store layouts)
56
+ - Read local package metadata and license artifacts from installed files
57
+ 8. Aggregate dependency records by enriching each installed package with:
58
+ - License declaration + `LICENSE` file inference/validation
59
+ - Advisory summaries and severity/risk rollups
60
+ - Root-cause/origin and runtime-impact heuristics
61
+ - Install-time execution signals
62
+ - Local package metadata (`description`, links, deprecation, TypeScript type availability, installed file count, CLI `bin` presence)
63
+ 9. Write final output as either:
64
+ - `dependency-radar.html` (self-contained report), or
65
+ - `dependency-radar.json` (raw aggregated model)
66
+ 10. Remove `.dependency-radar/` unless `--keep-temp` is set.
67
+
68
+ The scan is local-first: package metadata is read from `node_modules`; only audit/outdated commands require registry access.
69
+
70
+ ### `node_modules` crawling details
71
+
72
+ - Dependency metadata is read from installed package directories, not from registry documents.
73
+ - Package resolution is workspace-aware and PNPM-aware, including `.pnpm` virtual store paths.
74
+ - License discovery checks common file variants such as `LICENSE`, `LICENCE`, `COPYING`, and `NOTICE` (with or without extensions like `.md`).
75
+
76
+ ### Lockfile-first dependency graphing
77
+
78
+ - Dependency graph construction starts from lockfiles so deep transitive packages are captured without relying on large `* ls` JSON payloads.
79
+ - Lockfile detection is scoped to the scan root/workspace root (it does not walk outside the scanned project).
80
+ - If lockfile parsing cannot be used, Dependency Radar falls back to package-manager tree commands and continues with warnings when partial failures occur.
81
+
82
+ ### PNPM workspace hardening (problems solved)
83
+
84
+ - 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).
85
+ - Dependency Radar verifies PNPM entries against installed artifacts (`node_modules/.pnpm` and workspace-linked `node_modules` paths) before including them in the report.
86
+ - 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.
87
+ - Result: reports now reflect only dependencies that actually exist on disk and can be inspected locally.
88
+
89
+ ## Usage Heuristics (`usage.runtimeImpact` and `usage.introduction`)
90
+
91
+ These two fields are inferred from local signals. They are intended as review hints, not strict truth.
92
+
93
+ ### `usage.runtimeImpact`
94
+
95
+ `runtimeImpact` is inferred from the import graph and file-path classification:
96
+
97
+ 1. Dependency imports are collected from source files (`import`, `export ... from`, `require()`, and static `import()`).
98
+ 2. Each importing file is classified into one of: `runtime`, `build`, `testing`, `tooling`.
99
+ 3. Classification is path-pattern based (examples):
100
+ - `testing`: `__tests__`, `test`, `tests`, `e2e`, `cypress`, `playwright`, `*.test.*`, `*.spec.*`
101
+ - `tooling`: eslint/prettier/stylelint/commitlint/lint-staged/husky/renovate/release configs
102
+ - `build`: webpack/rollup/vite/tsconfig/babel/swc/esbuild/parcel/postcss/tailwind/storybook/turbo/nx configs and common `scripts/build*` paths
103
+ 4. Per dependency, category weights are summed from import counts.
104
+ 5. Result selection:
105
+ - Single dominant category => that category
106
+ - Strong majority (for example >= 70%) => that category
107
+ - Otherwise => `mixed`
108
+
109
+ ### `usage.introduction`
110
+
111
+ `introduction` is inferred from dependency graph roots, scope, and runtime impact:
112
+
113
+ 1. If dependency is direct => `direct`
114
+ 2. If `runtimeImpact` is `testing` => `testing`
115
+ 3. If `runtimeImpact` is `tooling` or `build` => `tooling`
116
+ 4. If inferred scope is `dev` => `tooling`
117
+ 5. If inferred scope is `peer` and `runtimeImpact` is not `runtime` => `tooling`
118
+ 6. If all root-cause direct dependencies are in a tooling allowlist => `tooling`
119
+ 7. If any root-cause direct dependency is in a framework allowlist => `framework`
120
+ 8. If root causes exist but none of the above match => `transitive`
121
+ 9. Otherwise => `unknown`
122
+
123
+ ### Validity and limits
124
+
125
+ - Valid as directional metadata for prioritization and triage.
126
+ - Not valid as a definitive runtime/ownership model.
127
+ - Accuracy depends on file naming conventions, static import detectability, and dependency graph quality from package manager output.
128
+
129
+ ## Upgrade Blockers Heuristic (`upgrade.blockers`, `upgrade.blocksNodeMajor`)
130
+
131
+ `upgrade.blockers` is a local, static heuristic for upgrade friction. It does not run package code and does not query external APIs.
132
+
133
+ ### How blockers are collected
134
+
135
+ For each installed dependency, Dependency Radar inspects local package metadata and install-surface signals and may add one or more blockers:
136
+
137
+ - `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.
138
+ - `peerDependency`: Added when the package declares at least one non-optional peer dependency (`peerDependencies`, excluding peers marked `peerDependenciesMeta.<name>.optional: true`).
139
+ - `nativeBindings`: Added when native build/binary surface is detected (`binding.gyp`, `.node` binaries, or native build tooling in scripts such as `node-gyp`/`prebuild`).
140
+ - `installScripts`: Added when install lifecycle hooks are present (`preinstall`, `install`, or `postinstall`).
141
+ - `deprecated`: Added when the package is marked deprecated in installed metadata.
142
+
143
+ ### `blocksNodeMajor` meaning
144
+
145
+ `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:
146
+
147
+ - `nodeEngine`
148
+ - `nativeBindings`
149
+ - `installScripts`
150
+
151
+ It is not set from `peerDependency` or `deprecated` alone.
152
+
153
+ ### Accuracy and limits
154
+
155
+ - High signal: `nativeBindings`, `deprecated`, and non-optional `peerDependency` are generally reliable local indicators.
156
+ - Medium signal: `nodeEngine` is heuristic range parsing; unusual semver expressions may be under- or over-classified.
157
+ - Medium signal: `installScripts` indicates lifecycle execution surface, not guaranteed breakage.
158
+ - The field represents friction likelihood, not a guaranteed upgrade failure.
159
+ - Future versions may expand blocker categories; consumers should handle unknown blocker strings defensively.
160
+
33
161
  ## License Scanning
34
162
 
35
163
  Dependency Radar validates SPDX licenses declared in `package.json` and can infer licenses from `LICENSE` files when declarations are missing or invalid. It works offline and uses a bundled SPDX identifier list (generated at build time) with no runtime network access. Each dependency gets a structured license record with:
@@ -40,6 +168,10 @@ Dependency Radar validates SPDX licenses declared in `package.json` and can infe
40
168
 
41
169
  This logic applies to all dependencies (direct and transitive). Inferred licenses are never treated as authoritative over valid declared SPDX expressions.
42
170
 
171
+ `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`.
172
+ In the HTML report, the License badge shows a trailing `*` when status is `mismatch`.
173
+ When a dependency repository resolves to GitHub, the expanded License section links to `package.json` and `LICENSE` source files for faster verification.
174
+
43
175
 
44
176
  ## Setup
45
177
 
@@ -101,6 +233,14 @@ Show options:
101
233
  npx dependency-radar --help
102
234
  ```
103
235
 
236
+ ## Package Manager Support
237
+
238
+ - npm: Supported for lockfile-first dependency tree (`npm-shrinkwrap.json` or `package-lock.json`), audit, outdated, single-package, and workspaces.
239
+ - pnpm: Supported for lockfile-first dependency tree (`pnpm-lock.yaml`), audit, outdated, and workspaces (with `pnpm list` fallback depth retries when required).
240
+ - Yarn Classic (v1, node_modules linker): Supported for lockfile-first dependency tree (`yarn.lock`), audit, outdated, and workspaces.
241
+ - 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.
242
+ - Yarn Plug'n'Play (`nodeLinker: pnp`): Not supported yet.
243
+
104
244
  ## Scripts
105
245
 
106
246
  - `npm run build` – generate SPDX/report assets and compile TypeScript to `dist/`
@@ -111,32 +251,23 @@ npx dependency-radar --help
111
251
  - `npm run build:report-ui` – build report UI assets
112
252
  - `npm run build:report` – rebuild report assets used by the CLI
113
253
 
114
- ### Fixture scripts:
115
-
116
- - `npm run fixtures:install` – install core fixture dependencies
117
- - `npm run fixtures:install:all` – install all fixture dependencies
118
- - `npm run fixtures:scan` – scan the core fixture set
119
- - `npm run fixtures:install:npm`
120
- - `npm run fixtures:install:npm-heavy`
121
- - `npm run fixtures:install:pnpm`
122
- - `npm run fixtures:install:pnpm-hoisted`
123
- - `npm run fixtures:install:yarn`
124
- - `npm run fixtures:install:yarn-berry`
125
- - `npm run fixtures:install:optional`
126
- - `npm run fixtures:scan:npm`
127
- - `npm run fixtures:scan:npm-heavy`
128
- - `npm run fixtures:scan:pnpm`
129
- - `npm run fixtures:scan:pnpm-hoisted`
130
- - `npm run fixtures:scan:yarn`
131
- - `npm run fixtures:scan:yarn-berry`
132
- - `npm run fixtures:scan:optional`
133
- - `npm run fixtures:scan:no-node-modules`
254
+ ### Test scripts:
255
+
256
+ - `npm run test:unit` – run Vitest unit tests
257
+ - `npm run test:unit:watch` – watch mode for fast local iteration
258
+ - `npm run test:fixtures` – run curated fixture integration tests (mostly offline scans)
259
+ - `npm run test:fixtures:online` – run online fixture checks (audit/outdated regression coverage)
260
+ - `npm run test:fixtures:all` – run all fixture integration tests
261
+ - `npm run test:release` – full pre-release gate (`build` + unit + fixture + package dry run)
262
+
263
+ Fixture orchestration lives in `/test-fixtures/package.json` with helper scripts under `/test-fixtures/scripts`.
134
264
 
135
265
  ## Notes
136
266
 
137
267
  - The target project must have dependencies installed (run `npm install`, `pnpm install`, or `yarn install` first).
138
268
  - The scan runs on your machine and does not upload your code or dependencies anywhere.
139
269
  - `npm audit`/`pnpm audit`/`yarn npm audit` and `npm outdated`/`pnpm outdated` perform registry lookups; use `--offline` for offline-only scans.
270
+ - On some Yarn Berry setups, `yarn outdated` is not available; the scan continues and marks outdated data as unavailable.
140
271
  - A temporary `.dependency-radar` folder is created during the scan to store intermediate tool output.
141
272
  - Use `--keep-temp` to retain this folder for debugging; otherwise it is deleted automatically.
142
273
  - If some per-package tools fail (common in large workspaces), the scan continues and reports warnings; missing sections are marked unavailable where applicable.
@@ -154,7 +285,7 @@ The JSON schema matches the `AggregatedData` TypeScript interface in `src/types.
154
285
 
155
286
  ```ts
156
287
  export interface AggregatedData {
157
- schemaVersion: '1.2'; // Report schema version for compatibility checks
288
+ schemaVersion: '1.3'; // Report schema version for compatibility checks
158
289
  generatedAt: string; // ISO timestamp when the scan finished
159
290
  dependencyRadarVersion: string; // CLI version that produced the report
160
291
  git: {
@@ -162,6 +293,31 @@ export interface AggregatedData {
162
293
  };
163
294
  project: {
164
295
  projectDir: string; // Project path relative to the user's home directory (e.g. /Developer/app)
296
+ name?: string; // package.json#name from the scanned project root
297
+ version?: string; // package.json#version from the scanned project root
298
+ description?: string; // package.json#description
299
+ license?: string; // package.json#license
300
+ keywords?: string[]; // package.json#keywords
301
+ homepage?: string; // package.json#homepage
302
+ repository?: string; // repository URL (string or repository.url)
303
+ constraints?: {
304
+ os?: string[]; // package.json#os constraints
305
+ cpu?: string[]; // package.json#cpu constraints
306
+ enginesNode?: string; // package.json#engines.node
307
+ };
308
+ dependencyPolicy?: {
309
+ overrides?: Record<string, unknown>; // package.json overrides plus pnpm workspace overrides
310
+ resolutions?: Record<string, unknown>; // package.json#resolutions
311
+ };
312
+ dependencyPolicySummary?: {
313
+ hasOverrides: boolean;
314
+ overrideCount: number; // Top-level override entries
315
+ overriddenPackageNames?: string[]; // Package names parsed from override selectors
316
+ hasResolutions: boolean;
317
+ resolutionCount: number; // Top-level resolution entries
318
+ resolvedPackageNames?: string[]; // Package names parsed from resolution selectors
319
+ sources?: string[]; // Where policy came from (e.g. package.json#overrides, pnpm-workspace.yaml#overrides)
320
+ };
165
321
  };
166
322
  environment: {
167
323
  nodeVersion: string; // Node.js version from process.versions.node
@@ -169,10 +325,10 @@ export interface AggregatedData {
169
325
  minRequiredMajor: number; // Strictest Node major required by dependency engines (0 if unknown)
170
326
  platform?: string; // OS platform (process.platform)
171
327
  arch?: string; // CPU architecture (process.arch)
172
- ci?: boolean; // True when running in CI (process.env.CI === 'true')
328
+ ci?: boolean; // True when CI indicators are detected
173
329
  packageManagerField?: string; // package.json packageManager field (e.g. pnpm@9.1.0)
174
- packageManager?: 'npm' | 'pnpm' | 'yarn'; // Package manager used to scan
175
- packageManagerVersion?: string; // Version of the package manager used to scan
330
+ packageManager?: 'npm' | 'pnpm' | 'yarn'; // Package manager used for dependency/audit/outdated collection
331
+ packageManagerVersion?: string; // Version of the selected package manager (when available)
176
332
  toolVersions?: {
177
333
  npm?: string;
178
334
  pnpm?: string;
@@ -181,15 +337,25 @@ export interface AggregatedData {
181
337
  };
182
338
  workspaces: {
183
339
  enabled: boolean; // True when the scan used workspace aggregation
184
- type?: 'npm' | 'pnpm' | 'yarn' | 'none'; // Workspace type if detected
185
- packageCount?: number; // Number of workspace packages scanned
340
+ type?: 'npm' | 'pnpm' | 'yarn' | 'none'; // Workspace mode (CLI currently always emits this)
341
+ packageCount?: number; // Number of workspace packages scanned (CLI currently always emits this)
342
+ workspacePackages?: WorkspacePackage[]; // Lightweight first-party workspace metadata
186
343
  };
187
344
  summary: {
188
- dependencyCount: number; // Total dependencies in the graph
189
- directCount: number; // Dependencies listed in package.json
190
- transitiveCount: number; // Dependencies pulled in by other dependencies
345
+ dependencyCount: number; // Total EXTERNAL dependencies in the graph
346
+ directCount: number; // External dependencies listed in package.json
347
+ transitiveCount: number; // External dependencies pulled in by other dependencies
348
+ };
349
+ dependencies: Record<string, DependencyRecord>; // External third-party packages keyed by name@version
350
+ }
351
+
352
+ export interface WorkspacePackage {
353
+ name: string; // Workspace package name from package.json
354
+ relativePath: string; // Workspace-relative path (e.g. apps/web)
355
+ directExternal: {
356
+ runtime: number; // Unique direct external deps from dependencies + optionalDependencies
357
+ dev: number; // Unique direct external deps from devDependencies
191
358
  };
192
- dependencies: Record<string, DependencyRecord>; // Keyed by name@version
193
359
  }
194
360
 
195
361
  export interface DependencyRecord {
@@ -198,6 +364,8 @@ export interface DependencyRecord {
198
364
  name: string; // Package name from npm metadata
199
365
  version: string; // Installed version from npm ls
200
366
  description?: string; // Description from the installed package.json (if present)
367
+ fileCount?: number; // Number of files in the installed package folder (excluding nested node_modules)
368
+ hasBin?: true; // True if package.json declares at least one executable in `bin`
201
369
  deprecated: boolean; // True if the package.json has a deprecated flag
202
370
  links: {
203
371
  npm: string; // npm package page URL
@@ -231,7 +399,7 @@ export interface DependencyRecord {
231
399
  | 'invalid-spdx'
232
400
  | 'unknown';
233
401
  };
234
- licenseRisk: 'green' | 'amber' | 'red'; // Risk classification derived from declared/inferred SPDX ids
402
+ licenseRisk: 'green' | 'amber' | 'red'; // Risk classification derived from declared/inferred SPDX ids (mismatch is escalated to at least amber)
235
403
  };
236
404
  security: {
237
405
  summary: {
@@ -243,20 +411,20 @@ export interface DependencyRecord {
243
411
  risk: 'green' | 'amber' | 'red'; // Risk classification derived from audit counts
244
412
  };
245
413
  advisories?: Array<{
246
- id: string; // GHSA identifier
414
+ id: string; // Advisory identifier (GHSA, npm advisory ID, or source-specific fallback)
247
415
  title: string; // Human-readable advisory title
248
416
  severity: 'low' | 'moderate' | 'high' | 'critical';
249
417
  vulnerableRange: string; // Semver range
250
418
  fixAvailable: boolean; // True if npm audit indicates a fix exists
251
- url: string; // Advisory URL
419
+ url: string; // Advisory URL (may be empty when unavailable)
252
420
  }>;
253
421
  };
254
422
  upgrade: {
255
423
  nodeEngine: string | null; // engines.node from the package.json (if present)
256
- outdatedStatus?: 'current' | 'patch' | 'minor' | 'major' | 'unknown'; // Derived from npm outdated (if present)
257
- latestVersion?: string; // npm latest version (present only when status is not current)
258
- blockers?: Array<'nodeEngine' | 'peerDependency' | 'nativeBindings' | 'deprecated'>; // Reasons for upgrade friction
259
- blocksNodeMajor?: boolean; // True if local signals indicate a node major bump is risky
424
+ outdatedStatus?: 'current' | 'patch' | 'minor' | 'major' | 'unknown'; // Derived from npm outdated (field is omitted rather than set to 'current')
425
+ latestVersion?: string; // Latest version from outdated data (present for patch/minor/major when known)
426
+ blockers?: Array<'nodeEngine' | 'peerDependency' | 'nativeBindings' | 'installScripts' | 'deprecated'>; // Reasons for upgrade friction
427
+ blocksNodeMajor?: boolean; // Present when local signals indicate a Node major bump is risky
260
428
  };
261
429
  usage: {
262
430
  direct: boolean; // True if declared in package.json (dependencies/devDependencies/etc.)
@@ -286,8 +454,8 @@ export interface DependencyRecord {
286
454
  // Only installed dependencies have full dependency records in the top-level list.
287
455
  dep?: Record<string, [string, string | null]>; // Declared runtime deps
288
456
  dev?: Record<string, [string, string | null]>; // Declared dev deps
289
- peer?: Record<string, [string, string | null]>; // Declared peer deps
290
457
  opt?: Record<string, [string, string | null]>; // Declared optional deps
458
+ peer?: Record<string, [string, string | null]>; // Declared peer deps
291
459
  };
292
460
  };
293
461
  execution?: {