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/README.md +214 -119
- package/dist/cli.js +402 -50
- package/dist/failOn.js +177 -0
- package/dist/report-assets.js +2 -2
- package/dist/report.js +70 -3
- package/dist/runners/importGraphRunner.js +28 -5
- package/dist/runners/lockfileGraph.js +40 -22
- package/dist/runners/lockfileParsers.js +434 -0
- package/dist/runners/npmAudit.js +28 -11
- package/dist/runners/npmLs.js +58 -18
- package/dist/runners/npmOutdated.js +37 -16
- package/package.json +4 -8
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
|
-
<
|
|
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
|
|
401
|
+
return str
|
|
402
|
+
.replace(/&/g, '&')
|
|
403
|
+
.replace(/</g, '<')
|
|
404
|
+
.replace(/>/g, '>')
|
|
405
|
+
.replace(/"/g, '"');
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
1016
|
+
const parsed = (0, lockfileParsers_1.parseYamlLike)(raw);
|
|
999
1017
|
parseCache.set(cacheKey, parsed);
|
|
1000
1018
|
return parsed;
|
|
1001
1019
|
}
|