From 092a04857f107f2a4b4959c0ec106401731541fc Mon Sep 17 00:00:00 2001 From: naps62-yolo Date: Thu, 28 May 2026 19:47:32 +0000 Subject: [PATCH] feat(maestro): build maestro-web SPA bundle in build.rs include_dir! now embeds a freshly built web/dist on every compile; pnpm + node are hard build requirements. Fixes the placeholder served by cargo install --git. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/maestro-web/build.rs | 66 +++++++++++++++++++++ crates/maestro-web/tests/embedded_bundle.rs | 21 +++++++ 2 files changed, 87 insertions(+) create mode 100644 crates/maestro-web/build.rs create mode 100644 crates/maestro-web/tests/embedded_bundle.rs diff --git a/crates/maestro-web/build.rs b/crates/maestro-web/build.rs new file mode 100644 index 0000000..2549375 --- /dev/null +++ b/crates/maestro-web/build.rs @@ -0,0 +1,66 @@ +//! 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()); + } + + 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", + ); +} + +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}"); + } +} diff --git a/crates/maestro-web/tests/embedded_bundle.rs b/crates/maestro-web/tests/embedded_bundle.rs new file mode 100644 index 0000000..0448f43 --- /dev/null +++ b/crates/maestro-web/tests/embedded_bundle.rs @@ -0,0 +1,21 @@ +//! The compiled binary must embed the real SPA bundle, never the placeholder. +//! `build.rs` guarantees `web/dist` is populated before `include_dir!` runs. + +use axum::body::to_bytes; +use maestro_web::static_bundle::serve_index; + +#[tokio::test] +async fn embedded_index_is_real_spa_not_placeholder() { + let resp = serve_index(); + let body = to_bytes(resp.into_body(), usize::MAX).await.unwrap(); + let html = String::from_utf8(body.to_vec()).unwrap(); + + assert!( + html.contains(r#"id="app""#), + "served index is missing the SPA mount point; got:\n{html}" + ); + assert!( + !html.contains("maestro-web is running"), + "served index is the placeholder, not the built bundle" + ); +}