use std::env::{args, var}; use std::fs; use std::io::Cursor; use std::path::{Path, PathBuf}; use std::process::exit; use std::sync::mpsc; use std::sync::mpsc::TryRecvError; use std::thread::sleep; use std::time::Duration; use colored::Colorize; use notify::{Event, EventKind, RecursiveMode, Watcher}; use tiny_http::{Response, Server, StatusCode}; use wasm_pack::command::build::{BuildOptions, Target}; use wasm_pack::command::run_wasm_pack; use wasm_pack::progressbar::LogLevel; fn workspace_dir() -> PathBuf { let output = std::process::Command::new(env!("CARGO")) .arg("locate-project") .arg("--workspace") .arg("--message-format=plain") .output() .unwrap() .stdout; let cargo_path = Path::new(std::str::from_utf8(&output).unwrap().trim()); cargo_path.parent().unwrap().to_path_buf() } fn build_client() -> anyhow::Result<()> { let cli = wasm_pack::Cli { cmd: wasm_pack::command::Command::Build(BuildOptions { path: Some(workspace_dir().join("crates/client")), scope: None, mode: Default::default(), disable_dts: false, weak_refs: false, reference_types: false, target: Target::Web, debug: false, dev: false, release: false, profiling: false, out_dir: "pkg".to_string(), out_name: None, no_pack: false, no_opt: true, extra_options: vec![], }), verbosity: 0, quiet: true, log_level: LogLevel::Error, }; run_wasm_pack(cli.cmd) } fn try_build_client() -> bool { match build_client() { Ok(_) => { println!("{} -- Client package built successfully", "✓ Success".green().bold()); true }, Err(e) => { eprintln!("{} -- Client package failed to build: {}", "✗ Failed".red().bold(), e); false } } } fn start_server() { let bind = var("BIND").unwrap_or("[::]:8000".to_string()); let server = Server::http(&bind).unwrap(); println!("{} on {bind}", "✓ Listening".magenta().bold()); for req in server.incoming_requests() { let mut path = Path::new(req.url()); if path == Path::new("/") { path = Path::new("/index.html"); } let path = path.strip_prefix(Path::new("/")).unwrap(); let full_path = workspace_dir().join("crates/client").join(path); let content = match fs::read(full_path.clone()) { Ok(r) => r, Err(_) => { let _ = req.respond(Response::new(StatusCode::from(404), vec![], Cursor::new(vec![]), None, None)); continue; } }; let len = content.len(); let header = tiny_http::Header::from_bytes( &b"Content-Type"[..], if full_path.to_str().unwrap().ends_with(".html") { &b"text/html"[..] } else if full_path.to_str().unwrap().ends_with(".js") { &b"text/javascript"[..] } else if full_path.to_str().unwrap().ends_with(".wasm") { &b"application/wasm"[..] } else { &b"application/octet-stream"[..] } ).unwrap(); let response = Response::new( StatusCode::from(200), vec![ header ], Cursor::new(content), Some(len), None ); let _ = req.respond(response); } } fn main() { let mut args = args(); let subcommand = args.nth(1).unwrap(); match subcommand.as_str() { "client" => { if !try_build_client() { exit(1); } }, "watch" | "serve" => { let serve = subcommand == "serve"; if serve { std::thread::spawn(start_server); } try_build_client(); let (tx, rx) = mpsc::channel::>(); // Use recommended_watcher() to automatically select the best implementation // for your platform. The `EventHandler` passed to this constructor can be a // closure, a `std::sync::mpsc::Sender`, a `crossbeam_channel::Sender`, or // another type the trait is implemented for. let mut watcher = notify::recommended_watcher(tx).unwrap(); // Add a path to be watched. All files and directories at that path and // below will be monitored for changes. watcher.watch(&workspace_dir().join("crates"), RecursiveMode::Recursive).unwrap(); println!("{}", "[Watch] 🛈 Watching for file changes".blue().bold()); // Block forever, printing out events as they come in let mut needs_rebuild = false; loop { let res = rx.try_recv(); let res = match res { Ok(r) => r, Err(TryRecvError::Empty) => { if needs_rebuild { // wait 1s then check again, then rebuild sleep(Duration::from_secs(1)); if let Ok(r) = rx.try_recv() { r } else { try_build_client(); needs_rebuild = false; rx.recv().unwrap() } } else { rx.recv().unwrap() } }, Err(TryRecvError::Disconnected) => panic!("{:?}", TryRecvError::Disconnected) }; match res { Ok(event) => { if let EventKind::Modify(_) = event.kind { let has_non_generated_update = false; for path in &event.paths { if !path.to_str().unwrap().contains("client/pkg") && !path.to_str().unwrap().ends_with("~") { needs_rebuild = true; } } } }, Err(e) => { eprintln!("{} -- Error watching for files: {}", "[Watch] ✗ Error".red().bold(), e); }, } } }, _ => panic!("unsupported command") } }