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/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 +81 -25
- 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/dist/utils.js +36 -1
- 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
|
/**
|
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
*
|