dependency-radar 0.5.1 → 0.6.1

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
@@ -63,7 +63,7 @@ function buildHtml(data) {
63
63
  month: 'short',
64
64
  year: 'numeric',
65
65
  hour: '2-digit',
66
- minute: '2-digit'
66
+ minute: '2-digit',
67
67
  }).format(date);
68
68
  }
69
69
  }
@@ -199,6 +199,16 @@ ${safeCssContent}
199
199
  </svg>
200
200
  <input type="search" id="search" placeholder="Search packages..." />
201
201
  </div>
202
+ <div class="view-switch" id="view-switch">
203
+ <button
204
+ type="button"
205
+ class="view-switch-btn"
206
+ id="view-graph-btn"
207
+ data-view="graph"
208
+ >
209
+ Graph View
210
+ </button>
211
+ </div>
202
212
  <button
203
213
  type="button"
204
214
  class="filters-toggle"
@@ -319,7 +329,60 @@ ${safeCssContent}
319
329
 
320
330
  <!-- Main Content -->
321
331
  <main class="main-content">
322
- <div id="dependency-list" class="dependency-grid"></div>
332
+ <section class="view-panel active" id="list-view" data-view="list" aria-hidden="false">
333
+ <div id="dependency-list" class="dependency-grid"></div>
334
+ </section>
335
+ <section class="view-panel" id="graph-view" data-view="graph" aria-hidden="true">
336
+ <div class="graph-canvas-shell" id="graph-canvas-shell">
337
+ <canvas id="graph-canvas"></canvas>
338
+ <div class="graph-overlay-top">
339
+ <button type="button" class="graph-back-btn" id="graph-back-btn">Back to List View</button>
340
+ <div class="graph-key" aria-label="Graph key">
341
+ <span class="graph-workspace-label">Key</span>
342
+ <div class="graph-key-items">
343
+ <span class="graph-key-item">
344
+ <span class="graph-key-dot dependency" aria-hidden="true"></span>
345
+ <span>Dependency</span>
346
+ </span>
347
+ <span class="graph-key-item">
348
+ <span class="graph-key-dot dev-dependency" aria-hidden="true"></span>
349
+ <span>Dev-Dependency</span>
350
+ </span>
351
+ <span class="graph-key-item">
352
+ <span class="graph-key-dot sub-dependency" aria-hidden="true"></span>
353
+ <span>Sub-Dependency</span>
354
+ </span>
355
+ </div>
356
+ </div>
357
+ <div class="graph-overlay-left" id="graph-workspace-wrap">
358
+ <label class="graph-workspace-label" for="graph-workspace">Workspace</label>
359
+ <select id="graph-workspace" class="graph-workspace-select"></select>
360
+ </div>
361
+ </div>
362
+ <div class="graph-controls graph-overlay graph-overlay-right" id="graph-controls">
363
+ <div class="dpad">
364
+ <button type="button" class="graph-control-btn up" data-action="pan-down" aria-label="Pan Up">▲</button>
365
+ <button type="button" class="graph-control-btn left" data-action="pan-right" aria-label="Pan Left">◀</button>
366
+ <div class="center-spacer" aria-hidden="true"></div>
367
+ <button type="button" class="graph-control-btn right" data-action="pan-left" aria-label="Pan Right">▶</button>
368
+ <button type="button" class="graph-control-btn down" data-action="pan-up" aria-label="Pan Down">▼</button>
369
+ </div>
370
+ <div class="zoom-controls">
371
+ <button type="button" class="graph-control-btn" data-action="zoom-in" aria-label="Zoom In">+</button>
372
+ <button type="button" class="graph-control-btn" data-action="zoom-out" aria-label="Zoom Out">−</button>
373
+ </div>
374
+ <button type="button" class="graph-control-btn reset-btn" data-action="reset">reset</button>
375
+ </div>
376
+ <div class="graph-popover" id="graph-popover" hidden>
377
+ <div class="graph-popover-name" id="graph-popover-name"></div>
378
+ <div class="graph-popover-meta" id="graph-popover-version"></div>
379
+ <div class="graph-popover-meta" id="graph-popover-license"></div>
380
+ <div class="graph-popover-meta" id="graph-popover-vulns"></div>
381
+ <div class="graph-popover-meta" id="graph-popover-amplification"></div>
382
+ <button type="button" class="graph-popover-action" id="graph-open-list">Open in List</button>
383
+ </div>
384
+ </div>
385
+ </section>
323
386
  </main>
324
387
 
325
388
  <footer class="report-footer">
