dependency-radar 0.6.1 → 0.8.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 +189 -25
- package/dist/aggregator.js +35 -9
- package/dist/cli.js +501 -103
- package/dist/compare.js +79 -0
- package/dist/explain.js +193 -0
- package/dist/failOn.js +16 -2
- package/dist/findings.js +166 -0
- package/dist/generated/spdx.js +3 -0
- package/dist/nodeEngine.js +181 -0
- package/dist/outputFormats.js +185 -0
- package/dist/report-assets.js +2 -2
- package/dist/report.js +138 -72
- package/dist/runners/importGraphRunner.js +9 -5
- package/dist/runners/lockfileGraph.js +144 -1
- package/dist/runners/lockfileSignals.js +303 -0
- package/dist/runners/npmLs.js +15 -0
- package/dist/schema.js +107 -0
- package/dist/utils.js +62 -3
- package/dist/why.js +69 -0
- package/dist/workspaceFilter.js +25 -0
- package/package.json +8 -5
- package/dist/runners/depcheckRunner.js +0 -23
- package/dist/runners/licenseChecker.js +0 -33
- package/dist/runners/madgeRunner.js +0 -29
package/dist/report.js
CHANGED
|
@@ -38,12 +38,12 @@ async function renderReport(data, outputPath) {
|
|
|
38
38
|
await promises_1.default.writeFile(outputPath, html, 'utf8');
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
41
|
+
* Generate a complete standalone HTML document for the Dependency Radar report using the provided aggregated data.
|
|
42
42
|
*
|
|
43
|
-
* The returned document embeds sanitized CSS and JS
|
|
43
|
+
* The returned document embeds sanitized inline CSS and JS, includes the JSON-serialized `data` (with `<` escaped), computes a CTA URL from `data.dependencyRadarVersion`, and formats `data.generatedAt` into a human-friendly timestamp when parsable. Interpolated values (for example `project.projectDir`, the formatted date, and the CTA URL) are HTML-escaped for safe embedding.
|
|
44
44
|
*
|
|
45
|
-
* @param data -
|
|
46
|
-
* @returns The full HTML document
|
|
45
|
+
* @param data - AggregatedData used to populate the report; must include at least `project.projectDir`, `generatedAt`, and `dependencyRadarVersion`
|
|
46
|
+
* @returns The full HTML document as a string
|
|
47
47
|
*/
|
|
48
48
|
function buildHtml(data) {
|
|
49
49
|
const json = JSON.stringify(data).replace(/</g, '\\u003c');
|
|
@@ -170,20 +170,49 @@ ${safeCssContent}
|
|
|
170
170
|
<h1>Dependency Radar</h1>
|
|
171
171
|
<div class="header-meta">
|
|
172
172
|
<span class="meta-item"><span class="meta-label">Project</span> <strong id="project-path">${escapeHtml(data.project.projectDir)}</strong></span>
|
|
173
|
-
<span class="meta-item" id="git-branch-item" style="display: none;"><span class="meta-label">Branch</span> <strong id="git-branch"></strong></span>
|
|
174
|
-
<span class="meta-item" id="node-item" style="display: none;"><span class="meta-label">Node</span> <strong id="node-version"></strong></span>
|
|
175
173
|
<span class="meta-item"><span class="meta-label">Generated</span> <strong id="formatted-date">${escapeHtml(formattedDate)}</strong></span>
|
|
176
|
-
<span class="header-disclaimer" id="node-disclaimer" style="display: none;"></span>
|
|
177
174
|
</div>
|
|
178
175
|
</div>
|
|
179
176
|
</div>
|
|
180
177
|
<div class="cta-section">
|
|
181
|
-
<a href="${escapeHtml(ctaUrl)}" class="cta-
|
|
182
|
-
|
|
183
|
-
|
|
178
|
+
<a href="${escapeHtml(ctaUrl)}" class="cta-card" target="_blank" rel="noopener" id="cta-primary-link">
|
|
179
|
+
<span class="cta-icon" aria-hidden="true">
|
|
180
|
+
<svg
|
|
181
|
+
class="cta-icon-svg lucide lucide-radar-icon lucide-radar"
|
|
182
|
+
width="24"
|
|
183
|
+
height="24"
|
|
184
|
+
viewBox="0 0 24 24"
|
|
185
|
+
fill="none"
|
|
186
|
+
stroke="currentColor"
|
|
187
|
+
stroke-width="2"
|
|
188
|
+
stroke-linecap="round"
|
|
189
|
+
stroke-linejoin="round"
|
|
190
|
+
>
|
|
191
|
+
<path d="M19.07 4.93A10 10 0 0 0 6.99 3.34" />
|
|
192
|
+
<path d="M4 6h.01" />
|
|
193
|
+
<path d="M2.29 9.62A10 10 0 1 0 21.31 8.35" />
|
|
194
|
+
<path d="M16.24 7.76A6 6 0 1 0 8.23 16.67" />
|
|
195
|
+
<path d="M12 18h.01" />
|
|
196
|
+
<path d="M17.99 11.66A6 6 0 0 1 15.77 16.67" />
|
|
197
|
+
<circle cx="12" cy="12" r="2" />
|
|
198
|
+
<path d="m13.41 10.59 5.66-5.66" />
|
|
199
|
+
</svg>
|
|
200
|
+
</span>
|
|
201
|
+
<span class="cta-copy">
|
|
202
|
+
<span class="cta-title">Unlock deeper analysis</span>
|
|
203
|
+
<span class="cta-text">Deep analysis, risk modelling, and actionable insights.</span>
|
|
204
|
+
</span>
|
|
205
|
+
<span class="cta-action">
|
|
206
|
+
<span class="cta-button">Upgrade this scan <span class="cta-arrow">→</span></span>
|
|
207
|
+
<span class="cta-privacy">
|
|
208
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
209
|
+
<path d="M12 3 5 6v5c0 4.5 2.9 8.5 7 10 4.1-1.5 7-5.5 7-10V6l-7-3Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
|
210
|
+
<path d="m9 12 2 2 4-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
211
|
+
</svg>
|
|
212
|
+
Secure. Private. No code sent.
|
|
213
|
+
</span>
|
|
214
|
+
</span>
|
|
184
215
|
</a>
|
|
185
|
-
<p class="cta-text">Beyond the standalone report</p>
|
|
186
|
-
<a href="${escapeHtml(ctaUrl)}" target="_blank" rel="noopener" class="cta-url" id="cta-secondary-link">dependency-radar.com</a>
|
|
187
216
|
</div>
|
|
188
217
|
</div>
|
|
189
218
|
</header>
|
|
@@ -197,8 +226,29 @@ ${safeCssContent}
|
|
|
197
226
|
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
198
227
|
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
|
199
228
|
</svg>
|
|
200
|
-
<input type="search" id="search" placeholder="Search packages..." />
|
|
229
|
+
<input type="search" id="search" placeholder="Search packages..." aria-label="Search packages" />
|
|
201
230
|
</div>
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
class="filters-toggle"
|
|
234
|
+
id="filters-toggle"
|
|
235
|
+
aria-expanded="false"
|
|
236
|
+
aria-controls="filter-controls"
|
|
237
|
+
>
|
|
238
|
+
Filters
|
|
239
|
+
<span class="filter-count-badge" id="filter-count-badge" hidden></span>
|
|
240
|
+
<span class="chevron">▼</span>
|
|
241
|
+
</button>
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
class="filters-toggle metadata-toggle"
|
|
245
|
+
id="metadata-toggle"
|
|
246
|
+
aria-expanded="false"
|
|
247
|
+
aria-controls="metadata-panel"
|
|
248
|
+
>
|
|
249
|
+
Metadata
|
|
250
|
+
<span class="chevron">▼</span>
|
|
251
|
+
</button>
|
|
202
252
|
<div class="view-switch" id="view-switch">
|
|
203
253
|
<button
|
|
204
254
|
type="button"
|
|
@@ -209,21 +259,24 @@ ${safeCssContent}
|
|
|
209
259
|
Graph View
|
|
210
260
|
</button>
|
|
211
261
|
</div>
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
262
|
+
<div class="theme-toggle">
|
|
263
|
+
<span class="theme-toggle-label">Theme</span>
|
|
264
|
+
<button
|
|
265
|
+
type="button"
|
|
266
|
+
class="theme-switch"
|
|
267
|
+
id="theme-switch"
|
|
268
|
+
aria-label="Toggle dark/light mode"
|
|
269
|
+
aria-pressed="false"
|
|
270
|
+
></button>
|
|
271
|
+
</div>
|
|
221
272
|
</div>
|
|
222
273
|
|
|
223
|
-
<div class="filter-controls" id="filter-controls">
|
|
274
|
+
<div class="filter-controls" id="filter-controls" role="dialog" aria-label="Dependency filters" aria-hidden="true" inert>
|
|
224
275
|
<div class="filter-controls-row">
|
|
276
|
+
<div class="filter-panel-section">
|
|
277
|
+
<div class="filter-panel-title">Dependency</div>
|
|
225
278
|
<div class="filter-group">
|
|
226
|
-
<
|
|
279
|
+
<label class="filter-label" for="direct-filter">Type</label>
|
|
227
280
|
<select id="direct-filter">
|
|
228
281
|
<option value="all">All</option>
|
|
229
282
|
<option value="direct">Dependency</option>
|
|
@@ -232,7 +285,7 @@ ${safeCssContent}
|
|
|
232
285
|
</div>
|
|
233
286
|
|
|
234
287
|
<div class="filter-group">
|
|
235
|
-
<
|
|
288
|
+
<label class="filter-label" for="runtime-filter">Scope</label>
|
|
236
289
|
<select id="runtime-filter">
|
|
237
290
|
<option value="all">All</option>
|
|
238
291
|
<option value="runtime">Runtime</option>
|
|
@@ -241,16 +294,59 @@ ${safeCssContent}
|
|
|
241
294
|
<option value="peer">Peer</option>
|
|
242
295
|
</select>
|
|
243
296
|
</div>
|
|
297
|
+
</div>
|
|
244
298
|
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
299
|
+
<div class="filter-panel-section filter-panel-context">
|
|
300
|
+
<div class="filter-panel-title">Context</div>
|
|
301
|
+
<div class="filter-group workspace-filter-group hidden" id="workspace-filter-wrap">
|
|
302
|
+
<label class="filter-label" for="workspace-filter">Workspace</label>
|
|
303
|
+
<select id="workspace-filter">
|
|
304
|
+
<option value="all">All workspaces</option>
|
|
305
|
+
</select>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div class="filter-panel-section">
|
|
310
|
+
<div class="filter-panel-title">Risk</div>
|
|
311
|
+
<div class="license-filter-panel" id="license-panel">
|
|
312
|
+
<div class="license-filter-inner">
|
|
313
|
+
<div class="license-filter-header">
|
|
314
|
+
<span class="license-filter-title">License categories</span>
|
|
315
|
+
<div class="license-quick-actions">
|
|
316
|
+
<button type="button" class="quick-action-btn" id="license-all">Show All</button>
|
|
317
|
+
<button type="button" class="quick-action-btn" id="license-friendly">Business-Friendly Only</button>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="license-groups">
|
|
321
|
+
<label class="license-group-checkbox">
|
|
322
|
+
<input type="checkbox" id="license-permissive" checked />
|
|
323
|
+
<span class="license-dot permissive"></span>
|
|
324
|
+
<span id="license-permissive-label">Permissive</span>
|
|
325
|
+
</label>
|
|
326
|
+
<label class="license-group-checkbox">
|
|
327
|
+
<input type="checkbox" id="license-weak-copyleft" checked />
|
|
328
|
+
<span class="license-dot weak-copyleft"></span>
|
|
329
|
+
<span id="license-weak-copyleft-label">Weak Copyleft</span>
|
|
330
|
+
</label>
|
|
331
|
+
<label class="license-group-checkbox">
|
|
332
|
+
<input type="checkbox" id="license-strong-copyleft" checked />
|
|
333
|
+
<span class="license-dot strong-copyleft"></span>
|
|
334
|
+
<span id="license-strong-copyleft-label">Strong Copyleft</span>
|
|
335
|
+
</label>
|
|
336
|
+
<label class="license-group-checkbox">
|
|
337
|
+
<input type="checkbox" id="license-unknown" checked />
|
|
338
|
+
<span class="license-dot unknown"></span>
|
|
339
|
+
<span id="license-unknown-label">Other / Unknown</span>
|
|
340
|
+
</label>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
249
344
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
345
|
+
<label class="checkbox-filter">
|
|
346
|
+
<input type="checkbox" id="has-vulns" />
|
|
347
|
+
<span id="has-vulns-label">Has vulnerabilities</span>
|
|
348
|
+
</label>
|
|
349
|
+
</div>
|
|
254
350
|
|
|
255
351
|
<!-- Sort dropdown - visible on mobile, hidden on desktop (replaced by column headers) -->
|
|
256
352
|
<div class="filter-group sort-wrapper mobile-only" id="mobile-sort">
|
|
@@ -267,48 +363,18 @@ ${safeCssContent}
|
|
|
267
363
|
<button type="button" class="sort-direction-btn" id="sort-direction" title="Toggle sort direction">↑</button>
|
|
268
364
|
</div>
|
|
269
365
|
|
|
270
|
-
<div class="
|
|
271
|
-
<
|
|
272
|
-
<div class="theme-switch" id="theme-switch" title="Toggle dark/light mode"></div>
|
|
366
|
+
<div class="filter-panel-actions">
|
|
367
|
+
<button type="button" class="quick-action-btn" id="clear-all-filters">Clear all filters</button>
|
|
273
368
|
</div>
|
|
274
369
|
</div>
|
|
275
370
|
</div>
|
|
276
371
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
<div class="license-quick-actions">
|
|
284
|
-
<button type="button" class="quick-action-btn" id="license-all">Show All</button>
|
|
285
|
-
<button type="button" class="quick-action-btn" id="license-friendly">Business-Friendly Only</button>
|
|
286
|
-
</div>
|
|
287
|
-
</div>
|
|
288
|
-
<div class="license-groups">
|
|
289
|
-
<label class="license-group-checkbox">
|
|
290
|
-
<input type="checkbox" id="license-permissive" checked />
|
|
291
|
-
<span class="license-dot permissive"></span>
|
|
292
|
-
Permissive (MIT, BSD, Apache, ISC)
|
|
293
|
-
</label>
|
|
294
|
-
<label class="license-group-checkbox">
|
|
295
|
-
<input type="checkbox" id="license-weak-copyleft" checked />
|
|
296
|
-
<span class="license-dot weak-copyleft"></span>
|
|
297
|
-
Weak Copyleft (LGPL, MPL, EPL)
|
|
298
|
-
</label>
|
|
299
|
-
<label class="license-group-checkbox">
|
|
300
|
-
<input type="checkbox" id="license-strong-copyleft" checked />
|
|
301
|
-
<span class="license-dot strong-copyleft"></span>
|
|
302
|
-
Strong Copyleft (GPL, AGPL)
|
|
303
|
-
</label>
|
|
304
|
-
<label class="license-group-checkbox">
|
|
305
|
-
<input type="checkbox" id="license-unknown" checked />
|
|
306
|
-
<span class="license-dot unknown"></span>
|
|
307
|
-
Other / Unknown
|
|
308
|
-
</label>
|
|
309
|
-
</div>
|
|
310
|
-
</div>
|
|
311
|
-
</div>
|
|
372
|
+
<div class="metadata-panel" id="metadata-panel" role="dialog" aria-label="Report metadata" hidden></div>
|
|
373
|
+
|
|
374
|
+
<div class="active-filters-row" id="active-filters-row" hidden>
|
|
375
|
+
<span class="active-filters-label">Active filters:</span>
|
|
376
|
+
<div class="active-filter-chips" id="active-filter-chips"></div>
|
|
377
|
+
<button type="button" class="active-filter-clear" id="active-filter-clear">Clear all</button>
|
|
312
378
|
</div>
|
|
313
379
|
|
|
314
380
|
<!-- Results summary and column headers row -->
|
|
@@ -354,7 +420,7 @@ ${safeCssContent}
|
|
|
354
420
|
</span>
|
|
355
421
|
</div>
|
|
356
422
|
</div>
|
|
357
|
-
<div class="graph-
|
|
423
|
+
<div class="graph-workspace-wrap" id="graph-workspace-wrap">
|
|
358
424
|
<label class="graph-workspace-label" for="graph-workspace">Workspace</label>
|
|
359
425
|
<select id="graph-workspace" class="graph-workspace-select"></select>
|
|
360
426
|
</div>
|
|
@@ -8,7 +8,14 @@ const path_1 = __importDefault(require("path"));
|
|
|
8
8
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
9
|
const module_1 = require("module");
|
|
10
10
|
const utils_1 = require("../utils");
|
|
11
|
-
const IGNORED_DIRS = new Set([
|
|
11
|
+
const IGNORED_DIRS = new Set([
|
|
12
|
+
'node_modules',
|
|
13
|
+
'dist',
|
|
14
|
+
'build',
|
|
15
|
+
'coverage',
|
|
16
|
+
'storybook-static',
|
|
17
|
+
'.dependency-radar'
|
|
18
|
+
]);
|
|
12
19
|
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
13
20
|
/**
|
|
14
21
|
* Builds an import graph for a project and optionally writes it to disk.
|
|
@@ -28,10 +35,7 @@ async function runImportGraph(projectPath, tempDir, options = {}) {
|
|
|
28
35
|
const persistToDisk = options.persistToDisk !== false;
|
|
29
36
|
const targetFile = path_1.default.join(tempDir, 'import-graph.json');
|
|
30
37
|
try {
|
|
31
|
-
const
|
|
32
|
-
const hasSrc = await (0, utils_1.pathExists)(srcPath);
|
|
33
|
-
const entry = hasSrc ? srcPath : projectPath;
|
|
34
|
-
const files = await collectSourceFiles(entry);
|
|
38
|
+
const files = await collectSourceFiles(projectPath);
|
|
35
39
|
const fileGraph = {};
|
|
36
40
|
const packageGraph = {};
|
|
37
41
|
const packageCounts = {};
|
|
@@ -32,9 +32,12 @@ async function tryBuildDependencyTreeFromLockfile(projectPath, tool, lockfileSea
|
|
|
32
32
|
else if (tool === 'npm') {
|
|
33
33
|
result = parseNpmTree(projectPath, searchRoot);
|
|
34
34
|
}
|
|
35
|
-
else {
|
|
35
|
+
else if (tool === 'yarn') {
|
|
36
36
|
result = parseYarnTree(projectPath, searchRoot);
|
|
37
37
|
}
|
|
38
|
+
else {
|
|
39
|
+
result = parseBunTree(projectPath, searchRoot);
|
|
40
|
+
}
|
|
38
41
|
treeCache.set(cacheKey, result || null);
|
|
39
42
|
return result;
|
|
40
43
|
}
|
|
@@ -43,6 +46,146 @@ async function tryBuildDependencyTreeFromLockfile(projectPath, tool, lockfileSea
|
|
|
43
46
|
return undefined;
|
|
44
47
|
}
|
|
45
48
|
}
|
|
49
|
+
function stripJsonComments(raw) {
|
|
50
|
+
let out = '';
|
|
51
|
+
let quote;
|
|
52
|
+
let escaped = false;
|
|
53
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
54
|
+
const ch = raw[i];
|
|
55
|
+
const next = raw[i + 1];
|
|
56
|
+
if (quote) {
|
|
57
|
+
out += ch;
|
|
58
|
+
if (escaped)
|
|
59
|
+
escaped = false;
|
|
60
|
+
else if (ch === '\\')
|
|
61
|
+
escaped = true;
|
|
62
|
+
else if (ch === quote)
|
|
63
|
+
quote = undefined;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (ch === '"' || ch === "'") {
|
|
67
|
+
quote = ch;
|
|
68
|
+
out += ch;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (ch === '/' && next === '/') {
|
|
72
|
+
while (i < raw.length && raw[i] !== '\n')
|
|
73
|
+
i += 1;
|
|
74
|
+
out += '\n';
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (ch === '/' && next === '*') {
|
|
78
|
+
i += 2;
|
|
79
|
+
while (i < raw.length && !(raw[i] === '*' && raw[i + 1] === '/'))
|
|
80
|
+
i += 1;
|
|
81
|
+
i += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
out += ch;
|
|
85
|
+
}
|
|
86
|
+
return out.replace(/,\s*([}\]])/g, '$1');
|
|
87
|
+
}
|
|
88
|
+
function parseBunTree(projectPath, searchRoot) {
|
|
89
|
+
const lockPath = findUpwards(projectPath, ['bun.lock'], searchRoot);
|
|
90
|
+
if (!lockPath)
|
|
91
|
+
return undefined;
|
|
92
|
+
const raw = readCachedText(lockPath);
|
|
93
|
+
if (!raw)
|
|
94
|
+
return undefined;
|
|
95
|
+
const packageJsonPath = path_1.default.join(projectPath, 'package.json');
|
|
96
|
+
if (!safePathExists(packageJsonPath))
|
|
97
|
+
return undefined;
|
|
98
|
+
const packageJson = readJsonSafe(packageJsonPath);
|
|
99
|
+
if (!packageJson || typeof packageJson !== 'object')
|
|
100
|
+
return undefined;
|
|
101
|
+
const parsed = readBunJsonLock(raw);
|
|
102
|
+
if (parsed) {
|
|
103
|
+
return { sourceFile: lockPath, data: buildBunJsonResolvedTree(parsed, packageJson) };
|
|
104
|
+
}
|
|
105
|
+
if (raw.trim().startsWith('{'))
|
|
106
|
+
return undefined;
|
|
107
|
+
const yarnLike = parseYarnV1(raw) || parseYarnV2(raw);
|
|
108
|
+
if (!yarnLike)
|
|
109
|
+
return undefined;
|
|
110
|
+
return { sourceFile: lockPath, data: buildYarnResolvedTree(yarnLike, packageJson) };
|
|
111
|
+
}
|
|
112
|
+
function readBunJsonLock(raw) {
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(stripJsonComments(raw));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function buildBunJsonResolvedTree(lock, packageJson) {
|
|
121
|
+
const packages = (lock === null || lock === void 0 ? void 0 : lock.packages) && typeof lock.packages === 'object' ? lock.packages : {};
|
|
122
|
+
const rootDeps = collectPackageJsonDependencySpecs(packageJson);
|
|
123
|
+
const dependencies = {};
|
|
124
|
+
const memo = new Map();
|
|
125
|
+
const stack = new Set();
|
|
126
|
+
for (const [depName, depSpec] of Object.entries(rootDeps)) {
|
|
127
|
+
const node = buildBunJsonNode(depName, depSpec, packages, memo, stack);
|
|
128
|
+
if (node)
|
|
129
|
+
dependencies[node.name] = node;
|
|
130
|
+
}
|
|
131
|
+
return { dependencies };
|
|
132
|
+
}
|
|
133
|
+
function normalizeBunPackageEntry(rawEntry) {
|
|
134
|
+
const entry = Array.isArray(rawEntry)
|
|
135
|
+
? rawEntry.find((item) => item && typeof item === 'object' && !Array.isArray(item))
|
|
136
|
+
: rawEntry;
|
|
137
|
+
if (!entry || typeof entry !== 'object')
|
|
138
|
+
return undefined;
|
|
139
|
+
const version = typeof entry.version === 'string'
|
|
140
|
+
? entry.version
|
|
141
|
+
: typeof entry.resolution === 'string'
|
|
142
|
+
? extractVersionFromBunResolution(entry.resolution)
|
|
143
|
+
: undefined;
|
|
144
|
+
return {
|
|
145
|
+
...(version ? { version } : {}),
|
|
146
|
+
...(entry.dependencies && typeof entry.dependencies === 'object' ? { dependencies: entry.dependencies } : {}),
|
|
147
|
+
...(entry.optionalDependencies && typeof entry.optionalDependencies === 'object' ? { optionalDependencies: entry.optionalDependencies } : {})
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function extractVersionFromBunResolution(value) {
|
|
151
|
+
const match = value.match(/@npm:([^#\s]+)/);
|
|
152
|
+
return match ? match[1] : undefined;
|
|
153
|
+
}
|
|
154
|
+
function findBunPackageEntry(name, spec, packages) {
|
|
155
|
+
const exactKey = `${name}@${spec}`;
|
|
156
|
+
if (packages[exactKey])
|
|
157
|
+
return packages[exactKey];
|
|
158
|
+
if (packages[name])
|
|
159
|
+
return packages[name];
|
|
160
|
+
const prefix = `${name}@`;
|
|
161
|
+
const matches = Object.keys(packages).filter((candidate) => candidate.startsWith(prefix));
|
|
162
|
+
return matches.length === 1 ? packages[matches[0]] : undefined;
|
|
163
|
+
}
|
|
164
|
+
function buildBunJsonNode(name, spec, packages, memo, stack) {
|
|
165
|
+
const memoKey = `${name}@${spec}`;
|
|
166
|
+
if (memo.has(memoKey))
|
|
167
|
+
return memo.get(memoKey);
|
|
168
|
+
if (stack.has(memoKey))
|
|
169
|
+
return undefined;
|
|
170
|
+
const entry = normalizeBunPackageEntry(findBunPackageEntry(name, spec, packages));
|
|
171
|
+
if (!(entry === null || entry === void 0 ? void 0 : entry.version))
|
|
172
|
+
return undefined;
|
|
173
|
+
const out = { name, version: entry.version, dependencies: {} };
|
|
174
|
+
stack.add(memoKey);
|
|
175
|
+
const childDeps = mergeStringRecord(entry.dependencies, entry.optionalDependencies);
|
|
176
|
+
for (const childName of Object.keys(childDeps)) {
|
|
177
|
+
if (isWorkspaceLikeSpecifier(childDeps[childName]))
|
|
178
|
+
continue;
|
|
179
|
+
const child = buildBunJsonNode(childName, childDeps[childName], packages, memo, stack);
|
|
180
|
+
if (child)
|
|
181
|
+
out.dependencies[child.name] = child;
|
|
182
|
+
}
|
|
183
|
+
stack.delete(memoKey);
|
|
184
|
+
if (out.dependencies && Object.keys(out.dependencies).length === 0)
|
|
185
|
+
delete out.dependencies;
|
|
186
|
+
memo.set(memoKey, out);
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
46
189
|
/**
|
|
47
190
|
* Builds a dependency tree from a pnpm lockfile for the given project path.
|
|
48
191
|
*
|