use colored::Colorize;
use notify::{Event, EventKind, RecursiveMode, Watcher};
use std::env::{args, var};
use std::fs;
use std::io::{Cursor, stdout, Write};
use std::path::{Path, PathBuf};
use std::process::{exit, Command};
use std::sync::mpsc;
use std::sync::mpsc::TryRecvError;
use std::thread::sleep;
use std::time::{Duration, Instant};
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::<notify::Result<Event>>();
// 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
);
}
}
}
}
"asset:process" => {
// png-ify textures
let paths = fs::read_dir(workspace_dir().join("crates/unified/assets/vector_textures/")).unwrap();
let output_dir = workspace_dir().join("crates/unified/assets/textures/");
for path in paths {
let path = path.unwrap().path();
let Some(extension) = path.extension() else { continue };
if extension != "svg" { continue; }
let Some(filename) = path.file_stem() else { continue };
print!("[{}] {}.svg", ">".blue(), filename.to_string_lossy());
stdout().flush().unwrap();
let start = Instant::now();
let tree = {
let mut opt = resvg::usvg::Options {
resources_dir: std::fs::canonicalize(&path).ok().and_then(|p| p.parent().map(|p| p.to_path_buf())),
..resvg::usvg::Options::default()
};
opt.fontdb_mut().load_system_fonts();
let svg_data = std::fs::read(&path).unwrap();
resvg::usvg::Tree::from_data(&svg_data, &opt).unwrap()
};
let pixmap_size = tree.size().to_int_size();
let mut pixmap = resvg::tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap();
resvg::render(&tree, resvg::tiny_skia::Transform::default(), &mut pixmap.as_mut());
pixmap.save_png(output_dir.join(format!("{}.png", filename.to_string_lossy()))).unwrap();
println!(" {} {}.png {} {}", "-->".dimmed(), filename.to_string_lossy(), "✓ success".green(), format!("{}ms", start.elapsed().as_millis()).dimmed());
}
},
"unified:web" => {
let start = Instant::now();
let target = workspace_dir().join("target/");
let unified = workspace_dir().join("crates/unified/");
exec("cargo", "build --package starkingdoms --lib --target wasm32-unknown-unknown -F wasm --no-default-features");
let wasm_file = target.join("wasm32-unknown-unknown/debug/starkingdoms.wasm");
exec("wasm-bindgen", format!("--target web --typescript --out-dir {} {}", unified.join("web/").display(), wasm_file.display()).as_str());
println!("{} {} {}", "-->".dimmed(), "✓ done".green(), format!("{}ms", start.elapsed().as_millis()).dimmed());
},
_ => panic!("unsupported command"),
}
}
fn exec(program: &str, args: &str) {
println!("[{}] {} {}", "+".blue(), program, args);
Command::new(program)
.args(args.split(" ").collect::<Vec<&str>>())
.spawn()
.unwrap()
.wait()
.unwrap();
}