Add exporter modules
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-01-30 00:59:51 +00:00
parent 55b2e0801e
commit 15913bc173

View File

@@ -0,0 +1,160 @@
import * as fs from 'fs';
import { DependencyGraph, GraphNode, GraphEdge, GraphCluster, ExportOptions } from '../types';
export class DOTExporter {
private graph: DependencyGraph;
private options: Required<ExportOptions>;
constructor(graph: DependencyGraph, options: ExportOptions) {
this.graph = graph;
this.options = {
format: options.format,
outputPath: options.outputPath,
includeMetadata: options.includeMetadata ?? false,
layout: options.layout ?? 'dot',
rankDir: options.rankDir ?? 'TB'
};
}
export(): string {
const lines: string[] = [];
lines.push('digraph typeflow {');
lines.push(' graph [');
lines.push(` rankdir=${this.options.rankDir}`);
lines.push(' splines=true');
lines.push(' overlap=false');
lines.push(' nodesep=0.5');
lines.push(' ranksep=0.5');
lines.push(' ]');
lines.push('');
lines.push(' node [');
lines.push(' shape=box');
lines.push(' style=filled');
lines.push(' fontname="Helvetica"');
lines.push(' fontsize=10');
lines.push(' ]');
lines.push('');
lines.push(' edge [');
lines.push(' fontname="Helvetica"');
lines.push(' fontsize=8');
lines.push(' ]');
lines.push('');
if (this.graph.clusters.length > 0) {
for (const cluster of this.graph.clusters) {
this.writeCluster(lines, cluster);
}
lines.push('');
}
for (const edge of this.graph.edges) {
this.writeEdge(lines, edge);
}
lines.push('');
for (const node of this.graph.nodes.values()) {
this.writeNode(lines, node);
}
if (this.options.includeMetadata) {
lines.push('');
lines.push(' subgraph cluster_metadata {');
lines.push(' label="Metadata"');
lines.push(' style=dashed');
lines.push(` nodeCount [label="Nodes: ${this.graph.metadata.nodeCount}"]`);
lines.push(` edgeCount [label="Edges: ${this.graph.metadata.edgeCount}"]`);
lines.push(` createdAt [label="Created: ${this.graph.metadata.createdAt}"]`);
lines.push(' }');
}
lines.push('}');
return lines.join('\n');
}
private writeCluster(lines: string[], cluster: GraphCluster): void {
lines.push(` subgraph ${cluster.id} {`);
lines.push(` label="${cluster.label}"`);
if (cluster.style) {
lines.push(` style="${cluster.style}"`);
}
lines.push(' color=lightgray');
for (const nodeId of cluster.nodeIds) {
lines.push(` ${this.escapeId(nodeId)}`);
}
lines.push(' }');
}
private writeNode(lines: string[], node: GraphNode): void {
const nodeId = this.escapeId(node.id);
const label = this.getNodeLabel(node);
const fillcolor = this.getNodeColor(node.kind);
lines.push(` ${nodeId} [`);
lines.push(` label="${label}"`);
lines.push(` fillcolor="${fillcolor}"`);
lines.push(' ]');
}
private writeEdge(lines: string[], edge: GraphEdge): void {
const sourceId = this.escapeId(edge.source);
const targetId = this.escapeId(edge.target);
const props: string[] = [];
if (edge.style) {
props.push(`style="${edge.style}"`);
}
if (edge.label) {
props.push(`label="${edge.label}"`);
}
if (edge.weight !== undefined) {
props.push(`weight=${edge.weight}`);
}
const propsStr = props.length > 0 ? ` [${props.join(', ')}]` : '';
lines.push(` ${sourceId} -> ${targetId}${propsStr}`);
}
private getNodeLabel(node: GraphNode): string {
const name = node.label;
const fileName = node.filePath.split('/').pop() || node.filePath;
const line = node.lineNumber;
return `${name}\\n(${fileName}:${line})`;
}
private getNodeColor(kind: string): string {
const colors: Record<string, string> = {
interface: '#e1f5fe',
type_alias: '#f3e5f5',
class: '#e8f5e9',
enum: '#fff3e0',
function: '#fce4ec',
import: '#ffebee',
external: '#fafafa'
};
return colors[kind] || '#ffffff';
}
private escapeId(id: string): string {
return id.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^(\d)/, '_$1');
}
toFile(): void {
const content = this.export();
fs.writeFileSync(this.options.outputPath, content, 'utf-8');
}
}
export function exportToDOT(graph: DependencyGraph, options: ExportOptions): string {
const exporter = new DOTExporter(graph, options);
return exporter.export();
}