dependency-radar 0.3.0 → 0.3.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 +50 -13
- package/dist/cli.js +34 -15
- package/dist/runners/npmLs.js +116 -10
- package/dist/utils.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
# Dependency Radar
|
|
2
2
|
|
|
3
|
-
Dependency Radar is a
|
|
3
|
+
Dependency Radar is a CLI tool that inspects a Node.js project’s installed dependencies and generates a single, human-readable HTML report. The report highlights dependency structure, usage, licences, vulnerabilities, and other signals that help you understand risk and complexity hidden in your node_modules folder.
|
|
4
|
+
|
|
5
|
+
The simplest way to get started is:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx dependency-radar
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This runs a scan against the current project and writes a self-contained `dependency-radar.html` report you can open locally, share with teammates, or attach to tickets and documentation.
|
|
4
12
|
|
|
5
13
|
## What it does
|
|
6
14
|
|
|
7
|
-
- Analyses installed dependencies
|
|
8
|
-
- Combines multiple
|
|
9
|
-
- Shows direct vs
|
|
15
|
+
- Analyses installed dependencies by running standard package manager tooling (npm, pnpm, or yarn)
|
|
16
|
+
- Combines multiple signals (audit results, dependency graph data, import usage, and heuristics) into a single report
|
|
17
|
+
- Shows direct vs transitive dependencies, dependency depth, and parent relationships
|
|
10
18
|
- Highlights licences, known vulnerabilities, install-time scripts, native modules, and package footprint
|
|
11
|
-
- Produces a single self-contained HTML file you can share
|
|
19
|
+
- Produces a single self-contained HTML file with no external assets, which you can easily share
|
|
12
20
|
|
|
13
21
|
## What it is not
|
|
14
22
|
|
|
15
|
-
- Not a CI service or hosted platform
|
|
23
|
+
- Not a CI service or hosted scanning platform
|
|
16
24
|
- Not a replacement for dedicated security scanners
|
|
17
25
|
- Not a bundler or build tool
|
|
18
26
|
- Not a dependency updater
|
|
19
27
|
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
For teams that want deeper analysis, long-term tracking, and additional enrichment (such as ecosystem and maintenance signals), Dependency Radar also offers an optional premium service.
|
|
31
|
+
See https://dependency-radar.com for details.
|
|
20
32
|
|
|
21
33
|
## License Scanning
|
|
22
34
|
|
|
@@ -91,18 +103,43 @@ npx dependency-radar --help
|
|
|
91
103
|
|
|
92
104
|
## Scripts
|
|
93
105
|
|
|
94
|
-
- `npm run build` – compile TypeScript to `dist/`
|
|
95
|
-
- `npm run dev` – run a scan from source (`ts-node`)
|
|
96
|
-
- `npm run scan` – run a scan from the built output
|
|
106
|
+
- `npm run build` – generate SPDX/report assets and compile TypeScript to `dist/`
|
|
107
|
+
- `npm run dev` – run a scan from source (`ts-node src/cli.ts scan`)
|
|
108
|
+
- `npm run scan` – run a scan from the built output (`node dist/cli.js scan`)
|
|
109
|
+
- `npm run dev:report` – run the report UI dev server
|
|
110
|
+
- `npm run build:spdx` – rebuild bundled SPDX identifiers
|
|
111
|
+
- `npm run build:report-ui` – build report UI assets
|
|
112
|
+
- `npm run build:report` – rebuild report assets used by the CLI
|
|
113
|
+
|
|
114
|
+
### Fixture scripts:
|
|
115
|
+
|
|
116
|
+
- `npm run fixtures:install` – install core fixture dependencies
|
|
117
|
+
- `npm run fixtures:install:all` – install all fixture dependencies
|
|
118
|
+
- `npm run fixtures:scan` – scan the core fixture set
|
|
119
|
+
- `npm run fixtures:install:npm`
|
|
120
|
+
- `npm run fixtures:install:npm-heavy`
|
|
121
|
+
- `npm run fixtures:install:pnpm`
|
|
122
|
+
- `npm run fixtures:install:pnpm-hoisted`
|
|
123
|
+
- `npm run fixtures:install:yarn`
|
|
124
|
+
- `npm run fixtures:install:yarn-berry`
|
|
125
|
+
- `npm run fixtures:install:optional`
|
|
126
|
+
- `npm run fixtures:scan:npm`
|
|
127
|
+
- `npm run fixtures:scan:npm-heavy`
|
|
128
|
+
- `npm run fixtures:scan:pnpm`
|
|
129
|
+
- `npm run fixtures:scan:pnpm-hoisted`
|
|
130
|
+
- `npm run fixtures:scan:yarn`
|
|
131
|
+
- `npm run fixtures:scan:yarn-berry`
|
|
132
|
+
- `npm run fixtures:scan:optional`
|
|
133
|
+
- `npm run fixtures:scan:no-node-modules`
|
|
97
134
|
|
|
98
135
|
## Notes
|
|
99
136
|
|
|
100
|
-
- The target project must have
|
|
101
|
-
- The scan
|
|
102
|
-
- `npm audit` and `npm outdated` perform registry lookups; use `--offline` for offline-only scans.
|
|
137
|
+
- The target project must have dependencies installed (run `npm install`, `pnpm install`, or `yarn install` first).
|
|
138
|
+
- The scan runs on your machine and does not upload your code or dependencies anywhere.
|
|
139
|
+
- `npm audit`/`pnpm audit`/`yarn npm audit` and `npm outdated`/`pnpm outdated` perform registry lookups; use `--offline` for offline-only scans.
|
|
103
140
|
- A temporary `.dependency-radar` folder is created during the scan to store intermediate tool output.
|
|
104
141
|
- Use `--keep-temp` to retain this folder for debugging; otherwise it is deleted automatically.
|
|
105
|
-
- If
|
|
142
|
+
- If some per-package tools fail (common in large workspaces), the scan continues and reports warnings; missing sections are marked unavailable where applicable.
|
|
106
143
|
|
|
107
144
|
## Output
|
|
108
145
|
|
package/dist/cli.js
CHANGED
|
@@ -688,6 +688,7 @@ function openInBrowser(filePath) {
|
|
|
688
688
|
child.unref();
|
|
689
689
|
}
|
|
690
690
|
async function run() {
|
|
691
|
+
var _a;
|
|
691
692
|
const opts = parseArgs(process.argv.slice(2));
|
|
692
693
|
if (opts.command !== "scan") {
|
|
693
694
|
printHelp();
|
|
@@ -701,6 +702,7 @@ async function run() {
|
|
|
701
702
|
let outputPath = path_1.default.resolve(opts.out);
|
|
702
703
|
const startTime = Date.now();
|
|
703
704
|
let dependencyCount = 0;
|
|
705
|
+
let outputCreated = false;
|
|
704
706
|
try {
|
|
705
707
|
const stat = await promises_1.default.stat(outputPath).catch(() => undefined);
|
|
706
708
|
const endsWithSeparator = opts.out.endsWith("/") || opts.out.endsWith("\\");
|
|
@@ -781,7 +783,10 @@ async function run() {
|
|
|
781
783
|
opts.audit
|
|
782
784
|
? (0, npmAudit_1.runPackageAudit)(meta.path, pkgTempDir, scanManager, yarnVersion).catch((err) => ({ ok: false, error: String(err) }))
|
|
783
785
|
: Promise.resolve(undefined),
|
|
784
|
-
(0, npmLs_1.runNpmLs)(meta.path, pkgTempDir, scanManager
|
|
786
|
+
(0, npmLs_1.runNpmLs)(meta.path, pkgTempDir, scanManager, {
|
|
787
|
+
contextLabel: meta.name,
|
|
788
|
+
onProgress: (line) => spinner.log(line),
|
|
789
|
+
}).catch((err) => ({ ok: false, error: String(err) })),
|
|
785
790
|
(0, importGraphRunner_1.runImportGraph)(meta.path, pkgTempDir).catch((err) => ({ ok: false, error: String(err) })),
|
|
786
791
|
opts.outdated
|
|
787
792
|
? (0, npmOutdated_1.runPackageOutdated)(meta.path, pkgTempDir, scanManager).catch((err) => ({ ok: false, error: String(err) }))
|
|
@@ -829,14 +834,20 @@ async function run() {
|
|
|
829
834
|
const auditFailure = opts.audit
|
|
830
835
|
? perPackageAudit.find((r) => r && !r.ok)
|
|
831
836
|
: undefined;
|
|
832
|
-
const
|
|
833
|
-
|
|
837
|
+
const lsFailures = perPackageLs
|
|
838
|
+
.map((result, index) => ({ result, meta: packageMetas[index] }))
|
|
839
|
+
.filter((entry) => entry.result && !entry.result.ok);
|
|
840
|
+
const importFailures = perPackageImportGraph.filter((r) => r && !r.ok);
|
|
834
841
|
if (auditFailure) {
|
|
835
842
|
spinner.log(`Audit warning: ${auditFailure.error || "Audit failed"}`);
|
|
836
843
|
}
|
|
837
|
-
if (
|
|
838
|
-
const
|
|
839
|
-
|
|
844
|
+
if (lsFailures.length > 0) {
|
|
845
|
+
const packageList = lsFailures.map((entry) => { var _a; return (_a = entry.meta) === null || _a === void 0 ? void 0 : _a.name; }).filter(Boolean);
|
|
846
|
+
spinner.log(`Dependency tree warning: ${lsFailures.length} package${lsFailures.length === 1 ? "" : "s"} failed (${packageList.join(", ")}).`);
|
|
847
|
+
spinner.log(`First dependency tree error: ${((_a = lsFailures[0].result) === null || _a === void 0 ? void 0 : _a.error) || "pnpm ls failed"}`);
|
|
848
|
+
}
|
|
849
|
+
if (importFailures.length > 0) {
|
|
850
|
+
spinner.log(`Import graph warning: ${importFailures.length} package${importFailures.length === 1 ? "" : "s"} failed (${importFailures[0].error || "import graph failed"})`);
|
|
840
851
|
}
|
|
841
852
|
const aggregated = await (0, aggregator_1.aggregateData)({
|
|
842
853
|
projectPath,
|
|
@@ -865,17 +876,25 @@ async function run() {
|
|
|
865
876
|
if (workspace.type !== "none") {
|
|
866
877
|
console.log(`Detected ${workspace.type.toUpperCase()} workspace with ${packagePaths.length} package${packagePaths.length === 1 ? "" : "s"}.`);
|
|
867
878
|
}
|
|
868
|
-
if (
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
879
|
+
if (dependencyCount > 0) {
|
|
880
|
+
if (opts.json) {
|
|
881
|
+
await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
|
|
882
|
+
await promises_1.default.writeFile(outputPath, JSON.stringify(aggregated, null, 2), "utf8");
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
await (0, report_1.renderReport)(aggregated, outputPath);
|
|
886
|
+
}
|
|
887
|
+
outputCreated = true;
|
|
874
888
|
}
|
|
875
889
|
spinner.stop(true);
|
|
876
890
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
877
891
|
console.log(`✔ Scan complete: ${dependencyCount} dependencies analysed in ${elapsed}s`);
|
|
878
|
-
|
|
892
|
+
if (outputCreated) {
|
|
893
|
+
console.log(`✔ ${opts.json ? "JSON" : "Report"} written to ${outputPath}`);
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
console.log(`✖ No dependencies were found - ${opts.json ? "JSON file" : "Report"} not created`);
|
|
897
|
+
}
|
|
879
898
|
}
|
|
880
899
|
catch (err) {
|
|
881
900
|
spinner.stop(false);
|
|
@@ -890,11 +909,11 @@ async function run() {
|
|
|
890
909
|
console.log(`✔ Temporary data kept at ${tempDir}`);
|
|
891
910
|
}
|
|
892
911
|
}
|
|
893
|
-
if (opts.open && !isCI()) {
|
|
912
|
+
if (opts.open && outputCreated && !isCI()) {
|
|
894
913
|
console.log(`↗ Opening ${path_1.default.basename(outputPath)} using system default ${opts.json ? "application" : "browser"}.`);
|
|
895
914
|
openInBrowser(outputPath);
|
|
896
915
|
}
|
|
897
|
-
else if (opts.open && isCI()) {
|
|
916
|
+
else if (opts.open && outputCreated && isCI()) {
|
|
898
917
|
console.log("✖ Skipping auto-open in CI environment.");
|
|
899
918
|
}
|
|
900
919
|
// Always show CTA as the last output
|
package/dist/runners/npmLs.js
CHANGED
|
@@ -6,10 +6,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.runNpmLs = runNpmLs;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const utils_1 = require("../utils");
|
|
9
|
+
const PNPM_DEPTH_ATTEMPTS = ['Infinity', '8', '4', '2', '1'];
|
|
10
|
+
const PNPM_MAX_OLD_SPACE_SIZE_MB = '8192';
|
|
9
11
|
// Normalize package-manager-specific list output into a shared dependency tree.
|
|
10
|
-
async function runNpmLs(projectPath, tempDir, tool = 'npm') {
|
|
12
|
+
async function runNpmLs(projectPath, tempDir, tool = 'npm', options = {}) {
|
|
11
13
|
const targetFile = path_1.default.join(tempDir, `${tool}-ls.json`);
|
|
12
14
|
try {
|
|
15
|
+
if (tool === 'pnpm') {
|
|
16
|
+
return await runPnpmLsWithFallback(projectPath, targetFile, options);
|
|
17
|
+
}
|
|
13
18
|
const { args, normalize } = buildLsCommand(tool);
|
|
14
19
|
const result = await (0, utils_1.runCommand)(tool, args, { cwd: projectPath });
|
|
15
20
|
const parsed = parseJsonOutput(result.stdout);
|
|
@@ -19,9 +24,7 @@ async function runNpmLs(projectPath, tempDir, tool = 'npm') {
|
|
|
19
24
|
return { ok: true, data: normalized, file: targetFile };
|
|
20
25
|
}
|
|
21
26
|
await (0, utils_1.writeJsonFile)(targetFile, { stdout: result.stdout, stderr: result.stderr, code: result.code });
|
|
22
|
-
const error = result.code
|
|
23
|
-
? `${tool} ls exited with code ${result.code}`
|
|
24
|
-
: `Failed to parse ${tool} ls output`;
|
|
27
|
+
const error = buildLsFailureMessage(tool, result.code, result.stderr);
|
|
25
28
|
return { ok: false, error, file: targetFile };
|
|
26
29
|
}
|
|
27
30
|
catch (err) {
|
|
@@ -30,12 +33,6 @@ async function runNpmLs(projectPath, tempDir, tool = 'npm') {
|
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
function buildLsCommand(tool) {
|
|
33
|
-
if (tool === 'pnpm') {
|
|
34
|
-
return {
|
|
35
|
-
args: ['list', '--json', '--depth', 'Infinity'],
|
|
36
|
-
normalize: normalizePnpmTree
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
36
|
if (tool === 'yarn') {
|
|
40
37
|
return {
|
|
41
38
|
args: ['list', '--json', '--depth', 'Infinity'],
|
|
@@ -47,6 +44,115 @@ function buildLsCommand(tool) {
|
|
|
47
44
|
normalize: normalizeNpmTree
|
|
48
45
|
};
|
|
49
46
|
}
|
|
47
|
+
async function runPnpmLsWithFallback(projectPath, targetFile, options) {
|
|
48
|
+
const normalize = normalizePnpmTree;
|
|
49
|
+
const attempts = [];
|
|
50
|
+
const env = {
|
|
51
|
+
NODE_OPTIONS: ensureNodeMaxOldSpaceSize(process.env.NODE_OPTIONS, PNPM_MAX_OLD_SPACE_SIZE_MB)
|
|
52
|
+
};
|
|
53
|
+
for (let index = 0; index < PNPM_DEPTH_ATTEMPTS.length; index++) {
|
|
54
|
+
const depth = PNPM_DEPTH_ATTEMPTS[index];
|
|
55
|
+
const result = await (0, utils_1.runCommand)('pnpm', ['list', '--json', '--depth', depth], {
|
|
56
|
+
cwd: projectPath,
|
|
57
|
+
env
|
|
58
|
+
});
|
|
59
|
+
const parsed = parseJsonOutput(result.stdout);
|
|
60
|
+
const normalized = normalize(parsed);
|
|
61
|
+
const outOfMemory = isOutOfMemoryError(result.stderr);
|
|
62
|
+
attempts.push({
|
|
63
|
+
depth,
|
|
64
|
+
code: result.code,
|
|
65
|
+
stdoutBytes: Buffer.byteLength(result.stdout || '', 'utf8'),
|
|
66
|
+
stderrPreview: trimText(result.stderr, 1200),
|
|
67
|
+
outOfMemory
|
|
68
|
+
});
|
|
69
|
+
if (normalized) {
|
|
70
|
+
if (index > 0) {
|
|
71
|
+
progress(options, `✔ PNPM ls recovered for workspace: ${formatContextLabel(options)} (depth=${depth})`);
|
|
72
|
+
}
|
|
73
|
+
await (0, utils_1.writeJsonFile)(targetFile, normalized);
|
|
74
|
+
return { ok: true, data: normalized, file: targetFile };
|
|
75
|
+
}
|
|
76
|
+
const reason = describeAttemptFailure(result.code, result.stderr);
|
|
77
|
+
progress(options, `✖ Failed pnpm ls for workspace: ${formatContextLabel(options)} (depth=${depth}; ${reason})`);
|
|
78
|
+
const nextDepth = PNPM_DEPTH_ATTEMPTS[index + 1];
|
|
79
|
+
if (nextDepth) {
|
|
80
|
+
progress(options, `✔ Retrying pnpm ls for workspace: ${formatContextLabel(options)} (depth=${nextDepth})`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
await (0, utils_1.writeJsonFile)(targetFile, {
|
|
84
|
+
error: 'pnpm ls retries exhausted',
|
|
85
|
+
nodeOptions: env.NODE_OPTIONS,
|
|
86
|
+
attempts
|
|
87
|
+
});
|
|
88
|
+
const sawOom = attempts.some((attempt) => attempt.outOfMemory);
|
|
89
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
90
|
+
if (sawOom) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: 'pnpm ls ran out of memory while building the dependency tree (retried with lower depths).',
|
|
94
|
+
file: targetFile
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const suffix = lastAttempt && typeof lastAttempt.code === 'number'
|
|
98
|
+
? ` Last exit code: ${lastAttempt.code}.`
|
|
99
|
+
: '';
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
error: `Failed to parse pnpm ls output after retries.${suffix}`,
|
|
103
|
+
file: targetFile
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function progress(options, line) {
|
|
107
|
+
if (typeof options.onProgress === 'function') {
|
|
108
|
+
options.onProgress(line);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function formatContextLabel(options) {
|
|
112
|
+
var _a;
|
|
113
|
+
const label = (_a = options.contextLabel) === null || _a === void 0 ? void 0 : _a.trim();
|
|
114
|
+
return label || '(unknown package)';
|
|
115
|
+
}
|
|
116
|
+
function describeAttemptFailure(code, stderr) {
|
|
117
|
+
if (isOutOfMemoryError(stderr))
|
|
118
|
+
return 'out of memory';
|
|
119
|
+
if (typeof code === 'number' && code !== 0)
|
|
120
|
+
return `exit code ${code}`;
|
|
121
|
+
if (code === null && stderr && stderr.trim())
|
|
122
|
+
return 'terminated before completion';
|
|
123
|
+
return 'no parseable JSON output';
|
|
124
|
+
}
|
|
125
|
+
function ensureNodeMaxOldSpaceSize(existing, megabytes) {
|
|
126
|
+
const token = '--max-old-space-size=';
|
|
127
|
+
if (typeof existing === 'string' && existing.includes(token)) {
|
|
128
|
+
return existing;
|
|
129
|
+
}
|
|
130
|
+
const option = `${token}${megabytes}`;
|
|
131
|
+
return existing && existing.trim() ? `${existing.trim()} ${option}` : option;
|
|
132
|
+
}
|
|
133
|
+
function isOutOfMemoryError(stderr) {
|
|
134
|
+
return /heap out of memory|Reached heap limit|Allocation failed - JavaScript heap out of memory/i.test(stderr || '');
|
|
135
|
+
}
|
|
136
|
+
function trimText(text, maxChars) {
|
|
137
|
+
if (!text)
|
|
138
|
+
return '';
|
|
139
|
+
const trimmed = text.trim();
|
|
140
|
+
if (trimmed.length <= maxChars)
|
|
141
|
+
return trimmed;
|
|
142
|
+
return trimmed.slice(trimmed.length - maxChars);
|
|
143
|
+
}
|
|
144
|
+
function buildLsFailureMessage(tool, code, stderr) {
|
|
145
|
+
if (isOutOfMemoryError(stderr)) {
|
|
146
|
+
return `${tool} ls ran out of memory while building the dependency tree`;
|
|
147
|
+
}
|
|
148
|
+
if (typeof code === 'number' && code !== 0) {
|
|
149
|
+
return `${tool} ls exited with code ${code}`;
|
|
150
|
+
}
|
|
151
|
+
if (code === null && stderr && stderr.trim()) {
|
|
152
|
+
return `${tool} ls failed before completion`;
|
|
153
|
+
}
|
|
154
|
+
return `Failed to parse ${tool} ls output`;
|
|
155
|
+
}
|
|
50
156
|
function parseJsonOutput(raw) {
|
|
51
157
|
if (!raw)
|
|
52
158
|
return undefined;
|
package/dist/utils.js
CHANGED
|
@@ -27,7 +27,8 @@ function runCommand(command, args, options = {}) {
|
|
|
27
27
|
return new Promise((resolve, reject) => {
|
|
28
28
|
const child = (0, child_process_1.spawn)(command, args, {
|
|
29
29
|
cwd: options.cwd,
|
|
30
|
-
shell: false
|
|
30
|
+
shell: false,
|
|
31
|
+
env: options.env ? { ...process.env, ...options.env } : process.env
|
|
31
32
|
});
|
|
32
33
|
const stdoutChunks = [];
|
|
33
34
|
const stderrChunks = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dependency-radar",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Local-first dependency analysis tool that generates a single HTML report showing risk, size, usage, and structure of your project's dependencies.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|