// Copyright 2025 Chris Bunting
// Brief: Builds dependency graphs from package manager data
// Scope: Creates and manages dependency tree structures for analysis
import { DependencyInfo, DependencyType, PackageManager } from '@mcp-code-analysis/shared-types';
import { Logger } from '../utils/Logger.js';
import graphlib from 'graphlib';
export interface DependencyNode {
name: string;
version: string;
type: DependencyType;
manager: PackageManager;
depth: number;
parent?: string;
metadata: {
licenses: string[];
vulnerabilities: any[];
size?: number;
downloadCount?: number;
lastUpdated?: string;
};
}
export interface DependencyEdge {
from: string;
to: string;
type: 'direct' | 'transitive';
versionConstraint?: string;
}
export interface DependencyGraph {
nodes: Map<string, DependencyNode>;
edges: DependencyEdge[];
graph: graphlib.Graph;
root: string;
}
export class DependencyGraphBuilder {
private logger: Logger;
constructor(logger?: Logger) {
this.logger = logger || new Logger();
}
async buildGraph(
rootPackage: string,
rootVersion: string,
dependencies: DependencyInfo[],
manager: PackageManager
): Promise<DependencyGraph> {
this.logger.debug(`Building dependency graph for ${rootPackage}@${rootVersion}`);
const graph = new graphlib.Graph({ directed: true });
const nodes = new Map<string, DependencyNode>();
const edges: DependencyEdge[] = [];
// Add root node
const rootNode: DependencyNode = {
name: rootPackage,
version: rootVersion,
type: DependencyType.PRODUCTION,
manager,
depth: 0,
metadata: {
licenses: [],
vulnerabilities: [],
},
};
nodes.set(this.getNodeId(rootPackage, rootVersion), rootNode);
graph.setNode(this.getNodeId(rootPackage, rootVersion), rootNode);
// Process dependencies recursively
await this.processDependencies(
rootPackage,
rootVersion,
dependencies,
manager,
1,
nodes,
edges,
graph
);
const dependencyGraph: DependencyGraph = {
nodes,
edges,
graph,
root: this.getNodeId(rootPackage, rootVersion),
};
this.logger.debug(`Built graph with ${nodes.size} nodes and ${edges.length} edges`);
return dependencyGraph;
}
private async processDependencies(
parentName: string,
parentVersion: string,
dependencies: DependencyInfo[],
manager: PackageManager,
depth: number,
nodes: Map<string, DependencyNode>,
edges: DependencyEdge[],
graph: graphlib.Graph
): Promise<void> {
const parentId = this.getNodeId(parentName, parentVersion);
for (const dep of dependencies) {
const nodeId = this.getNodeId(dep.name, dep.version);
// Create node if it doesn't exist
if (!nodes.has(nodeId)) {
const node: DependencyNode = {
name: dep.name,
version: dep.version,
type: dep.type,
manager: dep.manager || manager,
depth,
parent: parentId,
metadata: {
licenses: dep.licenses || [],
vulnerabilities: dep.vulnerabilities || [],
},
};
nodes.set(nodeId, node);
graph.setNode(nodeId, node);
}
// Create edge
const edge: DependencyEdge = {
from: parentId,
to: nodeId,
type: depth === 1 ? 'direct' : 'transitive',
};
edges.push(edge);
graph.setEdge(parentId, nodeId, edge);
// Process nested dependencies recursively
if (dep.dependencies && dep.dependencies.length > 0) {
await this.processDependencies(
dep.name,
dep.version,
dep.dependencies,
manager,
depth + 1,
nodes,
edges,
graph
);
}
}
}
private getNodeId(name: string, version: string): string {
return `${name}@${version}`;
}
findCircularDependencies(graph: DependencyGraph): string[][] {
this.logger.debug('Checking for circular dependencies');
const cycles: string[][] = [];
const visited = new Set<string>();
const recursionStack = new Set<string>();
const dfs = (nodeId: string, path: string[]): void => {
if (recursionStack.has(nodeId)) {
// Found a cycle
const cycleStart = path.indexOf(nodeId);
cycles.push([...path.slice(cycleStart), nodeId]);
return;
}
if (visited.has(nodeId)) {
return;
}
visited.add(nodeId);
recursionStack.add(nodeId);
path.push(nodeId);
const successors = graph.graph.successors(nodeId) || [];
for (const successor of successors) {
dfs(successor, [...path]);
}
recursionStack.delete(nodeId);
};
// Start DFS from root
dfs(graph.root, []);
this.logger.debug(`Found ${cycles.length} circular dependencies`);
return cycles;
}
findConflictingVersions(graph: DependencyGraph): Map<string, string[]> {
this.logger.debug('Checking for version conflicts');
const conflicts = new Map<string, string[]>();
const packageVersions = new Map<string, Set<string>>();
// Collect all versions for each package
for (const [_nodeId, node] of graph.nodes) {
if (!packageVersions.has(node.name)) {
packageVersions.set(node.name, new Set());
}
packageVersions.get(node.name)!.add(node.version);
}
// Find packages with multiple versions
for (const [packageName, versions] of packageVersions) {
if (versions.size > 1) {
conflicts.set(packageName, Array.from(versions));
}
}
this.logger.debug(`Found ${conflicts.size} packages with version conflicts`);
return conflicts;
}
calculateDependencyMetrics(graph: DependencyGraph): {
totalDependencies: number;
directDependencies: number;
transitiveDependencies: number;
maxDepth: number;
averageDepth: number;
packageCount: number;
} {
let totalDependencies = 0;
let directDependencies = 0;
let transitiveDependencies = 0;
let maxDepth = 0;
let totalDepth = 0;
const packageNames = new Set<string>();
for (const [_nodeId, node] of graph.nodes) {
if (node.depth === 0) continue; // Skip root
totalDependencies++;
packageNames.add(node.name);
if (node.depth === 1) {
directDependencies++;
} else {
transitiveDependencies++;
}
maxDepth = Math.max(maxDepth, node.depth);
totalDepth += node.depth;
}
return {
totalDependencies,
directDependencies,
transitiveDependencies,
maxDepth,
averageDepth: totalDependencies > 0 ? totalDepth / totalDependencies : 0,
packageCount: packageNames.size,
};
}
findCriticalPath(graph: DependencyGraph): string[] {
// Find the longest path from root to any leaf (critical path)
const longestPath: string[] = [];
const visited = new Set<string>();
const dfs = (nodeId: string, path: string[]): void => {
if (visited.has(nodeId)) return;
visited.add(nodeId);
path.push(nodeId);
const successors = graph.graph.successors(nodeId) || [];
if (successors.length === 0) {
// Leaf node
if (path.length > longestPath.length) {
longestPath.splice(0, longestPath.length, ...path);
}
} else {
for (const successor of successors) {
dfs(successor, [...path]);
}
}
path.pop();
};
dfs(graph.root, []);
return longestPath;
}
generateVisualizationData(graph: DependencyGraph): {
nodes: Array<{
id: string;
name: string;
version: string;
type: string;
depth: number;
vulnerabilities: number;
}>;
edges: Array<{
from: string;
to: string;
type: string;
}>;
} {
const nodes = Array.from(graph.nodes.values()).map(node => ({
id: this.getNodeId(node.name, node.version),
name: node.name,
version: node.version,
type: node.type,
depth: node.depth,
vulnerabilities: node.metadata.vulnerabilities.length,
}));
const edges = graph.edges.map(edge => ({
from: edge.from,
to: edge.to,
type: edge.type,
}));
return { nodes, edges };
}
}