UNPKG

@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
"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;