mod build;
mod ssr_dev;
mod ssr_host;
mod start_chunks;
mod start_dev;
mod start_host;
mod start_hot;
use deno_core as _;
use deno_napi as _;
use std::path::PathBuf;
use anyhow::Context;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "oj",
version,
about = "Experimental next generation frontend tooling"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Dev {
root: Option<PathBuf>,
#[arg(long)]
port: Option<u16>,
#[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>,
#[arg(long)]
mode: Option<String>,
#[arg(long)]
enable_cache: bool,
#[arg(long)]
no_cache: bool,
#[arg(long)]
lazy: bool,
},
Compile {
file: PathBuf,
#[arg(long)]
prod: bool,
},
#[command(name = "js-eval", hide = true)]
JsEval {
file: PathBuf,
#[arg(long)]
root: Option<PathBuf>,
},
#[command(name = "start-script", hide = true)]
StartScript {
script: PathBuf,
#[arg(long)]
root: PathBuf,
#[arg(long)]
serve: bool,
},
#[command(name = "engine-job", hide = true)]
EngineJob {
module: PathBuf,
#[arg(long)]
root: PathBuf,
#[arg(long)]
export: String,
#[arg(long)]
timeout_secs: u64,
#[arg(long)]
result: PathBuf,
},
Build {
root: Option<PathBuf>,
#[arg(long = "outDir", alias = "out")]
out: Option<PathBuf>,
#[arg(long)]
ssr: Option<String>,
#[arg(short = 'm', long)]
mode: Option<String>,
#[arg(short = 'c', long)]
config: Option<PathBuf>,
#[arg(long = "emptyOutDir")]
empty_out_dir: bool,
#[arg(long)]
base: Option<String>,
#[arg(long = "assetsDir")]
assets_dir: Option<String>,
#[arg(long = "assetsInlineLimit")]
assets_inline_limit: Option<u64>,
#[arg(long)]
target: Option<String>,
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
sourcemap: Option<String>,
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
minify: Option<String>,
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
manifest: Option<String>,
#[arg(long = "ssrManifest", num_args = 0..=1, default_missing_value = "true")]
ssr_manifest: Option<String>,
#[arg(short = 'w', long)]
watch: bool,
#[arg(long)]
app: bool,
},
Preview {
root: Option<PathBuf>,
#[arg(long = "outDir", alias = "out")]
out: Option<PathBuf>,
#[arg(long)]
port: Option<u16>,
#[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "true")]
host: Option<String>,
#[arg(short = 'c', long)]
config: Option<PathBuf>,
#[arg(long = "strictPort")]
strict_port: bool,
#[arg(long, num_args = 0..=1, default_missing_value = "/")]
open: Option<String>,
#[arg(long)]
base: Option<String>,
},
}
#[cfg(unix)]
fn reap_on_parent_death() {
let declared = std::env::var(PARENT_PID_ENV)
.ok()
.and_then(|v| v.parse::<libc::pid_t>().ok());
let parent = declared.unwrap_or_else(|| unsafe { libc::getppid() });
if unsafe { libc::getppid() } != parent {
std::process::exit(0);
}
std::thread::spawn(move || loop {
if unsafe { libc::getppid() } != parent {
std::process::exit(0);
}
std::thread::sleep(std::time::Duration::from_millis(200));
});
}
#[cfg(unix)]
const PARENT_PID_ENV: &str = oj_server::plugins::PARENT_PID_ENV;
#[cfg(not(unix))]
fn reap_on_parent_death() {}
fn raise_fd_limit() {
#[cfg(unix)]
unsafe {
let mut limits = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if 0 != libc::getrlimit(libc::RLIMIT_NOFILE, &mut limits) {
return;
}
if limits.rlim_cur == libc::RLIM_INFINITY {
return;
}
if limits.rlim_max == libc::RLIM_INFINITY {
let mut min = limits.rlim_cur;
let mut max = 1 << 20;
while min + 1 < max {
limits.rlim_cur = min + (max - min) / 2;
match libc::setrlimit(libc::RLIMIT_NOFILE, &limits) {
0 => min = limits.rlim_cur,
_ => max = limits.rlim_cur,
}
}
return;
}
if limits.rlim_cur < limits.rlim_max {
limits.rlim_cur = limits.rlim_max;
libc::setrlimit(libc::RLIMIT_NOFILE, &limits);
}
}
}
fn agent_detection_root(command: &Command) -> Option<Option<&PathBuf>> {
match command {
Command::Dev { root, .. } | Command::Build { root, .. } | Command::Preview { root, .. } => {
Some(root.as_ref())
}
_ => None,
}
}
fn detect_package_manager_agent(root_arg: Option<&PathBuf>) -> Option<String> {
let cwd = std::env::current_dir().ok()?;
let mut dir = match root_arg {
Some(arg) => {
let p = cwd.join(arg);
if p.is_dir() {
p
} else {
cwd.clone()
}
}
None => {
let playground = cwd.join("playground");
if playground.join("index.html").is_file() {
playground
} else {
cwd.clone()
}
}
};
let home = std::env::var_os("HOME").map(PathBuf::from);
let started_below_home = home
.as_deref()
.is_some_and(|h| dir.starts_with(h) && dir != h);
loop {
if started_below_home && home.as_deref() == Some(dir.as_path()) {
return None;
}
if let Ok(pkg) = std::fs::read_to_string(dir.join("package.json")) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&pkg) {
if let Some(pm) = v.get("packageManager").and_then(|p| p.as_str()) {
let mut parts = pm.splitn(2, '@');
let name = parts.next().unwrap_or_default();
let version = parts
.next()
.map(|v| v.split('+').next().unwrap_or(v))
.filter(|v| !v.is_empty())
.unwrap_or("0.0.0");
if !name.is_empty() {
return Some(format!("{name}/{version} oj/{}", env!("CARGO_PKG_VERSION")));
}
}
}
}
for (file, manager, version) in oj_server::PACKAGE_MANAGER_LOCKFILES {
let path = dir.join(file);
if path.exists() {
let version = sniffed_manager_version(&path, manager, version);
return Some(format!(
"{manager}/{version} oj/{}",
env!("CARGO_PKG_VERSION")
));
}
}
if dir.join(".git").is_dir() || !dir.pop() {
return None;
}
}
}
fn sniffed_manager_version(path: &std::path::Path, manager: &str, default_version: &str) -> String {
let head = || -> Option<String> {
use std::io::Read;
let mut buf = vec![0u8; 4096];
let n = std::fs::File::open(path).ok()?.read(&mut buf).ok()?;
buf.truncate(n);
Some(String::from_utf8_lossy(&buf).into_owned())
};
match (manager, path.file_name().and_then(|n| n.to_str())) {
("yarn", Some("yarn.lock")) => {
if head().is_some_and(|h| h.contains("__metadata:")) {
"4.0.0".to_string()
} else {
default_version.to_string()
}
}
("pnpm", Some("pnpm-lock.yaml" | "lock.yaml")) => {
if head().is_some_and(|h| {
h.contains("lockfileVersion: '6") || h.contains("lockfileVersion: 6")
}) {
"8.0.0".to_string()
} else {
default_version.to_string()
}
}
_ => default_version.to_string(),
}
}
fn main() -> anyhow::Result<()> {
raise_fd_limit();
if std::env::var_os("VITE_CONFIG_NATIVE_IGNORE_WARNING").is_none() {
std::env::set_var("VITE_CONFIG_NATIVE_IGNORE_WARNING", "true");
}
let cli = Cli::parse();
let agent_unset = std::env::var_os("npm_config_user_agent")
.map(|v| v.is_empty())
.unwrap_or(true);
if agent_unset {
if let Some(root) = agent_detection_root(&cli.command) {
if let Some(agent) = detect_package_manager_agent(root) {
std::env::set_var("npm_config_user_agent", agent);
}
}
}
oj_server::capture_startup_cwd();
#[cfg(unix)]
if matches!(
std::env::args().nth(1).as_deref(),
Some("engine-job") | Some("start-script")
) {
reap_on_parent_death();
std::env::remove_var(PARENT_PID_ENV);
}
oj_env::init();
oj_diag::set_json_output(oj_env::get().knobs.log_json);
oj_diag::install_panic_hook();
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_stack_size(oj_compiler::COMPILE_STACK_SIZE)
.build()?
.block_on(run(cli))
}
async fn run(cli: Cli) -> anyhow::Result<()> {
if let Ok(exe) = std::env::current_exe() {
oj_server::plugins::engine_jobs_via_subprocess(exe);
}
match cli.command {
Command::Dev {
root,
port,
ssr,
host,
config,
mode,
enable_cache,
no_cache,
lazy,
} => {
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, config, mode).await
} else {
oj_server::DevServer {
root,
port,
host,
config,
enable_cache,
no_cache,
lazy,
mode,
}
.run()
.await
}
}
Command::JsEval { file, root } => {
let root = root
.unwrap_or_else(|| PathBuf::from("."))
.canonicalize()
.context("engine root not found")?;
let mut config = oj_js::EngineConfig::new(&root);
config.code_cache_dir = Some(oj_server::engine_code_cache_dir(&root));
let engine =
oj_js::JsEngine::spawn(config, None, None).map_err(|e| anyhow::anyhow!("{e}"))?;
let value = engine
.eval(oj_js::EvalInput::Path(file), None)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
println!("{value}");
Ok(())
}
Command::StartScript {
script,
root,
serve: true,
} => {
let root = root.canonicalize().context("engine root not found")?;
let scripts = start_host::ScriptEngine::new(&root)?;
scripts.serve(&script).await
}
Command::StartScript { script, root, .. } => {
let root = root.canonicalize().context("engine root not found")?;
let mut buf = String::new();
std::io::Read::read_to_string(&mut std::io::stdin().lock(), &mut buf)
.context("start-script env on stdin")?;
let env: Vec<(String, String)> =
serde_json::from_str(&buf).context("start-script env json")?;
let scripts = start_host::ScriptEngine::new(&root)?;
scripts.run_async(&script, &env, "start script").await
}
Command::EngineJob {
module,
root,
export,
timeout_secs,
result,
} => {
let mut buf = String::new();
std::io::Read::read_to_string(&mut std::io::stdin().lock(), &mut buf)
.context("engine-job payload on stdin")?;
let payload: serde_json::Value =
serde_json::from_str(&buf).context("engine-job payload json")?;
let outcome = oj_server::plugins::run_engine_job_in_process(
&root,
&module,
&export,
payload,
(timeout_secs > 0).then(|| std::time::Duration::from_secs(timeout_secs)),
);
let envelope = oj_server::plugins::engine_job_envelope(&outcome);
std::fs::write(&result, serde_json::to_string(&envelope)?)
.context("engine-job result write")?;
Ok(())
}
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,
config,
empty_out_dir,
base,
assets_dir,
assets_inline_limit,
target,
sourcemap,
minify,
manifest,
ssr_manifest,
watch,
app: _,
} => {
let root = root.unwrap_or_else(|| {
let playground = PathBuf::from("playground");
if playground.join("index.html").is_file() {
playground
} else {
PathBuf::from(".")
}
});
let config = resolve_config_arg(&root, config);
if oj_server::is_tanstack_start_app(&root) {
let mode = mode.unwrap_or_else(|| "production".to_string());
start_dev::start_build(root, config, &mode, out).await
} else {
build::build(
root,
mode.as_deref(),
build::CliOptions {
out,
ssr,
empty_out_dir,
base,
assets_dir,
assets_inline_limit,
target,
sourcemap,
minify,
manifest,
ssr_manifest,
watch,
config,
},
)
.await
}
}
Command::Preview {
root,
out,
port,
host,
config: _,
strict_port,
open,
base,
} => {
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 mut opts = preview_options(&config, out_dir);
if let Some(p) = port {
opts.port = p;
}
if let Some(h) = host {
opts.host = Some(h);
}
if let Some(b) = base {
opts.base = b;
}
if strict_port {
opts.strict_port = true;
}
if let Some(o) = open {
opts.open = Some(o);
}
oj_server::preview(opts).await
}
}
}
fn preview_options(config: &oj_config::OjConfig, out_dir: PathBuf) -> oj_server::PreviewOptions {
let preview = config.preview.clone().unwrap_or_default();
let server = config.server.clone().unwrap_or_default();
if preview.proxy.as_ref().is_some_and(|p| !p.is_null()) || server.proxy.is_some() {
eprintln!("oj preview: (!) preview.proxy / server.proxy is not applied by the preview server yet.");
}
let open = match preview.open.as_ref() {
Some(serde_json::Value::Bool(true)) => Some("/".to_string()),
Some(serde_json::Value::String(p)) => Some(p.clone()),
Some(_) => None,
None => server.open.filter(|o| *o).map(|_| "/".to_string()),
};
let base = config.base.clone().unwrap_or_else(|| "/".into());
let base = if base.is_empty() || base.starts_with('.') {
"/".to_string()
} else {
format!("/{}/", base.trim_matches('/')).replace("//", "/")
};
oj_server::PreviewOptions {
dir: out_dir,
port: preview.port.unwrap_or(4173),
base,
headers: preview
.headers
.or(server.headers)
.map(|m| m.into_iter().collect())
.unwrap_or_default(),
host: preview.host.or(server.host),
strict_port: preview.strict_port.or(server.strict_port).unwrap_or(false),
open,
cors: preview.cors.or(server.cors),
allowed_hosts: preview.allowed_hosts.or(server.allowed_hosts),
spa_fallback: config.app_type.as_deref().unwrap_or("spa") == "spa",
assets_dir: oj_config::build_assets_dir(config),
https: preview.https.or(server.https),
}
}
fn resolve_config_arg(root: &std::path::Path, config: Option<PathBuf>) -> Option<PathBuf> {
config.map(|cfg| {
if cfg.is_absolute() {
cfg
} else {
root.join(cfg)
}
})
}
#[cfg(test)]
mod preview_tests {
use super::*;
#[test]
fn preview_options_inherit_from_server_except_port() {
let config: oj_config::OjConfig = serde_json::from_str(
r#"{"base":"app","appType":"mpa","build":{"assetsDir":"static"},
"server":{"port":3000,"strictPort":true,"open":true,"host":"0.0.0.0","cors":false,"allowedHosts":["a.test"],"headers":{"x-a":"1"}},
"preview":{"headers":{"x-b":"2"}}}"#,
)
.unwrap();
let o = preview_options(&config, PathBuf::from("dist"));
assert_eq!(o.port, 4173, "the port never inherits from server");
assert_eq!(o.base, "/app/");
assert!(o.strict_port);
assert_eq!(o.open.as_deref(), Some("/"));
assert_eq!(o.host.as_deref(), Some("0.0.0.0"));
assert!(matches!(o.cors, Some(oj_config::CorsConfig::Toggle(false))));
assert!(
matches!(o.allowed_hosts, Some(oj_config::AllowedHosts::List(ref l)) if l == &["a.test"])
);
assert_eq!(
o.headers,
vec![("x-b".to_string(), "2".to_string())],
"preview.headers wins over server.headers"
);
assert!(!o.spa_fallback, "appType mpa has no index.html fallback");
assert_eq!(o.assets_dir, "static");
let config: oj_config::OjConfig =
serde_json::from_str(r#"{"preview":{"port":5000,"open":"/docs","strictPort":false},"server":{"strictPort":true}}"#).unwrap();
let o = preview_options(&config, PathBuf::from("dist"));
assert_eq!(o.port, 5000);
assert_eq!(o.open.as_deref(), Some("/docs"));
assert!(!o.strict_port, "an explicit preview.strictPort wins");
assert!(o.spa_fallback);
assert_eq!(o.base, "/");
}
}