UNPKG

arkit

Version:

Visualises JavaScript, TypeScript and Flow codebases as meaningful and committable architecture diagrams

290 lines (247 loc) 9.5 kB
import { ConfigBase, OutputFormat, SavedString } from "./types"; import * as path from "path"; import * as fs from "fs"; import { debug, warn, info } from "./logger"; export type ConverterBackend = "wasm" | "java"; export class Converter { private readonly config: ConfigBase; private backend: ConverterBackend | null = null; constructor(config: ConfigBase) { this.config = config; } private getDotWrapperPath(): string | undefined { // Check dist/ (runtime) then fall back to src/../dist/ (ts-jest) const candidates = [ path.join(__dirname, "dot-wasm.js"), path.join(__dirname, "..", "dist", "dot-wasm.js"), ]; for (const candidate of candidates) { if (fs.existsSync(candidate)) { return candidate; } } return undefined; } // Initialize the appropriate backend private async initializeBackend(): Promise<ConverterBackend> { if (this.backend) { return this.backend; } // Try WASM wrapper first (eliminates GraphViz system dependency) // The dot-wasm.js script uses @hpcc-js/wasm-graphviz as a drop-in dot replacement const dotWrapperPath = this.getDotWrapperPath(); if (dotWrapperPath) { try { fs.chmodSync(dotWrapperPath, 0o755); } catch { // Ignore permission errors } this.backend = "wasm"; info( "Using Java PlantUML + @hpcc-js/wasm-graphviz (no GraphViz system dependency)", ); return "wasm"; } // Fall back to system GraphViz try { await import("node-plantuml"); this.backend = "java"; info( "Using Java PlantUML + system GraphViz (requires Java + GraphViz installed)", ); return "java"; } catch (javaError: any) { throw new Error( `No diagram renderer available.\n` + `Java/PlantUML error: ${javaError.message}\n\n` + `Please install Java Runtime Environment (JRE) 8+:\n` + ` - Windows: https://adoptium.net/\n` + ` - macOS: brew install openjdk\n` + ` - Linux: sudo apt-get install default-jre\n\n` + `Then either:\n` + ` 1. npm install @hpcc-js/wasm-graphviz (recommended, no GraphViz needed)\n` + ` 2. Install GraphViz: https://graphviz.org/download/`, ); } } async convert(pathOrType: string, puml: string): Promise<SavedString> { const fullExportPath = path.resolve(this.config.directory, pathOrType); const ext = path.extname(fullExportPath); const shouldConvertAndSave = Object.values<any>(OutputFormat).includes( ext.replace(".", ""), ); const shouldConvertAndOutput = Object.values<any>(OutputFormat).includes(pathOrType); if (fs.existsSync(fullExportPath)) { debug("Removing", fullExportPath); fs.unlinkSync(fullExportPath); } if (shouldConvertAndSave || shouldConvertAndOutput) { debug("Converting", ext ? fullExportPath : pathOrType); // Initialize backend on first conversion await this.initializeBackend(); return this.convertToImage(puml, ext || pathOrType) .then((image) => { if (shouldConvertAndSave) { debug("Saving", fullExportPath, image.length); return this.save(fullExportPath, image); } return image.toString(); }) .catch((err) => { throw err; }); } else { if (ext === ".puml") { debug("Saving", fullExportPath); return this.save(fullExportPath, puml); } return Promise.resolve(puml); } } private save(path: string, data: Buffer | string): Promise<SavedString> { const str = new SavedString(data.toString()); str.path = path; fs.writeFileSync(path, data); return Promise.resolve(str); } requestChain: Promise<any> = Promise.resolve(); async convertToImage(puml: string, format: string): Promise<Buffer> { const formatMatch = format.match(/\w{3}/); if (!formatMatch) { throw new Error(`Cannot identify image format from ${format}`); } const outputFormat = formatMatch[0]; // Validate format is supported if (outputFormat !== "svg" && outputFormat !== "png") { throw new Error( `Unsupported format: ${outputFormat}. Only svg and png are supported.`, ); } if (!this.backend) { throw new Error("Backend not initialized"); } if (this.backend === "wasm") { return this.convertWithWasm(puml, outputFormat); } else { return this.convertWithSystemGraphviz(puml, outputFormat); } } private async convertWithWasm(puml: string, format: string): Promise<Buffer> { debug( `Converting PlantUML to ${format} using Java PlantUML + @hpcc-js/wasm GraphViz`, ); try { const plantuml = await import("node-plantuml"); // Use PlantUML to generate the requested format (svg or png) const output = await new Promise<Buffer>((resolve, reject) => { const chunks: Buffer[] = []; const gen = plantuml.generate(puml, { format: format as "svg" | "png", dot: this.getDotWrapperPath(), }); gen.out.on("data", (chunk: Buffer) => chunks.push(chunk)); gen.out.on("end", () => { let result = Buffer.concat(chunks); // If SVG, check for multiple XML declarations (PlantUML error output) // and keep only the first complete SVG document if (format === "svg") { const svgString = result.toString("utf8"); const closingSvgTag = "</svg>"; const firstSvgEnd = svgString.indexOf(closingSvgTag); if (firstSvgEnd !== -1) { const afterFirstSvg = svgString.substring( firstSvgEnd + closingSvgTag.length, ); if (afterFirstSvg.includes('<?xml version="1.0"')) { // Multiple SVG documents found, keep only the first one result = Buffer.from( svgString.substring(0, firstSvgEnd + closingSvgTag.length), "utf8", ); debug( `Filtered duplicate PlantUML output (error SVG), kept first ${result.length} bytes (was ${svgString.length})`, ); } } } resolve(result); }); gen.out.on("error", reject); }); debug( `Successfully generated ${format} using WASM backend, size: ${output.length} bytes`, ); return output; } catch (error: any) { warn(`WASM conversion error: ${error.message}`); throw new Error(`WASM conversion failed: ${error.message}`); } } private convertWithSystemGraphviz( puml: string, format: string, ): Promise<Buffer> { debug( `Converting PlantUML to ${format} using Java PlantUML + system GraphViz`, ); return new Promise((resolve, reject) => { // Chain requests to avoid concurrent PlantUML execution issues this.requestChain = this.requestChain.then(async () => { return new Promise<Buffer>((resolveChain, rejectChain) => { const plantuml = require("node-plantuml"); const chunks: Buffer[] = []; const gen = plantuml.generate(puml, { format }); gen.out.on("data", (chunk: Buffer) => { chunks.push(chunk); }); gen.out.on("end", () => { const result = Buffer.concat(chunks); debug( `Successfully generated ${format} using system GraphViz, size: ${result.length} bytes`, ); resolveChain(result); resolve(result); }); gen.out.on("error", (err: Error) => { warn(`System GraphViz conversion error: ${err.message}`); const errorMsg = this.getSystemDependencyErrorMessage(err); rejectChain(new Error(errorMsg)); reject(new Error(errorMsg)); }); }); }); }); } private getSystemDependencyErrorMessage(err: Error): string { const errMsg = err.message || ""; if (errMsg.includes("ENOENT") || errMsg.includes("java")) { return ( `PlantUML conversion failed: Java is not installed or not in PATH.\n` + `Please install Java Runtime Environment (JRE) 8 or higher:\n` + ` - Windows: Download from https://adoptium.net/\n` + ` - macOS: brew install openjdk\n` + ` - Linux: sudo apt-get install default-jre\n\n` + `Install @hpcc-js/wasm-graphviz to eliminate GraphViz system dependency:\n` + ` npm install @hpcc-js/wasm-graphviz\n\n` + `Or install GraphViz:\n` + ` - Windows: Download from https://graphviz.org/download/\n` + ` - macOS: brew install graphviz\n` + ` - Linux: sudo apt-get install graphviz\n\n` + `Original error: ${errMsg}` ); } if (errMsg.includes("dot") || errMsg.includes("graphviz")) { return ( `GraphViz not found. Please either:\n` + ` 1. npm install @hpcc-js/wasm-graphviz (recommended, no system dependency)\n` + ` 2. Install GraphViz system-wide:\n` + ` - Windows: https://graphviz.org/download/\n` + ` - macOS: brew install graphviz\n` + ` - Linux: sudo apt-get install graphviz\n\n` + `Original error: ${errMsg}` ); } return `PlantUML conversion failed: ${errMsg}`; } }