use clap::{Arg, Command};
use rari::error::RariError;
use rari::server::{
Server,
config::{Config, Mode},
image::ImageOptimizer,
};
use rustls::crypto::CryptoProvider;
use tracing::error;
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let matches = Command::new("rari")
.version(env!("CARGO_PKG_VERSION"))
.about("rari HTTP Server")
.subcommand_required(false)
.arg_required_else_help(false)
.subcommand(
Command::new("optimize-images")
.about("Pre-optimize local images for production")
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.help("Enable verbose logging")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("dry-run")
.long("dry-run")
.help("Preview images that would be optimized without performing writes")
.action(clap::ArgAction::SetTrue),
),
)
.arg(
Arg::new("mode")
.short('m')
.long("mode")
.value_name("MODE")
.help("Server mode: development or production")
.value_parser(["development", "dev", "production", "prod"])
.default_value("development"),
)
.arg(
Arg::new("host")
.short('H')
.long("host")
.value_name("HOST")
.help("Server host address")
.default_value("127.0.0.1"),
)
.arg(
Arg::new("port")
.short('p')
.long("port")
.value_name("PORT")
.help("Server port")
.value_parser(clap::value_parser!(u16))
.default_value("3000"),
)
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.help("Enable verbose logging")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("quiet")
.short('q')
.long("quiet")
.help("Reduce log output")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if let Some(("optimize-images", sub_matches)) = matches.subcommand() {
init_logging_for_subcommand(sub_matches)?;
CryptoProvider::install_default(rustls::crypto::aws_lc_rs::default_provider())
.map_err(|_| "Failed to install rustls crypto provider")?;
let dry_run = sub_matches.get_flag("dry-run");
return run_optimize_images(dry_run).await;
}
init_logging(&matches)?;
CryptoProvider::install_default(rustls::crypto::aws_lc_rs::default_provider())
.map_err(|_| "Failed to install rustls crypto provider")?;
let config = load_configuration(&matches).await?;
let server = Server::new(config).await.map_err(|e| {
error!("Failed to create server: {}", e);
e
})?;
let shutdown_signal = setup_shutdown_signal();
tokio::select! {
result = server.start() => {
match result {
Ok(_) => {}
Err(e) => {
error!("Server error: {}", e);
return Err(e.into());
}
}
}
_ = shutdown_signal => {}
}
Ok(())
}
async fn run_optimize_images(
dry_run: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let project_path = std::env::current_dir()?;
let image_config_path = project_path.join("dist").join("server").join("image.json");
let image_config: rari::server::image::ImageConfig =
if tokio::fs::try_exists(&image_config_path).await? {
let config_content = tokio::fs::read_to_string(&image_config_path).await?;
serde_json::from_str(&config_content)?
} else {
return Ok(());
};
let has_work =
!image_config.preoptimize_manifest.is_empty() || !image_config.local_patterns.is_empty();
if !has_work {
return Ok(());
}
let optimizer = ImageOptimizer::new(image_config, &project_path);
if dry_run {
tracing::info!("Running in dry-run mode (preview only, no writes)...");
match optimizer.preoptimize_local_images_preview().await {
Ok(count) => {
tracing::info!("[DRY RUN] Would pre-optimize {} image variants", count);
Ok(())
}
Err(e) => {
tracing::error!("Failed to preview images: {}", e);
Err(e.into())
}
}
} else {
match optimizer.preoptimize_local_images().await {
Ok(count) => {
if count > 0 {
tracing::info!("Successfully pre-optimized {} image variants", count);
}
Ok(())
}
Err(e) => {
tracing::error!("Failed to pre-optimize images: {}", e);
Err(e.into())
}
}
}
}
#[allow(clippy::result_large_err)]
fn init_logging_for_subcommand(matches: &clap::ArgMatches) -> Result<(), RariError> {
let verbose = matches.get_flag("verbose");
let default_level = if verbose { "debug" } else { "info" };
let env_filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new(format!("rari={default_level}")))
.map_err(|e| RariError::configuration(format!("Failed to create log filter: {e}")))?;
tracing_subscriber::registry()
.with(env_filter)
.with(
fmt::layer()
.with_target(false)
.with_thread_ids(false)
.with_thread_names(false)
.with_file(false)
.with_line_number(false)
.compact(),
)
.init();
Ok(())
}
#[allow(clippy::result_large_err)]
fn init_logging(matches: &clap::ArgMatches) -> Result<(), RariError> {
let verbose = matches.get_flag("verbose");
let quiet = matches.get_flag("quiet");
let default_level = if verbose {
"debug"
} else if quiet {
"warn"
} else {
"info"
};
let env_filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new(format!("rari={default_level}")))
.map_err(|e| RariError::configuration(format!("Failed to create log filter: {e}")))?;
tracing_subscriber::registry()
.with(env_filter)
.with(
fmt::layer()
.with_target(true)
.with_thread_ids(false)
.with_thread_names(false)
.with_file(verbose)
.with_line_number(verbose)
.compact(),
)
.init();
Ok(())
}
async fn load_configuration(matches: &clap::ArgMatches) -> Result<Config, RariError> {
let mode_str = matches
.get_one::<String>("mode")
.ok_or_else(|| RariError::configuration("Mode argument is required".to_string()))?;
let mode = match mode_str.as_str() {
"development" | "dev" => Mode::Development,
"production" | "prod" => Mode::Production,
mode => return Err(RariError::configuration(format!("Invalid mode: {mode}"))),
};
let mut config = match Config::from_env() {
Ok(config) => config,
Err(e) => {
error!("No environment config found, using defaults: {}", e);
Config::new(mode)
}
};
config.mode = mode;
if let Some(host) = matches.get_one::<String>("host") {
config.server.host = host.to_string();
}
if let Some(&port) = matches.get_one::<u16>("port") {
config.server.port = port;
}
validate_configuration(&config)?;
Ok(config)
}
#[allow(clippy::result_large_err)]
fn validate_configuration(config: &Config) -> Result<(), RariError> {
if config.server.port == 0 {
return Err(RariError::configuration("Server port cannot be 0".to_string()));
}
if config.vite.port == 0 {
return Err(RariError::configuration("Vite port cannot be 0".to_string()));
}
if config.server.port == config.vite.port {
return Err(RariError::configuration(
"Server and Vite ports cannot be the same".to_string(),
));
}
if config.server.host.is_empty() {
return Err(RariError::configuration("Server host cannot be empty".to_string()));
}
Ok(())
}
async fn setup_shutdown_signal() {
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
let mut sigterm =
signal(SignalKind::terminate()).expect("Failed to create SIGTERM handler");
let mut sigint = signal(SignalKind::interrupt()).expect("Failed to create SIGINT handler");
tokio::select! {
_ = sigterm.recv() => {}
_ = sigint.recv() => {}
_ = tokio::signal::ctrl_c() => {}
}
}
#[cfg(windows)]
{
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
}
}
}