@hugsylabs/hugsy-compiler
Version:
Configuration compiler for Claude Code settings
187 lines • 6.5 kB
JavaScript
/**
* Dependency graph utilities for detecting circular dependencies
*/
export class DependencyGraph {
adjacencyList = new Map();
visiting = new Set();
visited = new Set();
cycleDetected = null;
/**
* Build dependency graph from config
*/
buildGraph(dependencies) {
this.adjacencyList.clear();
for (const [node, deps] of dependencies) {
if (!this.adjacencyList.has(node)) {
this.adjacencyList.set(node, new Set());
}
const depList = Array.isArray(deps) ? deps : [deps];
for (const dep of depList) {
this.adjacencyList.get(node)?.add(dep);
// Ensure all nodes exist in the graph
if (!this.adjacencyList.has(dep)) {
this.adjacencyList.set(dep, new Set());
}
}
}
}
/**
* Detect cycles in the dependency graph using DFS
*/
detectCycles(dependencies) {
this.buildGraph(dependencies);
this.visiting.clear();
this.visited.clear();
this.cycleDetected = null;
for (const node of this.adjacencyList.keys()) {
if (!this.visited.has(node)) {
const path = [];
this.dfs(node, path);
if (this.cycleDetected) {
return this.cycleDetected;
}
}
}
return null;
}
/**
* DFS traversal to detect cycles
*/
dfs(node, path) {
if (this.visiting.has(node)) {
// Found a cycle
const cycleStartIndex = path.indexOf(node);
const cycle = path.slice(cycleStartIndex).concat(node);
this.cycleDetected = {
message: `Circular dependency detected: ${cycle.join(' -> ')}`,
cycle,
path: cycle.join(' -> ')
};
return true;
}
if (this.visited.has(node)) {
return false;
}
this.visiting.add(node);
path.push(node);
const neighbors = this.adjacencyList.get(node) ?? new Set();
for (const neighbor of neighbors) {
if (this.dfs(neighbor, [...path])) {
return true;
}
}
path.pop();
this.visiting.delete(node);
this.visited.add(node);
return false;
}
/**
* Get load order using topological sort
* Returns null if there's a cycle
* The order is such that dependencies come before their dependents
*/
getLoadOrder(dependencies) {
// First check for cycles
if (this.detectCycles(dependencies)) {
return null;
}
// Build reverse graph for topological sort (if A depends on B, we need B before A)
const reverseGraph = new Map();
const allNodes = new Set();
for (const [node, deps] of dependencies) {
allNodes.add(node);
if (!reverseGraph.has(node)) {
reverseGraph.set(node, new Set());
}
const depList = Array.isArray(deps) ? deps : (deps ? [deps] : []);
for (const dep of depList) {
allNodes.add(dep);
if (!reverseGraph.has(dep)) {
reverseGraph.set(dep, new Set());
}
// Add edge from dependency to dependent
reverseGraph.get(dep)?.add(node);
}
}
// Calculate in-degree for each node (number of dependencies)
const inDegree = new Map();
for (const node of allNodes) {
inDegree.set(node, 0);
}
for (const [node, deps] of dependencies) {
const depList = Array.isArray(deps) ? deps : (deps ? [deps] : []);
inDegree.set(node, depList.length);
}
// Find nodes with no dependencies
const queue = [];
for (const [node, degree] of inDegree) {
if (degree === 0) {
queue.push(node);
}
}
const result = [];
while (queue.length > 0) {
const node = queue.shift();
result.push(node);
// Process all nodes that depend on this node
const dependents = reverseGraph.get(node) ?? new Set();
for (const dependent of dependents) {
const newDegree = (inDegree.get(dependent) ?? 1) - 1;
inDegree.set(dependent, newDegree);
if (newDegree === 0) {
queue.push(dependent);
}
}
}
// If not all nodes were processed, there's a cycle
if (result.length !== allNodes.size) {
return null;
}
return result;
}
/**
* Static helper to detect cycles in HugsyConfig
*/
static detectConfigCycles(configPath, configs, visited = new Set()) {
if (visited.has(configPath)) {
const cycle = Array.from(visited).concat(configPath);
return {
message: `Circular dependency detected in presets: ${cycle.join(' -> ')}`,
cycle,
path: cycle.join(' -> ')
};
}
visited.add(configPath);
const config = configs.get(configPath);
if (config?.extends) {
const extendsList = Array.isArray(config.extends) ? config.extends : [config.extends];
for (const extend of extendsList) {
const error = this.detectConfigCycles(extend, configs, new Set(visited));
if (error) {
return error;
}
}
}
return null;
}
/**
* Format cycle error for user-friendly display
*/
static formatCycleError(error) {
const lines = [
'❌ Circular dependency detected!',
'',
'Dependency chain:',
];
const cycle = error.cycle;
for (let i = 0; i < cycle.length - 1; i++) {
lines.push(` ${i + 1}. ${cycle[i]} → ${cycle[i + 1]}`);
}
lines.push('');
lines.push('To fix this issue:');
lines.push(` • Remove the extends reference from ${cycle[cycle.length - 2]} to ${cycle[cycle.length - 1]}`);
lines.push(` • Or restructure your presets to avoid circular dependencies`);
return lines.join('\n');
}
}
//# sourceMappingURL=dependency-graph.js.map