//! Build the embedded SPA bundle before the crate compiles. //! //! `static_bundle.rs` embeds `web/dist` at compile time via `include_dir!`. //! This script guarantees that directory holds a freshly built bundle by //! running `pnpm install --frozen-lockfile && pnpm build` in `web/`. pnpm and //! node are HARD requirements — a missing toolchain fails the build with an //! actionable message rather than silently shipping the placeholder. use std::env; use std::path::PathBuf; use std::process::Command; fn main() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set by cargo")); let web_dir = manifest_dir.join("../../web"); // Rebuild the bundle only when a frontend input changes — not on every // unrelated Rust recompile. for rel in [ "src", "index.html", "package.json", "pnpm-lock.yaml", "vite.config.ts", ] { println!("cargo:rerun-if-changed={}", web_dir.join(rel).display()); } // Also watch the embedded OUTPUT directory. The input watches above decide // whether THIS script re-runs and regenerates `web/dist`; but `include_dir!` // in `static_bundle.rs` reads `web/dist`, and cargo has no other declared // dependency on those output files. Without this watch, an incremental build // could regenerate `web/dist` yet keep the OLD bundle embedded. Tying the // crate's recompilation to the regenerated output makes the fresh assets // actually get re-embedded. let dist_dir = web_dir.join("dist"); println!("cargo:rerun-if-changed={}", dist_dir.display()); require_tool("pnpm"); require_tool("node"); run( Command::new("pnpm") .args(["install", "--frozen-lockfile"]) .current_dir(&web_dir), "pnpm install --frozen-lockfile", ); run( Command::new("pnpm").arg("build").current_dir(&web_dir), "pnpm build", ); // Sanity-check that pnpm build actually produced the bundle `include_dir!` // expects. A green build that embeds an empty/placeholder dir is worse than // a loud failure here. let index_html = dist_dir.join("index.html"); let produced = std::fs::metadata(&index_html) .map(|m| m.is_file() && m.len() > 0) .unwrap_or(false); if !produced { panic!( "`pnpm build` did not produce the expected bundle at {} \ (missing or empty) — check the vite `outDir`.", index_html.display() ); } } fn require_tool(tool: &str) { let ok = Command::new(tool) .arg("--version") .output() .map(|o| o.status.success()) .unwrap_or(false); if !ok { panic!( "maestro-web requires `{tool}` to build its embedded SPA bundle. \ Install pnpm + node (https://pnpm.io) or use a prebuilt binary." ); } } fn run(cmd: &mut Command, label: &str) { let status = cmd .status() .unwrap_or_else(|e| panic!("failed to spawn `{label}`: {e}")); if !status.success() { panic!("`{label}` failed with {status}"); } }