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
}
}
}