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 CHANGED
@@ -1,22 +1,34 @@
1
1
  # Dependency Radar
2
2
 
3
- Dependency Radar is a local-first 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, size, licences, vulnerabilities, and other signals that help you understand risk and complexity hidden in your node_modules folder.
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 using only local data (no SaaS, no uploads by default)
8
- - Combines multiple tools (npm audit, npm ls, import graph analysis) into a single report
9
- - Shows direct vs sub-dependencies, dependency depth, and parent relationships
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 or archive
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 node_modules installed (run npm install first).
101
- - The scan is local-first and does not upload your code or dependencies anywhere.
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 a tool fails, its section is marked as unavailable, but the report is still generated.
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).catch((err) => ({ ok: false, error: String(err) })),
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 lsFailure = perPackageLs.find((r) => r && !r.ok);
833
- const importFailure = perPackageImportGraph.find((r) => r && !r.ok);
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 (lsFailure || importFailure) {
838
- const err = lsFailure || importFailure;
839
- throw new Error((err === null || err === void 0 ? void 0 : err.error) || "Tool execution failed");
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 (opts.json) {
869
- await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
870
- await promises_1.default.writeFile(outputPath, JSON.stringify(aggregated, null, 2), "utf8");
871
- }
872
- else {
873
- await (0, report_1.renderReport)(aggregated, outputPath);
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
- console.log(`✔ ${opts.json ? "JSON" : "Report"} written to ${outputPath}`);
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
@@ -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 && result.code !== 0
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.0",
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": {