findGraphRoots.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const NO_MARKER = 0;
  7. const IN_PROGRESS_MARKER = 1;
  8. const DONE_MARKER = 2;
  9. const DONE_MAYBE_ROOT_CYCLE_MARKER = 3;
  10. const DONE_AND_ROOT_MARKER = 4;
  11. /**
  12. * @template T
  13. */
  14. class Node {
  15. /**
  16. * @param {T} item the value of the node
  17. */
  18. constructor(item) {
  19. this.item = item;
  20. /** @type {Set<Node<T>>} */
  21. this.dependencies = new Set();
  22. this.marker = NO_MARKER;
  23. /** @type {Cycle<T> | undefined} */
  24. this.cycle = undefined;
  25. this.incoming = 0;
  26. }
  27. }
  28. /**
  29. * @template T
  30. */
  31. class Cycle {
  32. constructor() {
  33. /** @type {Set<Node<T>>} */
  34. this.nodes = new Set();
  35. }
  36. }
  37. /**
  38. * @template T
  39. * @typedef {object} StackEntry
  40. * @property {Node<T>} node
  41. * @property {Node<T>[]} openEdges
  42. */
  43. /**
  44. * @template T
  45. * @param {Iterable<T>} items list of items
  46. * @param {function(T): Iterable<T>} getDependencies function to get dependencies of an item (items that are not in list are ignored)
  47. * @returns {Iterable<T>} graph roots of the items
  48. */
  49. module.exports = (items, getDependencies) => {
  50. /** @type {Map<T, Node<T>>} */
  51. const itemToNode = new Map();
  52. for (const item of items) {
  53. const node = new Node(item);
  54. itemToNode.set(item, node);
  55. }
  56. // early exit when there is only a single item
  57. if (itemToNode.size <= 1) return items;
  58. // grab all the dependencies
  59. for (const node of itemToNode.values()) {
  60. for (const dep of getDependencies(node.item)) {
  61. const depNode = itemToNode.get(dep);
  62. if (depNode !== undefined) {
  63. node.dependencies.add(depNode);
  64. }
  65. }
  66. }
  67. // Set of current root modules
  68. // items will be removed if a new reference to it has been found
  69. /** @type {Set<Node<T>>} */
  70. const roots = new Set();
  71. // Set of current cycles without references to it
  72. // cycles will be removed if a new reference to it has been found
  73. // that is not part of the cycle
  74. /** @type {Set<Cycle<T>>} */
  75. const rootCycles = new Set();
  76. // For all non-marked nodes
  77. for (const selectedNode of itemToNode.values()) {
  78. if (selectedNode.marker === NO_MARKER) {
  79. // deep-walk all referenced modules
  80. // in a non-recursive way
  81. // start by entering the selected node
  82. selectedNode.marker = IN_PROGRESS_MARKER;
  83. // keep a stack to avoid recursive walk
  84. /** @type {StackEntry<T>[]} */
  85. const stack = [
  86. {
  87. node: selectedNode,
  88. openEdges: Array.from(selectedNode.dependencies)
  89. }
  90. ];
  91. // process the top item until stack is empty
  92. while (stack.length > 0) {
  93. const topOfStack = stack[stack.length - 1];
  94. // Are there still edges unprocessed in the current node?
  95. if (topOfStack.openEdges.length > 0) {
  96. // Process one dependency
  97. const dependency =
  98. /** @type {Node<T>} */
  99. (topOfStack.openEdges.pop());
  100. switch (dependency.marker) {
  101. case NO_MARKER:
  102. // dependency has not be visited yet
  103. // mark it as in-progress and recurse
  104. stack.push({
  105. node: dependency,
  106. openEdges: Array.from(dependency.dependencies)
  107. });
  108. dependency.marker = IN_PROGRESS_MARKER;
  109. break;
  110. case IN_PROGRESS_MARKER: {
  111. // It's a in-progress cycle
  112. let cycle = dependency.cycle;
  113. if (!cycle) {
  114. cycle = new Cycle();
  115. cycle.nodes.add(dependency);
  116. dependency.cycle = cycle;
  117. }
  118. // set cycle property for each node in the cycle
  119. // if nodes are already part of a cycle
  120. // we merge the cycles to a shared cycle
  121. for (
  122. let i = stack.length - 1;
  123. stack[i].node !== dependency;
  124. i--
  125. ) {
  126. const node = stack[i].node;
  127. if (node.cycle) {
  128. if (node.cycle !== cycle) {
  129. // merge cycles
  130. for (const cycleNode of node.cycle.nodes) {
  131. cycleNode.cycle = cycle;
  132. cycle.nodes.add(cycleNode);
  133. }
  134. }
  135. } else {
  136. node.cycle = cycle;
  137. cycle.nodes.add(node);
  138. }
  139. }
  140. // don't recurse into dependencies
  141. // these are already on the stack
  142. break;
  143. }
  144. case DONE_AND_ROOT_MARKER:
  145. // This node has be visited yet and is currently a root node
  146. // But as this is a new reference to the node
  147. // it's not really a root
  148. // so we have to convert it to a normal node
  149. dependency.marker = DONE_MARKER;
  150. roots.delete(dependency);
  151. break;
  152. case DONE_MAYBE_ROOT_CYCLE_MARKER:
  153. // This node has be visited yet and
  154. // is maybe currently part of a completed root cycle
  155. // we found a new reference to the cycle
  156. // so it's not really a root cycle
  157. // remove the cycle from the root cycles
  158. // and convert it to a normal node
  159. rootCycles.delete(/** @type {Cycle<T>} */ (dependency.cycle));
  160. dependency.marker = DONE_MARKER;
  161. break;
  162. // DONE_MARKER: nothing to do, don't recurse into dependencies
  163. }
  164. } else {
  165. // All dependencies of the current node has been visited
  166. // we leave the node
  167. stack.pop();
  168. topOfStack.node.marker = DONE_MARKER;
  169. }
  170. }
  171. const cycle = selectedNode.cycle;
  172. if (cycle) {
  173. for (const node of cycle.nodes) {
  174. node.marker = DONE_MAYBE_ROOT_CYCLE_MARKER;
  175. }
  176. rootCycles.add(cycle);
  177. } else {
  178. selectedNode.marker = DONE_AND_ROOT_MARKER;
  179. roots.add(selectedNode);
  180. }
  181. }
  182. }
  183. // Extract roots from root cycles
  184. // We take the nodes with most incoming edges
  185. // inside of the cycle
  186. for (const cycle of rootCycles) {
  187. let max = 0;
  188. /** @type {Set<Node<T>>} */
  189. const cycleRoots = new Set();
  190. const nodes = cycle.nodes;
  191. for (const node of nodes) {
  192. for (const dep of node.dependencies) {
  193. if (nodes.has(dep)) {
  194. dep.incoming++;
  195. if (dep.incoming < max) continue;
  196. if (dep.incoming > max) {
  197. cycleRoots.clear();
  198. max = dep.incoming;
  199. }
  200. cycleRoots.add(dep);
  201. }
  202. }
  203. }
  204. for (const cycleRoot of cycleRoots) {
  205. roots.add(cycleRoot);
  206. }
  207. }
  208. // When roots were found, return them
  209. if (roots.size > 0) {
  210. return Array.from(roots, r => r.item);
  211. } else {
  212. throw new Error("Implementation of findGraphRoots is broken");
  213. }
  214. };