@beemo/dependency-graph
Version:
Generate a dependency graph for a list of packages, based on their defined `dependencies` and `peerDependencies`.
208 lines (207 loc) • 6.83 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const Node_1 = __importDefault(require("./Node"));
class Graph {
constructor(packages = []) {
this.mapped = false;
this.nodes = new Map();
this.packages = new Map();
this.addPackages(packages);
}
/**
* Add a package by name with an associated `package.json` object.
* Will map a dependency between the package and its dependees
* found in `dependencies` and `peerDependencies`.
*/
addPackage(pkg) {
if (this.mapped) {
this.resetNodes();
}
// Cache package data for later use
this.packages.set(pkg.name, pkg);
// Add node to the graph
this.addNode(pkg.name);
return this;
}
/**
* Add multiple packages.
*/
addPackages(packages = []) {
packages.forEach((pkg) => {
this.addPackage(pkg);
});
return this;
}
/**
* Resolve the dependency graph and return a list of all
* `package.json` objects in the order they are depended on.
*/
resolveList() {
return this.resolveBatchList().reduce((flatList, batchList) => {
flatList.push(...batchList);
return flatList;
}, []);
}
/**
* Resolve the dependency graph and return a tree of nodes for all
* `package.json` objects and their dependency mappings.
*/
resolveTree() {
this.mapDependencies();
const seen = new Set();
const resolve = (node, tree) => {
if (seen.has(node.name)) {
return;
}
// Only include nodes that have package data
const pkg = this.packages.get(node.name);
if (!pkg) {
return;
}
const branch = {
package: pkg,
};
this.sortByDependedOn(node.dependents).forEach((child) => {
resolve(child, branch);
});
if (tree.nodes) {
tree.nodes.push(branch);
}
else {
tree.nodes = [branch];
}
seen.add(node.name);
};
const trunk = {
nodes: [],
root: true,
};
this.sortByDependedOn(this.getRootNodes()).forEach((node) => resolve(node, trunk));
// Some nodes are missing, so they must be a cycle
if (seen.size !== this.nodes.size) {
this.detectCycle();
}
return trunk;
}
/**
* Resolve the dependency graph and return a list of batched
* `package.json` objects in the order they are depended on.
*/
resolveBatchList() {
this.mapDependencies();
const batches = [];
const seen = new Set();
const addBatch = () => {
const nextBatch = Array.from(this.nodes.values()).filter((node) => {
return (!seen.has(node) &&
(node.requirements.size === 0 ||
Array.from(node.requirements.values()).filter((dep) => !seen.has(dep)).length === 0));
});
// Some nodes are missing, so they must be a cycle
if (nextBatch.length === 0) {
this.detectCycle();
}
batches.push(this.sortByDependedOn(nextBatch).map((node) => this.packages.get(node.name)));
nextBatch.forEach((node) => seen.add(node));
if (seen.size !== this.nodes.size) {
addBatch();
}
};
addBatch();
return batches;
}
/**
* Add a node for the defined package name.
*/
addNode(name) {
// Cache node for constant lookups
this.nodes.set(name, new Node_1.default(name));
}
/**
* Dig through all nodes and attempt to find a circular dependency cycle.
*/
detectCycle() {
const dig = (node, cycle) => {
if (cycle.has(node)) {
const path = [...Array.from(cycle), node].map((n) => n.name).join(' -> ');
throw new Error(`Circular dependency detected: ${path}`);
}
cycle.add(node);
node.dependents.forEach((child) => dig(child, new Set(cycle)));
};
this.nodes.forEach((node) => dig(node, new Set()));
}
/**
* Return all nodes that can be considered "root",
* as determined by having no requirements.
*/
getRootNodes() {
const rootNodes = [];
this.nodes.forEach((node) => {
if (node.requirements.size === 0) {
rootNodes.push(node);
}
});
// If no root nodes are found, but nodes exist, then we have a cycle
if (rootNodes.length === 0 && this.nodes.size !== 0) {
this.detectCycle();
}
return rootNodes;
}
/**
* Map dependencies between all currently registered packages.
*/
mapDependencies() {
if (this.mapped) {
return;
}
this.mapped = true;
this.packages.forEach((pkg) => {
Object.keys(Object.assign(Object.assign({}, pkg.dependencies), pkg.peerDependencies)).forEach((depName) => {
this.mapDependency(pkg.name, depName);
});
});
}
/**
* Map a dependency link for a dependent (child) depending on a requirement (parent).
* Will link the parent and child accordingly, and will remove the child
* from the root if it exists.
*/
mapDependency(dependentName, requirementName) {
const requirement = this.nodes.get(requirementName);
const dependent = this.nodes.get(dependentName);
if (!requirement || !dependent) {
return;
}
// Child depends on parent
dependent.requirements.add(requirement);
// Parent is a dependee of child
requirement.dependents.add(dependent);
}
/**
* Remove all current nodes in the graph and add new root nodes for each package.
*/
resetNodes() {
this.mapped = false;
this.nodes = new Map();
this.packages.forEach((pkg) => {
this.addNode(pkg.name);
});
}
/**
* Sort a set of nodes by most depended on, fall back to alpha sort as tie breaker
*/
sortByDependedOn(nodes) {
return Array.from(nodes).sort((a, b) => {
const diff = b.dependents.size - a.dependents.size;
if (diff === 0) {
return a.name > b.name ? 1 : -1;
}
return diff;
});
}
}
exports.default = Graph;