diff --git a/tools/crate-hierarchy-viz/Cargo.lock b/tools/crate-hierarchy-viz/Cargo.lock new file mode 100644 index 0000000000..99daf9dc69 --- /dev/null +++ b/tools/crate-hierarchy-viz/Cargo.lock @@ -0,0 +1,377 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "clap" +version = "4.5.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "crate-hierarchy-viz" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "serde", + "toml", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] diff --git a/tools/crate-hierarchy-viz/Cargo.toml b/tools/crate-hierarchy-viz/Cargo.toml new file mode 100644 index 0000000000..90d02d554a --- /dev/null +++ b/tools/crate-hierarchy-viz/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "crate-hierarchy-viz" +version = "0.1.0" +edition = "2024" +description = "Tool to visualize the crate hierarchy in the Graphite workspace" + +[workspace] +members = ["."] + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +toml = "0.8" +anyhow = "1.0" +clap = { version = "4.5", features = ["derive"] } diff --git a/tools/crate-hierarchy-viz/generate-crate-viz.sh b/tools/crate-hierarchy-viz/generate-crate-viz.sh new file mode 100755 index 0000000000..38e5c0f7cc --- /dev/null +++ b/tools/crate-hierarchy-viz/generate-crate-viz.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Build the visualization tool if it doesn't exist +if [ ! -f "tools/crate-hierarchy-viz/target/debug/crate-hierarchy-viz" ]; then + echo "Building crate hierarchy visualization tool..." + cargo build +fi + +# Generate the DOT file +echo "Generating crate hierarchy graph..." +./target/debug/crate-hierarchy-viz --workspace ../.. --format dot --output crate-hierarchy.dot --exclude-dyn-any + +echo "Generating crate hierarchy graph (excluding dyn-any)..." +./target/debug/crate-hierarchy-viz --workspace ../.. --format dot --exclude-dyn-any --output crate-hierarchy-no-dyn-any.dot + +# Generate visualizations if graphviz is available +if command -v dot &> /dev/null; then + echo "Generating PNG visualizations..." + dot -Tpng crate-hierarchy.dot -o crate-hierarchy.png + dot -Tpng crate-hierarchy-no-dyn-any.dot -o crate-hierarchy-no-dyn-any.png + + echo "Generating SVG visualizations..." + dot -Tsvg crate-hierarchy.dot -o crate-hierarchy.svg + dot -Tsvg crate-hierarchy-no-dyn-any.dot -o crate-hierarchy-no-dyn-any.svg + + echo "Visualizations generated:" + echo " - crate-hierarchy.dot (GraphViz DOT format)" + echo " - crate-hierarchy.png (PNG image)" + echo " - crate-hierarchy.svg (SVG image)" + echo " - crate-hierarchy-no-dyn-any.dot (GraphViz DOT format, dyn-any excluded)" + echo " - crate-hierarchy-no-dyn-any.png (PNG image, dyn-any excluded)" + echo " - crate-hierarchy-no-dyn-any.svg (SVG image, dyn-any excluded)" +else + echo "GraphViz not found. Generated DOT file only:" + echo " - crate-hierarchy.dot" + echo "Install GraphViz to generate PNG/SVG visualizations" +fi diff --git a/tools/crate-hierarchy-viz/src/main.rs b/tools/crate-hierarchy-viz/src/main.rs new file mode 100644 index 0000000000..d2bcd17108 --- /dev/null +++ b/tools/crate-hierarchy-viz/src/main.rs @@ -0,0 +1,339 @@ +use anyhow::{Context, Result}; +use clap::Parser; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Parser)] +#[command(name = "crate-hierarchy-viz")] +#[command(about = "Visualize the crate hierarchy in the Graphite workspace")] +struct Args { + /// Workspace root directory (defaults to current directory) + #[arg(short, long)] + workspace: Option, + + /// Output format: dot, text + #[arg(short, long, default_value = "dot")] + format: String, + + /// Output file (defaults to stdout) + #[arg(short, long)] + output: Option, + + /// Include external dependencies (workspace dependencies) + #[arg(long)] + include_external: bool, + + /// Exclude dyn-any from the graph (it's used everywhere) + #[arg(long)] + exclude_dyn_any: bool, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceToml { + workspace: WorkspaceConfig, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceConfig { + members: Vec, + dependencies: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum WorkspaceDependency { + Simple(String), + Detailed { + path: Option, + version: Option, + workspace: Option, + #[serde(flatten)] + other: HashMap, + }, +} + +#[derive(Debug, Deserialize)] +struct CrateToml { + package: PackageConfig, + dependencies: Option>, +} + +#[derive(Debug, Deserialize)] +struct PackageConfig { + name: String, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum CrateDependency { + Simple(String), + Detailed { + path: Option, + workspace: Option, + version: Option, + optional: Option, + #[serde(flatten)] + other: HashMap, + }, +} + +#[derive(Debug, Clone)] +struct CrateInfo { + name: String, + path: PathBuf, + dependencies: Vec, + external_dependencies: Vec, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + let workspace_root = args.workspace.unwrap_or_else(|| std::env::current_dir().unwrap()); + let workspace_toml_path = workspace_root.join("Cargo.toml"); + + // Parse workspace Cargo.toml + let workspace_content = fs::read_to_string(&workspace_toml_path) + .with_context(|| format!("Failed to read {:?}", workspace_toml_path))?; + let workspace_toml: WorkspaceToml = toml::from_str(&workspace_content) + .with_context(|| "Failed to parse workspace Cargo.toml")?; + + // Get workspace dependencies (external crates defined at workspace level) + let workspace_deps: HashSet = workspace_toml + .workspace + .dependencies + .unwrap_or_default() + .keys() + .cloned() + .collect(); + + // Parse each member crate and build name mapping + let mut crates = Vec::new(); + let mut workspace_crate_names = HashSet::new(); + + // First pass: collect all workspace crate names + for member in &workspace_toml.workspace.members { + let crate_path = workspace_root.join(member); + let cargo_toml_path = crate_path.join("Cargo.toml"); + + if !cargo_toml_path.exists() { + eprintln!("Warning: Cargo.toml not found for member: {}", member); + continue; + } + + let crate_content = fs::read_to_string(&cargo_toml_path) + .with_context(|| format!("Failed to read {:?}", cargo_toml_path))?; + let crate_toml: CrateToml = toml::from_str(&crate_content) + .with_context(|| format!("Failed to parse Cargo.toml for {}", member))?; + + workspace_crate_names.insert(crate_toml.package.name.clone()); + } + + // Second pass: parse dependencies now that we know all workspace crate names + for member in &workspace_toml.workspace.members { + let crate_path = workspace_root.join(member); + let cargo_toml_path = crate_path.join("Cargo.toml"); + + if !cargo_toml_path.exists() { + continue; + } + + let crate_content = fs::read_to_string(&cargo_toml_path) + .with_context(|| format!("Failed to read {:?}", cargo_toml_path))?; + let crate_toml: CrateToml = toml::from_str(&crate_content) + .with_context(|| format!("Failed to parse Cargo.toml for {}", member))?; + + let mut dependencies = Vec::new(); + let mut external_dependencies = Vec::new(); + + if let Some(deps) = &crate_toml.dependencies { + for (dep_name, dep_config) in deps { + let is_workspace_crate = workspace_crate_names.contains(dep_name); + let is_workspace_dep = workspace_deps.contains(dep_name); + + let is_local_dep = match dep_config { + CrateDependency::Detailed { workspace: Some(true), .. } => is_workspace_dep, + CrateDependency::Detailed { path: Some(_), .. } => true, + CrateDependency::Simple(_) => is_workspace_dep, + _ => false, + }; + + // Check if this dependency has a different package name + let actual_dep_name = match dep_config { + CrateDependency::Detailed { other, .. } => { + // Check if there's a "package" field that renames the dependency + if let Some(toml::Value::String(package_name)) = other.get("package") { + package_name.clone() + } else { + dep_name.clone() + } + } + _ => dep_name.clone(), + }; + + let is_actual_workspace_crate = workspace_crate_names.contains(&actual_dep_name); + + if is_workspace_crate || is_actual_workspace_crate || is_local_dep { + dependencies.push(actual_dep_name); + } else { + external_dependencies.push(actual_dep_name); + } + } + } + + crates.push(CrateInfo { + name: crate_toml.package.name.clone(), + path: crate_path, + dependencies, + external_dependencies, + }); + } + + // Filter dependencies to only include workspace crates + for crate_info in &mut crates { + crate_info.dependencies.retain(|dep| workspace_crate_names.contains(dep)); + } + + // Generate output + let output = match args.format.as_str() { + "dot" => generate_dot_format(&crates, args.include_external, args.exclude_dyn_any)?, + "text" => generate_text_format(&crates, args.include_external, args.exclude_dyn_any)?, + _ => anyhow::bail!("Unsupported format: {}", args.format), + }; + + // Write output + if let Some(output_path) = args.output { + fs::write(&output_path, output) + .with_context(|| format!("Failed to write to {:?}", output_path))?; + println!("Output written to: {:?}", output_path); + } else { + print!("{}", output); + } + + Ok(()) +} + +fn generate_dot_format(crates: &[CrateInfo], include_external: bool, exclude_dyn_any: bool) -> Result { + let mut output = String::new(); + output.push_str("digraph CrateHierarchy {\n"); + output.push_str(" rankdir=LR;\n"); + output.push_str(" node [shape=box, style=\"rounded,filled\", fillcolor=lightblue];\n"); + output.push_str(" edge [color=gray];\n\n"); + + // Add subgraphs for different categories + output.push_str(" subgraph cluster_core {\n"); + output.push_str(" label=\"Core Components\";\n"); + output.push_str(" style=filled;\n"); + output.push_str(" fillcolor=lightgray;\n"); + + let core_crates: Vec<_> = crates.iter() + .filter(|c| c.name.starts_with("graphite-") || c.name == "editor") + .collect(); + + for crate_info in &core_crates { + output.push_str(&format!(" \"{}\";\n", crate_info.name)); + } + output.push_str(" }\n\n"); + + output.push_str(" subgraph cluster_nodegraph {\n"); + output.push_str(" label=\"Node Graph System\";\n"); + output.push_str(" style=filled;\n"); + output.push_str(" fillcolor=lightyellow;\n"); + + let nodegraph_crates: Vec<_> = crates.iter() + .filter(|c| c.name.starts_with("graphene-") || + c.name == "graph-craft" || + c.name == "interpreted-executor" || + c.name == "wgpu-executor" || + c.name == "node-macro" || + c.name == "preprocessor") + .collect(); + + for crate_info in &nodegraph_crates { + output.push_str(&format!(" \"{}\";\n", crate_info.name)); + } + output.push_str(" }\n\n"); + + output.push_str(" subgraph cluster_libraries {\n"); + output.push_str(" label=\"Libraries\";\n"); + output.push_str(" style=filled;\n"); + output.push_str(" fillcolor=lightgreen;\n"); + + let library_crates: Vec<_> = crates.iter() + .filter(|c| !c.name.starts_with("graphite-") && + !c.name.starts_with("graphene-") && + c.name != "graph-craft" && + c.name != "interpreted-executor" && + c.name != "wgpu-executor" && + c.name != "node-macro" && + c.name != "preprocessor" && + c.name != "editor") + .collect(); + + for crate_info in &library_crates { + output.push_str(&format!(" \"{}\";\n", crate_info.name)); + } + output.push_str(" }\n\n"); + + // Add dependencies as edges + for crate_info in crates { + for dep in &crate_info.dependencies { + if exclude_dyn_any && dep == "dyn-any" { + continue; + } + output.push_str(&format!(" \"{}\" -> \"{}\";\n", crate_info.name, dep)); + } + + if include_external { + for dep in &crate_info.external_dependencies { + if exclude_dyn_any && dep == "dyn-any" { + continue; + } + output.push_str(&format!(" \"{}\" -> \"{}\" [style=dashed, color=red];\n", crate_info.name, dep)); + } + } + } + + output.push_str("}\n"); + Ok(output) +} + +fn generate_text_format(crates: &[CrateInfo], include_external: bool, exclude_dyn_any: bool) -> Result { + let mut output = String::new(); + output.push_str("Graphite Workspace Crate Hierarchy\n"); + output.push_str("==================================\n\n"); + + for crate_info in crates { + output.push_str(&format!("Crate: {}\n", crate_info.name)); + output.push_str(&format!("Path: {}\n", crate_info.path.display())); + + let filtered_deps: Vec<_> = crate_info.dependencies.iter() + .filter(|dep| !exclude_dyn_any || *dep != "dyn-any") + .collect(); + + if !filtered_deps.is_empty() { + output.push_str("Workspace Dependencies:\n"); + for dep in filtered_deps { + output.push_str(&format!(" - {}\n", dep)); + } + } + + if include_external { + let filtered_external_deps: Vec<_> = crate_info.external_dependencies.iter() + .filter(|dep| !exclude_dyn_any || *dep != "dyn-any") + .collect(); + + if !filtered_external_deps.is_empty() { + output.push_str("External Dependencies:\n"); + for dep in filtered_external_deps { + output.push_str(&format!(" - {}\n", dep)); + } + } + } + + output.push_str("\n"); + } + + Ok(output) +} \ No newline at end of file