1import chalk from 'chalk'; 2 3import { 4 DefaultDependencyKind, 5 DependencyKind, 6 getListOfPackagesAsync, 7 Package, 8} from '../Packages'; 9import PackagesGraphEdge from './PackagesGraphEdge'; 10import PackagesGraphNode from './PackagesGraphNode'; 11 12type PackagesMap = Map<string, PackagesGraphNode>; 13 14export default class PackagesGraph { 15 nodes: PackagesMap = new Map(); 16 17 static async makeFromPublicPackages(): Promise<PackagesGraph> { 18 const packages = await getListOfPackagesAsync(); 19 const graph = new PackagesGraph(packages.filter((pkg) => pkg.packageJson.private !== true)); 20 return graph; 21 } 22 23 constructor(packages: Package[]) { 24 for (const pkg of packages) { 25 const node = new PackagesGraphNode(pkg); 26 this.nodes.set(pkg.packageName, node); 27 } 28 for (const node of this.nodes.values()) { 29 resolveDependencies(this, node); 30 } 31 } 32 33 getNode(packageName: string): PackagesGraphNode | null { 34 return this.nodes.get(packageName) ?? null; 35 } 36 37 getOriginNodes(): PackagesGraphNode[] { 38 return [...this.nodes.values()].filter((node) => node.depth === 0); 39 } 40} 41 42function resolveDependencies( 43 graph: PackagesGraph, 44 node: PackagesGraphNode, 45 visitedNodes: Record<string, boolean> = {} 46) { 47 const dependencies = node.pkg.getDependencies([ 48 DependencyKind.Normal, 49 DependencyKind.Dev, 50 DependencyKind.Peer, 51 DependencyKind.Optional, 52 ]); 53 54 // Mark the node as visited. 55 visitedNodes[node.name] = true; 56 57 for (const dependency of dependencies) { 58 const dependencyNode = graph.getNode(dependency.name); 59 60 if (!dependencyNode) { 61 // The dependency is probably not our package, so just skip it. 62 continue; 63 } 64 addDependency( 65 graph, 66 node, 67 dependencyNode, 68 dependency.kind, 69 dependency.versionRange, 70 visitedNodes 71 ); 72 } 73} 74 75function addDependency( 76 graph: PackagesGraph, 77 origin: PackagesGraphNode, 78 destination: PackagesGraphNode, 79 kind: DependencyKind = DependencyKind.Normal, 80 versionRange: string, 81 visitedNodes: Record<string, boolean> = {} 82) { 83 const existingEdge = origin.getOutgoingEdgeForNode(destination); 84 85 // Given nodes are already connected in that direction. 86 // Just make sure to add another kind. 87 if (existingEdge) { 88 existingEdge.addKind(kind); 89 return; 90 } 91 92 const edge = new PackagesGraphEdge(origin, destination, versionRange); 93 94 edge.addKind(kind); 95 96 // If the target node was already visited, mark the edge as a cycling edge. 97 edge.isCyclic = visitedNodes[destination.name] === true; 98 99 // Connect nodes. 100 origin.outgoingEdges.push(edge); 101 destination.incomingEdges.push(edge); 102 103 // The dependency node is at least one level deeper than the parent. 104 destination.depth = Math.max(origin.depth + 1, destination.depth); 105 106 if (edge.isCyclic) { 107 // Possible cycle in dependencies. Cycles in peer and optional dependency relations are fine though. 108 if (DefaultDependencyKind.includes(kind)) { 109 console.error( 110 chalk.red(`Detected a cycle in ${kind}! ${origin.name} -> ${destination.name}`) 111 ); 112 } 113 return; 114 } 115 resolveDependencies(graph, destination, { ...visitedNodes }); 116} 117