use anyhow::{anyhow, Result};
use colored::Colorize;
use std::path::Path;
use std::fs;
use ferrisup_common::fs::create_directory;
use ferrisup_common::cargo;
use toml_edit::{DocumentMut};
use crate::commands::test_mode::is_test_mode;
use super::project_structure::{analyze_project_structure, map_component_to_template};
use super::utils::{store_transformation_metadata, store_component_type_in_cargo, make_shared_component_accessible, update_root_file_references, add_component_to_workspace};
use super::ui::{get_input_with_default, select_option};
use super::constants::{get_formatted_component_types, get_component_type_names};
pub fn add_component(project_dir: &Path) -> Result<()> {
analyze_project_structure(project_dir)?;
let ferrisup_dir = project_dir.join(".ferrisup");
create_directory(&ferrisup_dir)?;
let component_types = get_formatted_component_types();
let component_idx = if is_test_mode() {
0 } else {
select_option("Select component type:", &component_types, 0)?
};
let component_type = get_component_type_names()[component_idx];
let mut component_name = get_input_with_default(
&format!("Component name [{}]", component_type),
component_type
)?;
if component_type == "shared" {
let mut is_available = false;
while !is_available {
match cargo::is_crate_name_available(&component_name) {
Ok(available) => {
if available {
is_available = true;
println!(
"{} {}",
"Success:".green().bold(),
format!("Crate name '{}' is available on crates.io", component_name).green()
);
} else {
println!(
"{} {}",
"Warning:".yellow().bold(),
format!("Crate name '{}' is already taken on crates.io", component_name).yellow()
);
component_name = get_input_with_default(
"Please enter a different name for your shared component",
&format!("{}-common", project_dir.file_name().unwrap_or_default().to_string_lossy())
)?;
}
},
Err(e) => {
println!(
"{} {}",
"Warning:".yellow().bold(),
format!("Could not check crate name availability: {}", e).yellow()
);
is_available = true;
}
}
}
}
let component_dir = project_dir.join(&component_name);
if component_dir.exists() {
println!(
"{} {}",
"Error:".red().bold(),
format!("Component directory '{}' already exists", component_name).red()
);
return Ok(());
}
let framework = select_framework_for_component_type(component_type)?;
println!(
"{}",
format!(
"Creating {} component with name: {}",
component_type, component_name
)
.blue()
);
let template = if component_type == "shared" {
"library"
} else {
map_component_to_template(component_type)
};
let current_dir = std::env::current_dir()?;
std::env::set_current_dir(project_dir)?;
let result = crate::commands::new::execute(
Some(&component_name),
Some(template),
framework.as_deref(),
None,
None,
false,
false,
false,
None,
);
std::env::set_current_dir(current_dir)?;
if let Err(e) = result {
println!("{} {}", "Error creating component:".red().bold(), e);
return Err(anyhow!("Failed to create component"));
}
if let Some(framework_name) = &framework {
println!("{} {}", "Using framework:".blue(), framework_name.cyan());
}
store_transformation_metadata(project_dir, &component_name, template, framework.as_deref())?;
store_component_type_in_cargo(&component_dir, template)?;
add_component_to_workspace(project_dir, &component_name)?;
if component_type == "shared" {
make_shared_component_accessible(project_dir, &component_name)?;
}
let files_to_keep_at_root: Vec<String> = Vec::new();
update_root_file_references(project_dir, &component_name, &files_to_keep_at_root)?;
Ok(())
}
pub fn add_component_without_workspace(project_dir: &Path) -> Result<()> {
println!(
"{}",
"Adding component to existing project structure...".blue()
);
let structure = analyze_project_structure(project_dir)?;
let project_name = &structure.project_name;
let component_types = get_formatted_component_types();
let component_idx = if is_test_mode() {
0 } else {
select_option("Select component type to add:", &component_types, 0)?
};
let component_type = get_component_type_names()[component_idx];
let framework = select_framework_for_component_type(component_type)?;
let template = map_component_to_template(component_type);
let ferrisup_dir = project_dir.join(".ferrisup");
create_directory(&ferrisup_dir)?;
store_transformation_metadata(project_dir, project_name, template, framework.as_deref())?;
store_component_type_in_cargo(project_dir, template)?;
if let Some(framework_name) = &framework {
add_framework_dependencies(project_dir, framework_name)?;
println!("{} {}", "Added dependencies for framework:".blue(), framework_name.cyan());
}
println!(
"{}",
"Component functionality added to existing project!".green()
);
Ok(())
}
fn select_framework_for_component_type(component_type: &str) -> Result<Option<String>> {
match component_type {
"client" => {
let frameworks = vec![
"leptos - Reactive web framework with fine-grained reactivity".to_string(),
"dioxus - Elegant React-like framework for desktop, web, and mobile".to_string(),
"tauri - Build smaller, faster, and more secure desktop applications".to_string(),
];
let framework_idx = if is_test_mode() {
0
} else {
select_option("Select framework:", &frameworks, 0)?
};
match framework_idx {
0 => Ok(Some("leptos".to_string())),
1 => Ok(Some("dioxus".to_string())),
2 => Ok(Some("tauri".to_string())),
_ => Ok(None),
}
}
"server" => {
let frameworks = vec![
"axum - Ergonomic and modular web framework by Tokio".to_string(),
"actix - Powerful, pragmatic, and extremely fast web framework".to_string(),
"poem - Full-featured and easy-to-use web framework".to_string(),
];
let framework_idx = if is_test_mode() {
0
} else {
select_option("Select framework:", &frameworks, 0)?
};
match framework_idx {
0 => Ok(Some("axum".to_string())),
1 => Ok(Some("actix".to_string())),
2 => Ok(Some("poem".to_string())),
_ => Ok(None),
}
}
"edge" => {
let providers = vec![
"cloudflare - Cloudflare Workers".to_string(),
"vercel - Vercel Edge Functions".to_string(),
"fastly - Fastly Compute@Edge".to_string(),
"aws - AWS Lambda@Edge".to_string(),
];
let provider_idx = if is_test_mode() {
0
} else {
select_option("Select provider:", &providers, 0)?
};
match provider_idx {
0 => Ok(Some("cloudflare".to_string())),
1 => Ok(Some("vercel".to_string())),
2 => Ok(Some("fastly".to_string())),
3 => Ok(Some("aws".to_string())),
_ => Ok(None),
}
}
"data-science" => {
let frameworks = vec![
"polars - Fast DataFrame library".to_string(),
"linfa - Machine learning framework".to_string(),
];
let framework_idx = if is_test_mode() {
0
} else {
select_option("Select framework:", &frameworks, 0)?
};
match framework_idx {
0 => Ok(Some("polars".to_string())),
1 => Ok(Some("linfa".to_string())),
_ => Ok(None),
}
}
_ => Ok(None),
}
}
fn add_framework_dependencies(project_dir: &Path, framework: &str) -> Result<()> {
let cargo_path = project_dir.join("Cargo.toml");
if !cargo_path.exists() {
return Ok(());
}
let cargo_content = fs::read_to_string(&cargo_path)?;
let mut cargo_doc = cargo_content.parse::<DocumentMut>()?;
if let Some(deps) = cargo_doc.get_mut("dependencies") {
if let Some(deps_table) = deps.as_table_mut() {
match framework {
"axum" => {
if deps_table.get("axum").is_none() {
deps_table.insert("axum", toml_edit::value("0.7.4"));
let mut tokio_table = toml_edit::Table::new();
tokio_table.insert("version", toml_edit::value("1.36.0"));
let mut features_array = toml_edit::Array::new();
features_array.push("full");
tokio_table.insert("features", toml_edit::value(features_array));
deps_table.insert("tokio", toml_edit::Item::Table(tokio_table));
}
}
"actix" => {
if deps_table.get("actix-web").is_none() {
deps_table.insert("actix-web", toml_edit::value("4.5.1"));
}
}
"poem" => {
if deps_table.get("poem").is_none() {
deps_table.insert("poem", toml_edit::value("2.0.0"));
}
}
"leptos" => {
if deps_table.get("leptos").is_none() {
deps_table.insert("leptos", toml_edit::value("0.6.5"));
}
}
"dioxus" => {
if deps_table.get("dioxus").is_none() {
deps_table.insert("dioxus", toml_edit::value("0.4.3"));
deps_table.insert("dioxus-web", toml_edit::value("0.4.3"));
}
}
"tauri" => {
if deps_table.get("tauri").is_none() {
let mut tauri_table = toml_edit::Table::new();
tauri_table.insert("version", toml_edit::value("1.6.0"));
let mut features_array = toml_edit::Array::new();
features_array.push("api-all");
tauri_table.insert("features", toml_edit::value(features_array));
deps_table.insert("tauri", toml_edit::Item::Table(tauri_table));
}
}
"polars" => {
if deps_table.get("polars").is_none() {
let mut polars_table = toml_edit::Table::new();
polars_table.insert("version", toml_edit::value("0.38.1"));
let mut features_array = toml_edit::Array::new();
features_array.push("lazy");
features_array.push("csv");
features_array.push("json");
polars_table.insert("features", toml_edit::value(features_array));
deps_table.insert("polars", toml_edit::Item::Table(polars_table));
}
}
"linfa" => {
if deps_table.get("linfa").is_none() {
deps_table.insert("linfa", toml_edit::value("0.7.0"));
deps_table.insert("ndarray", toml_edit::value("0.15.6"));
}
}
_ => {}
}
}
}
fs::write(cargo_path, cargo_doc.to_string())?;
Ok(())
}