plexus-peers 0.2.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 +85 -0
- package/bin/plexus.js +134 -0
- package/lib/ansi.js +19 -0
- package/lib/engine.js +31 -0
- package/lib/fix.js +109 -0
- package/lib/format.js +17 -0
- package/lib/graph.js +100 -0
- package/lib/html-escape.js +22 -0
- package/lib/npm.js +85 -0
- package/lib/render-html.js +541 -0
- package/lib/run-analysis.js +181 -0
- package/lib/server.js +77 -0
- package/lib/terminal.js +91 -0
- package/package.json +31 -0
- package/public/index.html +217 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Plexus
|
|
2
|
+
|
|
3
|
+
CLI and a small web UI to inspect **direct dependencies** from a `package.json`: peer dependency ranges vs what is installed, optional **BundlePhobia**-style bundle size hints, and a **`--fix`** mode that queries npm for upgrade and cascade notes.
|
|
4
|
+
|
|
5
|
+
Requires **Node.js 18+** (uses the built-in `fetch` API).
|
|
6
|
+
|
|
7
|
+
The npm package is **`plexus-peers`** (the short name `plexus` is already used on the registry by another project).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
From npm:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx plexus-peers --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
From a clone of this repo:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
node bin/plexus.js --help
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## CLI
|
|
25
|
+
|
|
26
|
+
Run from a project that has `package.json` (and usually `node_modules` after `npm install`):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx plexus-peers # Terminal report for the current directory
|
|
30
|
+
npx plexus-peers --dir ./my-app # Another project root
|
|
31
|
+
npx plexus-peers -f ./path/to/package.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Option | Description |
|
|
35
|
+
|--------|-------------|
|
|
36
|
+
| `--html` | Write an HTML report (`dep-graph.html` in the project root, or use `--out`) |
|
|
37
|
+
| `--out <path>` | HTML output path (with `--html`) |
|
|
38
|
+
| `--conflicts-only` | Only list packages that have peer issues |
|
|
39
|
+
| `--fix` | Call `npm` for latest metadata and print resolution hints (chatty; avoid on CI if logs matter) |
|
|
40
|
+
| `--bundlesize` | With `--html`, query [BundlePhobia](https://bundlephobia.com/) per direct dependency (slow, rate limits may apply) |
|
|
41
|
+
| `--pkg <name>` | Focus on one direct dependency and related peer rows |
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx plexus-peers --html
|
|
47
|
+
npx plexus-peers --html --fix --bundlesize --dir ./my-app
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Web UI (`serve`)
|
|
51
|
+
|
|
52
|
+
Starts a local server with a page to **upload** a `package.json`. The report is generated from the **npm registry** (semver resolution for each direct dependency). There is **no** local `node_modules`, so disk sizes are absent and only dependencies declared in the manifest contribute resolved versions for peers.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx plexus-peers serve
|
|
56
|
+
# http://127.0.0.1:3847 (override with --port)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
From the repo you can also run:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm start
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Optional checkboxes on the page match `--fix` and `--bundlesize`.
|
|
66
|
+
|
|
67
|
+
## CLI vs upload
|
|
68
|
+
|
|
69
|
+
| | CLI (filesystem) | Web upload |
|
|
70
|
+
|--|------------------|------------|
|
|
71
|
+
| **Versions** | Read from `node_modules` | Resolved from registry against ranges in `package.json` |
|
|
72
|
+
| **Disk size** | `du` on `node_modules` entries | Not available |
|
|
73
|
+
| **Peers only transitive** | Can still be read from disk if present | Only if that package is also a direct dependency |
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install
|
|
79
|
+
npm run plexus -- --help
|
|
80
|
+
npm start # serve UI on default port
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT
|
package/bin/plexus.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { runFilesystemAnalysis } = require('../lib/engine');
|
|
6
|
+
const { startServer } = require('../lib/server');
|
|
7
|
+
|
|
8
|
+
function printHelp() {
|
|
9
|
+
console.log(`
|
|
10
|
+
Plexus — peer dependencies, sizes, resolution hints
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
npx plexus-peers [options] Analyze package.json in current directory
|
|
14
|
+
npx plexus-peers serve [options] Start upload UI + API
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--dir, -C <path> Project root (contains package.json and node_modules)
|
|
18
|
+
--file, -f <path> Path to package.json (root = its directory)
|
|
19
|
+
--html Write HTML report (dep-graph.html in project root)
|
|
20
|
+
--out <path> HTML output path (with --html)
|
|
21
|
+
--conflicts-only Terminal: only packages with peer issues
|
|
22
|
+
--fix Query npm for upgrade / cascade hints (noisy on CI)
|
|
23
|
+
--bundlesize With --html: fetch gzip sizes from BundlePhobia (slow)
|
|
24
|
+
--pkg <name> Focus on one direct dependency and its peers
|
|
25
|
+
|
|
26
|
+
Serve:
|
|
27
|
+
--port <n> Port (default: 3847)
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
npx plexus-peers --html --dir ./my-app
|
|
31
|
+
npx plexus-peers serve --port 3847
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseArgv(argv) {
|
|
36
|
+
const out = {
|
|
37
|
+
command: 'analyze',
|
|
38
|
+
rootDir: null,
|
|
39
|
+
packageJsonPath: null,
|
|
40
|
+
port: 3847,
|
|
41
|
+
flags: {
|
|
42
|
+
conflictsOnly: false,
|
|
43
|
+
html: false,
|
|
44
|
+
fix: false,
|
|
45
|
+
bundleSize: false,
|
|
46
|
+
focusPkg: null,
|
|
47
|
+
outFile: null,
|
|
48
|
+
},
|
|
49
|
+
help: false,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < argv.length; i++) {
|
|
53
|
+
const a = argv[i];
|
|
54
|
+
if (a === 'serve' || a === 'web') {
|
|
55
|
+
out.command = 'serve';
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (a === '--help' || a === '-h') {
|
|
59
|
+
out.help = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (a === '--dir' || a === '-C') {
|
|
63
|
+
out.rootDir = path.resolve(argv[++i] ?? '');
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (a === '--file' || a === '-f') {
|
|
67
|
+
out.packageJsonPath = path.resolve(argv[++i] ?? '');
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (a === '--port') {
|
|
71
|
+
out.port = Number(argv[++i]) || 3847;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (a === '--out') {
|
|
75
|
+
out.flags.outFile = path.resolve(argv[++i] ?? '');
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (a === '--pkg') {
|
|
79
|
+
out.flags.focusPkg = argv[++i] ?? null;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (a === '--conflicts-only') {
|
|
83
|
+
out.flags.conflictsOnly = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (a === '--html') {
|
|
87
|
+
out.flags.html = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (a === '--fix') {
|
|
91
|
+
out.flags.fix = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (a === '--bundlesize') {
|
|
95
|
+
out.flags.bundleSize = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (a.startsWith('-')) {
|
|
99
|
+
console.error(`Unknown option: ${a}\nRun npx plexus-peers --help`);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (out.packageJsonPath) {
|
|
106
|
+
out.rootDir = path.dirname(out.packageJsonPath);
|
|
107
|
+
}
|
|
108
|
+
if (!out.rootDir) {
|
|
109
|
+
out.rootDir = process.cwd();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function main() {
|
|
116
|
+
const parsed = parseArgv(process.argv.slice(2));
|
|
117
|
+
if (!parsed) return;
|
|
118
|
+
if (parsed.help) {
|
|
119
|
+
printHelp();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (parsed.command === 'serve') {
|
|
124
|
+
startServer({ port: parsed.port });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await runFilesystemAnalysis({ rootDir: parsed.rootDir, flags: parsed.flags });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main().catch(err => {
|
|
132
|
+
console.error(err);
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
});
|
package/lib/ansi.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const c = {
|
|
4
|
+
reset: '\x1b[0m',
|
|
5
|
+
bold: '\x1b[1m',
|
|
6
|
+
red: '\x1b[31m',
|
|
7
|
+
yellow: '\x1b[33m',
|
|
8
|
+
green: '\x1b[32m',
|
|
9
|
+
cyan: '\x1b[36m',
|
|
10
|
+
gray: '\x1b[90m',
|
|
11
|
+
blue: '\x1b[34m',
|
|
12
|
+
magenta: '\x1b[35m',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function color(str, ...codes) {
|
|
16
|
+
return codes.map(code => c[code]).join('') + str + c.reset;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { color };
|
package/lib/engine.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plexus engine — dependency graph + peer conflict detection (filesystem or registry).
|
|
5
|
+
* Re-exports modular pieces; implementation lives in ./ansi, ./format, ./graph, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { color } = require('./ansi');
|
|
9
|
+
const { formatBytes } = require('./format');
|
|
10
|
+
const { createFsContext, buildGraph } = require('./graph');
|
|
11
|
+
const { printGraph, printSummary } = require('./terminal');
|
|
12
|
+
const { resolveConflicts } = require('./fix');
|
|
13
|
+
const { queryNpm, getBundleSize } = require('./npm');
|
|
14
|
+
const { renderHtml } = require('./render-html');
|
|
15
|
+
const { openHtmlReport, runFilesystemAnalysis, runRegistryAnalysis } = require('./run-analysis');
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
color,
|
|
19
|
+
formatBytes,
|
|
20
|
+
buildGraph,
|
|
21
|
+
createFsContext,
|
|
22
|
+
printGraph,
|
|
23
|
+
printSummary,
|
|
24
|
+
resolveConflicts,
|
|
25
|
+
queryNpm,
|
|
26
|
+
renderHtml,
|
|
27
|
+
getBundleSize,
|
|
28
|
+
runFilesystemAnalysis,
|
|
29
|
+
runRegistryAnalysis,
|
|
30
|
+
openHtmlReport,
|
|
31
|
+
};
|
package/lib/fix.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { color } = require('./ansi');
|
|
4
|
+
const { satisfies } = require('./graph');
|
|
5
|
+
const { queryNpm } = require('./npm');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* For each real conflict (version mismatch, not just missing optional):
|
|
9
|
+
* 1. Query npm for the latest version of the conflicting package.
|
|
10
|
+
* 2. Check if latest fixes the conflict (its new peer range accepts the installed version).
|
|
11
|
+
* 3. Check if latest introduces NEW conflicts (cascade detection).
|
|
12
|
+
*/
|
|
13
|
+
function resolveConflicts(graph, directDeps, getInstalledVersion, options = {}) {
|
|
14
|
+
const silent = options.silent === true;
|
|
15
|
+
const log = (...args) => {
|
|
16
|
+
if (!silent) console.log(...args);
|
|
17
|
+
};
|
|
18
|
+
const write = (...args) => {
|
|
19
|
+
if (!silent) process.stdout.write(...args);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const directSet = new Set(Object.keys(directDeps));
|
|
23
|
+
|
|
24
|
+
const conflicts = [];
|
|
25
|
+
for (const [pkgName, info] of Object.entries(graph)) {
|
|
26
|
+
if (!directSet.has(pkgName)) continue;
|
|
27
|
+
for (const peer of info.peerDeps) {
|
|
28
|
+
if (!peer.ok && peer.installed) {
|
|
29
|
+
conflicts.push({
|
|
30
|
+
pkg: pkgName,
|
|
31
|
+
peer: peer.name,
|
|
32
|
+
range: peer.range,
|
|
33
|
+
installed: peer.installed,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (conflicts.length === 0) return { resolutions: {}, cascades: [], suggestedDeps: null };
|
|
40
|
+
|
|
41
|
+
const pkgsToCheck = [...new Set(conflicts.map(c => c.pkg))];
|
|
42
|
+
|
|
43
|
+
log(color(`\nQuerying npm for ${pkgsToCheck.length} package(s)…`, 'gray'));
|
|
44
|
+
|
|
45
|
+
const resolutions = {};
|
|
46
|
+
|
|
47
|
+
for (const pkg of pkgsToCheck) {
|
|
48
|
+
write(` ${color(pkg, 'cyan')} … `);
|
|
49
|
+
const info = queryNpm(pkg);
|
|
50
|
+
if (!info) {
|
|
51
|
+
log(color('not found on npm', 'yellow'));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const latest = info['dist-tags']?.latest;
|
|
56
|
+
if (!latest) {
|
|
57
|
+
log(color('no dist-tags.latest', 'yellow'));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const newPeerDeps = info.peerDependencies ?? {};
|
|
62
|
+
const currentVersion = directDeps[pkg];
|
|
63
|
+
|
|
64
|
+
const fixes = conflicts
|
|
65
|
+
.filter(c => c.pkg === pkg)
|
|
66
|
+
.filter(c => {
|
|
67
|
+
const newRange = newPeerDeps[c.peer];
|
|
68
|
+
if (!newRange) return true;
|
|
69
|
+
return satisfies(c.installed, newRange);
|
|
70
|
+
})
|
|
71
|
+
.map(c => c.peer);
|
|
72
|
+
|
|
73
|
+
const stillConflicts = conflicts
|
|
74
|
+
.filter(c => c.pkg === pkg && !fixes.includes(c.peer))
|
|
75
|
+
.map(c => c.peer);
|
|
76
|
+
|
|
77
|
+
const cascades = Object.entries(newPeerDeps)
|
|
78
|
+
.map(([peerName, peerRange]) => {
|
|
79
|
+
const installedVer = getInstalledVersion(peerName);
|
|
80
|
+
if (!installedVer) return null;
|
|
81
|
+
const ok = satisfies(installedVer, peerRange);
|
|
82
|
+
return ok ? null : { peerName, peerRange, installedVer };
|
|
83
|
+
})
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
|
|
86
|
+
log(
|
|
87
|
+
latest === currentVersion.replace(/[\^~>=<]/g, '')
|
|
88
|
+
? color(`already latest (v${latest})`, 'gray')
|
|
89
|
+
: color(`v${currentVersion} → v${latest}`, 'green'),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
resolutions[pkg] = {
|
|
93
|
+
current: currentVersion,
|
|
94
|
+
latest,
|
|
95
|
+
fixes,
|
|
96
|
+
stillConflicts,
|
|
97
|
+
cascades,
|
|
98
|
+
newPeerDeps,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const upgrades = Object.fromEntries(
|
|
103
|
+
Object.entries(resolutions).map(([pkg, r]) => [pkg, r.latest]),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return { resolutions, upgrades };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { resolveConflicts };
|
package/lib/format.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function formatBytes(bytes) {
|
|
4
|
+
if (bytes === null || bytes === undefined) return null;
|
|
5
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
6
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
7
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function sizeClass(bytes) {
|
|
11
|
+
if (!bytes) return '';
|
|
12
|
+
if (bytes > 5 * 1024 * 1024) return 'size-large';
|
|
13
|
+
if (bytes > 1 * 1024 * 1024) return 'size-medium';
|
|
14
|
+
return 'size-small';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { formatBytes, sizeClass };
|
package/lib/graph.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
const semver = require('semver');
|
|
7
|
+
|
|
8
|
+
function getDiskSize(pkgName, nodeModulesPath) {
|
|
9
|
+
const pkgPath = path.join(nodeModulesPath, pkgName);
|
|
10
|
+
try {
|
|
11
|
+
const out = execFileSync('du', ['-sk', pkgPath], {
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
timeout: 8000,
|
|
14
|
+
maxBuffer: 256 * 1024,
|
|
15
|
+
});
|
|
16
|
+
const kb = parseInt(out.split(/\s/)[0], 10);
|
|
17
|
+
return isNaN(kb) ? null : kb * 1024;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createFsContext(rootDir) {
|
|
24
|
+
const nodeModulesPath = path.join(rootDir, 'node_modules');
|
|
25
|
+
function readPkg(pkgName) {
|
|
26
|
+
const pkgPath = path.join(nodeModulesPath, pkgName, 'package.json');
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function getInstalledVersion(pkgName) {
|
|
34
|
+
return readPkg(pkgName)?.version ?? null;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
nodeModulesPath,
|
|
38
|
+
readPkg,
|
|
39
|
+
getDiskSize: pkgName => getDiskSize(pkgName, nodeModulesPath),
|
|
40
|
+
getInstalledVersion,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function satisfies(installedVersion, requiredRange) {
|
|
45
|
+
if (!installedVersion) return false;
|
|
46
|
+
try {
|
|
47
|
+
const coerced = semver.coerce(installedVersion);
|
|
48
|
+
if (!coerced) return false;
|
|
49
|
+
return semver.satisfies(coerced, requiredRange, { includePrerelease: true });
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildGraph(directDeps, ctx) {
|
|
56
|
+
const { readPkg, getDiskSize, getInstalledVersion } = ctx;
|
|
57
|
+
const graph = {};
|
|
58
|
+
|
|
59
|
+
const toProcess = [...Object.keys(directDeps)];
|
|
60
|
+
const visited = new Set();
|
|
61
|
+
|
|
62
|
+
while (toProcess.length > 0) {
|
|
63
|
+
const pkgName = toProcess.shift();
|
|
64
|
+
if (visited.has(pkgName)) continue;
|
|
65
|
+
visited.add(pkgName);
|
|
66
|
+
|
|
67
|
+
const pkg = readPkg(pkgName);
|
|
68
|
+
if (!pkg) {
|
|
69
|
+
graph[pkgName] = { version: null, missing: true, peerDeps: [], requiredBy: [] };
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const peerDeps = pkg.peerDependencies ?? {};
|
|
74
|
+
const peerEntries = Object.entries(peerDeps).map(([name, range]) => {
|
|
75
|
+
const installed = getInstalledVersion(name);
|
|
76
|
+
const ok = satisfies(installed, range);
|
|
77
|
+
return { name, range, installed, ok };
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
graph[pkgName] = {
|
|
81
|
+
version: pkg.version,
|
|
82
|
+
missing: false,
|
|
83
|
+
diskSize: getDiskSize(pkgName),
|
|
84
|
+
peerDeps: peerEntries,
|
|
85
|
+
requiredBy: [],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
for (const { name } of peerEntries) {
|
|
89
|
+
if (!graph[name])
|
|
90
|
+
graph[name] = { version: null, missing: false, peerDeps: [], requiredBy: [] };
|
|
91
|
+
if (!graph[name].requiredBy.includes(pkgName)) {
|
|
92
|
+
graph[name].requiredBy.push(pkgName);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return graph;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { createFsContext, buildGraph, satisfies };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function escapeHtml(s) {
|
|
4
|
+
if (s == null || s === '') return '';
|
|
5
|
+
return String(s)
|
|
6
|
+
.replace(/&/g, '&')
|
|
7
|
+
.replace(/</g, '<')
|
|
8
|
+
.replace(/>/g, '>')
|
|
9
|
+
.replace(/"/g, '"')
|
|
10
|
+
.replace(/'/g, ''');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function npmPackageUrl(name) {
|
|
14
|
+
return `https://www.npmjs.com/package/${encodeURIComponent(String(name))}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Safe `id` / fragment slug for package names (not HTML-escaped — use only in id/href fragments). */
|
|
18
|
+
function pkgSlugForDom(name) {
|
|
19
|
+
return String(name).replace(/[@/]/g, '-').replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { escapeHtml, npmPackageUrl, pkgSlugForDom };
|
package/lib/npm.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
const semver = require('semver');
|
|
5
|
+
|
|
6
|
+
function queryNpm(pkg) {
|
|
7
|
+
try {
|
|
8
|
+
const out = execFileSync('npm', ['info', pkg, '--json'], {
|
|
9
|
+
encoding: 'utf8',
|
|
10
|
+
timeout: 12000,
|
|
11
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
12
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
13
|
+
}).trim();
|
|
14
|
+
return out ? JSON.parse(out) : null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function getBundleSize(pkgName, version) {
|
|
21
|
+
try {
|
|
22
|
+
const url = `https://bundlephobia.com/api/size?package=${encodeURIComponent(pkgName)}@${version}`;
|
|
23
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(10000) });
|
|
24
|
+
if (!res.ok) return null;
|
|
25
|
+
const data = await res.json();
|
|
26
|
+
return { size: data.size, gzip: data.gzip };
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function fetchResolvedPackageManifest(name, range) {
|
|
33
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(name)}`;
|
|
34
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(20000) });
|
|
35
|
+
if (!res.ok) return null;
|
|
36
|
+
const data = await res.json();
|
|
37
|
+
const versions = Object.keys(data.versions || {});
|
|
38
|
+
const best = semver.maxSatisfying(versions, range, { includePrerelease: true });
|
|
39
|
+
if (!best) return null;
|
|
40
|
+
return data.versions[best];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function registryFetchConcurrency() {
|
|
44
|
+
const n = Number(process.env.PLEXUS_REGISTRY_CONCURRENCY || 10);
|
|
45
|
+
return Math.max(1, Math.min(50, n > 0 ? n : 10));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function createRegistryContext(directDeps) {
|
|
49
|
+
const pkgs = new Map();
|
|
50
|
+
const names = Object.keys(directDeps);
|
|
51
|
+
const limit = registryFetchConcurrency();
|
|
52
|
+
for (let i = 0; i < names.length; i += limit) {
|
|
53
|
+
const batch = names.slice(i, i + limit);
|
|
54
|
+
await Promise.all(
|
|
55
|
+
batch.map(async name => {
|
|
56
|
+
try {
|
|
57
|
+
const manifest = await fetchResolvedPackageManifest(name, directDeps[name]);
|
|
58
|
+
pkgs.set(name, manifest);
|
|
59
|
+
} catch {
|
|
60
|
+
pkgs.set(name, null);
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readPkg(pkgName) {
|
|
67
|
+
return pkgs.get(pkgName) ?? null;
|
|
68
|
+
}
|
|
69
|
+
function getInstalledVersion(pkgName) {
|
|
70
|
+
return readPkg(pkgName)?.version ?? null;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
readPkg,
|
|
74
|
+
getDiskSize: () => null,
|
|
75
|
+
getInstalledVersion,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
queryNpm,
|
|
81
|
+
getBundleSize,
|
|
82
|
+
fetchResolvedPackageManifest,
|
|
83
|
+
createRegistryContext,
|
|
84
|
+
registryFetchConcurrency,
|
|
85
|
+
};
|