mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
M15.1: web build (eframe wasm) + xtask web
- apps/effectcraft-web (L6): eframe web runner booting the shared EffectcraftApp with a browser session: in-memory file table, <input type=file> pickers and drag-and-drop for open/import, downloads for Save and Render Queue outputs (several files as one stored .zip), and the window.effectcraft promise API over the control channel. Demo project on start. - wasm32 runtime fixes: web-time for Instant/SystemTime (render, engine, export, ui-egui); Frames renders queued jobs on the UI thread (Frames::pump) instead of a thread pool; Render Queue runs inline; relative outputs resolve under / ; control PNGs inline. - effectcraft-export: per-job output Sink (files to memory instead of disk); host FileExporter carries it; test that sink exports touch no file. - xtask web [--dev] [--serve PORT] (wasm-bindgen 0.2.129, optional wasm-opt, COOP/COEP server); effectcraft-web in WEB_CRATES. docs/web.md, headless Chrome smoke test, favicon (+attribution). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c925ac8ae8
commit
31f192e2a9
30 files changed
+1361
-92
No files matched your search
+121
-4
@@ -1,8 +1,11 @@
|
||||
//! Workspace automation: `cargo xtask <layers|assets|wasm|ci>`.
|
||||
//! Workspace automation: `cargo xtask <layers|assets|wasm|web|ci>`.
|
||||
//!
|
||||
//! - `layers`: enforces the dependency layering of `docs/architecture.md` §1 (downward-only edges,
|
||||
//! listed same-layer edges, no UI/OS crates below L5).
|
||||
//! - `wasm`: `cargo check --target wasm32-unknown-unknown` for every crate in L0–L4 and the egui UI.
|
||||
//! - `wasm`: `cargo check --target wasm32-unknown-unknown` for every crate in L0–L4, the egui UI
|
||||
//! and the web app.
|
||||
//! - `web [--dev] [--serve PORT]`: build the web app (`apps/effectcraft-web`) into
|
||||
//! `<target>/web/dist` with `wasm-bindgen` (docs/web.md); `--serve` serves it on localhost.
|
||||
//! - `assets`: every asset file (image, icon, font, LUT, audio, video…) has a complete
|
||||
//! `<file>.attribution` sidecar and an entry in `ATTRIBUTION.md` (AGENTS.md §1).
|
||||
//! - `ci`: fmt check, clippy -D warnings, tests, layers, assets, wasm.
|
||||
@@ -201,7 +204,7 @@ fn run(cmd: &mut Command) -> Result<(), String> {
|
||||
}
|
||||
|
||||
/// Crates above L4 that must also build for the web.
|
||||
const WEB_CRATES: &[&str] = &["effectcraft-ui-egui"];
|
||||
const WEB_CRATES: &[&str] = &["effectcraft-ui-egui", "effectcraft-web"];
|
||||
|
||||
fn wasm() -> Result<(), String> {
|
||||
let md = metadata()?;
|
||||
@@ -222,6 +225,119 @@ fn wasm() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The wasm-bindgen CLI must match the `wasm-bindgen` crate version exactly.
|
||||
const WASM_BINDGEN: &str = "0.2.129";
|
||||
|
||||
fn target_dir() -> std::path::PathBuf {
|
||||
std::env::var_os("CARGO_TARGET_DIR").map(std::path::PathBuf::from).unwrap_or_else(|| std::path::PathBuf::from("target"))
|
||||
}
|
||||
|
||||
/// Build the web app into `<target>/web/dist` (see docs/web.md).
|
||||
fn web(args: &[String]) -> Result<(), String> {
|
||||
let dev = args.iter().any(|a| a == "--dev");
|
||||
let serve = args.iter().position(|a| a == "--serve").map(|i| args.get(i + 1).and_then(|p| p.parse::<u16>().ok()).unwrap_or(8765));
|
||||
let profile = if dev { "dev" } else { "release" };
|
||||
let mut build = Command::new(env!("CARGO"));
|
||||
build.args(["build", "--target", "wasm32-unknown-unknown", "-p", "effectcraft-web", "--profile", profile]);
|
||||
run(&mut build)?;
|
||||
let out = Command::new("wasm-bindgen")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map_err(|_| format!("wasm-bindgen CLI not found: cargo install wasm-bindgen-cli --version {WASM_BINDGEN} --locked"))?;
|
||||
let v = String::from_utf8_lossy(&out.stdout);
|
||||
if !v.contains(WASM_BINDGEN) {
|
||||
return Err(format!(
|
||||
"wasm-bindgen CLI is {} but the crate is {WASM_BINDGEN}: cargo install wasm-bindgen-cli --version {WASM_BINDGEN} --locked",
|
||||
v.trim()
|
||||
));
|
||||
}
|
||||
let dir = if dev { "debug" } else { "release" };
|
||||
let wasm = target_dir().join("wasm32-unknown-unknown").join(dir).join("effectcraft_web.wasm");
|
||||
let dist = target_dir().join("web").join("dist");
|
||||
let _ = std::fs::remove_dir_all(&dist);
|
||||
std::fs::create_dir_all(&dist).map_err(|e| e.to_string())?;
|
||||
run(Command::new("wasm-bindgen").args(["--target", "web", "--no-typescript", "--out-dir"]).arg(&dist).arg(&wasm))?;
|
||||
let bg = dist.join("effectcraft_web_bg.wasm");
|
||||
if !dev && Command::new("wasm-opt").arg("--version").output().is_ok() {
|
||||
// optional: smaller and faster (binaryen); skipped when not installed
|
||||
let opt = dist.join("effectcraft_web_opt.wasm");
|
||||
run(Command::new("wasm-opt")
|
||||
.args(["-O2", "--enable-bulk-memory", "--enable-nontrapping-float-to-int", "--enable-sign-ext", "--enable-mutable-globals"])
|
||||
.arg(&bg)
|
||||
.arg("-o")
|
||||
.arg(&opt))?;
|
||||
std::fs::rename(&opt, &bg).map_err(|e| e.to_string())?;
|
||||
}
|
||||
let web = std::path::Path::new("apps/effectcraft-web/web");
|
||||
for e in std::fs::read_dir(web).map_err(|e| e.to_string())?.flatten() {
|
||||
let name = e.file_name().to_string_lossy().to_string();
|
||||
if !name.ends_with(".attribution") {
|
||||
std::fs::copy(e.path(), dist.join(&name)).map_err(|e| format!("{name}: {e}"))?;
|
||||
}
|
||||
}
|
||||
let size = |p: &std::path::Path| std::fs::metadata(p).map(|m| m.len()).unwrap_or(0);
|
||||
let total: u64 = std::fs::read_dir(&dist).map_err(|e| e.to_string())?.flatten().map(|e| size(&e.path())).sum();
|
||||
println!("web: {} ({:.1} MB wasm, {:.1} MB total)", dist.display(), size(&bg) as f64 / 1e6, total as f64 / 1e6);
|
||||
if let Some(port) = serve {
|
||||
serve_dir(&dist, port)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A tiny static file server for the web build (localhost only). Sends the cross-origin
|
||||
/// isolation headers so `crossOriginIsolated` is true, like a production deployment would.
|
||||
fn serve_dir(dir: &std::path::Path, port: u16) -> Result<(), String> {
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
let listener = std::net::TcpListener::bind(("127.0.0.1", port)).map_err(|e| format!("127.0.0.1:{port}: {e}"))?;
|
||||
println!("serving {} on http://127.0.0.1:{port}/", dir.display());
|
||||
for stream in listener.incoming().flatten() {
|
||||
let dir = dir.to_path_buf();
|
||||
std::thread::spawn(move || {
|
||||
let mut stream = stream;
|
||||
let mut line = String::new();
|
||||
let mut reader = BufReader::new(match stream.try_clone() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return,
|
||||
});
|
||||
if reader.read_line(&mut line).is_err() {
|
||||
return;
|
||||
}
|
||||
loop {
|
||||
let mut h = String::new();
|
||||
if reader.read_line(&mut h).map(|n| n == 0).unwrap_or(true) || h.trim().is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let path = line.split_whitespace().nth(1).unwrap_or("/").split(['?', '#']).next().unwrap_or("/").to_string();
|
||||
let rel = if path == "/" { "index.html".to_string() } else { path.trim_start_matches('/').replace("..", "") };
|
||||
let file = dir.join(&rel);
|
||||
let (status, body) = match std::fs::read(&file) {
|
||||
Ok(b) => ("200 OK", b),
|
||||
Err(_) => ("404 Not Found", b"not found".to_vec()),
|
||||
};
|
||||
let mime = match file.extension().and_then(|e| e.to_str()).unwrap_or("") {
|
||||
"html" => "text/html; charset=utf-8",
|
||||
"js" => "text/javascript",
|
||||
"wasm" => "application/wasm",
|
||||
"svg" => "image/svg+xml",
|
||||
"json" => "application/json",
|
||||
"png" => "image/png",
|
||||
"gif" => "image/gif",
|
||||
"ecproj" => "application/json",
|
||||
"mp4" => "video/mp4",
|
||||
"webm" => "video/webm",
|
||||
_ => "application/octet-stream",
|
||||
};
|
||||
let head = format!(
|
||||
"HTTP/1.1 {status}\r\nContent-Type: {mime}\r\nContent-Length: {}\r\nCross-Origin-Opener-Policy: same-origin\r\nCross-Origin-Embedder-Policy: require-corp\r\nCache-Control: no-cache\r\nConnection: close\r\n\r\n",
|
||||
body.len()
|
||||
);
|
||||
let _ = stream.write_all(head.as_bytes()).and_then(|_| stream.write_all(&body));
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ci() -> Result<(), String> {
|
||||
let cargo = env!("CARGO");
|
||||
run(Command::new(cargo).args(["fmt", "--check"]))?;
|
||||
@@ -239,7 +355,8 @@ fn main() -> ExitCode {
|
||||
"wasm" => wasm(),
|
||||
"assets" => assets(),
|
||||
"ci" => ci(),
|
||||
_ => Err("usage: cargo xtask <layers|assets|wasm|ci>".into()),
|
||||
"web" => web(&std::env::args().skip(2).collect::<Vec<_>>()),
|
||||
_ => Err("usage: cargo xtask <layers|assets|wasm|web [--dev] [--serve PORT]|ci>".into()),
|
||||
};
|
||||
match r {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
|
||||
Reference in new issue
Block a user