dependency-radar 0.1.1 → 0.2.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
@@ -5,7 +5,7 @@ Dependency Radar is a local-first CLI tool that inspects a Node.js project’s i
5
5
  ## What it does
6
6
 
7
7
  - Analyses installed dependencies using only local data (no SaaS, no uploads by default)
8
- - Combines multiple tools (npm audit, npm ls, license-checker, depcheck, madge) into a single report
8
+ - Combines multiple tools (npm audit, npm ls, import graph analysis) into a single report
9
9
  - Shows direct vs sub-dependencies, dependency depth, and parent relationships
10
10
  - Highlights licences, known vulnerabilities, install-time scripts, native modules, and package footprint
11
11
  - Produces a single self-contained HTML file you can share or archive
@@ -26,7 +26,7 @@ npm run build
26
26
 
27
27
  ## Requirements
28
28
 
29
- - Node.js 18+ (required by `madge`, one of the analyzers)
29
+ - Node.js 14.14+
30
30
 
31
31
  ## Usage
32
32
 
@@ -35,25 +35,40 @@ The simplest way to run Dependency Radar is via npx. It runs in the current dire
35
35
  Run a scan against the current project (writes `dependency-radar.html`):
36
36
 
37
37
  ```bash
38
- npx dependency-radar scan
38
+ npx dependency-radar
39
39
  ```
40
40
 
41
+ The `scan` command is the default and can also be run explicitly as `npx dependency-radar scan`.
42
+
43
+
41
44
  Specify a project and output path:
42
45
 
43
46
  ```bash
44
- npx dependency-radar scan --project ./my-app --out ./reports/dependency-radar.html
47
+ npx dependency-radar --project ./my-app --out ./reports/dependency-radar.html
45
48
  ```
46
49
 
47
50
  Keep the temporary `.dependency-radar` folder for debugging raw tool outputs:
48
51
 
49
52
  ```bash
50
- npx dependency-radar scan --keep-temp
53
+ npx dependency-radar --keep-temp
51
54
  ```
52
55
 
53
56
  Skip `npm audit` (useful for offline scans):
54
57
 
55
58
  ```bash
56
- npx dependency-radar scan --no-audit
59
+ npx dependency-radar --no-audit
60
+ ```
61
+
62
+ Output JSON instead of HTML report:
63
+
64
+ ```bash
65
+ npx dependency-radar --json
66
+ ```
67
+
68
+ Show options:
69
+
70
+ ```bash
71
+ npx dependency-radar --help
57
72
  ```
58
73
 
59
74
  ## Scripts
@@ -70,9 +85,68 @@ npx dependency-radar scan --no-audit
70
85
  - A temporary `.dependency-radar` folder is created during the scan to store intermediate tool output.
71
86
  - Use `--keep-temp` to retain this folder for debugging; otherwise it is deleted automatically.
72
87
  - If a tool fails, its section is marked as unavailable, but the report is still generated.
73
- - Node.js 18+ is required because `madge` (one of the analyzers) targets Node 18+.
74
88
 
75
89
  ## Output
76
90
 
77
91
  Dependency Radar writes a single HTML file (dependency-radar.html by default).
78
92
  The file is fully self-contained and can be opened locally in a browser, shared with others, or attached to tickets and documentation.
