use proc_macro::TokenStream as StdTokenStream;
use proc_macro2::{TokenStream, Span};
use quote::quote;
use std::env;
mod helpers;
mod config;
use helpers::*;
use config::*;
pub(crate) use wasmdev_core as core;
#[proc_macro_attribute]
pub fn main(attrs: StdTokenStream, main_fn: StdTokenStream) -> StdTokenStream {
match (|| -> Result<TokenStream, TokenStream> {
let wasm_fn: TokenStream = main_fn.into();
let config = parse_config_attrs(attrs.into())?;
let wasm_main_fn = make_wasm_main_fn(&wasm_fn)?;
let server_main_fn = make_server_main_fn(&wasm_fn, config)?;
Ok(quote! {
#[cfg(not(target_family = "wasm"))]
#server_main_fn
#[cfg(target_family = "wasm")]
#wasm_main_fn
})
})() {
Ok(tt) => tt,
Err(tt) => tt,
}.into()
}
fn make_wasm_main_fn(wasm_main_fn: &TokenStream) -> Result<TokenStream, TokenStream> {
let Some(wasm_main_fn_ident) = get_fn_name(wasm_main_fn) else {
return compiler_error!("No main function found");
};
Ok(quote! {
fn main() {
#wasm_main_fn
wasmdev::if_enabled_setup_panic_hook_once();
#wasm_main_fn_ident ();
}
})
}
fn make_server_main_fn(wasm_main_fn: &TokenStream, config: AttrConfig) -> Result<TokenStream, TokenStream> {
let config: BuildConfig = config.try_into()?;
let is_release = &config.is_release;
let index_html = &config.index_html;
let index_js = &config.index_js;
let target_path = &config.target_path;
let address = &config.attrs.addr.value;
let port = &config.attrs.port.value;
let watch = &config.attrs.watch.value;
let wasm_path = &config.wasm_path;
let index_js_path = &config.index_js_path;
let index_wasm_path = &config.index_wasm_path;
let proj_html_path = &config.proj_html_path;
let proj_src_path = &config.proj_src_path;
let proj_static_path = &config.proj_static_path;
let Some(wasm_main_fn_ident) = get_fn_name(wasm_main_fn) else {
return compiler_error!("No main function found");
};
let Ok(_) = std::fs::metadata(proj_static_path) else {
let span = config.attrs.path.tt.map(|tt| tt.span()).unwrap_or(Span::call_site());
return compiler_error!(span, "Error: Unable to read directory: {}", proj_static_path);
};
let Ok(_) = address.parse::<std::net::IpAddr>() else {
let span = config.attrs.addr.tt.map(|tt| tt.span()).unwrap_or(Span::call_site());
return compiler_error!(span, "Error: {} is not a valid ipv4 or ipv6 address", address);
};
let build_wasm_now = env::var("CARGO_WASMDEV").ok().is_none() && config.is_release;
let static_asset_cache = if build_wasm_now {
build_all_web_assets(&config)?
} else {
quote! {} };
Ok(quote!{
fn main() {
#wasm_main_fn
{
use std::net::TcpListener;
use std::path::PathBuf;
use std::str::from_utf8;
use std::fs;
use wasmdev::prelude::*;
use wasmdev::{Server, ServerConfig};
use wasmdev::{core, utils::make_watcher};
let is_release = #is_release;
let index_html = #index_html;
let index_js = #index_js;
let target_path = #target_path;
let address = #address;
let port = #port;
let watch = #watch;
let wasm_path = #wasm_path;
let index_js_path = #index_js_path;
let index_wasm_path = #index_wasm_path;
let proj_html_path = #proj_html_path;
let proj_src_path = #proj_src_path;
let proj_static_path = #proj_static_path;
#static_asset_cache
#wasm_main_fn_ident;
let server = Server::new();
server.configure(|config| {
config
.on_get_request("/")
.internal_redirect("/index.html")
.build();
config
.on_get_request("/index.html")
.set_response_body(index_html.as_bytes().to_vec())
.build();
});
let build_load_and_serve_app = {
let mut server = server.clone();
move || -> Option<()>{
eprintln!("\x1b[0m\x1b[0m\x1b[1m\x1b[32m Building\x1b[0m wasm target");
let _ = core::code::build_wasm(wasm_path, is_release, target_path)?;
let wasm_code = fs::read(&index_wasm_path).ok()?;
let js_code = fs::read(&index_js_path).ok()?;
let js_code = if is_release { core::code::minify_javascript(&js_code)? } else { js_code };
let code_did_update = server.configure(|config| {
config
.on_get_request("/index.js")
.set_response_body(js_code)
.build();
config
.on_get_request("/index.wasm")
.set_response_body(wasm_code)
.build()
});
if code_did_update {
eprintln!("\x1b[0m\x1b[0m\x1b[1m\x1b[32m Serving\x1b[0m /index.wasm, /index.js");
server.broadcast("reload /index.wasm".as_bytes());
}
Some(())
}
};
let file_path_to_req_path = move |path: &str|
path.replace(proj_static_path, "").replace("\\", "/");
let serve_static_files = || {
let file_paths = core::fs::list_files_recursively(proj_static_path)
.expect(&format!("Unable to list static assets: '{}'", proj_static_path));
let file_and_req_path_iter = file_paths.iter()
.filter_map(|file_path| file_path.to_str())
.map(|file_path| (file_path, file_path_to_req_path(file_path)))
.filter(|(_, req_path)| *req_path != "/index.html");
server.configure(|conf| {
for (file_path, req_path) in file_and_req_path_iter.clone(){
conf.on_get_request(&req_path)
.lazy_load(file_path)
.build();
}
});
for (_, req_path) in file_and_req_path_iter{
eprintln!("\x1b[0m\x1b[0m\x1b[1m\x1b[32m Serving\x1b[0m {}", req_path);
}
};
let load_and_serve_file = {
let mut server = server.clone();
move |paths: Vec<PathBuf> | {
for file_path in paths {
let file_path = file_path.as_path();
let Some(req_path) = file_path.to_str().map(file_path_to_req_path) else { continue };
if req_path == "/index.html" { continue }; let Ok(file_contents) = fs::read(file_path) else { continue };
let file_did_update = server.configure(|config| config
.on_get_request(&req_path)
.set_response_body(file_contents)
.build()
);
if file_did_update {
eprintln!("\x1b[0m\x1b[0m\x1b[1m\x1b[32m Serving\x1b[0m {}", req_path);
server.broadcast(format!("reload {}", req_path).as_bytes());
}
}
}
};
let load_and_serve_index_html = {
let mut server = server.clone();
move || {
let Ok(index_html) = fs::read(&proj_html_path) else { return };
let index_html = from_utf8(&index_html).expect("index.html is not utf8 encoded.");
let index_html = format!("{}\n<script type=\"module\">{}</script>",index_html, index_js);
let file_did_update = server.configure(|config| config
.on_get_request("/index.html")
.set_response_body(index_html.as_bytes().to_vec())
.build()
);
if file_did_update {
eprintln!("\x1b[0m\x1b[0m\x1b[1m\x1b[32m Serving\x1b[0m /index.html");
server.broadcast("reload /index.html".as_bytes());
}
}
};
serve_static_files();
load_and_serve_index_html();
build_load_and_serve_app();
let _watchers = if watch { Some((
make_watcher(&proj_static_path, move |paths| { load_and_serve_file(paths); })
.expect("Unable to watch static files folder, required for hot-reload when updated."),
make_watcher(&proj_src_path, move |_| { build_load_and_serve_app(); })
.expect("Unable to watch src folder, required for hot-reload."),
make_watcher(&proj_html_path, move |_| { load_and_serve_index_html(); }),
))} else { None };
let addr = format!("{}:{}", address, port);
let Ok(tcp_socket) = TcpListener::bind(addr) else {
panic!("Unable to bind tcp port: {}", port)
};
let Ok(addr) = tcp_socket.local_addr() else {
panic!("Unable to get local socket address.")
};
let addr_char_count = addr.to_string().chars().into_iter().count();
eprint!(" ┏━━━━━━━━");
for _ in 0..addr_char_count { eprint!("━") };
eprintln!("━┓");
eprintln!("\x1b[0m\x1b[0m\x1b[1m\x1b[32m Serving\x1b[0m ┃\x1b[1m http://{} \x1b[0m┃ <= Click to open your app! ", addr);
eprint!(" ┗━━━━━━━━");
for _ in 0..addr_char_count { eprint!("━") };
eprintln!("━┛");
let Ok(()) = server.listen(tcp_socket) else {
panic!("Unable to handle incomming connection")
};
}
}
})
}