@@ -2,7 +2,9 @@ use anyhow::{Context, Result};
22use serde:: Deserialize ;
33use std:: collections:: { HashMap , HashSet } ;
44use std:: fs;
5+ use std:: io:: Write ;
56use std:: path:: PathBuf ;
7+ use std:: process:: Command ;
68
79#[ derive( Debug , Deserialize ) ]
810struct WorkspaceToml {
@@ -173,17 +175,76 @@ fn main() -> Result<()> {
173175
174176 remove_transitive_dependencies ( & mut crates) ;
175177
176- // Generate DOT format and write to output file
178+ // Generate DOT format, convert to SVG, and write to output file
177179 let dot_content = generate_dot ( & crates) ;
180+ let svg_content = dot_to_svg ( & dot_content) ?;
178181
179182 if let Some ( parent) = output_path. parent ( ) {
180183 fs:: create_dir_all ( parent) . with_context ( || format ! ( "Failed to create directory {:?}" , parent) ) ?;
181184 }
182- fs:: write ( & output_path, & dot_content ) . with_context ( || format ! ( "Failed to write to {:?}" , output_path) ) ?;
185+ fs:: write ( & output_path, & svg_content ) . with_context ( || format ! ( "Failed to write to {:?}" , output_path) ) ?;
183186
184187 Ok ( ( ) )
185188}
186189
190+ /// Convert a DOT graph string to SVG by shelling out to @viz-js/viz via Node.js
191+ fn dot_to_svg ( dot : & str ) -> Result < String > {
192+ let temp_dir = std:: env:: temp_dir ( ) . join ( "crate-hierarchy-viz" ) ;
193+ fs:: create_dir_all ( & temp_dir) . with_context ( || "Failed to create temp directory" ) ?;
194+
195+ // Install @viz-js/viz into the temp directory if not already present
196+ let node_modules = temp_dir. join ( "node_modules" ) . join ( "@viz-js" ) ;
197+ if !node_modules. exists ( ) {
198+ let npm = if cfg ! ( target_os = "windows" ) { "npm.cmd" } else { "npm" } ;
199+ let status = Command :: new ( npm)
200+ . args ( [ "install" , "--prefix" , & temp_dir. to_string_lossy ( ) , "@viz-js/viz" ] )
201+ . stdout ( std:: process:: Stdio :: null ( ) )
202+ . stderr ( std:: process:: Stdio :: piped ( ) )
203+ . status ( )
204+ . with_context ( || "Failed to run `npm install`. Is Node.js installed?" ) ?;
205+ if !status. success ( ) {
206+ anyhow:: bail!( "Executing `npm install @viz-js/viz` failed" ) ;
207+ }
208+ }
209+
210+ // Write a small script that reads DOT from stdin and outputs SVG
211+ let script_path = temp_dir. join ( "convert.mjs" ) ;
212+ fs:: write (
213+ & script_path,
214+ r#"
215+ import { instance } from "@viz-js/viz";
216+ let dot = "";
217+ for await (const chunk of process.stdin) dot += chunk;
218+ const viz = await instance();
219+ process.stdout.write(viz.renderString(dot, { format: "svg" }));
220+ "#
221+ . trim ( ) ,
222+ ) ?;
223+
224+ let mut child = Command :: new ( "node" )
225+ . arg ( & script_path)
226+ . stdin ( std:: process:: Stdio :: piped ( ) )
227+ . stdout ( std:: process:: Stdio :: piped ( ) )
228+ . stderr ( std:: process:: Stdio :: piped ( ) )
229+ . spawn ( )
230+ . with_context ( || "Failed to spawn `node`. Is Node.js installed?" ) ?;
231+
232+ // Write DOT content to stdin then close the pipe
233+ child. stdin . take ( ) . unwrap ( ) . write_all ( dot. as_bytes ( ) ) . with_context ( || "Failed to write DOT content to stdin" ) ?;
234+
235+ let output = child. wait_with_output ( ) . with_context ( || "Failed to wait for `node` process" ) ?;
236+
237+ // Clean up the temp script (node_modules is intentionally kept as a cache)
238+ let _ = fs:: remove_file ( & script_path) ;
239+
240+ if !output. status . success ( ) {
241+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
242+ anyhow:: bail!( "DOT to SVG conversion failed (exit code {:?}):\n {}" , output. status. code( ) , stderr) ;
243+ }
244+
245+ String :: from_utf8 ( output. stdout ) . with_context ( || "SVG output was not valid UTF-8" )
246+ }
247+
187248fn generate_dot ( crates : & [ CrateInfo ] ) -> String {
188249 let mut out = String :: new ( ) ;
189250 out. push_str ( "digraph CrateHierarchy {\n " ) ;
0 commit comments