From 15913bc17341d8c570df4b2071a16193b8ecedee Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Fri, 30 Jan 2026 00:59:51 +0000 Subject: [PATCH] Add exporter modules --- src/exporters/dotExporter.ts | 160 +++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/exporters/dotExporter.ts diff --git a/src/exporters/dotExporter.ts b/src/exporters/dotExporter.ts new file mode 100644 index 0000000..1244c7b --- /dev/null +++ b/src/exporters/dotExporter.ts @@ -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; + + 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 = { + 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(); +}