dependency-radar 0.2.0 → 0.3.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/dist/cli.js CHANGED
@@ -5,44 +5,637 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const path_1 = __importDefault(require("path"));
8
+ const child_process_1 = require("child_process");
9
+ const os_1 = require("os");
8
10
  const aggregator_1 = require("./aggregator");
9
11
  const importGraphRunner_1 = require("./runners/importGraphRunner");
10
12
  const npmAudit_1 = require("./runners/npmAudit");
11
13
  const npmLs_1 = require("./runners/npmLs");
14
+ const npmOutdated_1 = require("./runners/npmOutdated");
12
15
  const report_1 = require("./report");
13
16
  const promises_1 = __importDefault(require("fs/promises"));
14
17
  const utils_1 = require("./utils");
18
+ function normalizeSlashes(p) {
19
+ return p.split(path_1.default.sep).join("/");
20
+ }
21
+ function isCI() {
22
+ return Boolean(process.env.CI === "true" ||
23
+ process.env.CI === "TRUE" ||
24
+ process.env.CI === "1" ||
25
+ process.env.GITHUB_ACTIONS ||
26
+ process.env.GITLAB_CI ||
27
+ process.env.CIRCLECI ||
28
+ process.env.JENKINS_URL ||
29
+ process.env.BUILDKITE);
30
+ }
31
+ async function listDirs(parent) {
32
+ const entries = await promises_1.default
33
+ .readdir(parent, { withFileTypes: true })
34
+ .catch(() => []);
35
+ return entries
36
+ .filter((e) => { var _a; return (_a = e === null || e === void 0 ? void 0 : e.isDirectory) === null || _a === void 0 ? void 0 : _a.call(e); })
37
+ .map((e) => path_1.default.join(parent, e.name));
38
+ }
39
+ async function expandWorkspacePattern(root, pattern) {
40
+ // Minimal glob support for common workspaces:
41
+ // - "packages/*", "apps/*"
42
+ // - "packages/**" (recursive)
43
+ // - "./packages/*" (leading ./)
44
+ const cleaned = pattern.trim().replace(/^[.][/\\]/, "");
45
+ if (!cleaned)
46
+ return [];
47
+ // Disallow node_modules and hidden by default
48
+ const parts = cleaned.split(/[/\\]/g).filter(Boolean);
49
+ const isRecursive = parts.includes("**");
50
+ // Find the segment containing * or **
51
+ const starIndex = parts.findIndex((p) => p === "*" || p === "**");
52
+ if (starIndex === -1) {
53
+ const abs = path_1.default.resolve(root, cleaned);
54
+ return (await (0, utils_1.pathExists)(abs)) ? [abs] : [];
55
+ }
56
+ const baseParts = parts.slice(0, starIndex);
57
+ const baseDir = path_1.default.resolve(root, baseParts.join(path_1.default.sep));
58
+ if (!(await (0, utils_1.pathExists)(baseDir)))
59
+ return [];
60
+ if (parts[starIndex] === "*" && starIndex === parts.length - 1) {
61
+ // one-level children
62
+ return await listDirs(baseDir);
63
+ }
64
+ if (parts[starIndex] === "**") {
65
+ // recursive directories under base
66
+ const out = [];
67
+ async function walk(dir) {
68
+ const children = await listDirs(dir);
69
+ for (const child of children) {
70
+ if (path_1.default.basename(child) === "node_modules")
71
+ continue;
72
+ if (path_1.default.basename(child).startsWith("."))
73
+ continue;
74
+ out.push(child);
75
+ await walk(child);
76
+ }
77
+ }
78
+ await walk(baseDir);
79
+ return out;
80
+ }
81
+ // Fallback: treat as one-level
82
+ return await listDirs(baseDir);
83
+ }
84
+ async function readJsonFile(filePath) {
85
+ try {
86
+ const raw = await promises_1.default.readFile(filePath, "utf8");
87
+ return JSON.parse(raw);
88
+ }
89
+ catch {
90
+ return undefined;
91
+ }
92
+ }
93
+ async function getToolVersion(tool, cwd) {
94
+ try {
95
+ const result = await (0, utils_1.runCommand)(tool, ["--version"], { cwd });
96
+ const raw = (result.stdout || "").trim();
97
+ if (!raw)
98
+ return undefined;
99
+ return raw.split(/\s+/)[0];
100
+ }
101
+ catch {
102
+ return undefined;
103
+ }
104
+ }
105
+ function compactToolVersions(versions) {
106
+ const out = {};
107
+ for (const [key, value] of Object.entries(versions)) {
108
+ if (value)
109
+ out[key] = value;
110
+ }
111
+ return Object.keys(out).length > 0 ? out : undefined;
112
+ }
113
+ async function detectWorkspace(projectPath) {
114
+ const rootPkgPath = path_1.default.join(projectPath, "package.json");
115
+ const rootPkg = await readJsonFile(rootPkgPath);
116
+ const inferredManager = inferPackageManager(rootPkg);
117
+ const pnpmWorkspacePath = path_1.default.join(projectPath, "pnpm-workspace.yaml");
118
+ const hasPnpmWorkspace = await (0, utils_1.pathExists)(pnpmWorkspacePath);
119
+ let type = "none";
120
+ let patterns = [];
121
+ if (hasPnpmWorkspace) {
122
+ type = "pnpm";
123
+ // very small YAML parser for the only thing we care about: `packages:` list.
124
+ const yaml = await promises_1.default.readFile(pnpmWorkspacePath, "utf8");
125
+ const lines = yaml.split(/\r?\n/);
126
+ let inPackages = false;
127
+ for (const line of lines) {
128
+ const trimmed = line.trim();
129
+ if (!trimmed)
130
+ continue;
131
+ if (/^packages\s*:\s*$/.test(trimmed)) {
132
+ inPackages = true;
133
+ continue;
134
+ }
135
+ if (inPackages) {
136
+ // stop when we hit a new top-level key
137
+ if (/^[A-Za-z0-9_-]+\s*:/.test(trimmed) && !trimmed.startsWith("-")) {
138
+ inPackages = false;
139
+ continue;
140
+ }
141
+ const m = trimmed.match(/^[-]\s*["']?([^"']+)["']?\s*$/);
142
+ if (m && m[1])
143
+ patterns.push(m[1].trim());
144
+ }
145
+ }
146
+ }
147
+ // npm/yarn workspaces
148
+ if (type === "none" && rootPkg && rootPkg.workspaces) {
149
+ type = inferredManager || "npm";
150
+ if (Array.isArray(rootPkg.workspaces))
151
+ patterns = rootPkg.workspaces;
152
+ else if (Array.isArray(rootPkg.workspaces.packages))
153
+ patterns = rootPkg.workspaces.packages;
154
+ // try to detect yarn berry pnp (unsupported) later via .yarnrc.yml
155
+ const yarnrc = path_1.default.join(projectPath, ".yarnrc.yml");
156
+ if (await (0, utils_1.pathExists)(yarnrc)) {
157
+ const y = await promises_1.default.readFile(yarnrc, "utf8");
158
+ if (/nodeLinker\s*:\s*pnp/.test(y)) {
159
+ return { type: "yarn", packagePaths: [] };
160
+ }
161
+ }
162
+ }
163
+ if (type === "none") {
164
+ return { type: "none", packagePaths: [projectPath] };
165
+ }
166
+ // Expand patterns and keep only folders that contain package.json
167
+ const candidates = [];
168
+ for (const pat of patterns) {
169
+ const expanded = await expandWorkspacePattern(projectPath, pat);
170
+ candidates.push(...expanded);
171
+ }
172
+ const unique = Array.from(new Set(candidates.map((p) => path_1.default.resolve(p)))).filter((p) => !normalizeSlashes(p).includes("/node_modules/"));
173
+ const packagePaths = [];
174
+ for (const dir of unique) {
175
+ const pkgJson = path_1.default.join(dir, "package.json");
176
+ if (await (0, utils_1.pathExists)(pkgJson))
177
+ packagePaths.push(dir);
178
+ }
179
+ // Always include root if it contains a name (some repos keep a root package)
180
+ if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "package.json"))) {
181
+ // root may already be in the list; keep unique
182
+ if (!packagePaths.includes(projectPath)) {
183
+ // Only include root as a scanned package if it looks like a real package
184
+ const root = await readJsonFile(path_1.default.join(projectPath, "package.json"));
185
+ if (root &&
186
+ typeof root.name === "string" &&
187
+ root.name.trim().length > 0) {
188
+ packagePaths.push(projectPath);
189
+ }
190
+ }
191
+ }
192
+ return { type, packagePaths: packagePaths.sort() };
193
+ }
194
+ function inferPackageManager(rootPkg) {
195
+ const raw = typeof (rootPkg === null || rootPkg === void 0 ? void 0 : rootPkg.packageManager) === "string"
196
+ ? rootPkg.packageManager.trim()
197
+ : "";
198
+ if (!raw)
199
+ return undefined;
200
+ if (raw.startsWith("pnpm@") || raw === "pnpm")
201
+ return "pnpm";
202
+ if (raw.startsWith("yarn@") || raw === "yarn")
203
+ return "yarn";
204
+ if (raw.startsWith("npm@") || raw === "npm")
205
+ return "npm";
206
+ return undefined;
207
+ }
208
+ async function detectPackageManager(projectPath, rootPkg, workspaceType) {
209
+ const inferred = inferPackageManager(rootPkg);
210
+ if (inferred)
211
+ return inferred;
212
+ if (workspaceType === "pnpm" || workspaceType === "yarn")
213
+ return workspaceType;
214
+ const yarnrc = path_1.default.join(projectPath, ".yarnrc.yml");
215
+ if (await (0, utils_1.pathExists)(yarnrc)) {
216
+ const y = await promises_1.default.readFile(yarnrc, "utf8");
217
+ if (/nodeLinker\s*:\s*pnp/.test(y)) {
218
+ return "yarn";
219
+ }
220
+ }
221
+ if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".pnpm")))
222
+ return "pnpm";
223
+ if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".yarn-state.yml")))
224
+ return "yarn";
225
+ return "npm";
226
+ }
227
+ async function detectScanManager(projectPath, fallback) {
228
+ if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "pnpm-lock.yaml")))
229
+ return "pnpm";
230
+ if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "yarn.lock")))
231
+ return "yarn";
232
+ if ((await (0, utils_1.pathExists)(path_1.default.join(projectPath, "package-lock.json"))) ||
233
+ (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "npm-shrinkwrap.json")))) {
234
+ return "npm";
235
+ }
236
+ if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".pnpm")))
237
+ return "pnpm";
238
+ if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".yarn-state.yml")))
239
+ return "yarn";
240
+ return fallback;
241
+ }
242
+ async function readWorkspacePackageMeta(rootPath, packagePaths) {
243
+ const out = [];
244
+ for (const p of packagePaths) {
245
+ const pkg = await readJsonFile(path_1.default.join(p, "package.json"));
246
+ const name = pkg && typeof pkg.name === "string" && pkg.name.trim()
247
+ ? pkg.name.trim()
248
+ : path_1.default.basename(p);
249
+ out.push({ path: p, name, pkg: pkg || {} });
250
+ }
251
+ return out;
252
+ }
253
+ function mergeDepsFromWorkspace(pkgs) {
254
+ var _a, _b, _c;
255
+ const merged = {
256
+ dependencies: {},
257
+ devDependencies: {},
258
+ optionalDependencies: {},
259
+ };
260
+ for (const entry of pkgs) {
261
+ const deps = ((_a = entry.pkg) === null || _a === void 0 ? void 0 : _a.dependencies) || {};
262
+ const dev = ((_b = entry.pkg) === null || _b === void 0 ? void 0 : _b.devDependencies) || {};
263
+ const opt = ((_c = entry.pkg) === null || _c === void 0 ? void 0 : _c.optionalDependencies) || {};
264
+ Object.assign(merged.dependencies, deps);
265
+ Object.assign(merged.devDependencies, dev);
266
+ Object.assign(merged.optionalDependencies, opt);
267
+ }
268
+ return merged;
269
+ }
270
+ function mergeAuditResults(results) {
271
+ const defined = results.filter(Boolean);
272
+ if (defined.length === 0)
273
+ return undefined;
274
+ const base = {};
275
+ for (const r of defined) {
276
+ if (!r || typeof r !== "object")
277
+ continue;
278
+ // npm audit v7+ shape: { vulnerabilities: {..} }
279
+ if (r.vulnerabilities && typeof r.vulnerabilities === "object") {
280
+ base.vulnerabilities = base.vulnerabilities || {};
281
+ for (const [k, v] of Object.entries(r.vulnerabilities)) {
282
+ if (!base.vulnerabilities[k])
283
+ base.vulnerabilities[k] = v;
284
+ else {
285
+ // merge counts best-effort
286
+ const existing = base.vulnerabilities[k];
287
+ base.vulnerabilities[k] = { ...existing, ...v };
288
+ }
289
+ }
290
+ }
291
+ // legacy shape
292
+ if (r.advisories && typeof r.advisories === "object") {
293
+ base.advisories = base.advisories || {};
294
+ Object.assign(base.advisories, r.advisories);
295
+ }
296
+ // keep metadata if present
297
+ if (r.metadata && !base.metadata)
298
+ base.metadata = r.metadata;
299
+ }
300
+ return base;
301
+ }
302
+ function collectDeclaredDeps(pkg) {
303
+ const out = new Set();
304
+ const sections = [
305
+ pkg === null || pkg === void 0 ? void 0 : pkg.dependencies,
306
+ pkg === null || pkg === void 0 ? void 0 : pkg.devDependencies,
307
+ pkg === null || pkg === void 0 ? void 0 : pkg.optionalDependencies,
308
+ pkg === null || pkg === void 0 ? void 0 : pkg.peerDependencies,
309
+ ];
310
+ for (const deps of sections) {
311
+ if (deps && typeof deps === "object") {
312
+ Object.keys(deps).forEach((name) => out.add(name));
313
+ }
314
+ }
315
+ return Array.from(out);
316
+ }
317
+ function parseOutdatedData(data, unknownNames) {
318
+ const entries = [];
319
+ if (!data || typeof data !== "object")
320
+ return entries;
321
+ if (Array.isArray(data)) {
322
+ for (const entry of data) {
323
+ if (!entry || typeof entry !== "object")
324
+ continue;
325
+ const name = typeof entry.name === "string" ? entry.name : undefined;
326
+ const current = typeof entry.current === "string" ? entry.current : "";
327
+ const latest = typeof entry.latest === "string" ? entry.latest : undefined;
328
+ const type = typeof entry.type === "string" ? entry.type.toLowerCase() : "";
329
+ if (!name || !current)
330
+ continue;
331
+ let status = "unknown";
332
+ if (type === "patch" || type === "minor" || type === "major") {
333
+ status = type;
334
+ }
335
+ else if (latest) {
336
+ status = classifyOutdated(current, latest);
337
+ }
338
+ if (status === "current")
339
+ continue;
340
+ if (status === "major" || status === "minor" || status === "patch") {
341
+ entries.push({
342
+ name,
343
+ currentVersion: current,
344
+ status,
345
+ latestVersion: latest,
346
+ });
347
+ continue;
348
+ }
349
+ entries.push({ name, currentVersion: current, status: "unknown" });
350
+ }
351
+ return entries;
352
+ }
353
+ for (const [name, info] of Object.entries(data)) {
354
+ if (!info || typeof info !== "object") {
355
+ unknownNames.add(name);
356
+ continue;
357
+ }
358
+ const current = typeof info.current === "string" ? info.current : "";
359
+ const latest = typeof info.latest === "string" ? info.latest : undefined;
360
+ const type = typeof info.type === "string" ? info.type.toLowerCase() : "";
361
+ if (!current) {
362
+ unknownNames.add(name);
363
+ continue;
364
+ }
365
+ let status = "unknown";
366
+ if (type === "patch" || type === "minor" || type === "major") {
367
+ status = type;
368
+ }
369
+ else if (latest) {
370
+ status = classifyOutdated(current, latest);
371
+ }
372
+ if (status === "current")
373
+ continue;
374
+ if (status === "major" || status === "minor" || status === "patch") {
375
+ if (latest) {
376
+ entries.push({
377
+ name,
378
+ currentVersion: current,
379
+ status,
380
+ latestVersion: latest,
381
+ });
382
+ }
383
+ else {
384
+ entries.push({ name, currentVersion: current, status: "unknown" });
385
+ }
386
+ continue;
387
+ }
388
+ entries.push({ name, currentVersion: current, status: "unknown" });
389
+ }
390
+ return entries;
391
+ }
392
+ function parseSimpleVersion(value) {
393
+ if (!value || typeof value !== "string")
394
+ return undefined;
395
+ const trimmed = value.trim();
396
+ if (!trimmed)
397
+ return undefined;
398
+ if (trimmed.includes("-") || trimmed.includes("+"))
399
+ return undefined;
400
+ const match = trimmed.match(/^v?(\d+)\.(\d+)\.(\d+)$/);
401
+ if (!match)
402
+ return undefined;
403
+ const major = Number.parseInt(match[1], 10);
404
+ const minor = Number.parseInt(match[2], 10);
405
+ const patch = Number.parseInt(match[3], 10);
406
+ if ([major, minor, patch].some((n) => Number.isNaN(n)))
407
+ return undefined;
408
+ return { major, minor, patch };
409
+ }
410
+ function classifyOutdated(current, latest) {
411
+ const currentVer = parseSimpleVersion(current);
412
+ const latestVer = parseSimpleVersion(latest);
413
+ if (!currentVer || !latestVer)
414
+ return "unknown";
415
+ if (currentVer.major !== latestVer.major)
416
+ return "major";
417
+ if (currentVer.minor !== latestVer.minor)
418
+ return "minor";
419
+ if (currentVer.patch !== latestVer.patch)
420
+ return "patch";
421
+ return "current";
422
+ }
423
+ function mergeOutdatedResults(results) {
424
+ const entries = [];
425
+ const unknownNames = new Set();
426
+ for (let i = 0; i < results.length; i++) {
427
+ const attempt = results[i];
428
+ if (!attempt.attempted)
429
+ continue;
430
+ const result = attempt.result;
431
+ if (!result ||
432
+ !result.ok ||
433
+ !result.data ||
434
+ typeof result.data !== "object") {
435
+ continue;
436
+ }
437
+ entries.push(...parseOutdatedData(result.data, unknownNames));
438
+ }
439
+ if (entries.length === 0 && unknownNames.size === 0) {
440
+ return undefined;
441
+ }
442
+ const merged = new Map();
443
+ for (const entry of entries) {
444
+ const key = `${entry.name}@${entry.currentVersion}`;
445
+ const existing = merged.get(key);
446
+ if (!existing) {
447
+ merged.set(key, entry);
448
+ continue;
449
+ }
450
+ if (existing.status !== entry.status ||
451
+ existing.latestVersion !== entry.latestVersion) {
452
+ merged.set(key, {
453
+ name: entry.name,
454
+ currentVersion: entry.currentVersion,
455
+ status: "unknown",
456
+ });
457
+ }
458
+ }
459
+ return {
460
+ entries: Array.from(merged.values()),
461
+ unknownNames: Array.from(unknownNames),
462
+ };
463
+ }
464
+ function mergeImportGraphs(rootPath, packageMetas, graphs) {
465
+ const files = {};
466
+ const packages = {};
467
+ const packageCounts = {};
468
+ const unresolvedImports = [];
469
+ for (let i = 0; i < graphs.length; i++) {
470
+ const g = graphs[i];
471
+ const meta = packageMetas[i];
472
+ if (!g || typeof g !== "object")
473
+ continue;
474
+ const relBase = path_1.default
475
+ .relative(rootPath, meta.path)
476
+ .split(path_1.default.sep)
477
+ .join("/");
478
+ const prefix = relBase ? `${relBase}/` : "";
479
+ const gf = g.files || {};
480
+ const gp = g.packages || {};
481
+ const gc = g.packageCounts || {};
482
+ for (const [k, v] of Object.entries(gf)) {
483
+ files[`${prefix}${k}`] = Array.isArray(v)
484
+ ? v.map((x) => `${prefix}${x}`)
485
+ : [];
486
+ }
487
+ for (const [k, v] of Object.entries(gp)) {
488
+ packages[`${prefix}${k}`] = Array.isArray(v) ? v : [];
489
+ }
490
+ for (const [k, v] of Object.entries(gc)) {
491
+ if (!v || typeof v !== "object")
492
+ continue;
493
+ const next = {};
494
+ for (const [dep, count] of Object.entries(v)) {
495
+ if (typeof count === "number")
496
+ next[dep] = count;
497
+ }
498
+ packageCounts[`${prefix}${k}`] = next;
499
+ }
500
+ const unresolved = Array.isArray(g.unresolvedImports)
501
+ ? g.unresolvedImports
502
+ : [];
503
+ unresolved.forEach((u) => {
504
+ if (u &&
505
+ typeof u.importer === "string" &&
506
+ typeof u.specifier === "string") {
507
+ unresolvedImports.push({
508
+ importer: `${prefix}${u.importer}`,
509
+ specifier: u.specifier,
510
+ });
511
+ }
512
+ });
513
+ }
514
+ return { files, packages, packageCounts, unresolvedImports };
515
+ }
516
+ function buildWorkspaceUsageMap(packageMetas, dependencyGraphs) {
517
+ var _a, _b, _c, _d;
518
+ const usage = new Map();
519
+ const add = (depName, pkgName) => {
520
+ if (!depName)
521
+ return;
522
+ if (!usage.has(depName))
523
+ usage.set(depName, new Set());
524
+ usage.get(depName).add(pkgName);
525
+ };
526
+ // From declared deps
527
+ for (const meta of packageMetas) {
528
+ const pkgName = meta.name;
529
+ const deps = ((_a = meta.pkg) === null || _a === void 0 ? void 0 : _a.dependencies) || {};
530
+ const dev = ((_b = meta.pkg) === null || _b === void 0 ? void 0 : _b.devDependencies) || {};
531
+ const opt = ((_c = meta.pkg) === null || _c === void 0 ? void 0 : _c.optionalDependencies) || {};
532
+ const peer = ((_d = meta.pkg) === null || _d === void 0 ? void 0 : _d.peerDependencies) || {};
533
+ Object.keys(deps).forEach((d) => {
534
+ add(d, pkgName);
535
+ });
536
+ Object.keys(dev).forEach((d) => {
537
+ add(d, pkgName);
538
+ });
539
+ Object.keys(opt).forEach((d) => {
540
+ add(d, pkgName);
541
+ });
542
+ Object.keys(peer).forEach((d) => {
543
+ add(d, pkgName);
544
+ });
545
+ }
546
+ // From npm ls trees (transitives)
547
+ const walk = (node, pkgName) => {
548
+ if (!node || typeof node !== "object")
549
+ return;
550
+ const name = node.name;
551
+ if (typeof name === "string")
552
+ add(name, pkgName);
553
+ const deps = node.dependencies;
554
+ if (deps && typeof deps === "object") {
555
+ for (const [depName, child] of Object.entries(deps)) {
556
+ add(depName, pkgName);
557
+ walk(child, pkgName);
558
+ }
559
+ }
560
+ };
561
+ for (let i = 0; i < dependencyGraphs.length; i++) {
562
+ const data = dependencyGraphs[i];
563
+ const meta = packageMetas[i];
564
+ if (!data || typeof data !== "object")
565
+ continue;
566
+ const deps = data.dependencies;
567
+ if (deps && typeof deps === "object") {
568
+ for (const [depName, child] of Object.entries(deps)) {
569
+ add(depName, meta.name);
570
+ walk(child, meta.name);
571
+ }
572
+ }
573
+ }
574
+ const out = new Map();
575
+ for (const [k, set] of usage.entries()) {
576
+ out.set(k, Array.from(set).sort());
577
+ }
578
+ return out;
579
+ }
580
+ function buildCombinedDependencyGraph(rootPath, packageMetas, dependencyGraphs) {
581
+ var _a;
582
+ // Build a synthetic root with each workspace package as a top-level node.
583
+ // This avoids object-key collisions for normal packages and preserves per-package roots.
584
+ const dependencies = {};
585
+ for (let i = 0; i < dependencyGraphs.length; i++) {
586
+ const data = dependencyGraphs[i];
587
+ const meta = packageMetas[i];
588
+ if (!meta)
589
+ continue;
590
+ const version = typeof ((_a = meta.pkg) === null || _a === void 0 ? void 0 : _a.version) === "string" ? meta.pkg.version : "workspace";
591
+ const nodeDeps = data &&
592
+ typeof data === "object" &&
593
+ data.dependencies &&
594
+ typeof data.dependencies === "object"
595
+ ? data.dependencies
596
+ : {};
597
+ dependencies[meta.name] = {
598
+ name: meta.name,
599
+ version,
600
+ dependencies: nodeDeps,
601
+ };
602
+ }
603
+ return { name: "dependency-radar-workspace", version: "0.0.0", dependencies };
604
+ }
15
605
  function parseArgs(argv) {
16
606
  const opts = {
17
- command: 'scan',
607
+ command: "scan",
18
608
  project: process.cwd(),
19
- out: 'dependency-radar.html',
609
+ out: "dependency-radar.html",
20
610
  keepTemp: false,
21
- maintenance: false,
22
611
  audit: true,
23
- json: false
612
+ outdated: true,
613
+ json: false,
614
+ open: false,
24
615
  };
25
616
  const args = [...argv];
26
- if (args[0] && !args[0].startsWith('-')) {
617
+ if (args[0] && !args[0].startsWith("-")) {
27
618
  opts.command = args.shift();
28
619
  }
29
620
  while (args.length) {
30
621
  const arg = args.shift();
31
622
  if (!arg)
32
623
  break;
33
- if (arg === '--project' && args[0])
624
+ if (arg === "--project" && args[0])
34
625
  opts.project = args.shift();
35
- else if (arg === '--out' && args[0])
626
+ else if (arg === "--out" && args[0])
36
627
  opts.out = args.shift();
37
- else if (arg === '--keep-temp')
628
+ else if (arg === "--keep-temp")
38
629
  opts.keepTemp = true;
39
- else if (arg === '--maintenance')
40
- opts.maintenance = true;
41
- else if (arg === '--no-audit')
630
+ else if (arg === "--offline") {
42
631
  opts.audit = false;
43
- else if (arg === '--json')
632
+ opts.outdated = false;
633
+ }
634
+ else if (arg === "--json")
44
635
  opts.json = true;
45
- else if (arg === '--help' || arg === '-h') {
636
+ else if (arg === "--open")
637
+ opts.open = true;
638
+ else if (arg === "--help" || arg === "-h") {
46
639
  printHelp();
47
640
  process.exit(0);
48
641
  }
@@ -59,80 +652,234 @@ Options:
59
652
  --out <path> Output HTML file (default: dependency-radar.html)
60
653
  --json Write aggregated data to JSON (default filename: dependency-radar.json)
61
654
  --keep-temp Keep .dependency-radar folder
62
- --maintenance Enable slow maintenance checks (npm registry calls)
63
- --no-audit Skip npm audit (useful for offline scans)
655
+ --offline Skip npm audit and npm outdated (useful for offline scans)
656
+ --open Open the generated report using the system default application
64
657
  `);
65
658
  }
659
+ function openInBrowser(filePath) {
660
+ const normalizedPath = filePath.replace(/\\/g, "/");
661
+ let child;
662
+ switch ((0, os_1.platform)()) {
663
+ case "darwin":
664
+ child = (0, child_process_1.spawn)("open", [normalizedPath], {
665
+ stdio: "ignore",
666
+ shell: false,
667
+ detached: true,
668
+ });
669
+ break;
670
+ case "win32":
671
+ child = (0, child_process_1.spawn)("cmd", ["/c", "start", "", normalizedPath], {
672
+ stdio: "ignore",
673
+ shell: false,
674
+ detached: true,
675
+ });
676
+ break;
677
+ default:
678
+ child = (0, child_process_1.spawn)("xdg-open", [normalizedPath], {
679
+ stdio: "ignore",
680
+ shell: false,
681
+ detached: true,
682
+ });
683
+ break;
684
+ }
685
+ child.on("error", (err) => {
686
+ console.warn("Could not open report:", err.message);
687
+ });
688
+ child.unref();
689
+ }
66
690
  async function run() {
67
691
  const opts = parseArgs(process.argv.slice(2));
68
- if (opts.command !== 'scan') {
692
+ if (opts.command !== "scan") {
69
693
  printHelp();
70
694
  process.exit(1);
71
695
  return;
72
696
  }
73
697
  const projectPath = path_1.default.resolve(opts.project);
74
- if (opts.json && opts.out === 'dependency-radar.html') {
75
- opts.out = 'dependency-radar.json';
698
+ if (opts.json && opts.out === "dependency-radar.html") {
699
+ opts.out = "dependency-radar.json";
76
700
  }
77
701
  let outputPath = path_1.default.resolve(opts.out);
78
702
  const startTime = Date.now();
79
703
  let dependencyCount = 0;
80
704
  try {
81
705
  const stat = await promises_1.default.stat(outputPath).catch(() => undefined);
82
- const endsWithSeparator = opts.out.endsWith('/') || opts.out.endsWith('\\');
706
+ const endsWithSeparator = opts.out.endsWith("/") || opts.out.endsWith("\\");
83
707
  const hasExtension = Boolean(path_1.default.extname(outputPath));
84
- if ((stat && stat.isDirectory()) || endsWithSeparator || (!stat && !hasExtension)) {
85
- outputPath = path_1.default.join(outputPath, opts.json ? 'dependency-radar.json' : 'dependency-radar.html');
708
+ if ((stat && stat.isDirectory()) ||
709
+ endsWithSeparator ||
710
+ (!stat && !hasExtension)) {
711
+ outputPath = path_1.default.join(outputPath, opts.json ? "dependency-radar.json" : "dependency-radar.html");
86
712
  }
87
713
  }
88
714
  catch (e) {
89
715
  // ignore, best-effort path normalization
90
716
  }
91
- const tempDir = path_1.default.join(projectPath, '.dependency-radar');
92
- const stopSpinner = startSpinner(`Scanning project at ${projectPath}`);
717
+ const tempDir = path_1.default.join(projectPath, ".dependency-radar");
718
+ // Workspace detection and reporting
719
+ const workspace = await detectWorkspace(projectPath);
720
+ if (workspace.type === "yarn" && workspace.packagePaths.length === 0) {
721
+ console.error("Yarn Plug'n'Play (nodeLinker: pnp) detected. This is not supported yet.");
722
+ console.error("Switch to nodeLinker: node-modules or run in a non-PnP environment.");
723
+ process.exit(1);
724
+ return;
725
+ }
726
+ const rootPkg = await readJsonFile(path_1.default.join(projectPath, "package.json"));
727
+ const packageManager = await detectPackageManager(projectPath, rootPkg, workspace.type);
728
+ const scanManager = await detectScanManager(projectPath, packageManager);
729
+ const packageManagerField = typeof (rootPkg === null || rootPkg === void 0 ? void 0 : rootPkg.packageManager) === "string"
730
+ ? rootPkg.packageManager.trim()
731
+ : undefined;
732
+ const [npmVersion, pnpmVersion, yarnVersion] = await Promise.all([
733
+ getToolVersion("npm", projectPath),
734
+ getToolVersion("pnpm", projectPath),
735
+ getToolVersion("yarn", projectPath),
736
+ ]);
737
+ const toolVersions = compactToolVersions({
738
+ npm: npmVersion,
739
+ pnpm: pnpmVersion,
740
+ yarn: yarnVersion,
741
+ });
742
+ const packageManagerVersion = scanManager === "npm"
743
+ ? npmVersion
744
+ : scanManager === "pnpm"
745
+ ? pnpmVersion
746
+ : yarnVersion;
747
+ if (packageManager === "yarn") {
748
+ const yarnrc = path_1.default.join(projectPath, ".yarnrc.yml");
749
+ if (await (0, utils_1.pathExists)(yarnrc)) {
750
+ const y = await promises_1.default.readFile(yarnrc, "utf8");
751
+ if (/nodeLinker\s*:\s*pnp/.test(y)) {
752
+ console.error("Yarn Plug'n'Play (nodeLinker: pnp) detected. This is not supported yet.");
753
+ console.error("Switch to nodeLinker: node-modules or run in a non-PnP environment.");
754
+ process.exit(1);
755
+ return;
756
+ }
757
+ }
758
+ }
759
+ const packagePaths = workspace.packagePaths;
760
+ const workspaceLabel = workspace.type === "none"
761
+ ? "Single project"
762
+ : `${workspace.type.toUpperCase()} workspace`;
763
+ console.log(`✔ ${workspaceLabel} detected`);
764
+ if (workspace.type !== "none" && scanManager !== workspace.type) {
765
+ console.log(`✔ Using ${scanManager.toUpperCase()} for dependency data (lockfile detected)`);
766
+ }
767
+ const spinner = startSpinner(`Scanning ${workspaceLabel} at ${projectPath}`);
93
768
  try {
94
769
  await (0, utils_1.ensureDir)(tempDir);
95
- const [auditResult, npmLsResult, importGraphResult] = await Promise.all([
96
- opts.audit ? (0, npmAudit_1.runNpmAudit)(projectPath, tempDir) : Promise.resolve(undefined),
97
- (0, npmLs_1.runNpmLs)(projectPath, tempDir),
98
- (0, importGraphRunner_1.runImportGraph)(projectPath, tempDir)
99
- ]);
100
- if (opts.maintenance) {
101
- stopSpinner(true);
102
- console.log('Running maintenance checks (slow mode)');
103
- console.log('This may take several minutes depending on dependency count.');
770
+ // Run tools per package for best coverage.
771
+ const packageMetas = await readWorkspacePackageMeta(projectPath, packagePaths);
772
+ const perPackageAudit = [];
773
+ const perPackageLs = [];
774
+ const perPackageImportGraph = [];
775
+ const perPackageOutdated = [];
776
+ for (const meta of packageMetas) {
777
+ spinner.update(`Scanning ${workspaceLabel} (${perPackageLs.length + 1}/${packageMetas.length}) at ${projectPath}`);
778
+ const pkgTempDir = path_1.default.join(tempDir, meta.name.replace(/[^a-zA-Z0-9._-]/g, "_"));
779
+ await (0, utils_1.ensureDir)(pkgTempDir);
780
+ const [a, l, ig, o] = await Promise.all([
781
+ opts.audit
782
+ ? (0, npmAudit_1.runPackageAudit)(meta.path, pkgTempDir, scanManager, yarnVersion).catch((err) => ({ ok: false, error: String(err) }))
783
+ : Promise.resolve(undefined),
784
+ (0, npmLs_1.runNpmLs)(meta.path, pkgTempDir, scanManager).catch((err) => ({ ok: false, error: String(err) })),
785
+ (0, importGraphRunner_1.runImportGraph)(meta.path, pkgTempDir).catch((err) => ({ ok: false, error: String(err) })),
786
+ opts.outdated
787
+ ? (0, npmOutdated_1.runPackageOutdated)(meta.path, pkgTempDir, scanManager).catch((err) => ({ ok: false, error: String(err) }))
788
+ : Promise.resolve(undefined),
789
+ ]);
790
+ perPackageAudit.push(a);
791
+ perPackageLs.push(l);
792
+ perPackageImportGraph.push(ig);
793
+ perPackageOutdated.push({ attempted: Boolean(opts.outdated), result: o });
794
+ }
795
+ if (opts.audit) {
796
+ const auditOk = perPackageAudit.every((r) => r && r.ok);
797
+ if (auditOk) {
798
+ spinner.log(`✔ ${scanManager.toUpperCase()} audit data collected`);
799
+ }
800
+ else {
801
+ spinner.log(`✖ ${scanManager.toUpperCase()} audit data unavailable`);
802
+ }
803
+ }
804
+ if (opts.outdated) {
805
+ const outdatedOk = perPackageOutdated.every((r) => r.result && r.result.ok);
806
+ if (outdatedOk) {
807
+ spinner.log(`✔ ${scanManager.toUpperCase()} outdated data collected`);
808
+ }
809
+ else {
810
+ spinner.log(`✖ ${scanManager.toUpperCase()} outdated data unavailable`);
811
+ }
812
+ }
813
+ const mergedAuditData = mergeAuditResults(perPackageAudit.map((r) => (r && r.ok ? r.data : undefined)));
814
+ const mergedGraphData = workspace.type === "none"
815
+ ? perPackageLs[0] && perPackageLs[0].ok
816
+ ? perPackageLs[0].data
817
+ : undefined
818
+ : buildCombinedDependencyGraph(projectPath, packageMetas, perPackageLs.map((r) => (r && r.ok ? r.data : undefined)));
819
+ const mergedImportGraphData = mergeImportGraphs(projectPath, packageMetas, perPackageImportGraph.map((r) => (r && r.ok ? r.data : undefined)));
820
+ const workspaceUsage = buildWorkspaceUsageMap(packageMetas, perPackageLs.map((r) => (r && r.ok ? r.data : undefined)));
821
+ const outdatedResult = mergeOutdatedResults(perPackageOutdated);
822
+ const auditResult = mergedAuditData
823
+ ? { ok: true, data: mergedAuditData }
824
+ : undefined;
825
+ const npmLsResult = { ok: true, data: mergedGraphData };
826
+ const importGraphResult = { ok: true, data: mergedImportGraphData };
827
+ // Build a merged package.json view for aggregator direct-dep checks.
828
+ const mergedPkgForAggregator = mergeDepsFromWorkspace(packageMetas);
829
+ const auditFailure = opts.audit
830
+ ? perPackageAudit.find((r) => r && !r.ok)
831
+ : undefined;
832
+ const lsFailure = perPackageLs.find((r) => r && !r.ok);
833
+ const importFailure = perPackageImportGraph.find((r) => r && !r.ok);
834
+ if (auditFailure) {
835
+ spinner.log(`Audit warning: ${auditFailure.error || "Audit failed"}`);
836
+ }
837
+ if (lsFailure || importFailure) {
838
+ const err = lsFailure || importFailure;
839
+ throw new Error((err === null || err === void 0 ? void 0 : err.error) || "Tool execution failed");
104
840
  }
105
841
  const aggregated = await (0, aggregator_1.aggregateData)({
106
842
  projectPath,
107
- maintenanceEnabled: opts.maintenance,
108
- onMaintenanceProgress: opts.maintenance
109
- ? (current, total, name) => {
110
- process.stdout.write(`\r[${current}/${total}] ${name} `);
111
- }
112
- : undefined,
113
843
  auditResult,
114
844
  npmLsResult,
115
- importGraphResult
845
+ importGraphResult,
846
+ outdatedResult,
847
+ pkgOverride: mergedPkgForAggregator,
848
+ workspaceUsage,
849
+ resolvePaths: [
850
+ projectPath,
851
+ ...packagePaths.filter((p) => p !== projectPath),
852
+ ],
853
+ workspaceEnabled: workspace.type !== "none",
854
+ workspaceType: workspace.type,
855
+ workspacePackageCount: packagePaths.length,
856
+ packageManager: scanManager,
857
+ packageManagerVersion,
858
+ packageManagerField,
859
+ platform: process.platform,
860
+ arch: process.arch,
861
+ ci: isCI(),
862
+ ...(toolVersions ? { toolVersions } : {}),
116
863
  });
117
- dependencyCount = aggregated.dependencies.length;
118
- if (opts.maintenance) {
119
- process.stdout.write('\n');
864
+ dependencyCount = Object.keys(aggregated.dependencies).length;
865
+ if (workspace.type !== "none") {
866
+ console.log(`Detected ${workspace.type.toUpperCase()} workspace with ${packagePaths.length} package${packagePaths.length === 1 ? "" : "s"}.`);
120
867
  }
121
868
  if (opts.json) {
122
869
  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');
870
+ await promises_1.default.writeFile(outputPath, JSON.stringify(aggregated, null, 2), "utf8");
124
871
  }
125
872
  else {
126
873
  await (0, report_1.renderReport)(aggregated, outputPath);
127
874
  }
128
- stopSpinner(true);
129
- console.log(`${opts.json ? 'JSON' : 'Report'} written to ${outputPath}`);
875
+ spinner.stop(true);
130
876
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
131
- console.log(`Scan complete: ${dependencyCount} dependencies analysed in ${elapsed}s`);
877
+ console.log(`✔ Scan complete: ${dependencyCount} dependencies analysed in ${elapsed}s`);
878
+ console.log(`✔ ${opts.json ? "JSON" : "Report"} written to ${outputPath}`);
132
879
  }
133
880
  catch (err) {
134
- stopSpinner(false);
135
- console.error('Failed to generate report:', err);
881
+ spinner.stop(false);
882
+ console.error("Failed to generate report:", err);
136
883
  process.exit(1);
137
884
  }
138
885
  finally {
@@ -140,28 +887,82 @@ async function run() {
140
887
  await (0, utils_1.removeDir)(tempDir);
141
888
  }
142
889
  else {
143
- console.log(`Temporary data kept at ${tempDir}`);
890
+ console.log(`✔ Temporary data kept at ${tempDir}`);
144
891
  }
145
892
  }
893
+ if (opts.open && !isCI()) {
894
+ console.log(`↗ Opening ${path_1.default.basename(outputPath)} using system default ${opts.json ? "application" : "browser"}.`);
895
+ openInBrowser(outputPath);
896
+ }
897
+ else if (opts.open && isCI()) {
898
+ console.log("✖ Skipping auto-open in CI environment.");
899
+ }
146
900
  // Always show CTA as the last output
147
- console.log('');
148
- console.log('Get additional risk analysis and a management-ready summary at https://dependency-radar.com');
901
+ console.log("");
902
+ console.log("Get additional risk analysis and a management-ready summary at https://dependency-radar.com");
149
903
  }
150
904
  run();
151
905
  function startSpinner(text) {
152
- const frames = ['|', '/', '-', '\\'];
906
+ const frames = ["|", "/", "-", "\\"];
153
907
  let i = 0;
154
- process.stdout.write(`${frames[i]} ${text}`);
908
+ let currentText = text;
909
+ const shortenPathInMessage = (message) => {
910
+ const marker = ' at ';
911
+ const idx = message.lastIndexOf(marker);
912
+ if (idx === -1)
913
+ return message;
914
+ const head = message.slice(0, idx + marker.length);
915
+ const rawPath = message.slice(idx + marker.length).trim();
916
+ if (!rawPath)
917
+ return message;
918
+ const segments = rawPath.split(/[\\/]+/).filter(Boolean);
919
+ if (segments.length === 0)
920
+ return message;
921
+ const tail = segments.slice(-2).join('/');
922
+ return `${head}…/${tail}`;
923
+ };
924
+ const formatLine = (prefix, value) => {
925
+ if (!process.stdout.isTTY)
926
+ return `${prefix} ${value}`;
927
+ const displayValue = shortenPathInMessage(value);
928
+ const columns = process.stdout.columns || 0;
929
+ if (columns <= 0)
930
+ return `${prefix} ${displayValue}`;
931
+ const max = columns - (prefix.length + 1);
932
+ if (max <= 0)
933
+ return prefix;
934
+ if (displayValue.length <= max)
935
+ return `${prefix} ${displayValue}`;
936
+ const ellipsis = "…";
937
+ const keep = Math.max(0, max - ellipsis.length);
938
+ return `${prefix} ${displayValue.slice(0, keep)}${ellipsis}`;
939
+ };
940
+ process.stdout.write(formatLine(frames[i], currentText));
155
941
  const timer = setInterval(() => {
156
942
  i = (i + 1) % frames.length;
157
- process.stdout.write(`\r${frames[i]} ${text}`);
943
+ process.stdout.write(`\r\x1b[K${formatLine(frames[i], currentText)}`);
158
944
  }, 120);
159
945
  let stopped = false;
160
- return (success = true) => {
946
+ const stop = (success = true) => {
161
947
  if (stopped)
162
948
  return;
163
949
  stopped = true;
164
950
  clearInterval(timer);
165
- process.stdout.write(`\r${success ? '' : ''} ${text}\n`);
951
+ process.stdout.write(`\r\x1b[K${formatLine(success ? "" : "", currentText)}\n`);
952
+ };
953
+ const update = (nextText) => {
954
+ if (stopped)
955
+ return;
956
+ currentText = nextText;
957
+ process.stdout.write(`\r\x1b[K${formatLine(frames[i], currentText)}`);
958
+ };
959
+ const log = (line) => {
960
+ if (stopped) {
961
+ process.stdout.write(`${line}\n`);
962
+ return;
963
+ }
964
+ process.stdout.write(`\r\x1b[K${line}\n`);
965
+ process.stdout.write(formatLine(frames[i], currentText));
166
966
  };
967
+ return { stop, update, log };
167
968
  }