@@ -335,5 +398,9 @@ ${safeJsContent}
335
398
  </html>`;
336
399
  }
337
400
  function escapeHtml(str) {
338
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
401
+ return str
402
+ .replace(/&/g, '&amp;')
403
+ .replace(/</g, '&lt;')
404
+ .replace(/>/g, '&gt;')
405
+ .replace(/"/g, '&quot;');
339
406
  }
@@ -10,7 +10,22 @@ const module_1 = require("module");
10
10
  const utils_1 = require("../utils");
11
11
  const IGNORED_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', '.dependency-radar']);
12
12
  const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
13
- async function runImportGraph(projectPath, tempDir) {
13
+ /**
14
+ * Builds an import graph for a project and optionally writes it to disk.
15
+ *
16
+ * The produced graph maps each project-relative source file to its resolved local file dependencies,
17
+ * referenced packages, per-file package usage counts, and any unresolved import specifiers.
18
+ *
19
+ * @param options - Optional settings.
20
+ * @param options.persistToDisk - When `false`, the graph is not written to disk; defaults to `true`.
21
+ * @returns An object with:
22
+ * - `ok: true` and `data` containing `{ files, packages, packageCounts, unresolvedImports }` on success.
23
+ * When the graph was persisted to disk, a `file` field points to the written JSON file (`<tempDir>/import-graph.json`).
24
+ * - `ok: false` and `error` containing an error message on failure. If persistence was enabled, a `file` field may point to
25
+ * the JSON file containing the error object.
26
+ */
27
+ async function runImportGraph(projectPath, tempDir, options = {}) {
28
+ const persistToDisk = options.persistToDisk !== false;
14
29
  const targetFile = path_1.default.join(tempDir, 'import-graph.json');
15
30
  try {
16
31
  const srcPath = path_1.default.join(projectPath, 'src');
@@ -32,12 +47,20 @@ async function runImportGraph(projectPath, tempDir) {
32
47
  unresolvedImports.push(...resolved.unresolved.map((spec) => ({ importer: rel, specifier: spec })));
33
48
  }
34
49
  const output = { files: fileGraph, packages: packageGraph, packageCounts, unresolvedImports };
35
- await (0, utils_1.writeJsonFile)(targetFile, output);
36
- return { ok: true, data: output, file: targetFile };
50
+ if (persistToDisk) {
51
+ await (0, utils_1.writeJsonFile)(targetFile, output);
52
+ }
53
+ return { ok: true, data: output, ...(persistToDisk ? { file: targetFile } : {}) };
37
54
  }
38
55
  catch (err) {
39
- await (0, utils_1.writeJsonFile)(targetFile, { error: String(err) });
40
- return { ok: false, error: `import graph failed: ${String(err)}`, file: targetFile };
56
+ if (persistToDisk) {
57
+ await (0, utils_1.writeJsonFile)(targetFile, { error: String(err) });
58
+ }
59
+ return {
60
+ ok: false,
61
+ error: `import graph failed: ${String(err)}`,
62
+ ...(persistToDisk ? { file: targetFile } : {})
63
+ };
41
64
  }
42
65
  }
43
66
  async function collectSourceFiles(rootDir) {
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.tryBuildDependencyTreeFromLockfile = tryBuildDependencyTreeFromLockfile;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
- const yaml_1 = __importDefault(require("yaml"));
9
+ const lockfileParsers_1 = require("./lockfileParsers");
10
10
  const treeCache = new Map();
11
11
  const parseCache = new Map();
12
12
  /**
@@ -244,7 +244,10 @@ function buildPnpmNode(packageKey, snapshots, index, memo, installState, stack)
244
244
  dependencies: {}
245
245
  };
246
246
  stack.add(packageKey);
247
- const childRefs = mergeStringRecord(snapshot === null || snapshot === void 0 ? void 0 : snapshot.dependencies, mergeStringRecord(snapshot === null || snapshot === void 0 ? void 0 : snapshot.optionalDependencies, snapshot === null || snapshot === void 0 ? void 0 : snapshot.peerDependencies));
247
+ // pnpm snapshots already carry resolved installed deps in `dependencies`/`optionalDependencies`.
248
+ // Do not traverse `peerDependencies` ranges here: they can overwrite resolved child refs
249
+ // (for example `child: ^1.0.0` over `child: 1.0.0`) and incorrectly drop installed nodes.
250
+ const childRefs = mergeStringRecord(snapshot === null || snapshot === void 0 ? void 0 : snapshot.dependencies, snapshot === null || snapshot === void 0 ? void 0 : snapshot.optionalDependencies);
248
251
  for (const [childName, childRef] of Object.entries(childRefs)) {
249
252
  if (!childRef || isWorkspaceLikeSpecifier(childRef))
250
253
  continue;
@@ -502,6 +505,10 @@ function buildNpmNodeFromPackages(packageKey, fallbackName, packages, memo, stac
502
505
  version,
503
506
  dependencies: {}
504
507
  };
508
+ const packagePath = resolveNpmInstalledPath(packageKey, installState.lockDir);
509
+ if (packagePath) {
510
+ out.path = packagePath;
511
+ }
505
512
  if ((entry === null || entry === void 0 ? void 0 : entry.dev) !== undefined) {
506
513
  out.dev = Boolean(entry.dev);
507
514
  }
@@ -561,6 +568,33 @@ function resolveNpmPackagePath(fromPackageKey, depName, packages) {
561
568
  const rootCandidate = `node_modules/${depName}`;
562
569
  return rootCandidate in packages ? rootCandidate : undefined;
563
570
  }
571
+ /**
572
+ * Resolve an npm lockfile package key to the absolute filesystem path where that package would be installed under a given lock directory.
573
+ *
574
+ * @param packageKey - The package key from a lockfile (e.g., a normalized/package-relative path or package identifier).
575
+ * @param lockDir - The lockfile directory to treat as the installation root.
576
+ * @returns The absolute path inside `lockDir` corresponding to `packageKey` if it resolves to a location contained within `lockDir`, `undefined` otherwise.
577
+ */
578
+ function resolveNpmInstalledPath(packageKey, lockDir) {
579
+ const normalizedKey = normalizeLockPackageKey(packageKey);
580
+ if (!normalizedKey)
581
+ return undefined;
582
+ const segments = normalizedKey.split('/').filter(Boolean);
583
+ if (segments.length === 0)
584
+ return undefined;
585
+ for (const segment of segments) {
586
+ if (segment === '.' || segment === '..')
587
+ return undefined;
588
+ if (path_1.default.isAbsolute(segment))
589
+ return undefined;
590
+ }
591
+ const lockRoot = path_1.default.resolve(lockDir);
592
+ const resolved = path_1.default.resolve(lockRoot, ...segments);
593
+ const relative = path_1.default.relative(lockRoot, resolved);
594
+ if (relative.startsWith('..') || path_1.default.isAbsolute(relative))
595
+ return undefined;
596
+ return resolved;
597
+ }
564
598
  /**
565
599
  * Normalize a legacy npm lockfile node into a ResolvedNode.
566
600
  *
@@ -641,20 +675,7 @@ function parseYarnTree(projectPath, searchRoot) {
641
675
  * @returns A Map where each selector string maps to its YarnV1Entry, or `undefined` if parsing fails or the lockfile is not a Yarn v1 success parse
642
676
  */
643
677
  function parseYarnV1(raw) {
644
- // eslint-disable-next-line @typescript-eslint/no-var-requires
645
- const lockfile = require('@yarnpkg/lockfile');
646
- const parsed = lockfile.parse(raw);
647
- if (!parsed || parsed.type !== 'success' || !parsed.object)
648
- return undefined;
649
- const map = new Map();
650
- for (const [selectorKey, entry] of Object.entries(parsed.object)) {
651
- for (const selector of splitSelectors(selectorKey)) {
652
- if (!map.has(selector)) {
653
- map.set(selector, entry || {});
654
- }
655
- }
656
- }
657
- return map;
678
+ return (0, lockfileParsers_1.parseYarnV1Lockfile)(raw);
658
679
  }
659
680
  /**
660
681
  * Parse a Yarn v2 (Berry) lockfile YAML string into a selector-to-entry map.
@@ -663,7 +684,7 @@ function parseYarnV1(raw) {
663
684
  * @returns A Map where each lockfile selector maps to its YarnV2Entry, or `undefined` if the input could not be parsed into an object
664
685
  */
665
686
  function parseYarnV2(raw) {
666
- const parsed = yaml_1.default.parse(raw);
687
+ const parsed = (0, lockfileParsers_1.parseYamlLike)(raw);
667
688
  if (!parsed || typeof parsed !== 'object')
668
689
  return undefined;
669
690
  const map = new Map();
@@ -830,10 +851,7 @@ function collectPackageJsonDependencySpecs(pkg) {
830
851
  * @returns An array of trimmed, non-empty selector strings
831
852
  */
832
853
  function splitSelectors(selectorKey) {
833
- return selectorKey
834
- .split(',')
835
- .map((part) => part.trim())
836
- .filter(Boolean);
854
+ return (0, lockfileParsers_1.splitSelectorList)(selectorKey);
837
855
  }
838
856
  /**
839
857
  * Merge two arbitrary values into a string-to-string record, with entries from the second overriding the first.
@@ -995,7 +1013,7 @@ function getCachedYaml(filePath) {
995
1013
  const raw = readCachedText(filePath);
996
1014
  if (!raw)
997
1015
  return undefined;
998
- const parsed = yaml_1.default.parse(raw);
1016
+ const parsed = (0, lockfileParsers_1.parseYamlLike)(raw);
999
1017
  parseCache.set(cacheKey, parsed);
1000
1018
  return parsed;
1001
1019
  }