markmv
Version:
TypeScript CLI for markdown file operations with intelligent link refactoring
261 lines • 8.27 kB
JavaScript
/**
* 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