use fs_extra::dir::{copy, CopyOptions};
use std::path::PathBuf;
use std::thread::sleep;
use std::time::Duration;
use clap::{Parser, Subcommand};
use crate::app::App;
use crate::mode::Mode;
use crate::scaffold_project;
use crate::source_builder::{bundle_axum_source, check_tuono_folder, create_client_entry_files};
use crate::watch;
const TUONO_PORT: u16 = 3000;
const VITE_PORT: u16 = 3001;
#[derive(Subcommand, Debug)]
enum Actions {
Dev,
Build {
#[arg(short, long = "static")]
ssg: bool,
#[arg(short, long)]
no_js_emit: bool,
},
New {
folder_name: Option<String>,
#[arg(short, long)]
template: Option<String>,
},
}
#[derive(Parser, Debug)]
#[command(version, about = "The React/Rust full-stack framework")]
struct Args {
#[command(subcommand)]
action: Actions,
}
fn init_tuono_folder(mode: Mode) -> std::io::Result<App> {
check_tuono_folder()?;
let app = bundle_axum_source(mode)?;
create_client_entry_files()?;
Ok(app)
}
fn check_ports(mode: Mode) {
let rust_listener = std::net::TcpListener::bind(format!("0.0.0.0:{TUONO_PORT}"));
if let Err(_e) = rust_listener {
eprintln!("Error: Failed to bind to port {}", TUONO_PORT);
eprintln!(
"Please ensure that port {} is not already in use by another process or application.",
TUONO_PORT
);
std::process::exit(1);
}
if mode == Mode::Dev {
let vite_listener = std::net::TcpListener::bind(format!("0.0.0.0:{VITE_PORT}"));
if let Err(_e) = vite_listener {
eprintln!("Error: Failed to bind to port {}", VITE_PORT);
eprintln!(
"Please ensure that port {} is not already in use by another process or application.",
VITE_PORT
);
std::process::exit(1);
}
}
}
pub fn app() -> std::io::Result<()> {
let args = Args::parse();
match args.action {
Actions::Dev => {
check_ports(Mode::Dev);
let app = init_tuono_folder(Mode::Dev)?;
app.build_tuono_config()
.expect("Failed to build tuono.config.ts");
watch::watch().unwrap();
}
Actions::Build { ssg, no_js_emit } => {
let app = init_tuono_folder(Mode::Prod)?;
if no_js_emit {
println!("Rust build successfully finished");
return Ok(());
}
if ssg && app.has_dynamic_routes() {
println!("Cannot statically build dynamic routes");
return Ok(());
}
app.build_tuono_config()
.expect("Failed to build tuono.config.ts");
app.build_react_prod();
if ssg {
check_ports(Mode::Prod);
println!("SSG: generation started");
let static_dir = PathBuf::from("out/static");
if static_dir.is_dir() {
std::fs::remove_dir_all(&static_dir)
.expect("Failed to clear the out/static folder");
}
std::fs::create_dir(&static_dir).expect("Failed to create static output dir");
copy(
"./out/client",
static_dir,
&CopyOptions::new().overwrite(true).content_only(true),
)
.expect("Failed to clone assets into static output folder");
#[allow(clippy::zombie_processes)]
let mut rust_server = app.run_rust_server();
let reqwest = reqwest::blocking::Client::builder()
.user_agent("")
.build()
.expect("Failed to build reqwest client");
let mut is_server_ready = false;
while !is_server_ready {
let server_url = format!("http://localhost:{}", TUONO_PORT);
if reqwest.get(server_url).send().is_ok() {
is_server_ready = true
}
sleep(Duration::from_secs(1))
}
for (_, route) in app.route_map {
route.save_ssg_file(&reqwest)
}
let _ = rust_server.kill();
};
println!("Build successfully finished");
}
Actions::New {
folder_name,
template,
} => {
scaffold_project::create_new_project(folder_name, template);
}
}
Ok(())
}