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