dependency-radar 0.1.1
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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/aggregator.js +585 -0
- package/dist/cli.js +158 -0
- package/dist/index.js +18 -0
- package/dist/report.js +1525 -0
- package/dist/runners/depcheckRunner.js +23 -0
- package/dist/runners/licenseChecker.js +33 -0
- package/dist/runners/madgeRunner.js +29 -0
- package/dist/runners/npmAudit.js +31 -0
- package/dist/runners/npmLs.js +32 -0
- package/dist/types.js +2 -0
- package/dist/utils.js +148 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joseph Maynard
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Dependency Radar
|
|
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.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
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
|
|
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
|
|
12
|
+
|
|
13
|
+
## What it is not
|
|
14
|
+
|
|
15
|
+
- Not a CI service or hosted platform
|
|
16
|
+
- Not a replacement for dedicated security scanners
|
|
17
|
+
- Not a bundler or build tool
|
|
18
|
+
- Not a dependency updater
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install
|
|
24
|
+
npm run build
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- Node.js 18+ (required by `madge`, one of the analyzers)
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
The simplest way to run Dependency Radar is via npx. It runs in the current directory and writes an HTML report to disk.
|
|
34
|
+
|
|
35
|
+
Run a scan against the current project (writes `dependency-radar.html`):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx dependency-radar scan
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Specify a project and output path:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx dependency-radar scan --project ./my-app --out ./reports/dependency-radar.html
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Keep the temporary `.dependency-radar` folder for debugging raw tool outputs:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx dependency-radar scan --keep-temp
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Skip `npm audit` (useful for offline scans):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx dependency-radar scan --no-audit
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Scripts
|
|
60
|
+
|
|
61
|
+
- `npm run build` – compile TypeScript to `dist/`
|
|
62
|
+
- `npm run dev` – run a scan from source (`ts-node`)
|
|
63
|
+
- `npm run scan` – run a scan from the built output
|
|
64
|
+
|
|
65
|
+
## Notes
|
|
66
|
+
|
|
67
|
+
- The target project must have node_modules installed (run npm install first).
|
|
68
|
+
- The scan is local-first and does not upload your code or dependencies anywhere.
|
|
69
|
+
- `npm audit` performs registry lookups; use `--no-audit` for offline-only scans.
|
|
70
|
+
- A temporary `.dependency-radar` folder is created during the scan to store intermediate tool output.
|
|
71
|
+
- Use `--keep-temp` to retain this folder for debugging; otherwise it is deleted automatically.
|
|
72
|
+
- 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
|
+
|
|
75
|
+
## Output
|
|
76
|
+
|
|
77
|
+
Dependency Radar writes a single HTML file (dependency-radar.html by default).
|
|
78
|
+
The file is fully self-contained and can be opened locally in a browser, shared with others, or attached to tickets and documentation.
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.aggregateData = aggregateData;
|
|
7
|
+
const utils_1 = require("./utils");
|
|
8
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const dependencyRadarVersion = (0, utils_1.getDependencyRadarVersion)();
|
|
11
|
+
async function getGitBranch(projectPath) {
|
|
12
|
+
var _a;
|
|
13
|
+
try {
|
|
14
|
+
const result = await (0, utils_1.runCommand)('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
|
15
|
+
const branch = (_a = result.stdout) === null || _a === void 0 ? void 0 : _a.trim();
|
|
16
|
+
// HEAD means detached state
|
|
17
|
+
if (!branch || branch === 'HEAD') {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return branch;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function findRootCauses(node, nodeMap, pkg) {
|
|
27
|
+
// If it's a direct dependency, it's its own root cause
|
|
28
|
+
if (isDirectDependency(node.name, pkg)) {
|
|
29
|
+
return [node.name];
|
|
30
|
+
}
|
|
31
|
+
// BFS up the parent chain to find all direct dependencies that lead to this
|
|
32
|
+
const rootCauses = new Set();
|
|
33
|
+
const visited = new Set();
|
|
34
|
+
const queue = [...node.parents];
|
|
35
|
+
while (queue.length > 0) {
|
|
36
|
+
const parentKey = queue.shift();
|
|
37
|
+
if (visited.has(parentKey))
|
|
38
|
+
continue;
|
|
39
|
+
visited.add(parentKey);
|
|
40
|
+
const parent = nodeMap.get(parentKey);
|
|
41
|
+
if (!parent)
|
|
42
|
+
continue;
|
|
43
|
+
if (isDirectDependency(parent.name, pkg)) {
|
|
44
|
+
rootCauses.add(parent.name);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// Keep going up the chain
|
|
48
|
+
for (const grandparent of parent.parents) {
|
|
49
|
+
if (!visited.has(grandparent)) {
|
|
50
|
+
queue.push(grandparent);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Array.from(rootCauses).sort();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Normalize repository URLs to browsable HTTPS format.
|
|
59
|
+
* Handles: git+https://, git://, github:user/repo, git@github.com:user/repo.git
|
|
60
|
+
*/
|
|
61
|
+
function normalizeRepoUrl(url) {
|
|
62
|
+
if (!url)
|
|
63
|
+
return url;
|
|
64
|
+
// Handle shorthand: github:user/repo or user/repo
|
|
65
|
+
if (url.match(/^(github:|gitlab:|bitbucket:)?[\w-]+\/[\w.-]+$/)) {
|
|
66
|
+
const cleaned = url.replace(/^(github:|gitlab:|bitbucket:)/, '');
|
|
67
|
+
const host = url.startsWith('gitlab:') ? 'gitlab.com'
|
|
68
|
+
: url.startsWith('bitbucket:') ? 'bitbucket.org'
|
|
69
|
+
: 'github.com';
|
|
70
|
+
return `https://${host}/${cleaned}`;
|
|
71
|
+
}
|
|
72
|
+
// Handle git+https:// or git:// prefix
|
|
73
|
+
let normalized = url.replace(/^git\+/, '').replace(/^git:\/\//, 'https://');
|
|
74
|
+
// Handle git@host:user/repo.git SSH format
|
|
75
|
+
normalized = normalized.replace(/^git@([^:]+):(.+)$/, 'https://$1/$2');
|
|
76
|
+
// Remove .git suffix
|
|
77
|
+
normalized = normalized.replace(/\.git$/, '');
|
|
78
|
+
return normalized;
|
|
79
|
+
}
|
|
80
|
+
async function aggregateData(input) {
|
|
81
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
82
|
+
const pkg = await (0, utils_1.readPackageJson)(input.projectPath);
|
|
83
|
+
const raw = {
|
|
84
|
+
audit: (_a = input.auditResult) === null || _a === void 0 ? void 0 : _a.data,
|
|
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
|
|
89
|
+
};
|
|
90
|
+
const toolErrors = {};
|
|
91
|
+
if (input.auditResult && !input.auditResult.ok)
|
|
92
|
+
toolErrors['npm-audit'] = input.auditResult.error || 'unknown error';
|
|
93
|
+
if (input.npmLsResult && !input.npmLsResult.ok)
|
|
94
|
+
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';
|
|
101
|
+
// Get git branch
|
|
102
|
+
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);
|
|
108
|
+
const maintenanceCache = new Map();
|
|
109
|
+
const packageMetaCache = new Map();
|
|
110
|
+
const packageStatCache = new Map();
|
|
111
|
+
const dependencies = [];
|
|
112
|
+
const licenseFallbackCache = new Map();
|
|
113
|
+
const nodes = Array.from(nodeMap.values());
|
|
114
|
+
const totalDeps = nodes.length;
|
|
115
|
+
let maintenanceIndex = 0;
|
|
116
|
+
for (const node of nodes) {
|
|
117
|
+
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) ||
|
|
121
|
+
(await (0, utils_1.readLicenseFromPackageJson)(node.name, input.projectPath)) ||
|
|
122
|
+
{ license: undefined };
|
|
123
|
+
if (!licenseFallbackCache.has(node.name) && license.license) {
|
|
124
|
+
licenseFallbackCache.set(node.name, license);
|
|
125
|
+
}
|
|
126
|
+
const vulnerabilities = vulnMap.get(node.name) || emptyVulnSummary();
|
|
127
|
+
const licenseRisk = (0, utils_1.licenseRiskLevel)(license.license);
|
|
128
|
+
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' });
|
|
133
|
+
const maintenance = await resolveMaintenance(node.name, maintenanceCache, input.maintenanceEnabled, ++maintenanceIndex, totalDeps, input.onMaintenanceProgress);
|
|
134
|
+
if (!maintenanceCache.has(node.name)) {
|
|
135
|
+
maintenanceCache.set(node.name, maintenance);
|
|
136
|
+
}
|
|
137
|
+
const maintenanceRiskLevel = (0, utils_1.maintenanceRisk)(maintenance.lastPublished);
|
|
138
|
+
const runtimeData = classifyRuntime(node, pkg, nodeMap);
|
|
139
|
+
// Calculate root causes (direct dependencies that cause this to be installed)
|
|
140
|
+
const rootCauses = findRootCauses(node, nodeMap, pkg);
|
|
141
|
+
// Build dependedOnBy and dependsOn lists
|
|
142
|
+
const dependedOnBy = Array.from(node.parents).map(key => {
|
|
143
|
+
const parent = nodeMap.get(key);
|
|
144
|
+
return parent ? parent.name : key.split('@')[0];
|
|
145
|
+
});
|
|
146
|
+
const dependsOn = Array.from(node.children).map(key => {
|
|
147
|
+
const child = nodeMap.get(key);
|
|
148
|
+
return child ? child.name : key.split('@')[0];
|
|
149
|
+
});
|
|
150
|
+
const packageInsights = await gatherPackageInsights(node.name, input.projectPath, packageMetaCache, packageStatCache, node.parents.size, node.children.size, dependedOnBy, dependsOn);
|
|
151
|
+
dependencies.push({
|
|
152
|
+
name: node.name,
|
|
153
|
+
version: node.version,
|
|
154
|
+
key: node.key,
|
|
155
|
+
direct,
|
|
156
|
+
transitive: !direct,
|
|
157
|
+
depth: node.depth,
|
|
158
|
+
parents: Array.from(node.parents),
|
|
159
|
+
rootCauses,
|
|
160
|
+
license,
|
|
161
|
+
licenseRisk,
|
|
162
|
+
vulnerabilities,
|
|
163
|
+
vulnRisk,
|
|
164
|
+
maintenance,
|
|
165
|
+
maintenanceRisk: maintenanceRiskLevel,
|
|
166
|
+
usage,
|
|
167
|
+
identity: packageInsights.identity,
|
|
168
|
+
dependencySurface: packageInsights.dependencySurface,
|
|
169
|
+
sizeFootprint: packageInsights.sizeFootprint,
|
|
170
|
+
buildPlatform: packageInsights.buildPlatform,
|
|
171
|
+
moduleSystem: packageInsights.moduleSystem,
|
|
172
|
+
typescript: packageInsights.typescript,
|
|
173
|
+
graph: packageInsights.graph,
|
|
174
|
+
links: packageInsights.links,
|
|
175
|
+
importInfo,
|
|
176
|
+
runtimeClass: runtimeData.classification,
|
|
177
|
+
runtimeReason: runtimeData.reason,
|
|
178
|
+
outdated: { status: 'unknown' },
|
|
179
|
+
raw: {}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
dependencies.sort((a, b) => a.name.localeCompare(b.name));
|
|
183
|
+
return {
|
|
184
|
+
generatedAt: new Date().toISOString(),
|
|
185
|
+
projectPath: input.projectPath,
|
|
186
|
+
dependencyRadarVersion,
|
|
187
|
+
gitBranch,
|
|
188
|
+
maintenanceEnabled: input.maintenanceEnabled,
|
|
189
|
+
dependencies,
|
|
190
|
+
toolErrors,
|
|
191
|
+
raw
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function buildNodeMap(lsData, pkg) {
|
|
195
|
+
const map = new Map();
|
|
196
|
+
const traverse = (node, depth, parentKey, providedName) => {
|
|
197
|
+
const nodeName = (node === null || node === void 0 ? void 0 : node.name) || providedName;
|
|
198
|
+
if (!node || !nodeName)
|
|
199
|
+
return;
|
|
200
|
+
const version = node.version || 'unknown';
|
|
201
|
+
const key = `${nodeName}@${version}`;
|
|
202
|
+
if (!map.has(key)) {
|
|
203
|
+
map.set(key, {
|
|
204
|
+
name: nodeName,
|
|
205
|
+
version,
|
|
206
|
+
key,
|
|
207
|
+
depth,
|
|
208
|
+
parents: new Set(parentKey ? [parentKey] : []),
|
|
209
|
+
children: new Set(),
|
|
210
|
+
dev: node.dev
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const existing = map.get(key);
|
|
215
|
+
existing.depth = Math.min(existing.depth, depth);
|
|
216
|
+
if (parentKey)
|
|
217
|
+
existing.parents.add(parentKey);
|
|
218
|
+
if (existing.dev === undefined && node.dev !== undefined)
|
|
219
|
+
existing.dev = node.dev;
|
|
220
|
+
if (!existing.children)
|
|
221
|
+
existing.children = new Set();
|
|
222
|
+
}
|
|
223
|
+
if (node.dependencies && typeof node.dependencies === 'object') {
|
|
224
|
+
Object.entries(node.dependencies).forEach(([depName, child]) => {
|
|
225
|
+
const childVersion = (child === null || child === void 0 ? void 0 : child.version) || 'unknown';
|
|
226
|
+
const childKey = `${depName}@${childVersion}`;
|
|
227
|
+
const current = map.get(key);
|
|
228
|
+
if (current) {
|
|
229
|
+
current.children.add(childKey);
|
|
230
|
+
}
|
|
231
|
+
traverse(child, depth + 1, key, depName);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
if (lsData && lsData.dependencies) {
|
|
236
|
+
Object.entries(lsData.dependencies).forEach(([depName, child]) => traverse(child, 1, undefined, depName));
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
const deps = Object.keys(pkg.dependencies || {});
|
|
240
|
+
const devDeps = Object.keys(pkg.devDependencies || {});
|
|
241
|
+
deps.forEach((name) => {
|
|
242
|
+
const version = pkg.dependencies[name];
|
|
243
|
+
const key = `${name}@${version}`;
|
|
244
|
+
map.set(key, { name, version, key, depth: 1, parents: new Set(), children: new Set(), dev: false });
|
|
245
|
+
});
|
|
246
|
+
devDeps.forEach((name) => {
|
|
247
|
+
const version = pkg.devDependencies[name];
|
|
248
|
+
const key = `${name}@${version}`;
|
|
249
|
+
map.set(key, { name, version, key, depth: 1, parents: new Set(), children: new Set(), dev: true });
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return map;
|
|
253
|
+
}
|
|
254
|
+
function parseVulnerabilities(auditData) {
|
|
255
|
+
const map = new Map();
|
|
256
|
+
if (!auditData)
|
|
257
|
+
return map;
|
|
258
|
+
const ensureEntry = (name) => {
|
|
259
|
+
if (!map.has(name)) {
|
|
260
|
+
map.set(name, emptyVulnSummary());
|
|
261
|
+
}
|
|
262
|
+
return map.get(name);
|
|
263
|
+
};
|
|
264
|
+
if (auditData.vulnerabilities) {
|
|
265
|
+
Object.values(auditData.vulnerabilities).forEach((item) => {
|
|
266
|
+
const name = item.name || 'unknown';
|
|
267
|
+
const severity = normalizeSeverity(item.severity);
|
|
268
|
+
const entry = ensureEntry(name);
|
|
269
|
+
entry.counts[severity] = (entry.counts[severity] || 0) + 1;
|
|
270
|
+
const viaList = Array.isArray(item.via) ? item.via : [];
|
|
271
|
+
viaList
|
|
272
|
+
.filter((v) => typeof v === 'object')
|
|
273
|
+
.forEach((vul) => {
|
|
274
|
+
const sev = normalizeSeverity(vul.severity) || severity;
|
|
275
|
+
entry.items.push({
|
|
276
|
+
title: vul.title || item.title || vul.name || name,
|
|
277
|
+
severity: sev,
|
|
278
|
+
url: vul.url,
|
|
279
|
+
vulnerableRange: vul.range,
|
|
280
|
+
fixAvailable: item.fixAvailable,
|
|
281
|
+
paths: item.nodes
|
|
282
|
+
});
|
|
283
|
+
entry.counts[sev] = (entry.counts[sev] || 0) + 0; // already counted above
|
|
284
|
+
});
|
|
285
|
+
entry.highestSeverity = computeHighestSeverity(entry.counts);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (auditData.advisories) {
|
|
289
|
+
Object.values(auditData.advisories).forEach((adv) => {
|
|
290
|
+
const name = adv.module_name || adv.module || 'unknown';
|
|
291
|
+
const severity = normalizeSeverity(adv.severity);
|
|
292
|
+
const entry = ensureEntry(name);
|
|
293
|
+
entry.items.push({
|
|
294
|
+
title: adv.title,
|
|
295
|
+
severity,
|
|
296
|
+
url: adv.url,
|
|
297
|
+
vulnerableRange: adv.vulnerable_versions,
|
|
298
|
+
fixAvailable: adv.fix_available,
|
|
299
|
+
paths: (adv.findings || []).flatMap((f) => f.paths || [])
|
|
300
|
+
});
|
|
301
|
+
entry.counts[severity] = (entry.counts[severity] || 0) + 1;
|
|
302
|
+
entry.highestSeverity = computeHighestSeverity(entry.counts);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
map.forEach((entry) => {
|
|
306
|
+
entry.highestSeverity = computeHighestSeverity(entry.counts);
|
|
307
|
+
});
|
|
308
|
+
return map;
|
|
309
|
+
}
|
|
310
|
+
function normalizeSeverity(sev) {
|
|
311
|
+
const s = typeof sev === 'string' ? sev.toLowerCase() : 'low';
|
|
312
|
+
if (s === 'moderate')
|
|
313
|
+
return 'moderate';
|
|
314
|
+
if (s === 'high')
|
|
315
|
+
return 'high';
|
|
316
|
+
if (s === 'critical')
|
|
317
|
+
return 'critical';
|
|
318
|
+
return 'low';
|
|
319
|
+
}
|
|
320
|
+
function emptyVulnSummary() {
|
|
321
|
+
return {
|
|
322
|
+
counts: { low: 0, moderate: 0, high: 0, critical: 0 },
|
|
323
|
+
items: [],
|
|
324
|
+
highestSeverity: 'none'
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function computeHighestSeverity(counts) {
|
|
328
|
+
if (counts.critical > 0)
|
|
329
|
+
return 'critical';
|
|
330
|
+
if (counts.high > 0)
|
|
331
|
+
return 'high';
|
|
332
|
+
if (counts.moderate > 0)
|
|
333
|
+
return 'moderate';
|
|
334
|
+
if (counts.low > 0)
|
|
335
|
+
return 'low';
|
|
336
|
+
return 'none';
|
|
337
|
+
}
|
|
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
|
|
348
|
+
};
|
|
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 };
|
|
355
|
+
}
|
|
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
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return map;
|
|
372
|
+
}
|
|
373
|
+
function buildImportInfo(graphData) {
|
|
374
|
+
if (!graphData || typeof graphData !== 'object')
|
|
375
|
+
return undefined;
|
|
376
|
+
const fanIn = {};
|
|
377
|
+
const fanOut = {};
|
|
378
|
+
Object.entries(graphData).forEach(([file, deps]) => {
|
|
379
|
+
fanOut[file] = Array.isArray(deps) ? deps.length : 0;
|
|
380
|
+
(deps || []).forEach((dep) => {
|
|
381
|
+
fanIn[dep] = (fanIn[dep] || 0) + 1;
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
return { files: graphData, fanIn, fanOut };
|
|
385
|
+
}
|
|
386
|
+
function isDirectDependency(name, pkg) {
|
|
387
|
+
return Boolean((pkg.dependencies && pkg.dependencies[name]) || (pkg.devDependencies && pkg.devDependencies[name]));
|
|
388
|
+
}
|
|
389
|
+
async function resolveMaintenance(name, cache, maintenanceEnabled, current, total, onProgress) {
|
|
390
|
+
if (cache.has(name))
|
|
391
|
+
return cache.get(name);
|
|
392
|
+
if (!maintenanceEnabled) {
|
|
393
|
+
return { status: 'unknown', reason: 'maintenance checks disabled' };
|
|
394
|
+
}
|
|
395
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress(current, total, name);
|
|
396
|
+
try {
|
|
397
|
+
await (0, utils_1.delay)(1000);
|
|
398
|
+
const res = await (0, utils_1.runCommand)('npm', ['view', name, 'time', '--json']);
|
|
399
|
+
const json = JSON.parse(res.stdout || '{}');
|
|
400
|
+
const timestamps = Object.values(json || {}).filter((v) => typeof v === 'string');
|
|
401
|
+
const lastPublished = timestamps.sort().pop();
|
|
402
|
+
if (lastPublished) {
|
|
403
|
+
const risk = (0, utils_1.maintenanceRisk)(lastPublished);
|
|
404
|
+
const status = risk === 'green' ? 'active' : risk === 'amber' ? 'quiet' : risk === 'red' ? 'stale' : 'unknown';
|
|
405
|
+
return { lastPublished, status, reason: 'npm view time' };
|
|
406
|
+
}
|
|
407
|
+
return { status: 'unknown', reason: 'npm view returned no data' };
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
return { status: 'unknown', reason: 'lookup failed' };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function classifyRuntime(node, pkg, map) {
|
|
414
|
+
if (pkg.dependencies && pkg.dependencies[node.name]) {
|
|
415
|
+
return { classification: 'runtime', reason: 'Declared in dependencies' };
|
|
416
|
+
}
|
|
417
|
+
if (pkg.devDependencies && pkg.devDependencies[node.name]) {
|
|
418
|
+
return { classification: 'dev-only', reason: 'Declared in devDependencies' };
|
|
419
|
+
}
|
|
420
|
+
if (node.dev) {
|
|
421
|
+
return { classification: 'dev-only', reason: 'npm ls marks as dev dependency' };
|
|
422
|
+
}
|
|
423
|
+
const hasRuntimeParent = Array.from(node.parents).some((parentKey) => {
|
|
424
|
+
const parent = map.get(parentKey);
|
|
425
|
+
return parent ? !parent.dev : false;
|
|
426
|
+
});
|
|
427
|
+
if (hasRuntimeParent) {
|
|
428
|
+
return { classification: 'runtime', reason: 'Transitive of runtime dependency' };
|
|
429
|
+
}
|
|
430
|
+
return { classification: 'build-time', reason: 'Only seen in dev dependency tree' };
|
|
431
|
+
}
|
|
432
|
+
async function gatherPackageInsights(name, projectPath, metaCache, statCache, fanIn, fanOut, dependedOnBy, dependsOn) {
|
|
433
|
+
var _a;
|
|
434
|
+
const meta = await loadPackageMeta(name, projectPath, metaCache);
|
|
435
|
+
const pkg = (meta === null || meta === void 0 ? void 0 : meta.pkg) || {};
|
|
436
|
+
const dir = meta === null || meta === void 0 ? void 0 : meta.dir;
|
|
437
|
+
const stats = dir ? await calculatePackageStats(dir, statCache) : undefined;
|
|
438
|
+
const dependencySurface = {
|
|
439
|
+
dependencies: Object.keys(pkg.dependencies || {}).length,
|
|
440
|
+
devDependencies: Object.keys(pkg.devDependencies || {}).length,
|
|
441
|
+
peerDependencies: Object.keys(pkg.peerDependencies || {}).length,
|
|
442
|
+
optionalDependencies: Object.keys(pkg.optionalDependencies || {}).length,
|
|
443
|
+
hasPeerDependencies: Object.keys(pkg.peerDependencies || {}).length > 0
|
|
444
|
+
};
|
|
445
|
+
const scripts = pkg.scripts || {};
|
|
446
|
+
const identity = {
|
|
447
|
+
deprecated: Boolean(pkg.deprecated),
|
|
448
|
+
nodeEngine: typeof ((_a = pkg.engines) === null || _a === void 0 ? void 0 : _a.node) === 'string' ? pkg.engines.node : null,
|
|
449
|
+
hasRepository: Boolean(pkg.repository),
|
|
450
|
+
hasFunding: Boolean(pkg.funding)
|
|
451
|
+
};
|
|
452
|
+
const moduleSystem = determineModuleSystem(pkg);
|
|
453
|
+
const typescript = determineTypes(pkg, (stats === null || stats === void 0 ? void 0 : stats.hasDts) || false);
|
|
454
|
+
const buildPlatform = {
|
|
455
|
+
nativeBindings: Boolean((stats === null || stats === void 0 ? void 0 : stats.hasNativeBinary) || (stats === null || stats === void 0 ? void 0 : stats.hasBindingGyp) || scriptsContainNativeBuild(scripts)),
|
|
456
|
+
installScripts: hasInstallScripts(scripts)
|
|
457
|
+
};
|
|
458
|
+
const sizeFootprint = {
|
|
459
|
+
installedSize: (stats === null || stats === void 0 ? void 0 : stats.size) || 0,
|
|
460
|
+
fileCount: (stats === null || stats === void 0 ? void 0 : stats.files) || 0
|
|
461
|
+
};
|
|
462
|
+
const graph = {
|
|
463
|
+
fanIn,
|
|
464
|
+
fanOut,
|
|
465
|
+
dependedOnBy,
|
|
466
|
+
dependsOn
|
|
467
|
+
};
|
|
468
|
+
// Extract package links
|
|
469
|
+
const links = {
|
|
470
|
+
npm: `https://www.npmjs.com/package/${name}`
|
|
471
|
+
};
|
|
472
|
+
// Repository can be string or object with url
|
|
473
|
+
if (pkg.repository) {
|
|
474
|
+
if (typeof pkg.repository === 'string') {
|
|
475
|
+
links.repository = normalizeRepoUrl(pkg.repository);
|
|
476
|
+
}
|
|
477
|
+
else if (pkg.repository.url) {
|
|
478
|
+
links.repository = normalizeRepoUrl(pkg.repository.url);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// Bugs can be string or object with url
|
|
482
|
+
if (pkg.bugs) {
|
|
483
|
+
if (typeof pkg.bugs === 'string') {
|
|
484
|
+
links.bugs = pkg.bugs;
|
|
485
|
+
}
|
|
486
|
+
else if (pkg.bugs.url) {
|
|
487
|
+
links.bugs = pkg.bugs.url;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// Homepage is a simple string
|
|
491
|
+
if (pkg.homepage && typeof pkg.homepage === 'string') {
|
|
492
|
+
links.homepage = pkg.homepage;
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
identity,
|
|
496
|
+
dependencySurface,
|
|
497
|
+
sizeFootprint,
|
|
498
|
+
buildPlatform,
|
|
499
|
+
moduleSystem,
|
|
500
|
+
typescript,
|
|
501
|
+
graph,
|
|
502
|
+
links
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
async function loadPackageMeta(name, projectPath, cache) {
|
|
506
|
+
if (cache.has(name))
|
|
507
|
+
return cache.get(name);
|
|
508
|
+
try {
|
|
509
|
+
const pkgJsonPath = require.resolve(path_1.default.join(name, 'package.json'), { paths: [projectPath] });
|
|
510
|
+
const pkgRaw = await promises_1.default.readFile(pkgJsonPath, 'utf8');
|
|
511
|
+
const pkg = JSON.parse(pkgRaw);
|
|
512
|
+
const meta = { pkg, dir: path_1.default.dirname(pkgJsonPath) };
|
|
513
|
+
cache.set(name, meta);
|
|
514
|
+
return meta;
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async function calculatePackageStats(dir, cache) {
|
|
521
|
+
if (cache.has(dir))
|
|
522
|
+
return cache.get(dir);
|
|
523
|
+
let size = 0;
|
|
524
|
+
let files = 0;
|
|
525
|
+
let hasDts = false;
|
|
526
|
+
let hasNativeBinary = false;
|
|
527
|
+
let hasBindingGyp = false;
|
|
528
|
+
async function walk(current) {
|
|
529
|
+
const entries = await promises_1.default.readdir(current, { withFileTypes: true });
|
|
530
|
+
for (const entry of entries) {
|
|
531
|
+
const full = path_1.default.join(current, entry.name);
|
|
532
|
+
if (entry.isSymbolicLink())
|
|
533
|
+
continue;
|
|
534
|
+
if (entry.isDirectory()) {
|
|
535
|
+
await walk(full);
|
|
536
|
+
}
|
|
537
|
+
else if (entry.isFile()) {
|
|
538
|
+
const stat = await promises_1.default.stat(full);
|
|
539
|
+
size += stat.size;
|
|
540
|
+
files += 1;
|
|
541
|
+
if (entry.name.endsWith('.d.ts'))
|
|
542
|
+
hasDts = true;
|
|
543
|
+
if (entry.name.endsWith('.node'))
|
|
544
|
+
hasNativeBinary = true;
|
|
545
|
+
if (entry.name === 'binding.gyp')
|
|
546
|
+
hasBindingGyp = true;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
await walk(dir);
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
// best-effort; ignore inaccessible paths
|
|
555
|
+
}
|
|
556
|
+
const result = { size, files, hasDts, hasNativeBinary, hasBindingGyp };
|
|
557
|
+
cache.set(dir, result);
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
function determineModuleSystem(pkg) {
|
|
561
|
+
const typeField = pkg.type;
|
|
562
|
+
const hasModuleField = Boolean(pkg.module);
|
|
563
|
+
const hasExports = pkg.exports !== undefined;
|
|
564
|
+
const conditionalExports = typeof pkg.exports === 'object' && pkg.exports !== null;
|
|
565
|
+
let format = 'unknown';
|
|
566
|
+
if (typeField === 'module')
|
|
567
|
+
format = 'esm';
|
|
568
|
+
else if (typeField === 'commonjs')
|
|
569
|
+
format = 'commonjs';
|
|
570
|
+
else if (hasModuleField || hasExports)
|
|
571
|
+
format = 'dual';
|
|
572
|
+
else
|
|
573
|
+
format = 'commonjs';
|
|
574
|
+
return { format, conditionalExports };
|
|
575
|
+
}
|
|
576
|
+
function determineTypes(pkg, hasDts) {
|
|
577
|
+
const hasBundled = Boolean(pkg.types || pkg.typings || hasDts);
|
|
578
|
+
return { types: hasBundled ? 'bundled' : 'none' };
|
|
579
|
+
}
|
|
580
|
+
function scriptsContainNativeBuild(scripts) {
|
|
581
|
+
return Object.values(scripts || {}).some((cmd) => typeof cmd === 'string' && /node-?gyp|node-pre-gyp/.test(cmd));
|
|
582
|
+
}
|
|
583
|
+
function hasInstallScripts(scripts) {
|
|
584
|
+
return ['preinstall', 'install', 'postinstall'].some((key) => typeof (scripts === null || scripts === void 0 ? void 0 : scripts[key]) === 'string' && scripts[key].trim().length > 0);
|
|
585
|
+
}
|