dependency-radar 0.5.0 → 0.6.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
@@ -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
  /**
@@ -437,6 +437,7 @@ function parseNpmTree(projectPath, searchRoot) {
437
437
  * @returns A ResolvedTree mapping root dependency names to resolved nodes, or `undefined` if no valid root entry exists in `packages`
438
438
  */
439
439
  function parseNpmTreeFromPackages(packages, projectPath, lockDir) {
440
+ const installState = createNpmInstallState(projectPath, lockDir);
440
441
  const projectRel = toPosixRelative(lockDir, projectPath);
441
442
  const rootKey = projectRel === '' ? '' : projectRel;
442
443
  if (!(rootKey in packages) && rootKey !== '') {
@@ -454,7 +455,7 @@ function parseNpmTreeFromPackages(packages, projectPath, lockDir) {
454
455
  const childKey = resolveNpmPackagePath(packageKey, depName, packages);
455
456
  if (!childKey)
456
457
  continue;
457
- const childNode = buildNpmNodeFromPackages(childKey, depName, packages, memo, stack);
458
+ const childNode = buildNpmNodeFromPackages(childKey, depName, packages, memo, stack, installState);
458
459
  if (childNode)
459
460
  dependencies[childNode.name] = childNode;
460
461
  }
@@ -470,18 +471,22 @@ function parseNpmTreeFromPackages(packages, projectPath, lockDir) {
470
471
  * @param stack - Recursion stack used to detect and avoid cycles while building the dependency graph.
471
472
  * @returns The constructed `ResolvedNode` for `packageKey`, or `undefined` if the entry is missing, invalid, cyclic, or cannot be resolved.
472
473
  */
473
- function buildNpmNodeFromPackages(packageKey, fallbackName, packages, memo, stack) {
474
+ function buildNpmNodeFromPackages(packageKey, fallbackName, packages, memo, stack, installState) {
474
475
  if (memo.has(packageKey))
475
476
  return memo.get(packageKey);
476
477
  if (stack.has(packageKey))
477
478
  return undefined;
479
+ if (!isNpmPackageInstalled(packageKey, installState)) {
480
+ memo.set(packageKey, undefined);
481
+ return undefined;
482
+ }
478
483
  const entry = packages[packageKey];
479
484
  if (!entry || typeof entry !== 'object')
480
485
  return undefined;
481
486
  if (entry.link === true && typeof entry.resolved === 'string') {
482
487
  const linkedKey = normalizeLockPackageKey(entry.resolved);
483
488
  if (linkedKey && linkedKey in packages) {
484
- const linkedNode = buildNpmNodeFromPackages(linkedKey, fallbackName, packages, memo, stack);
489
+ const linkedNode = buildNpmNodeFromPackages(linkedKey, fallbackName, packages, memo, stack, installState);
485
490
  memo.set(packageKey, linkedNode);
486
491
  return linkedNode;
487
492
  }
@@ -497,6 +502,10 @@ function buildNpmNodeFromPackages(packageKey, fallbackName, packages, memo, stac
497
502
  version,
498
503
  dependencies: {}
499
504
  };
505
+ const packagePath = resolveNpmInstalledPath(packageKey, installState.lockDir);
506
+ if (packagePath) {
507
+ out.path = packagePath;
508
+ }
500
509
  if ((entry === null || entry === void 0 ? void 0 : entry.dev) !== undefined) {
501
510
  out.dev = Boolean(entry.dev);
502
511
  }
@@ -506,7 +515,7 @@ function buildNpmNodeFromPackages(packageKey, fallbackName, packages, memo, stac
506
515
  const childKey = resolveNpmPackagePath(packageKey, depName, packages);
507
516
  if (!childKey)
508
517
  continue;
509
- const childNode = buildNpmNodeFromPackages(childKey, depName, packages, memo, stack);
518
+ const childNode = buildNpmNodeFromPackages(childKey, depName, packages, memo, stack, installState);
510
519
  if (!childNode)
511
520
  continue;
512
521
  out.dependencies[childNode.name] = childNode;
@@ -556,6 +565,33 @@ function resolveNpmPackagePath(fromPackageKey, depName, packages) {
556
565
  const rootCandidate = `node_modules/${depName}`;
557
566
  return rootCandidate in packages ? rootCandidate : undefined;
558
567
  }
568
+ /**
569
+ * Resolve an npm lockfile package key to the absolute filesystem path where that package would be installed under a given lock directory.
570
+ *
571
+ * @param packageKey - The package key from a lockfile (e.g., a normalized/package-relative path or package identifier).
572
+ * @param lockDir - The lockfile directory to treat as the installation root.
573
+ * @returns The absolute path inside `lockDir` corresponding to `packageKey` if it resolves to a location contained within `lockDir`, `undefined` otherwise.
574
+ */
575
+ function resolveNpmInstalledPath(packageKey, lockDir) {
576
+ const normalizedKey = normalizeLockPackageKey(packageKey);
577
+ if (!normalizedKey)
578
+ return undefined;
579
+ const segments = normalizedKey.split('/').filter(Boolean);
580
+ if (segments.length === 0)
581
+ return undefined;
582
+ for (const segment of segments) {
583
+ if (segment === '.' || segment === '..')
584
+ return undefined;
585
+ if (path_1.default.isAbsolute(segment))
586
+ return undefined;
587
+ }
588
+ const lockRoot = path_1.default.resolve(lockDir);
589
+ const resolved = path_1.default.resolve(lockRoot, ...segments);
590
+ const relative = path_1.default.relative(lockRoot, resolved);
591
+ if (relative.startsWith('..') || path_1.default.isAbsolute(relative))
592
+ return undefined;
593
+ return resolved;
594
+ }
559
595
  /**
560
596
  * Normalize a legacy npm lockfile node into a ResolvedNode.
561
597
  *
@@ -636,20 +672,7 @@ function parseYarnTree(projectPath, searchRoot) {
636
672
  * @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
637
673
  */
638
674
  function parseYarnV1(raw) {
639
- // eslint-disable-next-line @typescript-eslint/no-var-requires
640
- const lockfile = require('@yarnpkg/lockfile');
641
- const parsed = lockfile.parse(raw);
642
- if (!parsed || parsed.type !== 'success' || !parsed.object)
643
- return undefined;
644
- const map = new Map();
645
- for (const [selectorKey, entry] of Object.entries(parsed.object)) {
646
- for (const selector of splitSelectors(selectorKey)) {
647
- if (!map.has(selector)) {
648
- map.set(selector, entry || {});
649
- }
650
- }
651
- }
652
- return map;
675
+ return (0, lockfileParsers_1.parseYarnV1Lockfile)(raw);
653
676
  }
654
677
  /**
655
678
  * Parse a Yarn v2 (Berry) lockfile YAML string into a selector-to-entry map.
@@ -658,7 +681,7 @@ function parseYarnV1(raw) {
658
681
  * @returns A Map where each lockfile selector maps to its YarnV2Entry, or `undefined` if the input could not be parsed into an object
659
682
  */
660
683
  function parseYarnV2(raw) {
661
- const parsed = yaml_1.default.parse(raw);
684
+ const parsed = (0, lockfileParsers_1.parseYamlLike)(raw);
662
685
  if (!parsed || typeof parsed !== 'object')
663
686
  return undefined;
664
687
  const map = new Map();
@@ -825,10 +848,7 @@ function collectPackageJsonDependencySpecs(pkg) {
825
848
  * @returns An array of trimmed, non-empty selector strings
826
849
  */
827
850
  function splitSelectors(selectorKey) {
828
- return selectorKey
829
- .split(',')
830
- .map((part) => part.trim())
831
- .filter(Boolean);
851
+ return (0, lockfileParsers_1.splitSelectorList)(selectorKey);
832
852
  }
833
853
  /**
834
854
  * Merge two arbitrary values into a string-to-string record, with entries from the second overriding the first.
@@ -990,7 +1010,7 @@ function getCachedYaml(filePath) {
990
1010
  const raw = readCachedText(filePath);
991
1011
  if (!raw)
992
1012
  return undefined;
993
- const parsed = yaml_1.default.parse(raw);
1013
+ const parsed = (0, lockfileParsers_1.parseYamlLike)(raw);
994
1014
  parseCache.set(cacheKey, parsed);
995
1015
  return parsed;
996
1016
  }
@@ -1037,6 +1057,21 @@ function createPnpmInstallState(projectPath) {
1037
1057
  installedCache: new Map()
1038
1058
  };
1039
1059
  }
1060
+ /**
1061
+ * Create npm installation state used to filter lockfile package entries to actually installed paths.
1062
+ *
1063
+ * @param projectPath - Filesystem path inside the project to start discovery from
1064
+ * @param lockDir - Directory containing the npm lockfile
1065
+ * @returns Installation state with detected node_modules roots and per-package-key cache
1066
+ */
1067
+ function createNpmInstallState(projectPath, lockDir) {
1068
+ const nodeModulesRoots = findNodeModulesRoots(projectPath);
1069
+ return {
1070
+ enabled: nodeModulesRoots.length > 0,
1071
+ lockDir,
1072
+ installedByKeyCache: new Map()
1073
+ };
1074
+ }
1040
1075
  /**
1041
1076
  * Discover node_modules directories by walking upward from a starting path.
1042
1077
  *
@@ -1058,6 +1093,27 @@ function findNodeModulesRoots(startPath) {
1058
1093
  }
1059
1094
  return roots;
1060
1095
  }
1096
+ /**
1097
+ * Determine whether a package-lock `packages` entry key points to an installed package path.
1098
+ *
1099
+ * @param packageKey - Key from package-lock `packages` map
1100
+ * @param installState - npm installation state with lockDir and cache
1101
+ * @returns `true` when the key should be considered installed, `false` otherwise
1102
+ */
1103
+ function isNpmPackageInstalled(packageKey, installState) {
1104
+ if (!installState.enabled)
1105
+ return true;
1106
+ const normalizedKey = normalizeLockPackageKey(packageKey);
1107
+ if (!normalizedKey)
1108
+ return true;
1109
+ const cached = installState.installedByKeyCache.get(normalizedKey);
1110
+ if (cached !== undefined)
1111
+ return cached;
1112
+ const candidate = path_1.default.join(installState.lockDir, ...normalizedKey.split('/'));
1113
+ const installed = safePathExists(candidate);
1114
+ installState.installedByKeyCache.set(normalizedKey, installed);
1115
+ return installed;
1116
+ }
1061
1117
  /**
1062
1118
  * Read the names of entries in a directory, returning an empty array if the directory cannot be read.
1063
1119
  *