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/README.md +176 -19
- package/dist/aggregator.js +1021 -406
- package/dist/cli.js +857 -56
- package/dist/generated/spdx.js +855 -0
- package/dist/license.js +324 -0
- package/dist/report-assets.js +2 -2
- package/dist/report.js +88 -89
- package/dist/runners/importGraphRunner.js +11 -5
- package/dist/runners/npmAudit.js +81 -16
- package/dist/runners/npmLs.js +216 -15
- package/dist/runners/npmOutdated.js +115 -0
- package/dist/utils.js +159 -24
- package/package.json +23 -3
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:
|
|
607
|
+
command: "scan",
|
|
18
608
|
project: process.cwd(),
|
|
19
|
-
out:
|
|
609
|
+
out: "dependency-radar.html",
|
|
20
610
|
keepTemp: false,
|
|
21
|
-
maintenance: false,
|
|
22
611
|
audit: true,
|
|
23
|
-
|
|
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 ===
|
|
624
|
+
if (arg === "--project" && args[0])
|
|
34
625
|
opts.project = args.shift();
|
|
35
|
-
else if (arg ===
|
|
626
|
+
else if (arg === "--out" && args[0])
|
|
36
627
|
opts.out = args.shift();
|
|
37
|
-
else if (arg ===
|
|
628
|
+
else if (arg === "--keep-temp")
|
|
38
629
|
opts.keepTemp = true;
|
|
39
|
-
else if (arg ===
|
|
40
|
-
opts.maintenance = true;
|
|
41
|
-
else if (arg === '--no-audit')
|
|
630
|
+
else if (arg === "--offline") {
|
|
42
631
|
opts.audit = false;
|
|
43
|
-
|
|
632
|
+
opts.outdated = false;
|
|
633
|
+
}
|
|
634
|
+
else if (arg === "--json")
|
|
44
635
|
opts.json = true;
|
|
45
|
-
else if (arg ===
|
|
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
|
-
--
|
|
63
|
-
--
|
|
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 !==
|
|
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 ===
|
|
75
|
-
opts.out =
|
|
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(
|
|
706
|
+
const endsWithSeparator = opts.out.endsWith("/") || opts.out.endsWith("\\");
|
|
83
707
|
const hasExtension = Boolean(path_1.default.extname(outputPath));
|
|
84
|
-
if ((stat && stat.isDirectory()) ||
|
|
85
|
-
|
|
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,
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 (
|
|
119
|
-
|
|
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),
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
135
|
-
console.error(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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]
|
|
943
|
+
process.stdout.write(`\r\x1b[K${formatLine(frames[i], currentText)}`);
|
|
158
944
|
}, 120);
|
|
159
945
|
let stopped = false;
|
|
160
|
-
|
|
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 ?
|
|
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
|
}
|