@factorialco/shadowdog
Version:
<img src="https://raw.githubusercontent.com/factorialco/shadowdog/refs/heads/main/logo.png" alt="drawing" width="100"/>
167 lines (165 loc) • 5.59 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DependencyGraph = void 0;
const chalk_1 = __importDefault(require("chalk"));
const utils_1 = require("../utils");
// Node in the dependency graph
class DependencyNode {
constructor(object) {
this.object = object;
this.dependencies = new Set();
this.dependents = new Set();
}
}
class DependencyGraph {
constructor() {
this.nodes = new Map();
this.levels = null;
}
addNode(object) {
if (!this.nodes.has(object)) {
this.nodes.set(object, new DependencyNode(object));
}
return this.nodes.get(object);
}
buildGraph(objects) {
// First, create nodes and build output index
const outputIndex = new Map();
for (const obj of objects) {
this.addNode(obj);
for (const output of obj.outputs || []) {
outputIndex.set(output, obj);
}
}
// Then, establish dependencies
for (const obj of objects) {
const node = this.nodes.get(obj);
for (const file of obj.files || []) {
const dependencyObj = outputIndex.get(file);
if (dependencyObj) {
const dependencyNode = this.nodes.get(dependencyObj);
node.dependencies.add(dependencyNode);
dependencyNode.dependents.add(node);
}
}
}
this.topologicalSort();
return this;
}
topologicalSort() {
const visited = new Set();
const temp = new Set();
const levels = new Map();
const visit = (node, level = 0) => {
if (temp.has(node)) {
throw new Error('Circular dependency detected');
}
if (visited.has(node)) {
return levels.get(node);
}
temp.add(node);
let maxChildLevel = level;
for (const dep of node.dependencies) {
const childLevel = visit(dep, level + 1);
maxChildLevel = Math.max(maxChildLevel, childLevel + 1);
}
temp.delete(node);
visited.add(node);
levels.set(node, maxChildLevel);
return maxChildLevel;
};
// Process all nodes
for (const node of this.nodes.values()) {
if (!visited.has(node)) {
visit(node);
}
}
// Group nodes by level
this.levels = new Map();
for (const [node, level] of levels) {
if (!this.levels.has(level)) {
this.levels.set(level, new Set());
}
this.levels.get(level).add(node);
}
}
getStructure() {
if (!this.levels) {
throw new Error('Graph not built yet. Call buildGraph first.');
}
const structure = {
byLevel: {},
nodeConnections: {},
};
// Organize by levels
for (const [level, nodes] of this.levels) {
structure.byLevel[level] = Array.from(nodes).map((node) => ({
object: node.object,
dependencies: Array.from(node.dependencies).map((dep) => dep.object),
dependents: Array.from(node.dependents).map((dep) => dep.object),
}));
}
// Create node connections map
for (const [obj, node] of this.nodes) {
structure.nodeConnections[JSON.stringify(obj)] = {
dependencies: Array.from(node.dependencies).map((dep) => dep.object),
dependents: Array.from(node.dependents).map((dep) => dep.object),
};
}
return structure;
}
}
exports.DependencyGraph = DependencyGraph;
const filterCommandTasks = (tasks) => {
return tasks.reduce((acc, task) => {
if (task.type === 'command') {
acc.commandTasks.push(task);
}
else {
acc.otherTasks.push(task);
}
return acc;
}, { commandTasks: [], otherTasks: [] });
};
/*
Builds the dependency graph of watchers and returns the watchers in layers, in the order they
need to run to satisfy the dependencies.
This also detects circular dependencies and throws an error they're found.
*/
const organizeTasksInLayers = (tasks) => {
const { commandTasks, otherTasks } = filterCommandTasks(tasks);
const tasksInDependencyFormat = commandTasks.map((task) => {
return {
task,
outputs: task.config.artifacts.map((artifact) => artifact.output),
files: task.files,
};
});
const graph = new DependencyGraph();
const structure = graph.buildGraph(tasksInDependencyFormat).getStructure();
const parallelTasks = Object.values(structure.byLevel).map((level) => ({
type: 'parallel',
tasks: level.map((node) => node.object.task),
}));
(0, utils_1.logMessage)(`🌳 Building a command tree with ${chalk_1.default.cyan(parallelTasks.length)} layers.`);
return {
type: 'serial',
tasks: [...parallelTasks, ...otherTasks],
};
};
const command = (task) => {
switch (task.type) {
case 'parallel': {
return organizeTasksInLayers(task.tasks);
}
default: {
return task;
}
}
};
exports.default = {
command,
};