rank-subdeps 1.0.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 ADDED
@@ -0,0 +1,54 @@
1
+ # rank-subdeps
2
+
3
+ Rank your top-level dependencies by how many transitive subdependencies they bring in.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i -g rank-subdeps
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ From a project directory (with `node_modules` installed):
14
+
15
+ ```bash
16
+ rank-subdeps
17
+ ```
18
+
19
+ ### Options
20
+
21
+ | Flag | Description |
22
+ |------|--------------|
23
+ | `--json` | Output machine-readable JSON |
24
+ | `--top N` | Show a “Top N” summary (default: 10) |
25
+ | `-h, --help` | Show help |
26
+
27
+ ### Example output
28
+
29
+ ```
30
+ # name wanted(range) installed dev? subdeps
31
+ - ------------- -------------- ---------- ----- -------
32
+ 1 express ^4.19.2 4.19.2 no 69
33
+ 2 typescript ^5.6.2 5.6.2 yes 10
34
+ 3 chalk ^5.3.0 5.3.0 no 2
35
+
36
+ Top 10 by subdependencies:
37
+ 1. express → 69 subdeps (4.19.2)
38
+ 2. typescript → 10 subdeps (5.6.2) [dev]
39
+ 3. chalk → 2 subdeps (5.3.0)
40
+ ```
41
+
42
+ ## How it works
43
+
44
+ The CLI runs:
45
+
46
+ ```bash
47
+ npm ls --all --json
48
+ ```
49
+
50
+ It then counts **unique subdependencies** by `(name@version)` for each top-level dependency (from both `dependencies` and `devDependencies`).
51
+
52
+ ## License
53
+
54
+ MIT © 2025 Ēriks Remess
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ // ESM CLI: rank-subdeps
3
+ // Ranks top-level deps by number of unique transitive subdependencies using `npm ls --all --json`.
4
+ //
5
+ // Usage:
6
+ // rank-subdeps
7
+ // rank-subdeps --json
8
+ // rank-subdeps --top 20
9
+ //
10
+ // Notes:
11
+ // - Counts unique subdeps by (name@version) excluding the package itself.
12
+ // - Includes both dependencies and devDependencies.
13
+ // - Requires an installed tree (node_modules). Run `npm i` first.
14
+
15
+ import { readFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { execFileSync } from 'node:child_process';
18
+
19
+ function readJSON(p) {
20
+ try {
21
+ return JSON.parse(readFileSync(p, 'utf8'));
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function loadPkgJson(root) {
28
+ const pkg = readJSON(join(root, 'package.json'));
29
+ if (!pkg) {
30
+ console.error('No package.json found in current directory.');
31
+ process.exit(1);
32
+ }
33
+ return pkg;
34
+ }
35
+
36
+ function runNpmLs(root) {
37
+ const bin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
38
+ try {
39
+ const out = execFileSync(bin, ['ls', '--all', '--json'], {
40
+ cwd: root,
41
+ stdio: ['ignore', 'pipe', 'pipe'],
42
+ });
43
+ return JSON.parse(out.toString('utf8'));
44
+ } catch (err) {
45
+ const stdout = err?.stdout?.toString('utf8');
46
+ if (stdout) {
47
+ try {
48
+ return JSON.parse(stdout);
49
+ } catch {}
50
+ }
51
+ console.error('Failed to run "npm ls --all --json".');
52
+ if (err?.stderr) console.error(String(err.stderr));
53
+ process.exit(1);
54
+ }
55
+ }
56
+
57
+ const makeId = (name, version) => `${name}@${version || 'UNKNOWN'}`;
58
+
59
+ function collectUniqueSubdeps(node) {
60
+ // Collect unique (name@version) for all descendants of `node`
61
+ const seen = new Set();
62
+ const stack = [];
63
+
64
+ if (!node || !node.dependencies) return seen;
65
+
66
+ for (const [name, child] of Object.entries(node.dependencies)) {
67
+ stack.push([name, child]);
68
+ }
69
+
70
+ while (stack.length) {
71
+ const [name, cur] = stack.pop();
72
+ const id = makeId(name, cur?.version);
73
+ if (seen.has(id)) continue;
74
+ seen.add(id);
75
+ if (cur && cur.dependencies) {
76
+ for (const [n2, c2] of Object.entries(cur.dependencies)) {
77
+ stack.push([n2, c2]);
78
+ }
79
+ }
80
+ }
81
+ return seen;
82
+ }
83
+
84
+ const pad = (str, len) => {
85
+ str = String(str);
86
+ return str.length >= len ? str : str + ' '.repeat(len - str.length);
87
+ };
88
+
89
+ function parseArgs(argv) {
90
+ const args = { json: false, top: 10 };
91
+ for (let i = 2; i < argv.length; i++) {
92
+ const a = argv[i];
93
+ if (a === '--json') {
94
+ args.json = true;
95
+ } else if (a === '--top') {
96
+ const n = Number(argv[i + 1]);
97
+ if (!Number.isNaN(n) && n > 0) args.top = n;
98
+ i++;
99
+ } else if (a === '-h' || a === '--help') {
100
+ printHelpAndExit();
101
+ } else {
102
+ console.error(`Unknown argument: ${a}`);
103
+ printHelpAndExit(1);
104
+ }
105
+ }
106
+ return args;
107
+ }
108
+
109
+ function printHelpAndExit(code = 0) {
110
+ console.log(`rank-subdeps
111
+
112
+ Rank top-level dependencies by unique transitive subdependencies.
113
+
114
+ Usage:
115
+ rank-subdeps [--json] [--top N]
116
+
117
+ Options:
118
+ --json Output machine-readable JSON instead of a table
119
+ --top N Number of items to include in the "Top N" summary (default: 10)
120
+ -h, --help Show this help
121
+ `);
122
+ process.exit(code);
123
+ }
124
+
125
+ (function main() {
126
+ const args = parseArgs(process.argv);
127
+ const root = process.cwd();
128
+ const pkg = loadPkgJson(root);
129
+ const tree = runNpmLs(root);
130
+
131
+ const topDeps = {
132
+ ...(pkg.dependencies || {}),
133
+ ...(pkg.devDependencies || {}),
134
+ };
135
+
136
+ const isDev = {};
137
+ for (const name of Object.keys(pkg.devDependencies || {})) isDev[name] = true;
138
+
139
+ const results = [];
140
+
141
+ for (const [name, wanted] of Object.entries(topDeps)) {
142
+ const node = tree.dependencies?.[name];
143
+ if (!node) {
144
+ results.push({
145
+ name,
146
+ wanted,
147
+ installed: 'NOT INSTALLED',
148
+ dev: !!isDev[name],
149
+ subdeps: 0,
150
+ });
151
+ continue;
152
+ }
153
+
154
+ const uniqueSub = collectUniqueSubdeps(node);
155
+ results.push({
156
+ name,
157
+ wanted,
158
+ installed: node.version || 'UNKNOWN',
159
+ dev: !!isDev[name],
160
+ subdeps: uniqueSub.size,
161
+ });
162
+ }
163
+
164
+ results.sort((a, b) => b.subdeps - a.subdeps || a.name.localeCompare(b.name));
165
+
166
+ if (args.json) {
167
+ // JSON mode: full dataset
168
+ console.log(JSON.stringify({ results }, null, 2));
169
+ return;
170
+ }
171
+
172
+ // Pretty table
173
+ const header = ['#', 'name', 'wanted(range)', 'installed', 'dev?', 'subdeps'];
174
+ const rows = [header];
175
+
176
+ results.forEach((r, idx) => {
177
+ rows.push([
178
+ String(idx + 1),
179
+ r.name,
180
+ r.wanted,
181
+ r.installed,
182
+ r.dev ? 'yes' : 'no',
183
+ String(r.subdeps),
184
+ ]);
185
+ });
186
+
187
+ const widths = rows[0].map((_, i) => Math.max(...rows.map(row => String(row[i]).length)));
188
+ const line = row => row.map((cell, i) => pad(cell, widths[i])).join(' ');
189
+
190
+ console.log(line(rows[0]));
191
+ console.log(widths.map(w => '-'.repeat(w)).join(' '));
192
+ for (let i = 1; i < rows.length; i++) console.log(line(rows[i]));
193
+
194
+ const topN = results.slice(0, args.top);
195
+ const maxNameLen = Math.max(...topN.map(x => x.name.length), 4);
196
+ console.log(`\nTop ${args.top} by subdependencies:`);
197
+ topN.forEach((r, i) => {
198
+ console.log(
199
+ `${String(i + 1).padStart(2, ' ')}. ${pad(r.name, maxNameLen)} → ${r.subdeps} subdeps (${r.installed})${r.dev ? ' [dev]' : ''}`
200
+ );
201
+ });
202
+ })();
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "rank-subdeps",
3
+ "version": "1.0.0",
4
+ "description": "Rank top-level dependencies by number of transitive subdependencies",
5
+ "type": "module",
6
+ "bin": {
7
+ "rank-subdeps": "bin/rank-subdeps.js"
8
+ },
9
+ "files": [
10
+ "bin/"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Ēriks Remess <eriks@remess.lv>",
14
+ "keywords": [
15
+ "npm",
16
+ "dependencies",
17
+ "subdependencies",
18
+ "cli",
19
+ "analysis"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ }
27
+ }