UNPKG

markmv

Version:

TypeScript CLI for markdown file operations with intelligent link refactoring

261 lines 8.27 kB
/** * Builds and analyzes dependency relationships between markdown files. * * The DependencyGraph tracks file relationships through links, references, and imports to enable * intelligent file operations. It supports cycle detection, topological sorting, impact analysis, * and dependency-aware ordering for operations like joining and splitting. * * @category Core * * @example * Building a dependency graph * ```typescript * const graph = new DependencyGraph(); * * // Add files to the graph * await graph.addFile('intro.md'); * await graph.addFile('setup.md'); * await graph.addFile('usage.md'); * * // Analyze dependencies * const order = graph.getTopologicalOrder(); * console.log('Processing order:', order); * * // Check for circular dependencies * const cycles = graph.detectCycles(); * if (cycles.length > 0) { * console.warn('Circular dependencies detected'); * } * ``` * * @example * Impact analysis * ```typescript * const graph = new DependencyGraph(parsedFiles); * * // Find all files affected by changing api.md * const impacted = graph.getImpactedFiles('api.md'); * console.log(`${impacted.length} files will be affected`); * ``` */ export class DependencyGraph { nodes = new Map(); edges = new Map(); constructor(files = []) { if (files.length > 0) { this.build(files); } } build(files) { this.clear(); // Create nodes for all files for (const file of files) { this.addNode(file); } // Build dependency relationships for (const file of files) { this.addDependencies(file); } // Update dependents (reverse dependencies) this.updateDependents(); } addNode(file) { const node = { path: file.filePath, data: file, dependencies: new Set(file.dependencies), dependents: new Set(), }; this.nodes.set(file.filePath, node); this.edges.set(file.filePath, new Set(file.dependencies)); } addDependencies(file) { const dependencies = new Set(file.dependencies); this.edges.set(file.filePath, dependencies); const node = this.nodes.get(file.filePath); if (node) { node.dependencies = dependencies; } } updateDependents() { // Clear existing dependents for (const node of this.nodes.values()) { node.dependents.clear(); } // Rebuild dependents from dependencies for (const [filePath, dependencies] of this.edges) { for (const depPath of dependencies) { const depNode = this.nodes.get(depPath); if (depNode) { depNode.dependents.add(filePath); } } } // Update the parsed file data for (const node of this.nodes.values()) { node.data.dependents = Array.from(node.dependents); } } getNode(filePath) { return this.nodes.get(filePath); } getDependencies(filePath) { const dependencies = this.edges.get(filePath); return dependencies ? Array.from(dependencies) : []; } getDependents(filePath) { const node = this.nodes.get(filePath); return node ? Array.from(node.dependents) : []; } getTransitiveDependencies(filePath) { const visited = new Set(); const result = []; const visit = (path) => { if (visited.has(path)) return; visited.add(path); const dependencies = this.getDependencies(path); for (const dep of dependencies) { result.push(dep); visit(dep); } }; visit(filePath); return [...new Set(result)]; // Remove duplicates } getTransitiveDependents(filePath) { const visited = new Set(); const result = []; const visit = (path) => { if (visited.has(path)) return; visited.add(path); const dependents = this.getDependents(path); for (const dep of dependents) { result.push(dep); visit(dep); } }; visit(filePath); return [...new Set(result)]; // Remove duplicates } detectCircularDependencies() { const visited = new Set(); const recursionStack = new Set(); const cycles = []; const dfs = (path, currentPath) => { if (recursionStack.has(path)) { // Found a cycle const cycleStart = currentPath.indexOf(path); cycles.push([...currentPath.slice(cycleStart), path]); return; } if (visited.has(path)) return; visited.add(path); recursionStack.add(path); currentPath.push(path); const dependencies = this.getDependencies(path); for (const dep of dependencies) { dfs(dep, [...currentPath]); } recursionStack.delete(path); }; for (const filePath of this.nodes.keys()) { if (!visited.has(filePath)) { dfs(filePath, []); } } return cycles; } topologicalSort() { const visited = new Set(); const result = []; const dfs = (path) => { if (visited.has(path)) return; visited.add(path); const dependencies = this.getDependencies(path); for (const dep of dependencies) { dfs(dep); } result.push(path); }; for (const filePath of this.nodes.keys()) { if (!visited.has(filePath)) { dfs(filePath); } } return result; } updateFilePath(oldPath, newPath) { const node = this.nodes.get(oldPath); if (!node) return; // Update the node node.path = newPath; node.data.filePath = newPath; // Move the node to new key this.nodes.delete(oldPath); this.nodes.set(newPath, node); // Update edges const dependencies = this.edges.get(oldPath); if (dependencies) { this.edges.delete(oldPath); this.edges.set(newPath, dependencies); } // Update all references to this file in other nodes for (const [, deps] of this.edges) { if (deps.has(oldPath)) { deps.delete(oldPath); deps.add(newPath); } } // Update dependencies and dependents in nodes for (const otherNode of this.nodes.values()) { if (otherNode.dependencies.has(oldPath)) { otherNode.dependencies.delete(oldPath); otherNode.dependencies.add(newPath); } if (otherNode.dependents.has(oldPath)) { otherNode.dependents.delete(oldPath); otherNode.dependents.add(newPath); } } } removeNode(filePath) { const node = this.nodes.get(filePath); if (!node) return; // Remove from all dependency lists for (const otherNode of this.nodes.values()) { otherNode.dependencies.delete(filePath); otherNode.dependents.delete(filePath); } // Remove from edges for (const deps of this.edges.values()) { deps.delete(filePath); } // Remove the node itself this.nodes.delete(filePath); this.edges.delete(filePath); } clear() { this.nodes.clear(); this.edges.clear(); } getAllFiles() { return Array.from(this.nodes.keys()); } size() { return this.nodes.size; } toJSON() { const result = {}; for (const [filePath, dependencies] of this.edges) { result[filePath] = Array.from(dependencies); } return result; } } //# sourceMappingURL=dependency-graph.js.map