cyclic-package-graph-node.js 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. "use strict";
  2. let lastCollapsedNodeId = 0;
  3. /**
  4. * Represents a cyclic collection of nodes in a PackageGraph.
  5. * It is meant to be used as a black box, where the only exposed
  6. * information are the connections to the other nodes of the graph.
  7. * It can contain either `PackageGraphNode`s or other `CyclicPackageGraphNode`s.
  8. *
  9. * @extends {Map<string, import('..').PackageGraphNode | CyclicPackageGraphNode>}
  10. */
  11. class CyclicPackageGraphNode extends Map {
  12. constructor() {
  13. super();
  14. this.name = `(cycle) ${(lastCollapsedNodeId += 1)}`;
  15. /** @type {Map<string, import('..').PackageGraphNode | CyclicPackageGraphNode>} */
  16. this.localDependencies = new Map();
  17. /** @type {Map<string, import('..').PackageGraphNode | CyclicPackageGraphNode>} */
  18. this.localDependents = new Map();
  19. }
  20. // eslint-disable-next-line class-methods-use-this
  21. get isCycle() {
  22. return true;
  23. }
  24. /**
  25. * @returns {string} A representation of a cycle, like like `A -> B -> C -> A`.
  26. */
  27. toString() {
  28. const parts = Array.from(this, ([key, node]) =>
  29. node.isCycle ? `(nested cycle: ${node.toString()})` : key
  30. );
  31. // start from the origin
  32. parts.push(parts[0]);
  33. return parts.reverse().join(" -> ");
  34. }
  35. /**
  36. * Flattens a CyclicPackageGraphNode (which can have multiple level of cycles).
  37. */
  38. flatten() {
  39. /** @type {import('..').PackageGraphNode[]} */
  40. const result = [];
  41. for (const node of this.values()) {
  42. if (node.isCycle) {
  43. result.push(...node.flatten());
  44. } else {
  45. result.push(node);
  46. }
  47. }
  48. return result;
  49. }
  50. /**
  51. * Checks if a given node is contained in this cycle (or in a nested one)
  52. *
  53. * @param {string} name The name of the package to search in this cycle
  54. * @returns {boolean}
  55. */
  56. contains(name) {
  57. for (const [currentName, currentNode] of this) {
  58. if (currentNode.isCycle) {
  59. if (currentNode.contains(name)) {
  60. return true;
  61. }
  62. } else if (currentName === name) {
  63. return true;
  64. }
  65. }
  66. return false;
  67. }
  68. /**
  69. * Adds a graph node, or a nested cycle, to this group.
  70. *
  71. * @param {import('..').PackageGraphNode | CyclicPackageGraphNode} node
  72. */
  73. insert(node) {
  74. this.set(node.name, node);
  75. this.unlink(node);
  76. for (const [dependencyName, dependencyNode] of node.localDependencies) {
  77. if (!this.contains(dependencyName)) {
  78. this.localDependencies.set(dependencyName, dependencyNode);
  79. }
  80. }
  81. for (const [dependentName, dependentNode] of node.localDependents) {
  82. if (!this.contains(dependentName)) {
  83. this.localDependents.set(dependentName, dependentNode);
  84. }
  85. }
  86. }
  87. /**
  88. * Remove pointers to candidate node from internal collections.
  89. * @param {import('..').PackageGraphNode | CyclicPackageGraphNode} candidateNode instance to unlink
  90. */
  91. unlink(candidateNode) {
  92. // remove incoming edges ("indegree")
  93. this.localDependencies.delete(candidateNode.name);
  94. // remove outgoing edges ("outdegree")
  95. this.localDependents.delete(candidateNode.name);
  96. }
  97. }
  98. module.exports.CyclicPackageGraphNode = CyclicPackageGraphNode;