You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
232 lines
6.0 KiB
232 lines
6.0 KiB
1 month ago
|
/*
|
||
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
||
|
Author Tobias Koppers @sokra
|
||
|
*/
|
||
|
|
||
|
"use strict";
|
||
|
|
||
|
const NO_MARKER = 0;
|
||
|
const IN_PROGRESS_MARKER = 1;
|
||
|
const DONE_MARKER = 2;
|
||
|
const DONE_MAYBE_ROOT_CYCLE_MARKER = 3;
|
||
|
const DONE_AND_ROOT_MARKER = 4;
|
||
|
|
||
|
/**
|
||
|
* @template T
|
||
|
*/
|
||
|
class Node {
|
||
|
/**
|
||
|
* @param {T} item the value of the node
|
||
|
*/
|
||
|
constructor(item) {
|
||
|
this.item = item;
|
||
|
/** @type {Set<Node<T>>} */
|
||
|
this.dependencies = new Set();
|
||
|
this.marker = NO_MARKER;
|
||
|
/** @type {Cycle<T> | undefined} */
|
||
|
this.cycle = undefined;
|
||
|
this.incoming = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @template T
|
||
|
*/
|
||
|
class Cycle {
|
||
|
constructor() {
|
||
|
/** @type {Set<Node<T>>} */
|
||
|
this.nodes = new Set();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @template T
|
||
|
* @typedef {object} StackEntry
|
||
|
* @property {Node<T>} node
|
||
|
* @property {Node<T>[]} openEdges
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @template T
|
||
|
* @param {Iterable<T>} items list of items
|
||
|
* @param {function(T): Iterable<T>} getDependencies function to get dependencies of an item (items that are not in list are ignored)
|
||
|
* @returns {Iterable<T>} graph roots of the items
|
||
|
*/
|
||
|
module.exports = (items, getDependencies) => {
|
||
|
/** @type {Map<T, Node<T>>} */
|
||
|
const itemToNode = new Map();
|
||
|
for (const item of items) {
|
||
|
const node = new Node(item);
|
||
|
itemToNode.set(item, node);
|
||
|
}
|
||
|
|
||
|
// early exit when there is only a single item
|
||
|
if (itemToNode.size <= 1) return items;
|
||
|
|
||
|
// grab all the dependencies
|
||
|
for (const node of itemToNode.values()) {
|
||
|
for (const dep of getDependencies(node.item)) {
|
||
|
const depNode = itemToNode.get(dep);
|
||
|
if (depNode !== undefined) {
|
||
|
node.dependencies.add(depNode);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Set of current root modules
|
||
|
// items will be removed if a new reference to it has been found
|
||
|
/** @type {Set<Node<T>>} */
|
||
|
const roots = new Set();
|
||
|
|
||
|
// Set of current cycles without references to it
|
||
|
// cycles will be removed if a new reference to it has been found
|
||
|
// that is not part of the cycle
|
||
|
/** @type {Set<Cycle<T>>} */
|
||
|
const rootCycles = new Set();
|
||
|
|
||
|
// For all non-marked nodes
|
||
|
for (const selectedNode of itemToNode.values()) {
|
||
|
if (selectedNode.marker === NO_MARKER) {
|
||
|
// deep-walk all referenced modules
|
||
|
// in a non-recursive way
|
||
|
|
||
|
// start by entering the selected node
|
||
|
selectedNode.marker = IN_PROGRESS_MARKER;
|
||
|
|
||
|
// keep a stack to avoid recursive walk
|
||
|
/** @type {StackEntry<T>[]} */
|
||
|
const stack = [
|
||
|
{
|
||
|
node: selectedNode,
|
||
|
openEdges: Array.from(selectedNode.dependencies)
|
||
|
}
|
||
|
];
|
||
|
|
||
|
// process the top item until stack is empty
|
||
|
while (stack.length > 0) {
|
||
|
const topOfStack = stack[stack.length - 1];
|
||
|
|
||
|
// Are there still edges unprocessed in the current node?
|
||
|
if (topOfStack.openEdges.length > 0) {
|
||
|
// Process one dependency
|
||
|
const dependency =
|
||
|
/** @type {Node<T>} */
|
||
|
(topOfStack.openEdges.pop());
|
||
|
switch (dependency.marker) {
|
||
|
case NO_MARKER:
|
||
|
// dependency has not be visited yet
|
||
|
// mark it as in-progress and recurse
|
||
|
stack.push({
|
||
|
node: dependency,
|
||
|
openEdges: Array.from(dependency.dependencies)
|
||
|
});
|
||
|
dependency.marker = IN_PROGRESS_MARKER;
|
||
|
break;
|
||
|
case IN_PROGRESS_MARKER: {
|
||
|
// It's a in-progress cycle
|
||
|
let cycle = dependency.cycle;
|
||
|
if (!cycle) {
|
||
|
cycle = new Cycle();
|
||
|
cycle.nodes.add(dependency);
|
||
|
dependency.cycle = cycle;
|
||
|
}
|
||
|
// set cycle property for each node in the cycle
|
||
|
// if nodes are already part of a cycle
|
||
|
// we merge the cycles to a shared cycle
|
||
|
for (
|
||
|
let i = stack.length - 1;
|
||
|
stack[i].node !== dependency;
|
||
|
i--
|
||
|
) {
|
||
|
const node = stack[i].node;
|
||
|
if (node.cycle) {
|
||
|
if (node.cycle !== cycle) {
|
||
|
// merge cycles
|
||
|
for (const cycleNode of node.cycle.nodes) {
|
||
|
cycleNode.cycle = cycle;
|
||
|
cycle.nodes.add(cycleNode);
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
node.cycle = cycle;
|
||
|
cycle.nodes.add(node);
|
||
|
}
|
||
|
}
|
||
|
// don't recurse into dependencies
|
||
|
// these are already on the stack
|
||
|
break;
|
||
|
}
|
||
|
case DONE_AND_ROOT_MARKER:
|
||
|
// This node has be visited yet and is currently a root node
|
||
|
// But as this is a new reference to the node
|
||
|
// it's not really a root
|
||
|
// so we have to convert it to a normal node
|
||
|
dependency.marker = DONE_MARKER;
|
||
|
roots.delete(dependency);
|
||
|
break;
|
||
|
case DONE_MAYBE_ROOT_CYCLE_MARKER:
|
||
|
// This node has be visited yet and
|
||
|
// is maybe currently part of a completed root cycle
|
||
|
// we found a new reference to the cycle
|
||
|
// so it's not really a root cycle
|
||
|
// remove the cycle from the root cycles
|
||
|
// and convert it to a normal node
|
||
|
rootCycles.delete(/** @type {Cycle<T>} */ (dependency.cycle));
|
||
|
dependency.marker = DONE_MARKER;
|
||
|
break;
|
||
|
// DONE_MARKER: nothing to do, don't recurse into dependencies
|
||
|
}
|
||
|
} else {
|
||
|
// All dependencies of the current node has been visited
|
||
|
// we leave the node
|
||
|
stack.pop();
|
||
|
topOfStack.node.marker = DONE_MARKER;
|
||
|
}
|
||
|
}
|
||
|
const cycle = selectedNode.cycle;
|
||
|
if (cycle) {
|
||
|
for (const node of cycle.nodes) {
|
||
|
node.marker = DONE_MAYBE_ROOT_CYCLE_MARKER;
|
||
|
}
|
||
|
rootCycles.add(cycle);
|
||
|
} else {
|
||
|
selectedNode.marker = DONE_AND_ROOT_MARKER;
|
||
|
roots.add(selectedNode);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Extract roots from root cycles
|
||
|
// We take the nodes with most incoming edges
|
||
|
// inside of the cycle
|
||
|
for (const cycle of rootCycles) {
|
||
|
let max = 0;
|
||
|
/** @type {Set<Node<T>>} */
|
||
|
const cycleRoots = new Set();
|
||
|
const nodes = cycle.nodes;
|
||
|
for (const node of nodes) {
|
||
|
for (const dep of node.dependencies) {
|
||
|
if (nodes.has(dep)) {
|
||
|
dep.incoming++;
|
||
|
if (dep.incoming < max) continue;
|
||
|
if (dep.incoming > max) {
|
||
|
cycleRoots.clear();
|
||
|
max = dep.incoming;
|
||
|
}
|
||
|
cycleRoots.add(dep);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
for (const cycleRoot of cycleRoots) {
|
||
|
roots.add(cycleRoot);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// When roots were found, return them
|
||
|
if (roots.size > 0) {
|
||
|
return Array.from(roots, r => r.item);
|
||
|
}
|
||
|
|
||
|
throw new Error("Implementation of findGraphRoots is broken");
|
||
|
};
|