pnpm-viz 1.0.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 +86 -0
- package/dist/cli/app.js +42 -0
- package/dist/cli/export.js +16 -0
- package/dist/cli/index.js +36 -0
- package/dist/cli/server.js +89 -0
- package/dist/core/analysis.js +37 -0
- package/dist/core/parser.js +157 -0
- package/dist/core/types.js +1 -0
- package/dist/web/assets/main-BkCAhqoF.js +382 -0
- package/dist/web/index.html +12 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# pnpm-viz
|
|
2
|
+
|
|
3
|
+
**Visual companion for pnpm**
|
|
4
|
+
|
|
5
|
+
`pnpm-viz` parses your `pnpm-lock.yaml` and `pnpm-workspace.yaml` to generate interactive dependency graphs, version conflict heatmaps, and workspace flow diagrams.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Interactive CLI**: View workspace structure and duplicate stats directly in your terminal.
|
|
10
|
+
- **Heatmap Analysis**: Identify duplicate packages and potential version conflicts.
|
|
11
|
+
- **Web Dashboard**: Explore your dependency graph interactively in the browser.
|
|
12
|
+
- **Export**: Export your dependency graph to JSON for further analysis.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g pnpm-viz
|
|
18
|
+
# OR run directly
|
|
19
|
+
npx pnpm-viz
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### CLI Overview
|
|
25
|
+
|
|
26
|
+
Run in your project root:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm-viz .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Heatmap Mode
|
|
33
|
+
|
|
34
|
+
View duplicate packages in the terminal:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm-viz . --heatmap
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Web Dashboard
|
|
41
|
+
|
|
42
|
+
Launch the interactive web visualization:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pnpm-viz serve
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Export Data
|
|
49
|
+
|
|
50
|
+
Export the parsed graph to JSON:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pnpm-viz export . -o graph.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
1. Clone the repo.
|
|
59
|
+
2. Install dependencies: `npm install`
|
|
60
|
+
3. Build the project: `npm run build`
|
|
61
|
+
4. Run locally: `node dist/cli/index.js .`
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## Publishing
|
|
65
|
+
|
|
66
|
+
To publish a new version to npm:
|
|
67
|
+
|
|
68
|
+
1. **Login to npm** (if not already logged in):
|
|
69
|
+
```bash
|
|
70
|
+
npm login
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
2. **Bump the version**:
|
|
74
|
+
```bash
|
|
75
|
+
npm version patch # or minor, major
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
3. **Publish**:
|
|
79
|
+
```bash
|
|
80
|
+
npm publish
|
|
81
|
+
```
|
|
82
|
+
(This will automatically run `npm run build` before publishing)
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
package/dist/cli/app.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Text, Box, Newline } from 'ink';
|
|
4
|
+
import BigText from 'ink-big-text';
|
|
5
|
+
import { LockfileParser } from '../core/parser.js';
|
|
6
|
+
import { GraphAnalyzer } from '../core/analysis.js';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
const App = ({ path: projectPath, heatmap }) => {
|
|
9
|
+
const [graph, setGraph] = useState(null);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const loadGraph = async () => {
|
|
14
|
+
try {
|
|
15
|
+
const lockfilePath = path.join(path.resolve(projectPath), 'pnpm-lock.yaml');
|
|
16
|
+
const parser = new LockfileParser(lockfilePath);
|
|
17
|
+
const g = await parser.parse();
|
|
18
|
+
setGraph(g);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
setError(err.message);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
setLoading(false);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
loadGraph();
|
|
28
|
+
}, [projectPath]);
|
|
29
|
+
if (loading) {
|
|
30
|
+
return _jsx(Text, { children: "Loading pnpm-lock.yaml..." });
|
|
31
|
+
}
|
|
32
|
+
if (error) {
|
|
33
|
+
return _jsxs(Text, { color: "red", children: ["Error: ", error] });
|
|
34
|
+
}
|
|
35
|
+
if (!graph)
|
|
36
|
+
return null;
|
|
37
|
+
const analyzer = new GraphAnalyzer(graph);
|
|
38
|
+
const duplicates = analyzer.getDuplicates();
|
|
39
|
+
const stats = analyzer.getStats();
|
|
40
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", children: [_jsx(BigText, { text: "pnpm-viz", font: "chrome", colors: ['green', 'cyan'] }), _jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "Analyzed Project:" }), " ", path.resolve(projectPath)] }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "\u2714 Packages:" }), " ", stats.totalPackages] }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "\u2714 Dependencies:" }), " ", stats.totalDependencies] }), _jsxs(Text, { children: [_jsxs(Text, { color: stats.duplicateCount > 0 ? "yellow" : "green", bold: true, children: [stats.duplicateCount > 0 ? "⚠" : "✔", " Duplicates:"] }), " ", stats.duplicateCount] })] }), heatmap ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, underline: true, color: "yellow", backgroundColor: "black", children: " DUPLICATE PACKAGES HEATMAP " }), _jsx(Newline, {}), duplicates.length === 0 ? (_jsx(Text, { color: "green", children: "No duplicates found. Great job!" })) : (duplicates.slice(0, 10).map((dup, i) => (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", width: "80%", children: [_jsxs(Text, { bold: true, children: [i + 1, ". ", dup.name] }), _jsxs(Box, { children: [_jsxs(Text, { color: "red", children: [" ", dup.count, " versions "] }), _jsxs(Text, { dimColor: true, children: [" (", dup.versions.join(', '), ")"] })] })] }, dup.name)))), duplicates.length > 10 && _jsxs(Text, { dimColor: true, children: ["...and ", duplicates.length - 10, " more."] })] })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, underline: true, color: "cyan", children: "WORKSPACE IMPORTERS" }), Object.values(graph.nodes).filter(n => n.path && !n.path.includes('node_modules') && n.id !== 'root').map(node => (_jsxs(Text, { children: [" \u2022 ", node.name] }, node.id))), _jsx(Newline, {}), _jsx(Text, { dimColor: true, children: "Run with --heatmap to see duplicates." }), _jsx(Text, { dimColor: true, children: "Run 'pnpm-viz serve' to view graph." })] }))] }));
|
|
41
|
+
};
|
|
42
|
+
export default App;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { LockfileParser } from '../core/parser.js';
|
|
4
|
+
export async function exportGraph(projectPath, outputFile) {
|
|
5
|
+
try {
|
|
6
|
+
const lockfilePath = path.join(path.resolve(projectPath), 'pnpm-lock.yaml');
|
|
7
|
+
const parser = new LockfileParser(lockfilePath);
|
|
8
|
+
const graph = await parser.parse();
|
|
9
|
+
fs.writeFileSync(outputFile, JSON.stringify(graph, null, 2));
|
|
10
|
+
console.log(`Graph exported to ${outputFile}`);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
console.error('Export failed:', err.message);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import App from './app.js';
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program
|
|
8
|
+
.name('pnpm-viz')
|
|
9
|
+
.description('Visualize your pnpm dependencies')
|
|
10
|
+
.version('1.0.0')
|
|
11
|
+
.argument('[path]', 'Path to the project', '.')
|
|
12
|
+
.option('--heatmap', 'Show heatmap mode')
|
|
13
|
+
.action((path, options) => {
|
|
14
|
+
render(_jsx(App, { path: path, heatmap: options.heatmap }));
|
|
15
|
+
});
|
|
16
|
+
program
|
|
17
|
+
.command('serve')
|
|
18
|
+
.description('Launch the web dashboard')
|
|
19
|
+
.argument('[path]', 'Path to the project', '.')
|
|
20
|
+
.option('-p, --port <number>', 'Port to run on', '3000')
|
|
21
|
+
.action((path, options) => {
|
|
22
|
+
import('./server.js').then(module => {
|
|
23
|
+
module.startServer(path, parseInt(options.port));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
program
|
|
27
|
+
.command('export')
|
|
28
|
+
.description('Export graph as JSON')
|
|
29
|
+
.argument('[path]', 'Path to the project', '.')
|
|
30
|
+
.option('-o, --output <file>', 'Output file path', 'graph.json')
|
|
31
|
+
.action((path, options) => {
|
|
32
|
+
import('./export.js').then(module => {
|
|
33
|
+
module.exportGraph(path, options.output);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
program.parse();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import open from 'open';
|
|
6
|
+
import { LockfileParser } from '../core/parser.js';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
// dist/cli/server.js -> dist/web
|
|
10
|
+
const WEB_ROOT = path.resolve(__dirname, '../../dist/web');
|
|
11
|
+
export async function startServer(projectPath, port = 3000) {
|
|
12
|
+
const server = http.createServer(async (req, res) => {
|
|
13
|
+
// API Endpoint
|
|
14
|
+
if (req.url === '/api/graph') {
|
|
15
|
+
try {
|
|
16
|
+
const lockfilePath = path.join(path.resolve(projectPath), 'pnpm-lock.yaml');
|
|
17
|
+
const parser = new LockfileParser(lockfilePath);
|
|
18
|
+
const graph = await parser.parse();
|
|
19
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
20
|
+
res.end(JSON.stringify(graph));
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
24
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Static File Serving
|
|
29
|
+
let filePath = path.join(WEB_ROOT, req.url === '/' ? 'index.html' : req.url || 'index.html');
|
|
30
|
+
// Security check to prevent directory traversal
|
|
31
|
+
if (!filePath.startsWith(WEB_ROOT)) {
|
|
32
|
+
res.writeHead(403);
|
|
33
|
+
res.end('Forbidden');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const extname = path.extname(filePath);
|
|
37
|
+
let contentType = 'text/html';
|
|
38
|
+
switch (extname) {
|
|
39
|
+
case '.js':
|
|
40
|
+
contentType = 'text/javascript';
|
|
41
|
+
break;
|
|
42
|
+
case '.css':
|
|
43
|
+
contentType = 'text/css';
|
|
44
|
+
break;
|
|
45
|
+
case '.json':
|
|
46
|
+
contentType = 'application/json';
|
|
47
|
+
break;
|
|
48
|
+
case '.png':
|
|
49
|
+
contentType = 'image/png';
|
|
50
|
+
break;
|
|
51
|
+
case '.jpg':
|
|
52
|
+
contentType = 'image/jpg';
|
|
53
|
+
break;
|
|
54
|
+
case '.svg':
|
|
55
|
+
contentType = 'image/svg+xml';
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
fs.readFile(filePath, (error, content) => {
|
|
59
|
+
if (error) {
|
|
60
|
+
if (error.code === 'ENOENT') {
|
|
61
|
+
// SPA Fallback necessary? React Router? Not using router yet.
|
|
62
|
+
// basic fallback
|
|
63
|
+
fs.readFile(path.join(WEB_ROOT, 'index.html'), (err, content) => {
|
|
64
|
+
if (err) {
|
|
65
|
+
res.writeHead(404);
|
|
66
|
+
res.end('Not Found');
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
70
|
+
res.end(content, 'utf-8');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
res.writeHead(500);
|
|
76
|
+
res.end('Server Error: ' + error.code);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
81
|
+
res.end(content, 'utf-8');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
server.listen(port, () => {
|
|
86
|
+
console.log(`Server running at http://localhost:${port}/`);
|
|
87
|
+
open(`http://localhost:${port}/`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export class GraphAnalyzer {
|
|
2
|
+
constructor(graph) {
|
|
3
|
+
this.graph = graph;
|
|
4
|
+
}
|
|
5
|
+
getDuplicates() {
|
|
6
|
+
const packageMap = new Map();
|
|
7
|
+
Object.values(this.graph.nodes).forEach(node => {
|
|
8
|
+
if (node.id === 'root' || node.path === '.')
|
|
9
|
+
return;
|
|
10
|
+
const name = node.name;
|
|
11
|
+
const version = node.version;
|
|
12
|
+
if (!packageMap.has(name)) {
|
|
13
|
+
packageMap.set(name, new Set());
|
|
14
|
+
}
|
|
15
|
+
packageMap.get(name)?.add(version);
|
|
16
|
+
});
|
|
17
|
+
const duplicates = [];
|
|
18
|
+
packageMap.forEach((versions, name) => {
|
|
19
|
+
if (versions.size > 1) {
|
|
20
|
+
duplicates.push({
|
|
21
|
+
name,
|
|
22
|
+
versions: Array.from(versions),
|
|
23
|
+
count: versions.size
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
// Sort by count descending
|
|
28
|
+
return duplicates.sort((a, b) => b.count - a.count);
|
|
29
|
+
}
|
|
30
|
+
getStats() {
|
|
31
|
+
return {
|
|
32
|
+
totalPackages: Object.keys(this.graph.nodes).length,
|
|
33
|
+
totalDependencies: this.graph.edges.length,
|
|
34
|
+
duplicateCount: this.getDuplicates().length
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export class LockfileParser {
|
|
5
|
+
constructor(lockfilePath) {
|
|
6
|
+
this.lockfilePath = lockfilePath;
|
|
7
|
+
}
|
|
8
|
+
async parse() {
|
|
9
|
+
const content = fs.readFileSync(this.lockfilePath, 'utf8');
|
|
10
|
+
const lockfile = yaml.load(content);
|
|
11
|
+
const graph = {
|
|
12
|
+
nodes: {},
|
|
13
|
+
edges: [],
|
|
14
|
+
root: 'root',
|
|
15
|
+
};
|
|
16
|
+
// Handle root dependencies (monorepo root or single package)
|
|
17
|
+
// Note: This is a simplified parser. Real pnpm lockfiles are complex.
|
|
18
|
+
// We strictly follow pnpm v6+ lockfile structure (lockfileVersion 5.4, 6.0, 9.0 etc)
|
|
19
|
+
// Add logic to traverse 'importers' (workspaces) or root 'dependencies'
|
|
20
|
+
// Check for workspace file
|
|
21
|
+
const workspacePath = path.join(path.dirname(this.lockfilePath), 'pnpm-workspace.yaml');
|
|
22
|
+
let workspacePackages = [];
|
|
23
|
+
if (fs.existsSync(workspacePath)) {
|
|
24
|
+
try {
|
|
25
|
+
const workspaceContent = fs.readFileSync(workspacePath, 'utf8');
|
|
26
|
+
const workspace = yaml.load(workspaceContent);
|
|
27
|
+
if (workspace && Array.isArray(workspace.packages)) {
|
|
28
|
+
workspacePackages = workspace.packages;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
// ignore invalid workspace file
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (lockfile.importers) {
|
|
36
|
+
// It's a workspace
|
|
37
|
+
for (const [importerPath, importer] of Object.entries(lockfile.importers)) {
|
|
38
|
+
const nodeId = importerPath === '.' ? 'root' : importerPath;
|
|
39
|
+
// Try to find package.json for better name
|
|
40
|
+
let packageName = path.basename(importerPath === '.' ? process.cwd() : importerPath);
|
|
41
|
+
const packageJsonPath = path.join(path.dirname(this.lockfilePath), importerPath, 'package.json');
|
|
42
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
43
|
+
try {
|
|
44
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
45
|
+
if (pkg.name)
|
|
46
|
+
packageName = pkg.name;
|
|
47
|
+
}
|
|
48
|
+
catch (e) { /* ignore */ }
|
|
49
|
+
}
|
|
50
|
+
const node = {
|
|
51
|
+
id: nodeId,
|
|
52
|
+
name: packageName,
|
|
53
|
+
version: '0.0.0',
|
|
54
|
+
path: importerPath,
|
|
55
|
+
dependencies: [],
|
|
56
|
+
};
|
|
57
|
+
graph.nodes[nodeId] = node;
|
|
58
|
+
if (importer.dependencies) {
|
|
59
|
+
this.processDeps(importer.dependencies, nodeId, graph, 'prod');
|
|
60
|
+
}
|
|
61
|
+
if (importer.devDependencies) {
|
|
62
|
+
this.processDeps(importer.devDependencies, nodeId, graph, 'dev');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Single package
|
|
68
|
+
let packageName = 'root';
|
|
69
|
+
const packageJsonPath = path.join(path.dirname(this.lockfilePath), 'package.json');
|
|
70
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
71
|
+
try {
|
|
72
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
73
|
+
if (pkg.name)
|
|
74
|
+
packageName = pkg.name;
|
|
75
|
+
}
|
|
76
|
+
catch (e) { /* ignore */ }
|
|
77
|
+
}
|
|
78
|
+
const node = {
|
|
79
|
+
id: 'root',
|
|
80
|
+
name: packageName,
|
|
81
|
+
version: '0.0.0',
|
|
82
|
+
path: '.',
|
|
83
|
+
dependencies: [],
|
|
84
|
+
};
|
|
85
|
+
graph.nodes['root'] = node;
|
|
86
|
+
if (lockfile.dependencies) {
|
|
87
|
+
this.processDeps(lockfile.dependencies, 'root', graph, 'prod');
|
|
88
|
+
}
|
|
89
|
+
if (lockfile.packages) {
|
|
90
|
+
// in some lockfile versions, root deps are just in packages but referenced from '.' entry in importers
|
|
91
|
+
// if strictly checking 'dependencies' on root object fails, might be lockfile v6 style where root is in importers['.']
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Parse 'packages' section (the store)
|
|
95
|
+
if (lockfile.packages) {
|
|
96
|
+
for (const [pkgPath, pkg] of Object.entries(lockfile.packages)) {
|
|
97
|
+
const nodeId = pkgPath;
|
|
98
|
+
const node = {
|
|
99
|
+
id: nodeId,
|
|
100
|
+
name: this.extractName(pkgPath),
|
|
101
|
+
version: this.extractVersion(pkgPath),
|
|
102
|
+
path: pkgPath,
|
|
103
|
+
dependencies: [],
|
|
104
|
+
isDev: pkg.dev
|
|
105
|
+
};
|
|
106
|
+
graph.nodes[nodeId] = node;
|
|
107
|
+
if (pkg.dependencies) {
|
|
108
|
+
this.processDeps(pkg.dependencies, nodeId, graph, 'prod');
|
|
109
|
+
}
|
|
110
|
+
if (pkg.peerDependencies) {
|
|
111
|
+
this.processDeps(pkg.peerDependencies, nodeId, graph, 'peer');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return graph;
|
|
116
|
+
}
|
|
117
|
+
processDeps(deps, parentId, graph, type) {
|
|
118
|
+
for (const [name, ref] of Object.entries(deps)) {
|
|
119
|
+
// logic to resolve ref to a package path in 'packages'
|
|
120
|
+
// pnpm refs can be: '1.2.3', '/foo/1.2.3', 'link:../foo'
|
|
121
|
+
let targetId = ref;
|
|
122
|
+
if (ref.startsWith('link:')) {
|
|
123
|
+
// Local workspace link
|
|
124
|
+
// For now, simple resolution
|
|
125
|
+
targetId = ref.replace('link:', '');
|
|
126
|
+
// In monorepos, this might need normalization to match importer keys
|
|
127
|
+
}
|
|
128
|
+
else if (ref.includes('(')) {
|
|
129
|
+
// peer dep resolution, skip logic for now or clean
|
|
130
|
+
targetId = ref.split('(')[0];
|
|
131
|
+
}
|
|
132
|
+
// Trying to map simplified ref to package ID if possible
|
|
133
|
+
// In pnpm lockfile v6, keys in 'packages' are often just the resolution string (e.g. /@types/node@18.0.0)
|
|
134
|
+
// We need to match this.
|
|
135
|
+
// For now, create an edge. Whether the node exists or not will be checked by visualization
|
|
136
|
+
graph.edges.push({ source: parentId, target: targetId, type });
|
|
137
|
+
// Add dep to node (redundant but useful)
|
|
138
|
+
if (graph.nodes[parentId]) {
|
|
139
|
+
graph.nodes[parentId].dependencies.push(targetId);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
extractName(pkgPath) {
|
|
144
|
+
// Very naive extraction, pnpm paths vary wildly
|
|
145
|
+
// /@babel/core/7.1.0 -> @babel/core
|
|
146
|
+
// /foo@1.0.0 -> foo
|
|
147
|
+
const parts = pkgPath.split('/');
|
|
148
|
+
if (parts[1]?.startsWith('@')) {
|
|
149
|
+
return `${parts[1]}/${parts[2].split('@')[0]}`;
|
|
150
|
+
}
|
|
151
|
+
return parts[1]?.split('@')[0] || pkgPath;
|
|
152
|
+
}
|
|
153
|
+
extractVersion(pkgPath) {
|
|
154
|
+
const parts = pkgPath.split('@');
|
|
155
|
+
return parts[parts.length - 1] || '0.0.0';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|