93
+
94
+ ### JSON output
95
+
96
+ Use `--json` to write the aggregated scan data as JSON (defaults to `dependency-radar.json`).
97
+
98
+ The JSON schema matches the `AggregatedData` TypeScript interface in `src/types.ts`. For quick reference:
99
+
100
+ ```ts
101
+ export interface AggregatedData {
102
+ generatedAt: string;
103
+ projectPath: string;
104
+ gitBranch?: string;
105
+ dependencyRadarVersion?: string;
106
+ maintenanceEnabled: boolean;
107
+ environment: {
108
+ node: {
109
+ runtimeVersion: string;
110
+ runtimeMajor: number;
111
+ minRequiredMajor?: number;
112
+ source: 'dependency-engines' | 'project-engines' | 'unknown';
113
+ };
114
+ };
115
+ dependencies: DependencyRecord[];
116
+ toolErrors: Record<string, string>;
117
+ raw: RawOutputs;
118
+ importAnalysis?: ImportAnalysisSummary;
119
+ }
120
+ ```
121
+
122
+ For full details on `DependencyRecord`, `RawOutputs`, and related types, see `src/types.ts`.
123
+
124
+ ## Development
125
+
126
+ ### Report UI Development
127
+
128
+ The HTML report UI is developed in a separate Vite project located in `report-ui/`. This provides a proper development environment with hot reload, TypeScript support, and sample data.
129
+
130
+ **Start the development server:**
131
+
132
+ ```bash
133
+ npm run dev:report
134
+ ```
135
+
136
+ This opens the report UI in your browser with sample data covering all dependency states (various licenses, vulnerability severities, usage statuses, etc.).
137
+
138
+ **Build workflow:**
139
+
140
+ 1. Make changes in `report-ui/` (edit `style.css`, `main.ts`, `index.html`)
141
+ 2. Run `npm run build:report` to compile and inject assets into `src/report-assets.ts`
142
+ 3. Run `npm run build` to compile the full project (this runs `build:report` automatically)
143
+
144
+ **File structure:**
145
+
146
+ - `report-ui/index.html` – HTML template structure
147
+ - `report-ui/style.css` – All CSS styles
148
+ - `report-ui/main.ts` – TypeScript rendering logic
149
+ - `report-ui/sample-data.json` – Sample data for development
150
+ - `report-ui/types.ts` – Client-side TypeScript types
151
+ - `src/report-assets.ts` – Auto-generated file with bundled CSS/JS (do not edit directly)
152
+
@@ -78,64 +78,57 @@ function normalizeRepoUrl(url) {
78
78
  return normalized;
79
79
  }
