feat(console): embed the management UI in release binaries
- build locked Web dependencies automatically for release profiles - keep debug Cargo builds independent from the Vite development server - embed generated Host, Page, manifest, and service worker assets - serve exact control-plane assets with MIME and cache headers - reject non-canonical and unknown paths without an SPA fallback - cover embedded document boundaries and path validation
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
//! Builds the Solid management UI for embedded Release binaries.
|
||||
//!
|
||||
//! Debug builds intentionally return before invoking npm: during development
|
||||
//! Vite serves `web/` independently and proxies its API client to the Rust
|
||||
//! listener. Release builds use the lockfile, write only into Cargo `OUT_DIR`,
|
||||
//! and expose a cfg consumed by `src/web.rs`. The source tree therefore never
|
||||
//! needs a generated `dist/` for `cargo build --release`.
|
||||
//!
|
||||
//! Set `WASMELD_BUILD_WEB=1` to exercise the embedded path in another Cargo
|
||||
//! profile, or `NPM=/absolute/path/to/npm` when npm is not on `PATH`.
|
||||
|
||||
use std::{
|
||||
env, fs,
|
||||
path::PathBuf,
|
||||
process::{Command, ExitStatus},
|
||||
};
|
||||
|
||||
const WEB_INPUTS: &[&str] = &[
|
||||
"web/src",
|
||||
"web/public",
|
||||
"web/index.html",
|
||||
"web/page.html",
|
||||
"web/package.json",
|
||||
"web/package-lock.json",
|
||||
"web/tsconfig.json",
|
||||
"web/vite.config.ts",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rustc-check-cfg=cfg(wasmeld_embedded_web)");
|
||||
println!("cargo:rerun-if-env-changed=WASMELD_BUILD_WEB");
|
||||
println!("cargo:rerun-if-env-changed=NPM");
|
||||
for input in WEB_INPUTS {
|
||||
println!("cargo:rerun-if-changed={input}");
|
||||
}
|
||||
|
||||
let force = env::var("WASMELD_BUILD_WEB").is_ok_and(|value| value == "1");
|
||||
if env::var("PROFILE").as_deref() != Ok("release") && !force {
|
||||
return;
|
||||
}
|
||||
|
||||
let crate_dir = PathBuf::from(required_env("CARGO_MANIFEST_DIR"));
|
||||
let web_dir = crate_dir.join("web");
|
||||
let output_dir = PathBuf::from(required_env("OUT_DIR")).join("web");
|
||||
let npm = env::var_os("NPM")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from(if cfg!(windows) { "npm.cmd" } else { "npm" }));
|
||||
|
||||
if output_dir.exists() {
|
||||
fs::remove_dir_all(&output_dir).unwrap_or_else(|error| {
|
||||
panic!(
|
||||
"failed to clear embedded Web output {}: {error}",
|
||||
output_dir.display()
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
// `npm ci` is deliberate here: a release must not silently rewrite the
|
||||
// dependency graph represented by package-lock.json.
|
||||
run(
|
||||
Command::new(&npm).current_dir(&web_dir).args([
|
||||
"ci",
|
||||
"--ignore-scripts",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
]),
|
||||
"install locked Web dependencies",
|
||||
);
|
||||
run(
|
||||
Command::new(&npm)
|
||||
.current_dir(&web_dir)
|
||||
.env("WASMELD_WEB_OUT_DIR", &output_dir)
|
||||
.args(["run", "build"]),
|
||||
"build embedded Web application",
|
||||
);
|
||||
|
||||
for required in ["index.html", "page.html", "sw.js", "manifest.webmanifest"] {
|
||||
let path = output_dir.join(required);
|
||||
assert!(
|
||||
path.is_file(),
|
||||
"Web build did not produce required asset {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
println!("cargo:rustc-cfg=wasmeld_embedded_web");
|
||||
}
|
||||
|
||||
fn run(command: &mut Command, action: &str) {
|
||||
let status = command
|
||||
.status()
|
||||
.unwrap_or_else(|error| panic!("failed to {action}: {error}"));
|
||||
assert_success(status, action);
|
||||
}
|
||||
|
||||
fn assert_success(status: ExitStatus, action: &str) {
|
||||
assert!(
|
||||
status.success(),
|
||||
"failed to {action}: process exited with {status}"
|
||||
);
|
||||
}
|
||||
|
||||
fn required_env(name: &str) -> String {
|
||||
env::var(name).unwrap_or_else(|_| panic!("Cargo did not provide {name}"))
|
||||
}
|
||||
Reference in New Issue
Block a user