dependency-radar 0.3.1 → 0.5.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 +208 -40
- package/dist/aggregator.js +386 -45
- package/dist/cli.js +418 -78
- package/dist/cta.js +17 -0
- package/dist/generated/spdx.js +3 -0
- package/dist/report-assets.js +2 -2
- package/dist/report.js +241 -155
- package/dist/runners/lockfileGraph.js +1146 -0
- package/dist/runners/npmLs.js +135 -13
- package/dist/runners/npmOutdated.js +34 -18
- package/dist/utils.js +15 -1
- package/package.json +18 -23
package/dist/cli.js
CHANGED
|
@@ -90,6 +90,234 @@ async function readJsonFile(filePath) {
|
|
|
90
90
|
return undefined;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
function stripYamlInlineComment(rawLine) {
|
|
94
|
+
let inSingle = false;
|
|
95
|
+
let inDouble = false;
|
|
96
|
+
for (let i = 0; i < rawLine.length; i += 1) {
|
|
97
|
+
const ch = rawLine[i];
|
|
98
|
+
const prev = i > 0 ? rawLine[i - 1] : "";
|
|
99
|
+
if (ch === "'" && !inDouble) {
|
|
100
|
+
inSingle = !inSingle;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === '"' && !inSingle && prev !== "\\") {
|
|
104
|
+
inDouble = !inDouble;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (ch === "#" && !inSingle && !inDouble) {
|
|
108
|
+
return rawLine.slice(0, i);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return rawLine;
|
|
112
|
+
}
|
|
113
|
+
function unquoteYamlScalar(value) {
|
|
114
|
+
const trimmed = value.trim();
|
|
115
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
116
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
117
|
+
const quote = trimmed[0];
|
|
118
|
+
const inner = trimmed.slice(1, -1);
|
|
119
|
+
return quote === '"'
|
|
120
|
+
? inner
|
|
121
|
+
.replace(/\\"/g, '"')
|
|
122
|
+
.replace(/\\\\/g, "\\")
|
|
123
|
+
: inner.replace(/''/g, "'");
|
|
124
|
+
}
|
|
125
|
+
return trimmed;
|
|
126
|
+
}
|
|
127
|
+
function parseYamlScalar(value) {
|
|
128
|
+
const normalized = value.trim();
|
|
129
|
+
if (!normalized)
|
|
130
|
+
return "";
|
|
131
|
+
if (normalized === "{}")
|
|
132
|
+
return {};
|
|
133
|
+
if (normalized === "[]")
|
|
134
|
+
return [];
|
|
135
|
+
if (normalized === "null" || normalized === "~")
|
|
136
|
+
return null;
|
|
137
|
+
if (normalized === "true")
|
|
138
|
+
return true;
|
|
139
|
+
if (normalized === "false")
|
|
140
|
+
return false;
|
|
141
|
+
return unquoteYamlScalar(normalized);
|
|
142
|
+
}
|
|
143
|
+
function findYamlMapSeparator(content) {
|
|
144
|
+
let inSingle = false;
|
|
145
|
+
let inDouble = false;
|
|
146
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
147
|
+
const ch = content[i];
|
|
148
|
+
const prev = i > 0 ? content[i - 1] : "";
|
|
149
|
+
if (ch === "'" && !inDouble) {
|
|
150
|
+
inSingle = !inSingle;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (ch === '"' && !inSingle && prev !== "\\") {
|
|
154
|
+
inDouble = !inDouble;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (ch !== ":" || inSingle || inDouble)
|
|
158
|
+
continue;
|
|
159
|
+
const next = content[i + 1];
|
|
160
|
+
if (next === undefined || next === " " || next === "\t") {
|
|
161
|
+
return i;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return -1;
|
|
165
|
+
}
|
|
166
|
+
function toObjectRecord(value) {
|
|
167
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
function mergeRecordObjects(...objects) {
|
|
173
|
+
const merged = {};
|
|
174
|
+
for (const obj of objects) {
|
|
175
|
+
if (!obj)
|
|
176
|
+
continue;
|
|
177
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
178
|
+
merged[key] = val;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
182
|
+
}
|
|
183
|
+
function normalizeStringArray(value) {
|
|
184
|
+
if (typeof value === "string") {
|
|
185
|
+
const single = value.trim();
|
|
186
|
+
return single ? [single] : [];
|
|
187
|
+
}
|
|
188
|
+
if (!Array.isArray(value))
|
|
189
|
+
return [];
|
|
190
|
+
return value
|
|
191
|
+
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
|
192
|
+
.filter(Boolean);
|
|
193
|
+
}
|
|
194
|
+
function parseSimpleYaml(yaml) {
|
|
195
|
+
var _a, _b;
|
|
196
|
+
const lines = [];
|
|
197
|
+
for (const rawLine of yaml.split(/\r?\n/)) {
|
|
198
|
+
const noComment = stripYamlInlineComment(rawLine).replace(/\s+$/, "");
|
|
199
|
+
if (!noComment.trim())
|
|
200
|
+
continue;
|
|
201
|
+
const indent = (_b = (_a = noComment.match(/^(\s*)/)) === null || _a === void 0 ? void 0 : _a[1].length) !== null && _b !== void 0 ? _b : 0;
|
|
202
|
+
lines.push({
|
|
203
|
+
indent,
|
|
204
|
+
content: noComment.trim(),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
let index = 0;
|
|
208
|
+
const parseNode = (indentLevel) => {
|
|
209
|
+
if (index >= lines.length)
|
|
210
|
+
return undefined;
|
|
211
|
+
if (lines[index].indent < indentLevel)
|
|
212
|
+
return undefined;
|
|
213
|
+
if (lines[index].indent === indentLevel &&
|
|
214
|
+
lines[index].content.startsWith("- ")) {
|
|
215
|
+
return parseSequence(indentLevel);
|
|
216
|
+
}
|
|
217
|
+
return parseMapping(indentLevel);
|
|
218
|
+
};
|
|
219
|
+
const parseMapping = (indentLevel) => {
|
|
220
|
+
const out = {};
|
|
221
|
+
while (index < lines.length) {
|
|
222
|
+
const line = lines[index];
|
|
223
|
+
if (line.indent < indentLevel)
|
|
224
|
+
break;
|
|
225
|
+
if (line.indent > indentLevel) {
|
|
226
|
+
index += 1;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (line.content.startsWith("- "))
|
|
230
|
+
break;
|
|
231
|
+
const colonIndex = findYamlMapSeparator(line.content);
|
|
232
|
+
if (colonIndex <= 0) {
|
|
233
|
+
index += 1;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const key = unquoteYamlScalar(line.content.slice(0, colonIndex));
|
|
237
|
+
const valueToken = line.content.slice(colonIndex + 1).trim();
|
|
238
|
+
index += 1;
|
|
239
|
+
if (valueToken) {
|
|
240
|
+
out[key] = parseYamlScalar(valueToken);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (index < lines.length && lines[index].indent > indentLevel) {
|
|
244
|
+
out[key] = parseNode(lines[index].indent);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
out[key] = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
};
|
|
252
|
+
const parseSequence = (indentLevel) => {
|
|
253
|
+
const values = [];
|
|
254
|
+
while (index < lines.length) {
|
|
255
|
+
const line = lines[index];
|
|
256
|
+
if (line.indent < indentLevel)
|
|
257
|
+
break;
|
|
258
|
+
if (line.indent !== indentLevel || !line.content.startsWith("- "))
|
|
259
|
+
break;
|
|
260
|
+
const valueToken = line.content.slice(2).trim();
|
|
261
|
+
index += 1;
|
|
262
|
+
if (valueToken) {
|
|
263
|
+
values.push(parseYamlScalar(valueToken));
|
|
264
|
+
if (index < lines.length && lines[index].indent > indentLevel) {
|
|
265
|
+
// Consume malformed continuation lines to keep parser state stable.
|
|
266
|
+
parseNode(lines[index].indent);
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (index < lines.length && lines[index].indent > indentLevel) {
|
|
271
|
+
values.push(parseNode(lines[index].indent));
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
values.push(null);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return values;
|
|
278
|
+
};
|
|
279
|
+
const root = parseNode(0);
|
|
280
|
+
return toObjectRecord(root) || {};
|
|
281
|
+
}
|
|
282
|
+
function parsePnpmWorkspacePackagesFallback(yaml) {
|
|
283
|
+
const patterns = [];
|
|
284
|
+
const lines = yaml.split(/\r?\n/);
|
|
285
|
+
let inPackages = false;
|
|
286
|
+
for (const line of lines) {
|
|
287
|
+
const trimmed = line.trim();
|
|
288
|
+
if (!trimmed)
|
|
289
|
+
continue;
|
|
290
|
+
if (/^packages\s*:\s*$/.test(trimmed)) {
|
|
291
|
+
inPackages = true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (inPackages) {
|
|
295
|
+
if (/^[A-Za-z0-9_-]+\s*:/.test(trimmed) && !trimmed.startsWith("-")) {
|
|
296
|
+
inPackages = false;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const m = trimmed.match(/^[-]\s*["']?([^"']+)["']?\s*$/);
|
|
300
|
+
if (m && m[1])
|
|
301
|
+
patterns.push(m[1].trim());
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return patterns;
|
|
305
|
+
}
|
|
306
|
+
function parsePnpmWorkspaceFile(yaml) {
|
|
307
|
+
var _a;
|
|
308
|
+
const parsed = parseSimpleYaml(yaml);
|
|
309
|
+
const fromYaml = normalizeStringArray(parsed.packages);
|
|
310
|
+
const fromFallback = fromYaml.length > 0
|
|
311
|
+
? fromYaml
|
|
312
|
+
: parsePnpmWorkspacePackagesFallback(yaml);
|
|
313
|
+
const topLevelOverrides = toObjectRecord(parsed.overrides);
|
|
314
|
+
const pnpmOverrides = toObjectRecord((_a = toObjectRecord(parsed.pnpm)) === null || _a === void 0 ? void 0 : _a.overrides);
|
|
315
|
+
const overrides = mergeRecordObjects(topLevelOverrides, pnpmOverrides);
|
|
316
|
+
return {
|
|
317
|
+
packages: fromFallback,
|
|
318
|
+
...(overrides ? { overrides } : {}),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
93
321
|
async function getToolVersion(tool, cwd) {
|
|
94
322
|
try {
|
|
95
323
|
const result = await (0, utils_1.runCommand)(tool, ["--version"], { cwd });
|
|
@@ -110,55 +338,45 @@ function compactToolVersions(versions) {
|
|
|
110
338
|
}
|
|
111
339
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
112
340
|
}
|
|
341
|
+
async function detectYarnPnP(projectPath) {
|
|
342
|
+
if ((await (0, utils_1.pathExists)(path_1.default.join(projectPath, ".pnp.cjs"))) ||
|
|
343
|
+
(await (0, utils_1.pathExists)(path_1.default.join(projectPath, ".pnp.js")))) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
const yarnrc = path_1.default.join(projectPath, ".yarnrc.yml");
|
|
347
|
+
if (!(await (0, utils_1.pathExists)(yarnrc)))
|
|
348
|
+
return false;
|
|
349
|
+
const content = await promises_1.default.readFile(yarnrc, "utf8").catch(() => "");
|
|
350
|
+
return /nodeLinker\s*:\s*pnp\b/.test(content);
|
|
351
|
+
}
|
|
113
352
|
async function detectWorkspace(projectPath) {
|
|
114
353
|
const rootPkgPath = path_1.default.join(projectPath, "package.json");
|
|
115
354
|
const rootPkg = await readJsonFile(rootPkgPath);
|
|
116
355
|
const inferredManager = inferPackageManager(rootPkg);
|
|
356
|
+
const hasYarnLock = await (0, utils_1.pathExists)(path_1.default.join(projectPath, "yarn.lock"));
|
|
357
|
+
const hasYarnPnp = await detectYarnPnP(projectPath);
|
|
117
358
|
const pnpmWorkspacePath = path_1.default.join(projectPath, "pnpm-workspace.yaml");
|
|
118
359
|
const hasPnpmWorkspace = await (0, utils_1.pathExists)(pnpmWorkspacePath);
|
|
119
360
|
let type = "none";
|
|
120
361
|
let patterns = [];
|
|
362
|
+
let pnpmWorkspaceOverrides;
|
|
121
363
|
if (hasPnpmWorkspace) {
|
|
122
364
|
type = "pnpm";
|
|
123
|
-
// very small YAML parser for the only thing we care about: `packages:` list.
|
|
124
365
|
const yaml = await promises_1.default.readFile(pnpmWorkspacePath, "utf8");
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
}
|
|
366
|
+
const workspaceFile = parsePnpmWorkspaceFile(yaml);
|
|
367
|
+
patterns = workspaceFile.packages;
|
|
368
|
+
pnpmWorkspaceOverrides = workspaceFile.overrides;
|
|
369
|
+
}
|
|
370
|
+
if (hasYarnPnp) {
|
|
371
|
+
return { type: "yarn", packagePaths: [] };
|
|
146
372
|
}
|
|
147
373
|
// npm/yarn workspaces
|
|
148
374
|
if (type === "none" && rootPkg && rootPkg.workspaces) {
|
|
149
|
-
type = inferredManager || "npm";
|
|
375
|
+
type = inferredManager || (hasYarnLock ? "yarn" : "npm");
|
|
150
376
|
if (Array.isArray(rootPkg.workspaces))
|
|
151
377
|
patterns = rootPkg.workspaces;
|
|
152
378
|
else if (Array.isArray(rootPkg.workspaces.packages))
|
|
153
379
|
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
380
|
}
|
|
163
381
|
if (type === "none") {
|
|
164
382
|
return { type: "none", packagePaths: [projectPath] };
|
|
@@ -189,7 +407,11 @@ async function detectWorkspace(projectPath) {
|
|
|
189
407
|
}
|
|
190
408
|
}
|
|
191
409
|
}
|
|
192
|
-
return {
|
|
410
|
+
return {
|
|
411
|
+
type,
|
|
412
|
+
packagePaths: packagePaths.sort(),
|
|
413
|
+
...(pnpmWorkspaceOverrides ? { pnpmWorkspaceOverrides } : {}),
|
|
414
|
+
};
|
|
193
415
|
}
|
|
194
416
|
function inferPackageManager(rootPkg) {
|
|
195
417
|
const raw = typeof (rootPkg === null || rootPkg === void 0 ? void 0 : rootPkg.packageManager) === "string"
|
|
@@ -211,13 +433,8 @@ async function detectPackageManager(projectPath, rootPkg, workspaceType) {
|
|
|
211
433
|
return inferred;
|
|
212
434
|
if (workspaceType === "pnpm" || workspaceType === "yarn")
|
|
213
435
|
return workspaceType;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const y = await promises_1.default.readFile(yarnrc, "utf8");
|
|
217
|
-
if (/nodeLinker\s*:\s*pnp/.test(y)) {
|
|
218
|
-
return "yarn";
|
|
219
|
-
}
|
|
220
|
-
}
|
|
436
|
+
if (await detectYarnPnP(projectPath))
|
|
437
|
+
return "yarn";
|
|
221
438
|
if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".pnpm")))
|
|
222
439
|
return "pnpm";
|
|
223
440
|
if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".yarn-state.yml")))
|
|
@@ -250,20 +467,120 @@ async function readWorkspacePackageMeta(rootPath, packagePaths) {
|
|
|
250
467
|
}
|
|
251
468
|
return out;
|
|
252
469
|
}
|
|
253
|
-
function
|
|
254
|
-
|
|
470
|
+
function isWorkspaceLocalSpecifier(value) {
|
|
471
|
+
if (typeof value !== "string")
|
|
472
|
+
return false;
|
|
473
|
+
const trimmed = value.trim().toLowerCase();
|
|
474
|
+
return (trimmed.startsWith("workspace:") ||
|
|
475
|
+
trimmed.startsWith("link:") ||
|
|
476
|
+
trimmed.startsWith("file:"));
|
|
477
|
+
}
|
|
478
|
+
function normalizeRelativePath(rootPath, packagePath) {
|
|
479
|
+
const relative = path_1.default.relative(rootPath, packagePath);
|
|
480
|
+
const normalized = relative.split(path_1.default.sep).join("/");
|
|
481
|
+
return normalized && normalized.length > 0 ? normalized : ".";
|
|
482
|
+
}
|
|
483
|
+
function readDependencyEntries(source) {
|
|
484
|
+
if (!source || typeof source !== "object")
|
|
485
|
+
return [];
|
|
486
|
+
const entries = [];
|
|
487
|
+
for (const [name, spec] of Object.entries(source)) {
|
|
488
|
+
if (typeof name !== "string" || !name.trim())
|
|
489
|
+
continue;
|
|
490
|
+
if (typeof spec !== "string" || !spec.trim())
|
|
491
|
+
continue;
|
|
492
|
+
entries.push([name, spec.trim()]);
|
|
493
|
+
}
|
|
494
|
+
return entries;
|
|
495
|
+
}
|
|
496
|
+
function isWorkspaceLocalDependency(dependencyName, spec, workspacePackageNames) {
|
|
497
|
+
return workspacePackageNames.has(dependencyName) || isWorkspaceLocalSpecifier(spec);
|
|
498
|
+
}
|
|
499
|
+
function buildWorkspaceClassification(rootPath, packageMetas) {
|
|
500
|
+
var _a, _b, _c, _d, _e;
|
|
501
|
+
const workspacePackageNames = new Set(packageMetas.map((meta) => meta.name));
|
|
502
|
+
const workspacePackageIds = new Set();
|
|
503
|
+
const workspacePackagePaths = new Set();
|
|
504
|
+
const localDependencyNames = new Set();
|
|
505
|
+
const workspacePackages = [];
|
|
506
|
+
for (const meta of packageMetas) {
|
|
507
|
+
const version = typeof ((_a = meta.pkg) === null || _a === void 0 ? void 0 : _a.version) === "string" && meta.pkg.version.trim().length > 0
|
|
508
|
+
? meta.pkg.version.trim()
|
|
509
|
+
: "workspace";
|
|
510
|
+
workspacePackageIds.add(`${meta.name}@${version}`);
|
|
511
|
+
workspacePackagePaths.add(path_1.default.resolve(meta.path));
|
|
512
|
+
const runtimeExternal = new Set();
|
|
513
|
+
const devExternal = new Set();
|
|
514
|
+
const runtimeEntries = [
|
|
515
|
+
...readDependencyEntries((_b = meta.pkg) === null || _b === void 0 ? void 0 : _b.dependencies),
|
|
516
|
+
...readDependencyEntries((_c = meta.pkg) === null || _c === void 0 ? void 0 : _c.optionalDependencies),
|
|
517
|
+
];
|
|
518
|
+
const devEntries = readDependencyEntries((_d = meta.pkg) === null || _d === void 0 ? void 0 : _d.devDependencies);
|
|
519
|
+
const peerEntries = readDependencyEntries((_e = meta.pkg) === null || _e === void 0 ? void 0 : _e.peerDependencies);
|
|
520
|
+
for (const [depName, spec] of runtimeEntries) {
|
|
521
|
+
if (isWorkspaceLocalDependency(depName, spec, workspacePackageNames)) {
|
|
522
|
+
localDependencyNames.add(depName);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
runtimeExternal.add(depName);
|
|
526
|
+
}
|
|
527
|
+
for (const [depName, spec] of devEntries) {
|
|
528
|
+
if (isWorkspaceLocalDependency(depName, spec, workspacePackageNames)) {
|
|
529
|
+
localDependencyNames.add(depName);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
devExternal.add(depName);
|
|
533
|
+
}
|
|
534
|
+
for (const [depName, spec] of peerEntries) {
|
|
535
|
+
if (isWorkspaceLocalDependency(depName, spec, workspacePackageNames)) {
|
|
536
|
+
localDependencyNames.add(depName);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
workspacePackages.push({
|
|
540
|
+
name: meta.name,
|
|
541
|
+
relativePath: normalizeRelativePath(rootPath, meta.path),
|
|
542
|
+
directExternal: {
|
|
543
|
+
runtime: runtimeExternal.size,
|
|
544
|
+
dev: devExternal.size,
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
workspacePackages.sort((a, b) => {
|
|
549
|
+
const pathCompare = a.relativePath.localeCompare(b.relativePath);
|
|
550
|
+
if (pathCompare !== 0)
|
|
551
|
+
return pathCompare;
|
|
552
|
+
return a.name.localeCompare(b.name);
|
|
553
|
+
});
|
|
554
|
+
return {
|
|
555
|
+
workspacePackages,
|
|
556
|
+
workspacePackageNames,
|
|
557
|
+
workspacePackageIds,
|
|
558
|
+
workspacePackagePaths,
|
|
559
|
+
localDependencyNames,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function mergeDepsFromWorkspace(pkgs, workspacePackageNames, localDependencyNames) {
|
|
563
|
+
var _a, _b, _c, _d;
|
|
255
564
|
const merged = {
|
|
256
565
|
dependencies: {},
|
|
257
566
|
devDependencies: {},
|
|
258
567
|
optionalDependencies: {},
|
|
568
|
+
peerDependencies: {},
|
|
569
|
+
};
|
|
570
|
+
const mergeSection = (target, source) => {
|
|
571
|
+
for (const [depName, spec] of readDependencyEntries(source)) {
|
|
572
|
+
if (isWorkspaceLocalDependency(depName, spec, workspacePackageNames)) {
|
|
573
|
+
localDependencyNames.add(depName);
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
target[depName] = spec;
|
|
577
|
+
}
|
|
259
578
|
};
|
|
260
579
|
for (const entry of pkgs) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
Object.assign(merged.devDependencies, dev);
|
|
266
|
-
Object.assign(merged.optionalDependencies, opt);
|
|
580
|
+
mergeSection(merged.dependencies, (_a = entry.pkg) === null || _a === void 0 ? void 0 : _a.dependencies);
|
|
581
|
+
mergeSection(merged.devDependencies, (_b = entry.pkg) === null || _b === void 0 ? void 0 : _b.devDependencies);
|
|
582
|
+
mergeSection(merged.optionalDependencies, (_c = entry.pkg) === null || _c === void 0 ? void 0 : _c.optionalDependencies);
|
|
583
|
+
mergeSection(merged.peerDependencies, (_d = entry.pkg) === null || _d === void 0 ? void 0 : _d.peerDependencies);
|
|
267
584
|
}
|
|
268
585
|
return merged;
|
|
269
586
|
}
|
|
@@ -299,21 +616,6 @@ function mergeAuditResults(results) {
|
|
|
299
616
|
}
|
|
300
617
|
return base;
|
|
301
618
|
}
|
|
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
619
|
function parseOutdatedData(data, unknownNames) {
|
|
318
620
|
const entries = [];
|
|
319
621
|
if (!data || typeof data !== "object")
|
|
@@ -513,12 +815,16 @@ function mergeImportGraphs(rootPath, packageMetas, graphs) {
|
|
|
513
815
|
}
|
|
514
816
|
return { files, packages, packageCounts, unresolvedImports };
|
|
515
817
|
}
|
|
516
|
-
function buildWorkspaceUsageMap(packageMetas, dependencyGraphs) {
|
|
818
|
+
function buildWorkspaceUsageMap(packageMetas, dependencyGraphs, workspacePackageNames, localDependencyNames) {
|
|
517
819
|
var _a, _b, _c, _d;
|
|
518
820
|
const usage = new Map();
|
|
519
821
|
const add = (depName, pkgName) => {
|
|
520
822
|
if (!depName)
|
|
521
823
|
return;
|
|
824
|
+
if (workspacePackageNames.has(depName))
|
|
825
|
+
return;
|
|
826
|
+
if (localDependencyNames.has(depName))
|
|
827
|
+
return;
|
|
522
828
|
if (!usage.has(depName))
|
|
523
829
|
usage.set(depName, new Set());
|
|
524
830
|
usage.get(depName).add(pkgName);
|
|
@@ -687,6 +993,14 @@ function openInBrowser(filePath) {
|
|
|
687
993
|
});
|
|
688
994
|
child.unref();
|
|
689
995
|
}
|
|
996
|
+
/**
|
|
997
|
+
* Orchestrates the CLI "scan" command to collect, merge, and output dependency data for a project or workspace.
|
|
998
|
+
*
|
|
999
|
+
* Detects workspace type and package manager, runs per-package collectors (audit, dependency tree, import graph, outdated),
|
|
1000
|
+
* merges collected signals into a workspace-level model, and writes a JSON or HTML report to the configured output path.
|
|
1001
|
+
* Manages a temporary working directory (created under the project as .dependency-radar), respects CLI options such as
|
|
1002
|
+
* JSON output, audit/outdated toggles, keeping the temp directory, and optionally opening the generated output with the
|
|
1003
|
+
* system default application. Exits the process with a non-zero code on fatal errors. */
|
|
690
1004
|
async function run() {
|
|
691
1005
|
var _a;
|
|
692
1006
|
const opts = parseArgs(process.argv.slice(2));
|
|
@@ -717,15 +1031,32 @@ async function run() {
|
|
|
717
1031
|
// ignore, best-effort path normalization
|
|
718
1032
|
}
|
|
719
1033
|
const tempDir = path_1.default.join(projectPath, ".dependency-radar");
|
|
720
|
-
//
|
|
1034
|
+
// Stage 1: detect workspace/package-manager context and collect tool versions.
|
|
721
1035
|
const workspace = await detectWorkspace(projectPath);
|
|
1036
|
+
const yarnPnP = await detectYarnPnP(projectPath);
|
|
722
1037
|
if (workspace.type === "yarn" && workspace.packagePaths.length === 0) {
|
|
723
1038
|
console.error("Yarn Plug'n'Play (nodeLinker: pnp) detected. This is not supported yet.");
|
|
724
1039
|
console.error("Switch to nodeLinker: node-modules or run in a non-PnP environment.");
|
|
725
1040
|
process.exit(1);
|
|
726
1041
|
return;
|
|
727
1042
|
}
|
|
1043
|
+
const hasProjectNodeModules = await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules"));
|
|
1044
|
+
if (!hasProjectNodeModules) {
|
|
1045
|
+
const workspaceHint = workspace.type === "none"
|
|
1046
|
+
? "single project"
|
|
1047
|
+
: `${workspace.type.toUpperCase()} workspace`;
|
|
1048
|
+
const yarnHint = yarnPnP
|
|
1049
|
+
? " Yarn Plug'n'Play appears enabled; Dependency Radar currently requires node_modules linker."
|
|
1050
|
+
: "";
|
|
1051
|
+
console.warn(`⚠ node_modules was not found at ${projectPath}. Scan completeness may be reduced for this ${workspaceHint}. Run your package manager install (npm install, pnpm install, or yarn install) before scanning.${yarnHint}`);
|
|
1052
|
+
}
|
|
728
1053
|
const rootPkg = await readJsonFile(path_1.default.join(projectPath, "package.json"));
|
|
1054
|
+
const projectDependencyPolicy = workspace.pnpmWorkspaceOverrides
|
|
1055
|
+
? {
|
|
1056
|
+
overrides: workspace.pnpmWorkspaceOverrides,
|
|
1057
|
+
sources: ["pnpm-workspace.yaml#overrides"],
|
|
1058
|
+
}
|
|
1059
|
+
: undefined;
|
|
729
1060
|
const packageManager = await detectPackageManager(projectPath, rootPkg, workspace.type);
|
|
730
1061
|
const scanManager = await detectScanManager(projectPath, packageManager);
|
|
731
1062
|
const packageManagerField = typeof (rootPkg === null || rootPkg === void 0 ? void 0 : rootPkg.packageManager) === "string"
|
|
@@ -746,17 +1077,11 @@ async function run() {
|
|
|
746
1077
|
: scanManager === "pnpm"
|
|
747
1078
|
? pnpmVersion
|
|
748
1079
|
: yarnVersion;
|
|
749
|
-
if (packageManager === "yarn") {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
console.error("Yarn Plug'n'Play (nodeLinker: pnp) detected. This is not supported yet.");
|
|
755
|
-
console.error("Switch to nodeLinker: node-modules or run in a non-PnP environment.");
|
|
756
|
-
process.exit(1);
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
1080
|
+
if (packageManager === "yarn" && yarnPnP) {
|
|
1081
|
+
console.error("Yarn Plug'n'Play (nodeLinker: pnp) detected. This is not supported yet.");
|
|
1082
|
+
console.error("Switch to nodeLinker: node-modules or run in a non-PnP environment.");
|
|
1083
|
+
process.exit(1);
|
|
1084
|
+
return;
|
|
760
1085
|
}
|
|
761
1086
|
const packagePaths = workspace.packagePaths;
|
|
762
1087
|
const workspaceLabel = workspace.type === "none"
|
|
@@ -769,8 +1094,9 @@ async function run() {
|
|
|
769
1094
|
const spinner = startSpinner(`Scanning ${workspaceLabel} at ${projectPath}`);
|
|
770
1095
|
try {
|
|
771
1096
|
await (0, utils_1.ensureDir)(tempDir);
|
|
772
|
-
//
|
|
1097
|
+
// Stage 2: run per-package collectors and persist raw tool outputs.
|
|
773
1098
|
const packageMetas = await readWorkspacePackageMeta(projectPath, packagePaths);
|
|
1099
|
+
const workspaceClassification = buildWorkspaceClassification(projectPath, packageMetas);
|
|
774
1100
|
const perPackageAudit = [];
|
|
775
1101
|
const perPackageLs = [];
|
|
776
1102
|
const perPackageImportGraph = [];
|
|
@@ -785,6 +1111,7 @@ async function run() {
|
|
|
785
1111
|
: Promise.resolve(undefined),
|
|
786
1112
|
(0, npmLs_1.runNpmLs)(meta.path, pkgTempDir, scanManager, {
|
|
787
1113
|
contextLabel: meta.name,
|
|
1114
|
+
lockfileSearchRoot: projectPath,
|
|
788
1115
|
onProgress: (line) => spinner.log(line),
|
|
789
1116
|
}).catch((err) => ({ ok: false, error: String(err) })),
|
|
790
1117
|
(0, importGraphRunner_1.runImportGraph)(meta.path, pkgTempDir).catch((err) => ({ ok: false, error: String(err) })),
|
|
@@ -797,6 +1124,7 @@ async function run() {
|
|
|
797
1124
|
perPackageImportGraph.push(ig);
|
|
798
1125
|
perPackageOutdated.push({ attempted: Boolean(opts.outdated), result: o });
|
|
799
1126
|
}
|
|
1127
|
+
// Stage 3: merge per-package results into a workspace-level view.
|
|
800
1128
|
if (opts.audit) {
|
|
801
1129
|
const auditOk = perPackageAudit.every((r) => r && r.ok);
|
|
802
1130
|
if (auditOk) {
|
|
@@ -822,7 +1150,7 @@ async function run() {
|
|
|
822
1150
|
: undefined
|
|
823
1151
|
: buildCombinedDependencyGraph(projectPath, packageMetas, perPackageLs.map((r) => (r && r.ok ? r.data : undefined)));
|
|
824
1152
|
const mergedImportGraphData = mergeImportGraphs(projectPath, packageMetas, perPackageImportGraph.map((r) => (r && r.ok ? r.data : undefined)));
|
|
825
|
-
const workspaceUsage = buildWorkspaceUsageMap(packageMetas, perPackageLs.map((r) => (r && r.ok ? r.data : undefined)));
|
|
1153
|
+
const workspaceUsage = buildWorkspaceUsageMap(packageMetas, perPackageLs.map((r) => (r && r.ok ? r.data : undefined)), workspaceClassification.workspacePackageNames, workspaceClassification.localDependencyNames);
|
|
826
1154
|
const outdatedResult = mergeOutdatedResults(perPackageOutdated);
|
|
827
1155
|
const auditResult = mergedAuditData
|
|
828
1156
|
? { ok: true, data: mergedAuditData }
|
|
@@ -830,7 +1158,7 @@ async function run() {
|
|
|
830
1158
|
const npmLsResult = { ok: true, data: mergedGraphData };
|
|
831
1159
|
const importGraphResult = { ok: true, data: mergedImportGraphData };
|
|
832
1160
|
// Build a merged package.json view for aggregator direct-dep checks.
|
|
833
|
-
const mergedPkgForAggregator = mergeDepsFromWorkspace(packageMetas);
|
|
1161
|
+
const mergedPkgForAggregator = mergeDepsFromWorkspace(packageMetas, workspaceClassification.workspacePackageNames, workspaceClassification.localDependencyNames);
|
|
834
1162
|
const auditFailure = opts.audit
|
|
835
1163
|
? perPackageAudit.find((r) => r && !r.ok)
|
|
836
1164
|
: undefined;
|
|
@@ -849,6 +1177,7 @@ async function run() {
|
|
|
849
1177
|
if (importFailures.length > 0) {
|
|
850
1178
|
spinner.log(`Import graph warning: ${importFailures.length} package${importFailures.length === 1 ? "" : "s"} failed (${importFailures[0].error || "import graph failed"})`);
|
|
851
1179
|
}
|
|
1180
|
+
// Stage 4: aggregate all signals into the final report model.
|
|
852
1181
|
const aggregated = await (0, aggregator_1.aggregateData)({
|
|
853
1182
|
projectPath,
|
|
854
1183
|
auditResult,
|
|
@@ -856,6 +1185,8 @@ async function run() {
|
|
|
856
1185
|
importGraphResult,
|
|
857
1186
|
outdatedResult,
|
|
858
1187
|
pkgOverride: mergedPkgForAggregator,
|
|
1188
|
+
projectPackageJson: rootPkg,
|
|
1189
|
+
...(projectDependencyPolicy ? { projectDependencyPolicy } : {}),
|
|
859
1190
|
workspaceUsage,
|
|
860
1191
|
resolvePaths: [
|
|
861
1192
|
projectPath,
|
|
@@ -864,6 +1195,15 @@ async function run() {
|
|
|
864
1195
|
workspaceEnabled: workspace.type !== "none",
|
|
865
1196
|
workspaceType: workspace.type,
|
|
866
1197
|
workspacePackageCount: packagePaths.length,
|
|
1198
|
+
...(workspace.type !== "none"
|
|
1199
|
+
? {
|
|
1200
|
+
workspacePackages: workspaceClassification.workspacePackages,
|
|
1201
|
+
workspacePackageNames: workspaceClassification.workspacePackageNames,
|
|
1202
|
+
workspacePackageIds: workspaceClassification.workspacePackageIds,
|
|
1203
|
+
workspacePackagePaths: workspaceClassification.workspacePackagePaths,
|
|
1204
|
+
workspaceLocalDependencyNames: workspaceClassification.localDependencyNames,
|
|
1205
|
+
}
|
|
1206
|
+
: {}),
|
|
867
1207
|
packageManager: scanManager,
|
|
868
1208
|
packageManagerVersion,
|
|
869
1209
|
packageManagerField,
|
package/dist/cta.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CTA_BASE_URL = void 0;
|
|
4
|
+
exports.buildCtaUrl = buildCtaUrl;
|
|
5
|
+
exports.CTA_BASE_URL = 'https://dependency-radar.com/next-steps?source=standalone-report';
|
|
6
|
+
/**
|
|
7
|
+
* Builds a CTA URL that includes the CLI version as a `cli` query parameter.
|
|
8
|
+
*
|
|
9
|
+
* @param version - CLI version string to include; when `undefined`, empty, or whitespace-only, `unknown` is used
|
|
10
|
+
* @returns The CTA URL with an appended `cli` query parameter whose value is the URI-encoded CLI version (or `unknown` when version is missing or empty)
|
|
11
|
+
*/
|
|
12
|
+
function buildCtaUrl(version) {
|
|
13
|
+
const normalizedVersion = typeof version === 'string' && version.trim().length > 0
|
|
14
|
+
? version.trim()
|
|
15
|
+
: 'unknown';
|
|
16
|
+
return `${exports.CTA_BASE_URL}&cli=${encodeURIComponent(normalizedVersion)}`;
|
|
17
|
+
}
|