1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// 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 {
/// Start the dev server
Dev {
/// App root containing index.html (defaults to ./playground when present)
root: Option<PathBuf>,
/// Dev server port (overrides oj.config server.port; default 5199)
#[arg(long)]
port: Option<u16>,
/// Full bundle mode: serve one registry-runtime chunk instead of
/// native ESM modules (experimental)
#[arg(long)]
bundle: bool,
/// SSR dev mode: render this entry (exporting `render(): string`)
/// server-side, rebuilding on change with full page reload
#[arg(long)]
ssr: Option<String>,
},
/// Compile one file and print the output (debugging aid)
Compile {
file: PathBuf,
/// Production transform (no jsxDEV, no Fast Refresh instrumentation)
#[arg(long)]
prod: bool,
},
/// Production build (embedded Rolldown: shake, chunk, minify, hash)
Build {
/// App root containing index.html (defaults to ./playground when present)
root: Option<PathBuf>,
/// Output directory (overrides oj.config build.outDir; default dist)
#[arg(long)]
out: Option<PathBuf>,
/// SSR entry: build a Node server bundle (overrides oj.config build.ssr)
#[arg(long)]
ssr: Option<String>,
},
/// Preview a production build (static server over the build dir)
Preview {
/// App root (defaults to ./playground when present)
root: Option<PathBuf>,
/// Build dir to serve (overrides oj.config build.outDir; default dist)
#[arg(long)]
out: Option<PathBuf>,
/// Preview port (overrides oj.config preview.port; default 4173)
#[arg(long)]
port: Option<u16>,
},
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
match Cli::parse().command {
Command::Dev { root, port, bundle, ssr } => {
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).await
} else if oj_server::is_tanstack_start_app(&root) {
start_dev::start_dev(root, port).await
} else {
oj_server::DevServer { root, port, bundle }.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 } => {
let root = root.unwrap_or_else(|| {
let playground = PathBuf::from("playground");
if playground.join("index.html").is_file() { playground } else { PathBuf::from(".") }
});
if oj_server::is_tanstack_start_app(&root) {
start_dev::start_build(root).await
} else {
build::build(root, out, ssr).await
}
}
Command::Preview { root, out, port } => {
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());
// preview.headers, else server.headers (COOP/COEP, etc.).
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();
oj_server::preview(out_dir, port, base, headers).await
}
}
}