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 +54 -0
- package/bin/rank-subdeps.js +202 -0
- package/package.json +27 -0
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
|
+
}
|