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 +81 -7
- package/dist/aggregator.js +203 -69
- package/dist/cli.js +24 -15
- package/dist/report-assets.js +18 -0
- package/dist/report.js +15 -1286
- package/dist/runners/importGraphRunner.js +172 -0
- package/dist/utils.js +18 -2
- package/package.json +10 -10
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
|
package/dist/aggregator.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
96
|
-
toolErrors['
|
|
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((
|
|
104
|
-
const vulnMap = parseVulnerabilities((
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const
|
|
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
|
|
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
|
|
119
|
-
|
|
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 (!
|
|
124
|
-
|
|
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 =
|
|
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
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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(
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
532
|
+
const result = { classification: 'dev-only', reason: 'Declared in devDependencies' };
|
|
533
|
+
cache.set(nodeKey, result);
|
|
534
|
+
return result;
|
|
419
535
|
}
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
}
|