From a280fb6198367ea9a9b897b39021ba45cf39ddc6 Mon Sep 17 00:00:00 2001 From: rustmailer Date: Wed, 29 Jul 2026 00:24:06 +0800 Subject: [PATCH] feat(server): auto-build web frontend in build.rs --- crates/server/build.rs | 98 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/crates/server/build.rs b/crates/server/build.rs index c7711ed..2a87d47 100644 --- a/crates/server/build.rs +++ b/crates/server/build.rs @@ -4,8 +4,9 @@ fn main() -> Result<()> { if cfg!(target_os = "windows") { println!("cargo:rustc-link-lib=Rstrtmgr"); } + let output = Command::new("git") - .args(&["rev-parse", "--short", "HEAD"]) + .args(["rev-parse", "--short", "HEAD"]) .output() .expect("Failed to get git commit hash"); let git_hash = String::from_utf8(output.stdout) @@ -13,5 +14,100 @@ fn main() -> Result<()> { .trim() .to_string(); println!("cargo:rustc-env=GIT_HASH={}", git_hash); + + // Build frontend assets + build_frontend(); + Ok(()) } + +fn build_frontend() { + let web_dir = std::path::Path::new("../../web"); + let dist_dir = web_dir.join("dist"); + + // Re-run build.rs when frontend source or dependencies change. + println!("cargo:rerun-if-changed=../../web/package.json"); + println!("cargo:rerun-if-changed=../../web/pnpm-lock.yaml"); + println!("cargo:rerun-if-changed=../../web/tsconfig.json"); + println!("cargo:rerun-if-changed=../../web/vite.config.ts"); + println!("cargo:rerun-if-changed=../../web/index.html"); + println!("cargo:rerun-if-changed=../../web/src/"); + + // Find a working pnpm command. On Windows it may only be available + // via cmd.exe (PATH set by the installer, not inherited by bash). + let mut pnpm_cmd = None; + for candidate in &["pnpm", "pnpm.cmd"] { + if command_ok(candidate, &["--version"], web_dir) { + pnpm_cmd = Some(*candidate); + break; + } + } + // Windows fallback: run through cmd.exe which has the full system PATH. + if pnpm_cmd.is_none() && cfg!(target_os = "windows") { + if command_ok("cmd", &["/c", "pnpm.cmd", "--version"], web_dir) { + pnpm_cmd = Some("cmd"); + } + } + + let Some(cmd) = pnpm_cmd else { + // If dist/ already exists (built manually), allow the build to proceed. + if dist_dir.exists() { + eprintln!( + "WARNING: `pnpm` not found in PATH but `web/dist/` exists.\n\ + The embedded frontend may be stale. Install pnpm and run\n\ + `cd web && pnpm run build` to update it.\n" + ); + return; + } + eprintln!( + "\nERROR: `pnpm` not found. Install Node.js and pnpm, then run:\n\ + cd web && pnpm install && pnpm run build\n" + ); + std::process::exit(1); + }; + + if !web_dir.join("node_modules").exists() { + // Fresh clone — install dependencies first. + eprintln!("Installing frontend dependencies (one-time setup)..."); + if !run_pnpm(cmd, &["install"], web_dir) { + eprintln!("\nERROR: `pnpm install` failed. See output above.\n"); + std::process::exit(1); + } + } + + if !run_pnpm(cmd, &["run", "build"], web_dir) { + eprintln!("\nERROR: `pnpm run build` failed. See output above.\n"); + std::process::exit(1); + } +} + +fn command_ok(cmd: &str, args: &[&str], dir: &std::path::Path) -> bool { + Command::new(cmd) + .args(args) + .current_dir(dir) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + +fn run_pnpm(cmd: &str, args: &[&str], dir: &std::path::Path) -> bool { + if cmd == "cmd" { + let mut full_args = vec!["/c", "pnpm.cmd"]; + full_args.extend_from_slice(args); + Command::new("cmd") + .args(&full_args) + .current_dir(dir) + .status() + .map(|s| s.success()) + .unwrap_or(false) + } else { + Command::new(cmd) + .args(args) + .current_dir(dir) + .status() + .map(|s| s.success()) + .unwrap_or(false) + } +}