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) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 22:02:11 +00:00
co-authored by Claude Opus 4.8
parent 31e1b73193
commit 092a04857f
2 changed files with 87 additions and 0 deletions
+66
View File
@@ -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}");
}
}
@@ -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"
);
}