import chalk from 'chalk'; import { DefaultDependencyKind, DependencyKind, getListOfPackagesAsync, Package, } from '../Packages'; import PackagesGraphEdge from './PackagesGraphEdge'; import PackagesGraphNode from './PackagesGraphNode'; type PackagesMap = Map; export default class PackagesGraph { nodes: PackagesMap = new Map(); static async makeFromPublicPackages(): Promise { const packages = await getListOfPackagesAsync(); const graph = new PackagesGraph(packages.filter((pkg) => pkg.packageJson.private !== true)); return graph; } constructor(packages: Package[]) { for (const pkg of packages) { const node = new PackagesGraphNode(pkg); this.nodes.set(pkg.packageName, node); } for (const node of this.nodes.values()) { resolveDependencies(this, node); } } getNode(packageName: string): PackagesGraphNode | null { return this.nodes.get(packageName) ?? null; } getOriginNodes(): PackagesGraphNode[] { return [...this.nodes.values()].filter((node) => node.depth === 0); } } function resolveDependencies( graph: PackagesGraph, node: PackagesGraphNode, visitedNodes: Record = {} ) { const dependencies = node.pkg.getDependencies([ DependencyKind.Normal, DependencyKind.Dev, DependencyKind.Peer, DependencyKind.Optional, ]); // Mark the node as visited. visitedNodes[node.name] = true; for (const dependency of dependencies) { const dependencyNode = graph.getNode(dependency.name); if (!dependencyNode) { // The dependency is probably not our package, so just skip it. continue; } addDependency( graph, node, dependencyNode, dependency.kind, dependency.versionRange, visitedNodes ); } } function addDependency( graph: PackagesGraph, origin: PackagesGraphNode, destination: PackagesGraphNode, kind: DependencyKind = DependencyKind.Normal, versionRange: string, visitedNodes: Record = {} ) { const existingEdge = origin.getOutgoingEdgeForNode(destination); // Given nodes are already connected in that direction. // Just make sure to add another kind. if (existingEdge) { existingEdge.addKind(kind); return; } const edge = new PackagesGraphEdge(origin, destination, versionRange); edge.addKind(kind); // If the target node was already visited, mark the edge as a cycling edge. edge.isCyclic = visitedNodes[destination.name] === true; // Connect nodes. origin.outgoingEdges.push(edge); destination.incomingEdges.push(edge); // The dependency node is at least one level deeper than the parent. destination.depth = Math.max(origin.depth + 1, destination.depth); if (edge.isCyclic) { // Possible cycle in dependencies. Cycles in peer and optional dependency relations are fine though. if (DefaultDependencyKind.includes(kind)) { console.error( chalk.red(`Detected a cycle in ${kind}! ${origin.name} -> ${destination.name}`) ); } return; } resolveDependencies(graph, destination, { ...visitedNodes }); }