1import chalk from 'chalk';
2import { EOL } from 'os';
3
4import { DefaultDependencyKind, DependencyKind } from '../Packages';
5import PackagesGraph from './PackagesGraph';
6import PackagesGraphEdge from './PackagesGraphEdge';
7import PackagesGraphNode from './PackagesGraphNode';
8
9type NodeVisitingData = {
10  visited: Record<string, boolean>;
11  node: PackagesGraphNode;
12  edges: PackagesGraphEdge[];
13  kind: DependencyKind;
14  version: string;
15  level: number;
16  prefix: string;
17};
18
19export function printGraph(
20  graph: PackagesGraph,
21  packageNames: string[] = [],
22  kinds: DependencyKind[] = DefaultDependencyKind
23) {
24  function visitNodeEdges(visitingData: NodeVisitingData) {
25    const { node, edges, kind, version, level, prefix, visited } = visitingData;
26    const name = formatNodeName(node.name, kind, level);
27
28    if (visited[node.name] === true) {
29      process.stdout.write(chalk.dim(`${name} ...`) + EOL);
30      return;
31    }
32
33    visited[node.name] = true;
34
35    process.stdout.write(`${name} ${chalk.cyan(version)}` + EOL);
36
37    if (edges.length === 0 && level === 0) {
38      // It has no dependencies, but let's explicitly show that for the origin nodes.
39      process.stdout.write(edgePointer(true, false, false) + chalk.dim('none') + EOL);
40      return;
41    }
42    for (let i = 0; i < edges.length; i++) {
43      const edge = edges[i];
44      const isLast = i === edges.length - 1;
45      const nextEdges = edge.destination.getOutgoingEdgesOfKinds(kinds);
46      const hasMore = nextEdges.length > 0 && visited[edge.destination.name] !== true;
47
48      process.stdout.write(chalk.gray(prefix) + edgePointer(isLast, hasMore, edge.isCyclic));
49
50      visitNodeEdges({
51        node: edge.destination,
52        edges: nextEdges,
53        kind: edge.getDominantKind(),
54        version: edge.versionRange,
55        level: level + 1,
56        prefix: prefix + nodeIndent(isLast),
57        visited: { ...visited },
58      });
59    }
60  }
61
62  for (const node of graph.nodes.values()) {
63    if (packageNames.length > 0 && !packageNames.includes(node.name)) {
64      continue;
65    }
66    visitNodeEdges({
67      node,
68      edges: node.getOutgoingEdgesOfKinds(kinds),
69      kind: DependencyKind.Normal,
70      version: node.pkg.packageVersion,
71      level: 0,
72      prefix: '',
73      visited: {},
74    });
75  }
76}
77
78export function printNodeDependents(
79  node: PackagesGraphNode,
80  kinds: DependencyKind[] = DefaultDependencyKind
81) {
82  const nodes = node.getAllDependents(kinds);
83
84  if (nodes.length === 0) {
85    console.log(`${chalk.bold(node.name)} has no dependents`);
86    return;
87  }
88
89  console.log(`All dependents of the ${chalk.bold(node.name)} package:`);
90
91  for (const node of nodes) {
92    console.log(`- ${chalk.bold(node.name)}`);
93  }
94}
95
96function edgePointer(isLast: boolean, hasMore: boolean, isCyclic: boolean): string {
97  const rawPointer = [
98    isLast ? '└─' : '├─',
99    hasMore ? '┬─' : '──',
100    isCyclic ? chalk.red('∞') : '',
101    ' ',
102  ].join('');
103  return chalk.gray(rawPointer);
104}
105
106function nodeIndent(isLast: boolean): string {
107  return isLast ? '  ' : '│ ';
108}
109
110function formatNodeName(name: string, kind: DependencyKind, level: number): string {
111  if (level === 0) {
112    return chalk.bold(name);
113  }
114  switch (kind) {
115    case DependencyKind.Normal:
116      return chalk.green(name);
117    case DependencyKind.Dev:
118      return chalk.yellow(name);
119    case DependencyKind.Peer:
120      return chalk.magenta(name);
121    case DependencyKind.Optional:
122      return chalk.gray(name);
123  }
124}
125