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 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
+ }