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 {
#[arg(default_value = ".")]
project_path: PathBuf,
#[command(subcommand)]
command: Commands,
#[arg(long, global = true)]
json: bool,
}
#[derive(Subcommand)]
enum Commands {
ListModules,
ListClasses,
ListConfigs,
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(())
}