@ebowwa/dependency-graph 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 +89 -0
- package/dist/analysis.d.ts +31 -0
- package/dist/analysis.d.ts.map +1 -0
- package/dist/analysis.js +151 -0
- package/dist/analysis.js.map +1 -0
- package/dist/builder.d.ts +61 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +281 -0
- package/dist/builder.js.map +1 -0
- package/dist/formatters.d.ts +27 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +145 -0
- package/dist/formatters.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +38 -0
- package/src/analysis.ts +188 -0
- package/src/builder.ts +366 -0
- package/src/formatters.ts +191 -0
- package/src/index.ts +56 -0
- package/src/types.ts +58 -0
package/src/builder.ts
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ebowwa/dependency-graph - Graph Builder
|
|
3
|
+
*
|
|
4
|
+
* Builds dependency graphs from package.json and import analysis
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { join, dirname, relative } from "node:path";
|
|
9
|
+
import type {
|
|
10
|
+
DependencyGraph,
|
|
11
|
+
DependencyNode,
|
|
12
|
+
DependencyEdge,
|
|
13
|
+
BuildOptions,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
interface PackageInfo {
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
version: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class DependencyGraphBuilder {
|
|
23
|
+
private monorepoRoot: string;
|
|
24
|
+
private graph: DependencyGraph;
|
|
25
|
+
private packageCache: Map<string, { path: string; pkg: Record<string, unknown> }> = new Map();
|
|
26
|
+
|
|
27
|
+
constructor(monorepoRoot: string) {
|
|
28
|
+
this.monorepoRoot = monorepoRoot;
|
|
29
|
+
this.graph = {
|
|
30
|
+
nodes: new Map(),
|
|
31
|
+
edges: [],
|
|
32
|
+
reverseEdges: new Map(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build the complete dependency graph
|
|
38
|
+
*/
|
|
39
|
+
async build(options: BuildOptions = {}): Promise<DependencyGraph> {
|
|
40
|
+
const {
|
|
41
|
+
includeDevDependencies = false,
|
|
42
|
+
analyzeImports = true,
|
|
43
|
+
excludePatterns = [],
|
|
44
|
+
} = options;
|
|
45
|
+
|
|
46
|
+
// Discover all packages in the monorepo
|
|
47
|
+
const packages = await this.discoverPackages();
|
|
48
|
+
|
|
49
|
+
// Add nodes for all packages
|
|
50
|
+
for (const pkg of packages) {
|
|
51
|
+
this.addNode(pkg.name, pkg.path, "workspace", pkg.version);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Analyze dependencies for each package
|
|
55
|
+
for (const pkg of packages) {
|
|
56
|
+
await this.analyzePackageDependencies(
|
|
57
|
+
pkg,
|
|
58
|
+
includeDevDependencies,
|
|
59
|
+
excludePatterns
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (analyzeImports) {
|
|
63
|
+
await this.analyzeImports(pkg);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Build reverse edges for impact analysis
|
|
68
|
+
this.buildReverseEdges();
|
|
69
|
+
|
|
70
|
+
return this.graph;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Discover all package.json files in the monorepo
|
|
75
|
+
*/
|
|
76
|
+
private async discoverPackages(): Promise<PackageInfo[]> {
|
|
77
|
+
const packages: PackageInfo[] = [];
|
|
78
|
+
const seen = new Set<string>();
|
|
79
|
+
|
|
80
|
+
// Search in common locations
|
|
81
|
+
const searchPaths = [
|
|
82
|
+
this.monorepoRoot,
|
|
83
|
+
join(this.monorepoRoot, "packages"),
|
|
84
|
+
join(this.monorepoRoot, "packages/src"),
|
|
85
|
+
join(this.monorepoRoot, "apps"),
|
|
86
|
+
join(this.monorepoRoot, "MCP/packages"),
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (const searchPath of searchPaths) {
|
|
90
|
+
if (!existsSync(searchPath)) continue;
|
|
91
|
+
await this.searchDirectory(searchPath, packages, seen, 0, 4);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return packages;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Recursively search for package.json files
|
|
99
|
+
*/
|
|
100
|
+
private async searchDirectory(
|
|
101
|
+
dir: string,
|
|
102
|
+
packages: PackageInfo[],
|
|
103
|
+
seen: Set<string>,
|
|
104
|
+
depth: number,
|
|
105
|
+
maxDepth: number
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
if (depth > maxDepth) return;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
111
|
+
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
// Skip node_modules and hidden dirs
|
|
114
|
+
if (entry.name === "node_modules" || entry.name.startsWith("."))
|
|
115
|
+
continue;
|
|
116
|
+
if (entry.name === "dist" || entry.name === "build") continue;
|
|
117
|
+
|
|
118
|
+
const fullPath = join(dir, entry.name);
|
|
119
|
+
|
|
120
|
+
if (entry.isDirectory()) {
|
|
121
|
+
await this.searchDirectory(
|
|
122
|
+
fullPath,
|
|
123
|
+
packages,
|
|
124
|
+
seen,
|
|
125
|
+
depth + 1,
|
|
126
|
+
maxDepth
|
|
127
|
+
);
|
|
128
|
+
} else if (entry.name === "package.json") {
|
|
129
|
+
const packageDir = dirname(fullPath);
|
|
130
|
+
const relativePath = relative(this.monorepoRoot, packageDir);
|
|
131
|
+
|
|
132
|
+
if (seen.has(relativePath)) continue;
|
|
133
|
+
seen.add(relativePath);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const pkg = JSON.parse(readFileSync(fullPath, "utf-8"));
|
|
137
|
+
if (pkg.name && !pkg.private) {
|
|
138
|
+
packages.push({
|
|
139
|
+
name: pkg.name,
|
|
140
|
+
path: relativePath,
|
|
141
|
+
version: pkg.version || "0.0.0",
|
|
142
|
+
});
|
|
143
|
+
this.packageCache.set(pkg.name, { path: relativePath, pkg });
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Invalid package.json, skip
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
// Directory not accessible, skip
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Analyze package.json dependencies
|
|
157
|
+
*/
|
|
158
|
+
private async analyzePackageDependencies(
|
|
159
|
+
pkg: PackageInfo,
|
|
160
|
+
includeDev: boolean,
|
|
161
|
+
excludePatterns: string[]
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
const pkgPath = join(this.monorepoRoot, pkg.path, "package.json");
|
|
164
|
+
if (!existsSync(pkgPath)) return;
|
|
165
|
+
|
|
166
|
+
const packageJson = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
167
|
+
|
|
168
|
+
const depTypes = ["dependencies"];
|
|
169
|
+
if (includeDev)
|
|
170
|
+
depTypes.push("devDependencies", "peerDependencies", "optionalDependencies");
|
|
171
|
+
|
|
172
|
+
for (const depType of depTypes) {
|
|
173
|
+
const deps = packageJson[depType];
|
|
174
|
+
if (!deps) continue;
|
|
175
|
+
|
|
176
|
+
for (const [depName, depVersion] of Object.entries(
|
|
177
|
+
deps as Record<string, string>
|
|
178
|
+
)) {
|
|
179
|
+
// Check if this matches any exclude pattern
|
|
180
|
+
if (excludePatterns.some((pattern) => depName.match(pattern))) continue;
|
|
181
|
+
|
|
182
|
+
const isWorkspace =
|
|
183
|
+
depVersion === "workspace:*" ||
|
|
184
|
+
depVersion === "workspace:^" ||
|
|
185
|
+
depVersion === "workspace:~";
|
|
186
|
+
|
|
187
|
+
if (isWorkspace) {
|
|
188
|
+
// This is a workspace dependency
|
|
189
|
+
const targetPkg = this.packageCache.get(depName);
|
|
190
|
+
if (targetPkg) {
|
|
191
|
+
this.addEdge(pkg.name, depName, "workspace");
|
|
192
|
+
}
|
|
193
|
+
} else if (this.packageCache.has(depName)) {
|
|
194
|
+
// It's a local package but not using workspace: protocol
|
|
195
|
+
this.addEdge(pkg.name, depName, "workspace");
|
|
196
|
+
} else {
|
|
197
|
+
// External dependency
|
|
198
|
+
this.addNode(depName, "", "external", depVersion as string);
|
|
199
|
+
this.addEdge(pkg.name, depName, "external");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Analyze TypeScript/JavaScript imports
|
|
207
|
+
*/
|
|
208
|
+
private async analyzeImports(pkg: PackageInfo): Promise<void> {
|
|
209
|
+
const pkgDir = join(this.monorepoRoot, pkg.path);
|
|
210
|
+
|
|
211
|
+
// Common source directories
|
|
212
|
+
const sourceDirs = ["src", "lib", ""];
|
|
213
|
+
|
|
214
|
+
for (const sourceDir of sourceDirs) {
|
|
215
|
+
const searchPath = sourceDir ? join(pkgDir, sourceDir) : pkgDir;
|
|
216
|
+
if (!existsSync(searchPath)) continue;
|
|
217
|
+
await this.analyzeImportsInDirectory(pkg.name, searchPath);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Recursively analyze imports in a directory
|
|
223
|
+
*/
|
|
224
|
+
private async analyzeImportsInDirectory(
|
|
225
|
+
packageName: string,
|
|
226
|
+
dir: string
|
|
227
|
+
): Promise<void> {
|
|
228
|
+
try {
|
|
229
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
230
|
+
|
|
231
|
+
for (const entry of entries) {
|
|
232
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
233
|
+
if (entry.name === "dist" || entry.name === "build") continue;
|
|
234
|
+
|
|
235
|
+
const fullPath = join(dir, entry.name);
|
|
236
|
+
|
|
237
|
+
if (entry.isDirectory()) {
|
|
238
|
+
await this.analyzeImportsInDirectory(packageName, fullPath);
|
|
239
|
+
} else if (
|
|
240
|
+
entry.name.endsWith(".ts") ||
|
|
241
|
+
entry.name.endsWith(".tsx") ||
|
|
242
|
+
entry.name.endsWith(".js") ||
|
|
243
|
+
entry.name.endsWith(".jsx") ||
|
|
244
|
+
entry.name.endsWith(".mjs")
|
|
245
|
+
) {
|
|
246
|
+
await this.analyzeImportsInFile(packageName, fullPath);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
// Directory not accessible
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Analyze imports in a single file
|
|
256
|
+
*/
|
|
257
|
+
private async analyzeImportsInFile(
|
|
258
|
+
packageName: string,
|
|
259
|
+
filePath: string
|
|
260
|
+
): Promise<void> {
|
|
261
|
+
try {
|
|
262
|
+
const content = readFileSync(filePath, "utf-8");
|
|
263
|
+
|
|
264
|
+
// Match various import patterns
|
|
265
|
+
const importPatterns = [
|
|
266
|
+
// ES imports: import ... from '...' | import ... from "..."
|
|
267
|
+
/import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s*,?\s*)*\s+from\s+['"]([^'"]+)['"]/g,
|
|
268
|
+
// Dynamic imports: import('...')
|
|
269
|
+
/import\(['"]([^'"]+)['"]\)/g,
|
|
270
|
+
// require(): require('...') | require("...")
|
|
271
|
+
/require\(['"]([^'"]+)['"]\)/g,
|
|
272
|
+
// Export from: export ... from '...'
|
|
273
|
+
/export\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+)\s+from\s+)?['"]([^'"]+)['"]/g,
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
for (const pattern of importPatterns) {
|
|
277
|
+
let match;
|
|
278
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
279
|
+
const importPath = match[1];
|
|
280
|
+
|
|
281
|
+
// Skip relative imports
|
|
282
|
+
if (importPath.startsWith(".") || importPath.startsWith("/")) continue;
|
|
283
|
+
|
|
284
|
+
// Check if this is a known workspace package
|
|
285
|
+
for (const [pkgName, pkgData] of this.packageCache) {
|
|
286
|
+
if (
|
|
287
|
+
importPath === pkgName ||
|
|
288
|
+
importPath.startsWith(pkgName + "/")
|
|
289
|
+
) {
|
|
290
|
+
this.addEdge(packageName, pkgName, "import", importPath);
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
// File not readable
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Add a node to the graph
|
|
303
|
+
*/
|
|
304
|
+
private addNode(
|
|
305
|
+
name: string,
|
|
306
|
+
path: string,
|
|
307
|
+
type: "package" | "workspace" | "external",
|
|
308
|
+
version?: string
|
|
309
|
+
): void {
|
|
310
|
+
if (!this.graph.nodes.has(name)) {
|
|
311
|
+
this.graph.nodes.set(name, { name, path, type, version });
|
|
312
|
+
this.graph.reverseEdges.set(name, new Set());
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Add an edge to the graph
|
|
318
|
+
*/
|
|
319
|
+
private addEdge(
|
|
320
|
+
from: string,
|
|
321
|
+
to: string,
|
|
322
|
+
type: "workspace" | "external" | "import",
|
|
323
|
+
importPath?: string
|
|
324
|
+
): void {
|
|
325
|
+
// Skip self-references
|
|
326
|
+
if (from === to) return;
|
|
327
|
+
|
|
328
|
+
// Check if edge already exists
|
|
329
|
+
const existing = this.graph.edges.find((e) => e.from === from && e.to === to);
|
|
330
|
+
if (existing) return;
|
|
331
|
+
|
|
332
|
+
this.graph.edges.push({ from, to, type, importPath });
|
|
333
|
+
|
|
334
|
+
// Update reverse edges
|
|
335
|
+
if (!this.graph.reverseEdges.has(to)) {
|
|
336
|
+
this.graph.reverseEdges.set(to, new Set());
|
|
337
|
+
}
|
|
338
|
+
this.graph.reverseEdges.get(to)!.add(from);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Build reverse edges for impact analysis
|
|
343
|
+
*/
|
|
344
|
+
private buildReverseEdges(): void {
|
|
345
|
+
for (const [to, dependents] of this.graph.reverseEdges) {
|
|
346
|
+
// Ensure node exists
|
|
347
|
+
if (!this.graph.nodes.has(to)) {
|
|
348
|
+
this.graph.nodes.set(to, { name: to, path: "", type: "external" });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get the built graph
|
|
355
|
+
*/
|
|
356
|
+
getGraph(): DependencyGraph {
|
|
357
|
+
return this.graph;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get the monorepo root
|
|
362
|
+
*/
|
|
363
|
+
getMonorepoRoot(): string {
|
|
364
|
+
return this.monorepoRoot;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ebowwa/dependency-graph - Output Formatters
|
|
3
|
+
*
|
|
4
|
+
* Format dependency graphs for various output formats
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DependencyGraph, OutputFormat } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format the graph for output
|
|
11
|
+
*/
|
|
12
|
+
export function formatGraph(graph: DependencyGraph, format: OutputFormat): string {
|
|
13
|
+
switch (format) {
|
|
14
|
+
case "mermaid":
|
|
15
|
+
return formatAsMermaid(graph);
|
|
16
|
+
case "dot":
|
|
17
|
+
return formatAsDot(graph);
|
|
18
|
+
case "tree":
|
|
19
|
+
return formatAsTree(graph);
|
|
20
|
+
case "json":
|
|
21
|
+
default:
|
|
22
|
+
return formatAsJson(graph);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Format as JSON
|
|
28
|
+
*/
|
|
29
|
+
export function formatAsJson(graph: DependencyGraph): string {
|
|
30
|
+
return JSON.stringify(
|
|
31
|
+
{
|
|
32
|
+
nodes: Array.from(graph.nodes.values()),
|
|
33
|
+
edges: graph.edges,
|
|
34
|
+
},
|
|
35
|
+
null,
|
|
36
|
+
2
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format as Mermaid diagram
|
|
42
|
+
*/
|
|
43
|
+
export function formatAsMermaid(graph: DependencyGraph): string {
|
|
44
|
+
const lines = ["graph TD"];
|
|
45
|
+
|
|
46
|
+
// Add nodes
|
|
47
|
+
for (const [name, node] of graph.nodes) {
|
|
48
|
+
const label = node.type === "workspace" ? `📦 ${name}` : `📚 ${name}`;
|
|
49
|
+
lines.push(` ${sanitizeId(name)}["${label}"]`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Add edges
|
|
53
|
+
for (const edge of graph.edges) {
|
|
54
|
+
const from = sanitizeId(edge.from);
|
|
55
|
+
const to = sanitizeId(edge.to);
|
|
56
|
+
const label =
|
|
57
|
+
edge.type === "workspace"
|
|
58
|
+
? "workspace"
|
|
59
|
+
: edge.type === "import"
|
|
60
|
+
? "imports"
|
|
61
|
+
: "external";
|
|
62
|
+
lines.push(` ${from} -->|${label}| ${to}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Add styles
|
|
66
|
+
lines.push(" classDef workspace fill:#e1f5fe");
|
|
67
|
+
lines.push(" classDef external fill:#f5f5f5");
|
|
68
|
+
lines.push(" classDef import fill:#fff3e0");
|
|
69
|
+
|
|
70
|
+
for (const [name, node] of graph.nodes) {
|
|
71
|
+
const id = sanitizeId(name);
|
|
72
|
+
if (node.type === "workspace") {
|
|
73
|
+
lines.push(` class ${id} workspace`);
|
|
74
|
+
} else if (node.type === "external") {
|
|
75
|
+
lines.push(` class ${id} external`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Format as Graphviz DOT
|
|
84
|
+
*/
|
|
85
|
+
export function formatAsDot(graph: DependencyGraph): string {
|
|
86
|
+
const lines = ["digraph dependencies {"];
|
|
87
|
+
lines.push(" rankdir=LR;");
|
|
88
|
+
lines.push(" node [shape=box];");
|
|
89
|
+
|
|
90
|
+
// Add nodes
|
|
91
|
+
for (const [name, node] of graph.nodes) {
|
|
92
|
+
const color = node.type === "workspace" ? "lightblue" : "lightgray";
|
|
93
|
+
lines.push(` "${name}" [fillcolor=${color}, style=filled];`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Add edges
|
|
97
|
+
for (const edge of graph.edges) {
|
|
98
|
+
const style = edge.type === "workspace" ? "solid" : "dashed";
|
|
99
|
+
lines.push(
|
|
100
|
+
` "${edge.from}" -> "${edge.to}" [style=${style}, label="${edge.type}"];`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
lines.push("}");
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Format as ASCII tree
|
|
110
|
+
*/
|
|
111
|
+
export function formatAsTree(graph: DependencyGraph): string {
|
|
112
|
+
const lines: string[] = [];
|
|
113
|
+
const seen = new Set<string>();
|
|
114
|
+
|
|
115
|
+
// Find root nodes (no incoming edges from other workspace packages)
|
|
116
|
+
const workspaceNodes = Array.from(graph.nodes.values()).filter(
|
|
117
|
+
(n) => n.type === "workspace"
|
|
118
|
+
);
|
|
119
|
+
const incomingEdges = new Map<string, number>();
|
|
120
|
+
|
|
121
|
+
for (const node of workspaceNodes) {
|
|
122
|
+
incomingEdges.set(node.name, 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const edge of graph.edges) {
|
|
126
|
+
if (graph.nodes.get(edge.to)?.type === "workspace") {
|
|
127
|
+
incomingEdges.set(edge.to, (incomingEdges.get(edge.to) || 0) + 1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Start from roots
|
|
132
|
+
for (const [name, node] of graph.nodes) {
|
|
133
|
+
if (node.type === "workspace" && (incomingEdges.get(name) || 0) === 0) {
|
|
134
|
+
lines.push(`📦 ${name}`);
|
|
135
|
+
printTree(graph, name, "", seen, lines);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Add any disconnected nodes
|
|
140
|
+
for (const [name, node] of graph.nodes) {
|
|
141
|
+
if (!seen.has(name) && node.type === "workspace") {
|
|
142
|
+
lines.push(`📦 ${name} (disconnected)`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function printTree(
|
|
150
|
+
graph: DependencyGraph,
|
|
151
|
+
nodeName: string,
|
|
152
|
+
prefix: string,
|
|
153
|
+
seen: Set<string>,
|
|
154
|
+
lines: string[]
|
|
155
|
+
): void {
|
|
156
|
+
seen.add(nodeName);
|
|
157
|
+
|
|
158
|
+
// Find outgoing edges to other workspace packages
|
|
159
|
+
const outgoing = graph.edges.filter(
|
|
160
|
+
(e) =>
|
|
161
|
+
e.from === nodeName && graph.nodes.get(e.to)?.type === "workspace"
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < outgoing.length; i++) {
|
|
165
|
+
const edge = outgoing[i];
|
|
166
|
+
const isLast = i === outgoing.length - 1;
|
|
167
|
+
const connector = isLast ? "└──" : "├──";
|
|
168
|
+
const childPrefix = prefix + (isLast ? " " : "│ ");
|
|
169
|
+
|
|
170
|
+
lines.push(`${prefix}${connector} 📦 ${edge.to} [${edge.type}]`);
|
|
171
|
+
if (!seen.has(edge.to)) {
|
|
172
|
+
printTree(graph, edge.to, childPrefix, seen, lines);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Show external dependencies count
|
|
177
|
+
const externals = graph.edges.filter(
|
|
178
|
+
(e) =>
|
|
179
|
+
e.from === nodeName && graph.nodes.get(e.to)?.type === "external"
|
|
180
|
+
);
|
|
181
|
+
if (externals.length > 0) {
|
|
182
|
+
lines.push(`${prefix}└── 📚 ${externals.length} external dependencies`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Sanitize ID for Mermaid/DOT formats
|
|
188
|
+
*/
|
|
189
|
+
function sanitizeId(name: string): string {
|
|
190
|
+
return name.replace(/[^a-zA-Z0-9]/g, "_");
|
|
191
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ebowwa/dependency-graph
|
|
3
|
+
*
|
|
4
|
+
* Core dependency graph analysis and visualization for monorepos
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { DependencyGraphBuilder, formatGraph, analyzeImpact, findCircularDependencies } from '@ebowwa/dependency-graph';
|
|
9
|
+
*
|
|
10
|
+
* const builder = new DependencyGraphBuilder('/path/to/monorepo');
|
|
11
|
+
* const graph = await builder.build({ analyzeImports: true });
|
|
12
|
+
*
|
|
13
|
+
* // Format as Mermaid diagram
|
|
14
|
+
* const mermaid = formatGraph(graph, 'mermaid');
|
|
15
|
+
*
|
|
16
|
+
* // Find circular dependencies
|
|
17
|
+
* const cycles = findCircularDependencies(graph);
|
|
18
|
+
*
|
|
19
|
+
* // Analyze impact of changing a package
|
|
20
|
+
* const impact = analyzeImpact(graph, '@ebowwa/terminal');
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Types
|
|
25
|
+
export type {
|
|
26
|
+
DependencyNode,
|
|
27
|
+
DependencyEdge,
|
|
28
|
+
DependencyGraph,
|
|
29
|
+
ImportInfo,
|
|
30
|
+
BuildOptions,
|
|
31
|
+
ImpactResult,
|
|
32
|
+
OutputFormat,
|
|
33
|
+
PackageInfo,
|
|
34
|
+
} from "./types.js";
|
|
35
|
+
|
|
36
|
+
// Builder
|
|
37
|
+
export { DependencyGraphBuilder } from "./builder.js";
|
|
38
|
+
|
|
39
|
+
// Analysis
|
|
40
|
+
export {
|
|
41
|
+
findCircularDependencies,
|
|
42
|
+
analyzeImpact,
|
|
43
|
+
findUnusedPackages,
|
|
44
|
+
getPackageInfo,
|
|
45
|
+
getWorkspacePackages,
|
|
46
|
+
getExternalDependencies,
|
|
47
|
+
} from "./analysis.js";
|
|
48
|
+
|
|
49
|
+
// Formatters
|
|
50
|
+
export {
|
|
51
|
+
formatGraph,
|
|
52
|
+
formatAsJson,
|
|
53
|
+
formatAsMermaid,
|
|
54
|
+
formatAsDot,
|
|
55
|
+
formatAsTree,
|
|
56
|
+
} from "./formatters.js";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ebowwa/dependency-graph - Core Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for dependency graph analysis
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface DependencyNode {
|
|
8
|
+
name: string;
|
|
9
|
+
path: string;
|
|
10
|
+
type: "package" | "workspace" | "external";
|
|
11
|
+
version?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DependencyEdge {
|
|
15
|
+
from: string;
|
|
16
|
+
to: string;
|
|
17
|
+
type: "workspace" | "external" | "import";
|
|
18
|
+
importPath?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DependencyGraph {
|
|
22
|
+
nodes: Map<string, DependencyNode>;
|
|
23
|
+
edges: DependencyEdge[];
|
|
24
|
+
reverseEdges: Map<string, Set<string>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ImportInfo {
|
|
28
|
+
from: string;
|
|
29
|
+
imports: string[];
|
|
30
|
+
file: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BuildOptions {
|
|
34
|
+
includeDevDependencies?: boolean;
|
|
35
|
+
analyzeImports?: boolean;
|
|
36
|
+
excludePatterns?: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ImpactResult {
|
|
40
|
+
direct: string[];
|
|
41
|
+
transitive: string[];
|
|
42
|
+
all: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type OutputFormat = "json" | "mermaid" | "dot" | "tree";
|
|
46
|
+
|
|
47
|
+
export interface PackageInfo {
|
|
48
|
+
name: string;
|
|
49
|
+
type: "package" | "workspace" | "external";
|
|
50
|
+
path: string;
|
|
51
|
+
version?: string;
|
|
52
|
+
dependencies: Array<{
|
|
53
|
+
name: string;
|
|
54
|
+
type: "workspace" | "external" | "import";
|
|
55
|
+
version?: string;
|
|
56
|
+
}>;
|
|
57
|
+
dependents: string[];
|
|
58
|
+
}
|