components-rs 0.1.1

Static analysis tooling for Components.js dependency injection projects
Documentation
use std::path::PathBuf;

use clap::{Parser, Subcommand};
use components_rs::components::registry::ComponentRegistry;
use components_rs::components::types::ComponentType;
use components_rs::config::registry::ConfigRegistry;
use components_rs::fs::OsFs;
use components_rs::module_state::ModuleState;

#[derive(Parser)]
#[command(name = "components-js")]
#[command(about = "Analyze Components.js projects — list classes, configs, and modules")]
struct Cli {
    /// Path to the CJS project root (must have node_modules installed)
    #[arg(default_value = ".")]
    project_path: PathBuf,

    #[command(subcommand)]
    command: Commands,

    /// Output as JSON
    #[arg(long, global = true)]
    json: bool,
}

#[derive(Subcommand)]
enum Commands {
    /// List all discovered CJS modules
    ListModules,
    /// List all discovered component classes
    ListClasses,
    /// List all discovered configuration instances
    ListConfigs,
    /// Show full summary of the project
    Summary,
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
        )
        .init();

    let cli = Cli::parse();
    let fs = OsFs;

    let state = ModuleState::build(&fs, &cli.project_path).await?;

    let mut comp_registry = ComponentRegistry::new();
    comp_registry
        .register_available_modules(&fs, &state)
        .await?;
    comp_registry.finalize();

    match cli.command {
        Commands::ListModules => {
            if cli.json {
                let modules: Vec<_> = comp_registry.modules.values().collect();
                println!("{}", serde_json::to_string_pretty(&modules)?);
            } else {
                println!("CJS Modules ({}):", comp_registry.modules.len());
                println!("{}", "".repeat(80));
                for module in comp_registry.modules.values() {
                    println!(
                        "  {} (require: {})",
                        module.iri,
                        module.require_name.as_deref().unwrap_or("?")
                    );
                    println!(
                        "    Components: {}  Source: {}",
                        module.components.len(),
                        module.source_file
                    );
                }
            }
        }
        Commands::ListClasses => {
            if cli.json {
                let components: Vec<_> = comp_registry.components.values().collect();
                println!("{}", serde_json::to_string_pretty(&components)?);
            } else {
                let mut classes: Vec<_> = comp_registry
                    .components
                    .values()
                    .filter(|c| {
                        c.component_type == ComponentType::Class
                            || c.component_type == ComponentType::AbstractClass
                    })
                    .collect();
                classes.sort_by_key(|c| &c.iri);

                println!("CJS Classes ({}):", classes.len());
                println!("{}", "".repeat(80));
                for comp in classes {
                    let type_label = match comp.component_type {
                        ComponentType::AbstractClass => " [abstract]",
                        _ => "",
                    };
                    println!("  {}{}", comp.iri, type_label);
                    if let Some(ref elem) = comp.require_element {
                        println!("    requireElement: {elem}");
                    }
                    if !comp.parameters.is_empty() {
                        println!("    parameters:");
                        for param in &comp.parameters {
                            let range_str = param.range.as_deref().unwrap_or("any");
                            let flags: Vec<&str> = [
                                param.required.then_some("required"),
                                param.lazy.then_some("lazy"),
                                param.unique.then_some("unique"),
                            ]
                            .into_iter()
                            .flatten()
                            .collect();
                            let flags_str = if flags.is_empty() {
                                String::new()
                            } else {
                                format!(" [{}]", flags.join(", "))
                            };
                            println!("      - {} : {}{}", param.iri, range_str, flags_str);
                        }
                    }
                    if !comp.extends.is_empty() {
                        println!("    extends: {}", comp.extends.join(", "));
                    }
                    println!();
                }
            }
        }
        Commands::ListConfigs => {
            let mut config_registry = ConfigRegistry::new();
            config_registry.discover_configs(&fs, &state).await?;

            if cli.json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&config_registry.configs)?
                );
            } else {
                println!("CJS Config Instances ({}):", config_registry.configs.len());
                println!("{}", "".repeat(80));
                for config in &config_registry.configs {
                    println!("  {}", config.iri);
                    println!("    type: {}", config.component_type_iri);
                    println!("    source: {}", config.source_file);
                    if !config.parameters.is_empty() {
                        println!("    parameters:");
                        for (key, val) in &config.parameters {
                            let val_str = match val {
                                serde_json::Value::String(s) => s.clone(),
                                other => other.to_string(),
                            };
                            let truncated = if val_str.len() > 60 {
                                format!("{}...", &val_str[..57])
                            } else {
                                val_str
                            };
                            println!("      {key}: {truncated}");
                        }
                    }
                    println!();
                }
            }
        }
        Commands::Summary => {
            let mut config_registry = ConfigRegistry::new();
            config_registry.discover_configs(&fs, &state).await?;

            let class_count = comp_registry
                .components
                .values()
                .filter(|c| c.component_type == ComponentType::Class)
                .count();
            let abstract_count = comp_registry
                .components
                .values()
                .filter(|c| c.component_type == ComponentType::AbstractClass)
                .count();

            if cli.json {
                let summary = serde_json::json!({
                    "project_path": cli.project_path.display().to_string(),
                    "node_modules_count": state.node_module_paths.len(),
                    "cjs_modules_count": comp_registry.modules.len(),
                    "classes_count": class_count,
                    "abstract_classes_count": abstract_count,
                    "config_instances_count": config_registry.configs.len(),
                    "contexts_count": state.contexts.len(),
                    "import_paths_count": state.import_paths.len(),
                });
                println!("{}", serde_json::to_string_pretty(&summary)?);
            } else {
                println!("Components.js Project Summary");
                println!("{}", "".repeat(80));
                println!("  Project:          {}", cli.project_path.display());
                println!("  Node modules:     {}", state.node_module_paths.len());
                println!("  CJS modules:      {}", comp_registry.modules.len());
                println!("  Classes:          {class_count}");
                println!("  Abstract classes: {abstract_count}");
                println!("  Config instances: {}", config_registry.configs.len());
                println!("  Contexts:         {}", state.contexts.len());
                println!("  Import paths:     {}", state.import_paths.len());
            }
        }
    }

    Ok(())
}