docs(maestro): spec — build maestro-web SPA bundle at compile time
build.rs runs pnpm install+build before include_dir! embeds web/dist, so any install method (cargo install --git, --path, wizard) ships a real bundle instead of the placeholder. Hard-fail when pnpm/node missing; wizard prompts abort/skip; CI gains the node+pnpm toolchain. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# maestro-web: build the SPA bundle at compile time
|
||||
|
||||
**Date:** 2026-05-28
|
||||
**Status:** Approved (design)
|
||||
|
||||
## Problem
|
||||
|
||||
`maestro-web` embeds the SPA bundle at **compile time** via
|
||||
`include_dir!("$CARGO_MANIFEST_DIR/../../web/dist")` (`crates/maestro-web/src/static_bundle.rs`).
|
||||
Only `web/dist/.gitkeep` is checked into git — the real bundle is gitignored and
|
||||
must be produced by `pnpm -C web build`.
|
||||
|
||||
The installer's default source is `cargo install --git` (`maestro-installer`
|
||||
`build_source`). A fresh git clone has `web/dist` containing *only* `.gitkeep`,
|
||||
so `include_dir!` embeds an empty bundle and the running binary serves the
|
||||
"bundle not built" placeholder. **Every git-based install hits this** — it is
|
||||
guaranteed, not bad luck.
|
||||
|
||||
Secondary problems found while diagnosing:
|
||||
|
||||
- The placeholder (and `Justfile`'s `web:` target) tell the operator to run
|
||||
`cargo build -p maestro-web --release`, which writes `target/release/...`, but
|
||||
the systemd unit runs `~/.cargo/bin/maestro-web` (from `cargo install`). So
|
||||
following the placeholder's own advice never updates the running service.
|
||||
- `include_dir!` does not emit `cargo:rerun-if-changed` for the embedded
|
||||
directory. Once `maestro-web` is compiled with an empty `web/dist`, rebuilding
|
||||
after `pnpm build` does **not** re-embed the bundle — cargo sees no source
|
||||
change and reuses the stale object.
|
||||
|
||||
## Goal
|
||||
|
||||
A compiled `maestro-web` binary always embeds a real SPA bundle, regardless of
|
||||
install method (`cargo install --git`, `--path`, or the wizard). No placeholder
|
||||
for installed users.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. `crates/maestro-web/build.rs` (new)
|
||||
|
||||
Runs during every compile of the crate that cargo deems necessary (gated by the
|
||||
`rerun-if-changed` set below).
|
||||
|
||||
- Locate `web/` relative to `CARGO_MANIFEST_DIR` (`../../web`).
|
||||
- Resolve `pnpm` on `PATH`, and confirm `node` is present.
|
||||
**If either is missing → fail the build** (`panic!` from `build.rs`, which
|
||||
cargo surfaces as a build error) with an actionable message, e.g.:
|
||||
> `maestro-web requires pnpm + node to build its embedded SPA bundle. Install
|
||||
> them (https://pnpm.io) or use a prebuilt binary.`
|
||||
- Run, in `web/`:
|
||||
1. `pnpm install --frozen-lockfile` (a fresh `--git` clone has no
|
||||
`node_modules`, so install is required).
|
||||
2. `pnpm build`.
|
||||
If either exits non-zero, fail the build and surface stderr.
|
||||
- Emit `cargo:rerun-if-changed=` for the frontend inputs so the bundle rebuilds
|
||||
exactly when those change (and not on every Rust compile):
|
||||
- `../../web/src`
|
||||
- `../../web/index.html`
|
||||
- `../../web/package.json`
|
||||
- `../../web/pnpm-lock.yaml`
|
||||
- `../../web/vite.config.ts`
|
||||
|
||||
This also fixes the staleness bug: changing the frontend now reliably
|
||||
re-embeds.
|
||||
|
||||
### 2. `crates/maestro-web/src/static_bundle.rs` — unchanged
|
||||
|
||||
`include_dir!` still embeds `web/dist`, now guaranteed populated by `build.rs`.
|
||||
The placeholder path stays as harmless defense-in-depth; in a successful build
|
||||
it is unreachable. Existing tests remain valid.
|
||||
|
||||
### 3. Installer wizard — `handle_web` (`crates/maestro-installer/src/main.rs`)
|
||||
|
||||
The web step is already opt-in (interactive prompt, or silent refresh when a
|
||||
unit already exists). After the user opts in, **preflight pnpm + node** before
|
||||
`install_crate(WEB_CRATE)`:
|
||||
|
||||
- **Interactive, prerequisite missing:** prompt
|
||||
> `maestro-web needs pnpm + node, which aren't installed. [A]bort install or [S]kip web UI?`
|
||||
- *Abort* → exit the installer non-zero.
|
||||
- *Skip* → continue without the web binary; daemon/registry work already done
|
||||
is left intact. Return `Ok(false)` from `handle_web`.
|
||||
- **Non-interactive refresh path** (existing unit, no TTY): cannot prompt —
|
||||
print a clear warning that the web refresh is skipped because pnpm/node are
|
||||
missing, and leave the existing unit in place. Return `Ok(false)`.
|
||||
- **Both present:** proceed exactly as today. The existing `cargo install --git`
|
||||
now Just Works because `build.rs` builds the bundle inside the temp clone.
|
||||
|
||||
This keeps the daemon/registry install unaffected by a missing frontend
|
||||
toolchain, and never lets a raw `build.rs` panic leak to the user — the wizard
|
||||
catches the missing-prereq case first.
|
||||
|
||||
### 4. CI — `.gitea/workflows/ci.yml`
|
||||
|
||||
`clippy` and `test` compile `maestro-web`, which now requires the frontend
|
||||
toolchain. Add a step (before `clippy`) that installs Node.js + pnpm and runs
|
||||
`pnpm -C web install --frozen-lockfile`, so the workspace builds on the runner.
|
||||
Node/pnpm install goes in the existing "Install … build deps" step or a new
|
||||
adjacent step; `corepack enable` (or a pinned pnpm install) provides `pnpm`.
|
||||
|
||||
### 5. `Justfile` — unchanged
|
||||
|
||||
The `web:` target stays as a `--release` convenience and as the documented
|
||||
local build command. With `build.rs` in place its `pnpm build` step is partly
|
||||
redundant but harmless.
|
||||
|
||||
## Consequences accepted
|
||||
|
||||
- The `maestro-web` crate cannot be compiled without node + pnpm. This is
|
||||
intentional (hard fail). It affects: workspace CI (addressed in §4), any
|
||||
Rust-only contributor building the full workspace, and `cargo install --git`
|
||||
on a machine without node.
|
||||
- `cargo install --git` becomes slower (runs `pnpm install` + `pnpm build`), but
|
||||
produces a correct binary — the whole point.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Shipping a prebuilt release binary (considered, deferred — the build.rs +
|
||||
wizard path covers the reported problem).
|
||||
- Changing the data-plane / proxy behavior of `maestro-web`.
|
||||
|
||||
## Testing
|
||||
|
||||
- `build.rs` is exercised implicitly by every compile; a green
|
||||
`cargo build -p maestro-web` on a clean checkout (no prior `pnpm build`) that
|
||||
yields a binary whose `/` serves the real `index.html` is the acceptance
|
||||
signal. An integration check can assert the served root contains the SPA
|
||||
mount (`<div id="app">`) rather than the placeholder string.
|
||||
- Installer: unit-test the preflight decision (missing-prereq → abort/skip
|
||||
branch selection) without invoking cargo, following existing
|
||||
`maestro-installer` test patterns.
|
||||
- CI itself is the regression test for the toolchain requirement.
|
||||
Reference in New Issue
Block a user