mod build;
mod ssr_dev;
mod start_dev;
use std::path::PathBuf;
use anyhow::Context;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "oj", version, about = "A Rust-native build tool for React apps")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Dev {
root: Option<PathBuf>,
#[arg(long)]
port: Option<u16>,
#[arg(long)]
bundle: bool,
#[arg(long)]
ssr: Option<String>,
#[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "true")]
host: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
},
Compile {
file: PathBuf,
#[arg(long)]
prod: bool,
},
Build {
root: Option<PathBuf>,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
ssr: Option<String>,
#[arg(long)]
mode: Option<String>,
},
Preview {
root: Option<PathBuf>,
#[arg(long)]
out: Option<PathBuf>,
#[arg(long)]
port: Option<u16>,
#[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "true")]
host: Option<String>,
},
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
match Cli::parse().command {
Command::Dev { root, port, bundle, ssr, host, config } => {
let root = root.unwrap_or_else(|| {
let playground = PathBuf::from("playground");
if playground.join("index.html").is_file() { playground } else { PathBuf::from(".") }
});
if let Some(entry) = ssr {
ssr_dev::ssr_dev(root, entry, port, host).await
} else if oj_server::is_tanstack_start_app(&root) {
start_dev::start_dev(root, port, host).await
} else {
oj_server::DevServer { root, port, bundle, host, config }.run().await
}
}
Command::Compile { file, prod } => {
let source = std::fs::read_to_string(&file)
.with_context(|| format!("cannot read {}", file.display()))?;
let opts = if prod {
oj_compiler::CompileOptions::prod()
} else {
oj_compiler::CompileOptions::dev()
};
let output = oj_compiler::compile(&file, &source, &opts)?;
println!("{}", output.code);
Ok(())
}
Command::Build { root, out, ssr, mode } => {
let root = root.unwrap_or_else(|| {
let playground = PathBuf::from("playground");
if playground.join("index.html").is_file() { playground } else { PathBuf::from(".") }
});
let mode = mode.unwrap_or_else(|| "production".to_string());
if oj_server::is_tanstack_start_app(&root) {
start_dev::start_build(root).await
} else {
build::build(root, out, ssr, &mode).await
}
}
Command::Preview { root, out, port, host } => {
let root = root
.unwrap_or_else(|| {
let playground = PathBuf::from("playground");
if playground.join("index.html").is_file() {
playground
} else {
PathBuf::from(".")
}
})
.canonicalize()
.with_context(|| "app root not found")?;
let config = oj_config::load(&root).map_err(|e| anyhow::anyhow!("{e}"))?;
let out_dir = out
.or_else(|| config.build.as_ref().and_then(|b| b.out_dir.as_ref()).map(PathBuf::from))
.unwrap_or_else(|| PathBuf::from("dist"));
let out_dir = if out_dir.is_absolute() { out_dir } else { root.join(out_dir) };
let port = port
.or_else(|| config.preview.as_ref().and_then(|p| p.port))
.unwrap_or(4173);
let base = config.base.clone().unwrap_or_else(|| "/".into());
let headers: Vec<(String, String)> = config
.preview
.as_ref()
.and_then(|p| p.headers.clone())
.or_else(|| config.server.as_ref().and_then(|s| s.headers.clone()))
.map(|m| m.into_iter().collect())
.unwrap_or_default();
let host = host
.or_else(|| config.preview.as_ref().and_then(|p| p.host.clone()))
.or_else(|| config.server.as_ref().and_then(|s| s.host.clone()));
oj_server::preview(out_dir, port, base, headers, host).await
}
}
}