dependency-radar 0.7.0 → 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/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
- * Generate a full HTML document for the dependency radar report from the provided aggregated data.
41
+ * Generate a complete standalone HTML document for the Dependency Radar report using the provided aggregated data.
42
42
  *
43
- * The produced document embeds sanitized CSS and JavaScript assets, includes a JSON-serialized copy of `data` with `<` characters escaped, computes a CTA URL from `data.dependencyRadarVersion`, and formats `data.generatedAt` into a human-friendly timestamp when parsable. Values interpolated into the HTML (for example project path, formatted date, and CTA URL) are HTML-escaped.
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 - Aggregated data used to populate the report (project metadata, generatedAt timestamp, dependencyRadarVersion, and dependency list)
46
- * @returns The full HTML document for the dependency radar report
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-link" target="_blank" rel="noopener" id="cta-primary-link">
182
- Enrich this scan
183
- <span class="cta-arrow">→</span>
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
- <button
213
- type="button"
214
- class="filters-toggle"
215
- id="filters-toggle"
216
- aria-expanded="false"
217
- >
218
- Filters
219
- <span class="chevron">▼</span>
220
- </button>
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
- <span class="filter-label">Type</span>
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
- <span class="filter-label">Scope</span>
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
- <button type="button" class="license-filter-toggle" id="license-toggle">
246
- License Categories
247
- <span class="chevron">▼</span>
248
- </button>
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
- <label class="checkbox-filter">
251
- <input type="checkbox" id="has-vulns" />
252
- Has vulnerabilities
253
- </label>
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="theme-toggle">
271
- <span class="theme-toggle-label">Theme</span>
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
- <!-- Collapsible License Filter Panel -->
278
- <div class="license-filter-panel-row">
279
- <div class="license-filter-panel" id="license-panel">
280
- <div class="license-filter-inner">
281
- <div class="license-filter-header">
282
- <span class="license-filter-title">Filter by License Type</span>
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 -->
@@ -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(['node_modules', 'dist', 'build', 'coverage', '.dependency-radar']);
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 srcPath = path_1.default.join(projectPath, 'src');
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
  *