oj 0.0.21

Rust-native build tool for React apps
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Raphael Amorim

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