80
80
  async function aggregateData(input) {
81
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
81
+ var _a, _b, _c, _d, _e, _f;
82
82
  const pkg = await (0, utils_1.readPackageJson)(input.projectPath);
83
83
  const raw = {
84
84
  audit: (_a = input.auditResult) === null || _a === void 0 ? void 0 : _a.data,
85
85
  npmLs: (_b = input.npmLsResult) === null || _b === void 0 ? void 0 : _b.data,
86
- licenseChecker: (_c = input.licenseResult) === null || _c === void 0 ? void 0 : _c.data,
87
- depcheck: (_d = input.depcheckResult) === null || _d === void 0 ? void 0 : _d.data,
88
- madge: (_e = input.madgeResult) === null || _e === void 0 ? void 0 : _e.data
86
+ importGraph: (_c = input.importGraphResult) === null || _c === void 0 ? void 0 : _c.data
89
87
  };
90
88
  const toolErrors = {};
91
89
  if (input.auditResult && !input.auditResult.ok)
92
90
  toolErrors['npm-audit'] = input.auditResult.error || 'unknown error';
93
91
  if (input.npmLsResult && !input.npmLsResult.ok)
94
92
  toolErrors['npm-ls'] = input.npmLsResult.error || 'unknown error';
95
- if (input.licenseResult && !input.licenseResult.ok)
96
- toolErrors['license-checker'] = input.licenseResult.error || 'unknown error';
97
- if (input.depcheckResult && !input.depcheckResult.ok)
98
- toolErrors['depcheck'] = input.depcheckResult.error || 'unknown error';
99
- if (input.madgeResult && !input.madgeResult.ok)
100
- toolErrors['madge'] = input.madgeResult.error || 'unknown error';
93
+ if (input.importGraphResult && !input.importGraphResult.ok)
94
+ toolErrors['import-graph'] = input.importGraphResult.error || 'unknown error';
101
95
  // Get git branch
102
96
  const gitBranch = await getGitBranch(input.projectPath);
103
- const nodeMap = buildNodeMap((_f = input.npmLsResult) === null || _f === void 0 ? void 0 : _f.data, pkg);
104
- const vulnMap = parseVulnerabilities((_g = input.auditResult) === null || _g === void 0 ? void 0 : _g.data);
105
- const licenseData = normalizeLicenseData((_h = input.licenseResult) === null || _h === void 0 ? void 0 : _h.data);
106
- const depcheckUsage = buildUsageInfo((_j = input.depcheckResult) === null || _j === void 0 ? void 0 : _j.data);
107
- const importInfo = buildImportInfo((_k = input.madgeResult) === null || _k === void 0 ? void 0 : _k.data);
97
+ const nodeMap = buildNodeMap((_d = input.npmLsResult) === null || _d === void 0 ? void 0 : _d.data, pkg);
98
+ const vulnMap = parseVulnerabilities((_e = input.auditResult) === null || _e === void 0 ? void 0 : _e.data);
99
+ const importGraph = normalizeImportGraph((_f = input.importGraphResult) === null || _f === void 0 ? void 0 : _f.data);
100
+ const importInfo = buildImportInfo(importGraph.files);
101
+ const importAnalysis = buildImportAnalysis(importGraph, pkg);
102
+ const packageUsageCounts = new Map(Object.entries(importAnalysis.packageHotness));
108
103
  const maintenanceCache = new Map();
104
+ const runtimeCache = new Map();
109
105
  const packageMetaCache = new Map();
110
106
  const packageStatCache = new Map();
111
107
  const dependencies = [];
112
- const licenseFallbackCache = new Map();
108
+ const licenseCache = new Map();
109
+ const nodeEngineRanges = [];
113
110
  const nodes = Array.from(nodeMap.values());
114
111
  const totalDeps = nodes.length;
115
112
  let maintenanceIndex = 0;
116
113
  for (const node of nodes) {
117
114
  const direct = isDirectDependency(node.name, pkg);
118
- const license = licenseData.byKey.get(node.key) ||
119
- licenseData.byName.get(node.name) ||
120
- licenseFallbackCache.get(node.name) ||
115
+ const cachedLicense = licenseCache.get(node.name);
116
+ const license = cachedLicense ||
121
117
  (await (0, utils_1.readLicenseFromPackageJson)(node.name, input.projectPath)) ||
122
118
  { license: undefined };
123
- if (!licenseFallbackCache.has(node.name) && license.license) {
124
- licenseFallbackCache.set(node.name, license);
119
+ if (!licenseCache.has(node.name) && (license.license || license.licenseFile)) {
120
+ licenseCache.set(node.name, license);
125
121
  }
126
122
  const vulnerabilities = vulnMap.get(node.name) || emptyVulnSummary();
127
123
  const licenseRisk = (0, utils_1.licenseRiskLevel)(license.license);
128
124
  const vulnRisk = (0, utils_1.vulnRiskLevel)(vulnerabilities.counts);
129
- const usage = depcheckUsage.get(node.name) ||
130
- (((_l = input.depcheckResult) === null || _l === void 0 ? void 0 : _l.data)
131
- ? { status: 'used', reason: 'Not flagged as unused by depcheck' }
132
- : { status: 'unknown', reason: 'depcheck unavailable' });
125
+ const usage = buildUsageInfo(node.name, packageUsageCounts, pkg);
133
126
  const maintenance = await resolveMaintenance(node.name, maintenanceCache, input.maintenanceEnabled, ++maintenanceIndex, totalDeps, input.onMaintenanceProgress);
134
127
  if (!maintenanceCache.has(node.name)) {
135
128
  maintenanceCache.set(node.name, maintenance);
136
129
  }
137
130
  const maintenanceRiskLevel = (0, utils_1.maintenanceRisk)(maintenance.lastPublished);
138
- const runtimeData = classifyRuntime(node, pkg, nodeMap);
131
+ const runtimeData = classifyRuntime(node.key, pkg, nodeMap, runtimeCache);
139
132
  // Calculate root causes (direct dependencies that cause this to be installed)
140
133
  const rootCauses = findRootCauses(node, nodeMap, pkg);
141
134
  // Build dependedOnBy and dependsOn lists
@@ -148,6 +141,9 @@ async function aggregateData(input) {
148
141
  return child ? child.name : key.split('@')[0];
149
142
  });
150
143
  const packageInsights = await gatherPackageInsights(node.name, input.projectPath, packageMetaCache, packageStatCache, node.parents.size, node.children.size, dependedOnBy, dependsOn);
144
+ if (packageInsights.identity.nodeEngine) {
145
+ nodeEngineRanges.push(packageInsights.identity.nodeEngine);
146
+ }
151
147
  dependencies.push({
152
148
  name: node.name,
153
149
  version: node.version,
@@ -180,17 +176,91 @@ async function aggregateData(input) {
180
176
  });
181
177
  }
182
178
  dependencies.sort((a, b) => a.name.localeCompare(b.name));
179
+ const runtimeVersion = process.version.replace(/^v/, '');
180
+ const runtimeMajor = Number.parseInt(runtimeVersion.split('.')[0], 10);
181
+ const minRequiredMajor = deriveMinRequiredMajor(nodeEngineRanges);
183
182
  return {
184
183
  generatedAt: new Date().toISOString(),
185
184
  projectPath: input.projectPath,
186
185
  dependencyRadarVersion,
187
186
  gitBranch,
188
187
  maintenanceEnabled: input.maintenanceEnabled,
188
+ environment: {
189
+ node: {
190
+ runtimeVersion,
191
+ runtimeMajor: Number.isNaN(runtimeMajor) ? 0 : runtimeMajor,
192
+ minRequiredMajor,
193
+ source: minRequiredMajor === undefined ? 'unknown' : 'dependency-engines'
194
+ }
195
+ },
189
196
  dependencies,
190
197
  toolErrors,
191
- raw
198
+ raw,
199
+ importAnalysis
192
200
  };
193
201
  }
202
+ function deriveMinRequiredMajor(engineRanges) {
203
+ let strictest;
204
+ for (const range of engineRanges) {
205
+ const minMajor = parseMinMajorFromRange(range);
206
+ if (minMajor === undefined)
207
+ continue;
208
+ if (strictest === undefined || minMajor > strictest) {
209
+ strictest = minMajor;
210
+ }
211
+ }
212
+ return strictest;
213
+ }
214
+ function parseMinMajorFromRange(range) {
215
+ const normalized = range.trim();
216
+ if (!normalized)
217
+ return undefined;
218
+ const clauses = normalized.split('||').map((clause) => clause.trim()).filter(Boolean);
219
+ if (clauses.length === 0)
220
+ return undefined;
221
+ let rangeMin;
222
+ for (const clause of clauses) {
223
+ const clauseMin = parseMinMajorFromClause(clause);
224
+ // Conservative: skip ranges that allow any version in at least one clause.
225
+ if (clauseMin === undefined)
226
+ return undefined;
227
+ if (rangeMin === undefined || clauseMin < rangeMin) {
228
+ rangeMin = clauseMin;
229
+ }
230
+ }
231
+ return rangeMin;
232
+ }
233
+ function parseMinMajorFromClause(clause) {
234
+ const hyphenMatch = clause.match(/(\d+)\s*-\s*\d+/);
235
+ if (hyphenMatch) {
236
+ return Number.parseInt(hyphenMatch[1], 10);
237
+ }
238
+ const tokens = clause.replace(/,/g, ' ').split(/\s+/).filter(Boolean);
239
+ let clauseMin;
240
+ for (const token of tokens) {
241
+ if (token.startsWith('<'))
242
+ continue;
243
+ const major = parseMajorFromToken(token);
244
+ if (major === undefined)
245
+ continue;
246
+ if (clauseMin === undefined || major > clauseMin) {
247
+ clauseMin = major;
248
+ }
249
+ }
250
+ return clauseMin;
251
+ }
252
+ function parseMajorFromToken(token) {
253
+ const trimmed = token.trim();
254
+ if (!trimmed)
255
+ return undefined;
256
+ if (!/^[0-9^~=>v]/.test(trimmed))
257
+ return undefined;
258
+ const match = trimmed.match(/v?(\d+)/);
259
+ if (!match)
260
+ return undefined;
261
+ const major = Number.parseInt(match[1], 10);
262
+ return Number.isNaN(major) ? undefined : major;
263
+ }
194
264
  function buildNodeMap(lsData, pkg) {
195
265
  const map = new Map();
196
266
  const traverse = (node, depth, parentKey, providedName) => {
@@ -335,40 +405,73 @@ function computeHighestSeverity(counts) {
335
405
  return 'low';
336
406
  return 'none';
337
407
  }
338
- function normalizeLicenseData(data) {
339
- const byKey = new Map();
340
- const byName = new Map();
341
- if (!data)
342
- return { byKey, byName };
343
- Object.entries(data).forEach(([key, value]) => {
344
- const lic = Array.isArray(value.licenses) ? value.licenses.join(' OR ') : value.licenses;
345
- const entry = {
346
- license: lic,
347
- licenseFile: value.licenseFile || value.licenseFilePath
408
+ function normalizeImportGraph(data) {
409
+ if (data && typeof data === 'object' && data.files && data.packages) {
410
+ return {
411
+ files: data.files || {},
412
+ packages: data.packages || {},
413
+ unresolvedImports: Array.isArray(data.unresolvedImports) ? data.unresolvedImports : []
348
414
  };
349
- byKey.set(key, entry);
350
- const namePart = key.includes('@', 1) ? key.slice(0, key.lastIndexOf('@')) : key;
351
- if (!byName.has(namePart))
352
- byName.set(namePart, entry);
353
- });
354
- return { byKey, byName };
415
+ }
416
+ return { files: data || {}, packages: {}, unresolvedImports: [] };
355
417
  }
356
- function buildUsageInfo(depcheckData) {
357
- const map = new Map();
358
- if (!depcheckData)
359
- return map;
360
- const unused = new Set([...(depcheckData.dependencies || []), ...(depcheckData.devDependencies || [])]);
361
- unused.forEach((name) => {
362
- map.set(name, { status: 'unused', reason: 'Marked unused by depcheck' });
363
- });
364
- if (Array.isArray(depcheckData.dependencies) || Array.isArray(depcheckData.devDependencies)) {
365
- Object.keys(depcheckData.missing || {}).forEach((name) => {
366
- if (!map.has(name)) {
367
- map.set(name, { status: 'unknown', reason: 'Missing according to depcheck' });
368
- }
369
- });
418
+ function buildUsageInfo(name, packageUsageCounts, pkg) {
419
+ const declared = Boolean((pkg.dependencies && pkg.dependencies[name]) || (pkg.devDependencies && pkg.devDependencies[name]));
420
+ const importedCount = packageUsageCounts.get(name) || 0;
421
+ if (importedCount > 0) {
422
+ if (declared) {
423
+ return {
424
+ status: 'imported',
425
+ reason: `Imported by ${importedCount} file${importedCount === 1 ? '' : 's'} (static analysis)`
426
+ };
427
+ }
428
+ return {
429
+ status: 'undeclared',
430
+ reason: 'Imported but not declared (may rely on transitive resolution; pnpm will usually break this)'
431
+ };
370
432
  }
371
- return map;
433
+ if (declared) {
434
+ return {
435
+ status: 'not-imported',
436
+ reason: 'Declared but never statically imported (may be used via tooling, scripts, or runtime plugins)'
437
+ };
438
+ }
439
+ return {
440
+ status: 'unknown',
441
+ reason: 'Not statically imported; package is likely transitive or used dynamically'
442
+ };
443
+ }
444
+ function buildImportAnalysis(graph, pkg) {
445
+ const packageImporters = new Map();
446
+ Object.entries(graph.packages || {}).forEach(([file, packages]) => {
447
+ const unique = new Set(packages || []);
448
+ unique.forEach((pkgName) => {
449
+ if (!packageImporters.has(pkgName))
450
+ packageImporters.set(pkgName, new Set());
451
+ packageImporters.get(pkgName).add(file);
452
+ });
453
+ });
454
+ const packageHotness = {};
455
+ packageImporters.forEach((importers, name) => {
456
+ packageHotness[name] = importers.size;
457
+ });
458
+ const declared = new Set([
459
+ ...Object.keys(pkg.dependencies || {}),
460
+ ...Object.keys(pkg.devDependencies || {})
461
+ ]);
462
+ const undeclaredImports = Array.from(packageImporters.keys())
463
+ .filter((name) => !declared.has(name))
464
+ .sort();
465
+ return {
466
+ staticOnly: true,
467
+ notes: [
468
+ 'Import analysis is static only.',
469
+ 'Dynamic imports, runtime plugin loading, and tooling usage are not evaluated.'
470
+ ],
471
+ packageHotness,
472
+ undeclaredImports,
473
+ unresolvedImports: graph.unresolvedImports || []
474
+ };
372
475
  }
373
476
  function buildImportInfo(graphData) {
374
477
  if (!graphData || typeof graphData !== 'object')
@@ -410,24 +513,55 @@ async function resolveMaintenance(name, cache, maintenanceEnabled, current, tota
410
513
  return { status: 'unknown', reason: 'lookup failed' };
411
514
  }
412
515
  }
413
- function classifyRuntime(node, pkg, map) {
516
+ function classifyRuntime(nodeKey, pkg, map, cache) {
517
+ const cached = cache.get(nodeKey);
518
+ if (cached)
519
+ return cached;
520
+ const node = map.get(nodeKey);
521
+ if (!node) {
522
+ const fallback = { classification: 'build-time', reason: 'Unknown node in dependency graph' };
523
+ cache.set(nodeKey, fallback);
524
+ return fallback;
525
+ }
414
526
  if (pkg.dependencies && pkg.dependencies[node.name]) {
415
- return { classification: 'runtime', reason: 'Declared in dependencies' };
527
+ const result = { classification: 'runtime', reason: 'Declared in dependencies' };
528
+ cache.set(nodeKey, result);
529
+ return result;
416
530
  }
417
531
  if (pkg.devDependencies && pkg.devDependencies[node.name]) {
418
- return { classification: 'dev-only', reason: 'Declared in devDependencies' };
532
+ const result = { classification: 'dev-only', reason: 'Declared in devDependencies' };
533
+ cache.set(nodeKey, result);
534
+ return result;
419
535
  }
420
- if (node.dev) {
421
- return { classification: 'dev-only', reason: 'npm ls marks as dev dependency' };
536
+ // Memoized recursion to inherit runtime class from parents; conservative for cycles.
537
+ const parentClasses = [];
538
+ const inProgress = cache.get(`__visiting__${nodeKey}`);
539
+ if (inProgress) {
540
+ const cycleFallback = { classification: 'build-time', reason: 'Dependency cycle; defaulting to build-time' };
541
+ cache.set(nodeKey, cycleFallback);
542
+ return cycleFallback;
422
543
  }
423
- const hasRuntimeParent = Array.from(node.parents).some((parentKey) => {
544
+ cache.set(`__visiting__${nodeKey}`, { classification: 'build-time', reason: 'visiting' });
545
+ for (const parentKey of node.parents) {
424
546
  const parent = map.get(parentKey);
425
- return parent ? !parent.dev : false;
426
- });
427
- if (hasRuntimeParent) {
428
- return { classification: 'runtime', reason: 'Transitive of runtime dependency' };
547
+ if (!parent)
548
+ continue;
549
+ const parentClass = classifyRuntime(parentKey, pkg, map, cache).classification;
550
+ parentClasses.push(parentClass);
551
+ }
552
+ cache.delete(`__visiting__${nodeKey}`);
553
+ let result;
554
+ if (parentClasses.includes('runtime')) {
555
+ result = { classification: 'runtime', reason: 'Transitive of runtime dependency' };
556
+ }
557
+ else if (parentClasses.includes('build-time')) {
558
+ result = { classification: 'build-time', reason: 'Transitive of build-time dependency' };
429
559
  }
430
- return { classification: 'build-time', reason: 'Only seen in dev dependency tree' };
560
+ else {
561
+ result = { classification: 'dev-only', reason: 'Transitive of dev-only dependency' };
562
+ }
563
+ cache.set(nodeKey, result);
564
+ return result;
431
565
  }
432
566
  async function gatherPackageInsights(name, projectPath, metaCache, statCache, fanIn, fanOut, dependedOnBy, dependsOn) {
433
567
  var _a;
package/dist/cli.js CHANGED
@@ -6,9 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const aggregator_1 = require("./aggregator");
9
- const depcheckRunner_1 = require("./runners/depcheckRunner");
10
- const licenseChecker_1 = require("./runners/licenseChecker");
11
- const madgeRunner_1 = require("./runners/madgeRunner");
9
+ const importGraphRunner_1 = require("./runners/importGraphRunner");
12
10
  const npmAudit_1 = require("./runners/npmAudit");
13
11
  const npmLs_1 = require("./runners/npmLs");
14
12
  const report_1 = require("./report");
@@ -21,7 +19,8 @@ function parseArgs(argv) {
21
19
  out: 'dependency-radar.html',
22
20
  keepTemp: false,
23
21
  maintenance: false,
24
- audit: true
22
+ audit: true,
23
+ json: false
25
24
  };
26
25
  const args = [...argv];
27
26
  if (args[0] && !args[0].startsWith('-')) {
@@ -41,6 +40,8 @@ function parseArgs(argv) {
41
40
  opts.maintenance = true;
42
41
  else if (arg === '--no-audit')
43
42
  opts.audit = false;
43
+ else if (arg === '--json')
44
+ opts.json = true;
44
45
  else if (arg === '--help' || arg === '-h') {
45
46
  printHelp();
46
47
  process.exit(0);
@@ -49,11 +50,14 @@ function parseArgs(argv) {
49
50
  return opts;
50
51
  }
51
52
  function printHelp() {
52
- console.log(`dependency-radar scan [options]
53
+ console.log(`dependency-radar [scan] [options]
54
+
55
+ If no command is provided, \`scan\` is run by default.
53
56
 
54
57
  Options:
55
58
  --project <path> Project folder (default: cwd)
56
59
  --out <path> Output HTML file (default: dependency-radar.html)
60
+ --json Write aggregated data to JSON (default filename: dependency-radar.json)
57
61
  --keep-temp Keep .dependency-radar folder
58
62
  --maintenance Enable slow maintenance checks (npm registry calls)
59
63
  --no-audit Skip npm audit (useful for offline scans)
@@ -67,6 +71,9 @@ async function run() {
67
71
  return;
68
72
  }
69
73
  const projectPath = path_1.default.resolve(opts.project);
74
+ if (opts.json && opts.out === 'dependency-radar.html') {
75
+ opts.out = 'dependency-radar.json';
76
+ }
70
77
  let outputPath = path_1.default.resolve(opts.out);
71
78
  const startTime = Date.now();
72
79
  let dependencyCount = 0;
@@ -75,7 +82,7 @@ async function run() {
75
82
  const endsWithSeparator = opts.out.endsWith('/') || opts.out.endsWith('\\');
76
83
  const hasExtension = Boolean(path_1.default.extname(outputPath));
77
84
  if ((stat && stat.isDirectory()) || endsWithSeparator || (!stat && !hasExtension)) {
78
- outputPath = path_1.default.join(outputPath, 'dependency-radar.html');
85
+ outputPath = path_1.default.join(outputPath, opts.json ? 'dependency-radar.json' : 'dependency-radar.html');
79
86
  }
80
87
  }
81
88
  catch (e) {
@@ -85,12 +92,10 @@ async function run() {
85
92
  const stopSpinner = startSpinner(`Scanning project at ${projectPath}`);
86
93
  try {
87
94
  await (0, utils_1.ensureDir)(tempDir);
88
- const [auditResult, npmLsResult, licenseResult, depcheckResult, madgeResult] = await Promise.all([
95
+ const [auditResult, npmLsResult, importGraphResult] = await Promise.all([
89
96
  opts.audit ? (0, npmAudit_1.runNpmAudit)(projectPath, tempDir) : Promise.resolve(undefined),
90
97
  (0, npmLs_1.runNpmLs)(projectPath, tempDir),
91
- (0, licenseChecker_1.runLicenseChecker)(projectPath, tempDir),
92
- (0, depcheckRunner_1.runDepcheck)(projectPath, tempDir),
93
- (0, madgeRunner_1.runMadge)(projectPath, tempDir)
98
+ (0, importGraphRunner_1.runImportGraph)(projectPath, tempDir)
94
99
  ]);
95
100
  if (opts.maintenance) {
96
101
  stopSpinner(true);
@@ -107,17 +112,21 @@ async function run() {
107
112
  : undefined,
108
113
  auditResult,
109
114
  npmLsResult,
110
- licenseResult,
111
- depcheckResult,
112
- madgeResult
115
+ importGraphResult
113
116
  });
114
117
  dependencyCount = aggregated.dependencies.length;
115
118
  if (opts.maintenance) {
116
119
  process.stdout.write('\n');
117
120
  }
118
- await (0, report_1.renderReport)(aggregated, outputPath);
121
+ if (opts.json) {
122
+ await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
123
+ await promises_1.default.writeFile(outputPath, JSON.stringify(aggregated, null, 2), 'utf8');
124
+ }
125
+ else {
126
+ await (0, report_1.renderReport)(aggregated, outputPath);
127
+ }
119
128
  stopSpinner(true);
120
- console.log(`Report written to ${outputPath}`);
129
+ console.log(`${opts.json ? 'JSON' : 'Report'} written to ${outputPath}`);
121
130
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
122
131
  console.log(`Scan complete: ${dependencyCount} dependencies analysed in ${elapsed}s`);
123
132
  }