diff --git a/Cargo.toml b/Cargo.toml index 3a8b061ec..cd1dec98b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,6 @@ cargo_toml = "0.10.0" futures = "0.3.12" notify = "4.0.17" html_parser = "0.6.2" -tui = { version = "0.16.0", features = ["crossterm"] } -crossterm = "0.22.1" binary-install = "0.0.2" convert_case = "0.5.0" structopt = "0.3.25" @@ -30,6 +28,11 @@ cargo_metadata = "0.14.1" tokio = { version = "1.15.0", features = ["full"] } atty = "0.2.14" +axum = { version = "0.4.4", features = ["ws", "headers"] } +tower-http = { version = "0.2.0", features = ["fs", "trace"] } +headers = "0.3" +# hyper = { version = "0.14.11", features = ["full"] } + [[bin]] path = "src/main.rs" name = "dioxus" diff --git a/src/builder.rs b/src/builder.rs index b5bae9bf9..89451e163 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -37,7 +37,6 @@ pub fn build(config: &CrateConfig) -> Result<()> { .arg("wasm32-unknown-unknown") .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()); - ; if config.release { cmd.arg("--release"); @@ -54,7 +53,7 @@ pub fn build(config: &CrateConfig) -> Result<()> { let output = child.wait()?; if output.success() { - log::info!("Build complete! {:?}", reason); + log::info!("Build complete!"); } else { log::error!("Build failed!"); let mut reason = String::new(); @@ -63,7 +62,7 @@ pub fn build(config: &CrateConfig) -> Result<()> { } // [2] Establish the output directory structure - let bindgen_outdir = out_dir.join("wasm"); + let bindgen_outdir = out_dir.join("assets"); // [3] Bindgen the final binary for use easy linking let mut bindgen_builder = Bindgen::new(); @@ -100,8 +99,6 @@ pub fn build(config: &CrateConfig) -> Result<()> { // [5] Generate the html file with the module name // TODO: support names via options log::info!("Writing to '{:#?}' directory...", out_dir); - let mut file = std::fs::File::create(out_dir.join("index.html"))?; - file.write_all(gen_page("./wasm/module.js").as_str().as_bytes())?; let copy_options = fs_extra::dir::CopyOptions::new(); if static_dir.is_dir() { @@ -118,7 +115,7 @@ pub fn build(config: &CrateConfig) -> Result<()> { Ok(()) } -fn gen_page(module: &str) -> String { +pub fn gen_page(module: &str) -> String { format!( r#" @@ -132,7 +129,7 @@ fn gen_page(module: &str) -> String {
diff --git a/src/cli/build/mod.rs b/src/cli/build/mod.rs index f4b69d5d7..58d797485 100644 --- a/src/cli/build/mod.rs +++ b/src/cli/build/mod.rs @@ -13,8 +13,7 @@ pub struct Build { impl Build { pub fn build(self) -> anyhow::Result<()> { - - let mut crate_config = crate::CrateConfig::new()?; + let mut crate_config = crate::CrateConfig::new()?; // change the relase state. crate_config.with_release(self.build.release); @@ -23,4 +22,4 @@ impl Build { Ok(()) } -} \ No newline at end of file +} diff --git a/src/cli/cfg.rs b/src/cli/cfg.rs index feab0e61f..5736ebc19 100644 --- a/src/cli/cfg.rs +++ b/src/cli/cfg.rs @@ -67,6 +67,26 @@ pub struct ConfigOptsBuild { pub pattern_params: Option>, } +#[derive(Clone, Debug, Default, Deserialize, StructOpt)] +pub struct ConfigOptsServe { + /// The index HTML file to drive the bundling process [default: index.html] + #[structopt(parse(from_os_str))] + pub target: Option, + + /// Build in release mode [default: false] + #[structopt(long)] + #[serde(default)] + pub release: bool, + + /// The output dir for all final assets [default: dist] + #[structopt(short, long, parse(from_os_str))] + pub dist: Option, + + /// The public URL from which assets are to be served [default: /] + #[structopt(long, parse(from_str=parse_public_url))] + pub public_url: Option, +} + /// Ensure the given value for `--public-url` is formatted correctly. pub fn parse_public_url(val: &str) -> String { let prefix = if !val.starts_with('/') { "/" } else { "" }; diff --git a/src/cli/mod.rs b/src/cli/mod.rs index fa3bd2c77..32492459a 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -35,7 +35,6 @@ pub enum Commands { Translate(translate::Translate), // /// Build, watch & serve the Rust WASM app and all of its assets. Serve(serve::Serve), - // /// Clean output artifacts. // Clean(clean::Clean), diff --git a/src/cli/serve/develop.rs b/src/cli/serve/develop.rs index dd386b14e..27aba99ee 100644 --- a/src/cli/serve/develop.rs +++ b/src/cli/serve/develop.rs @@ -1,145 +1,145 @@ -use crate::{cli::DevelopOptions, config::CrateConfig, error::Result}; -use async_std::prelude::FutureExt; +// use crate::{cli::DevelopOptions, config::CrateConfig, error::Result}; +// use async_std::prelude::FutureExt; -use log::info; -use notify::{RecommendedWatcher, RecursiveMode, Watcher}; -use std::path::PathBuf; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; -use tide::http::mime::HTML; -use tide::http::Mime; +// use log::info; +// use notify::{RecommendedWatcher, RecursiveMode, Watcher}; +// use std::path::PathBuf; +// use std::sync::atomic::AtomicBool; +// use std::sync::Arc; +// use tide::http::mime::HTML; +// use tide::http::Mime; -pub struct DevelopState { - // - reload_on_change: bool, -} +// pub struct DevelopState { +// // +// reload_on_change: bool, +// } -pub async fn develop(options: DevelopOptions) -> Result<()> { - // - log::info!("Starting development server 🚀"); - let mut cfg = CrateConfig::new()?; - cfg.with_develop_options(&options); +// pub async fn develop(options: DevelopOptions) -> Result<()> { +// // +// log::info!("Starting development server 🚀"); +// let mut cfg = CrateConfig::new()?; +// cfg.with_develop_options(&options); - let out_dir = cfg.out_dir.clone(); +// let out_dir = cfg.out_dir.clone(); - let is_err = Arc::new(AtomicBool::new(false)); +// let is_err = Arc::new(AtomicBool::new(false)); - // Spawn the server onto a seperate task - // This lets the task progress while we handle file updates - let server = async_std::task::spawn(launch_server(out_dir, is_err.clone())); - let watcher = async_std::task::spawn(watch_directory(cfg.clone(), is_err.clone())); +// // Spawn the server onto a seperate task +// // This lets the task progress while we handle file updates +// let server = async_std::task::spawn(launch_server(out_dir, is_err.clone())); +// let watcher = async_std::task::spawn(watch_directory(cfg.clone(), is_err.clone())); - match server.race(watcher).await { - Err(e) => log::warn!("Error running development server, {:?}", e), - _ => {} - } +// match server.race(watcher).await { +// Err(e) => log::warn!("Error running development server, {:?}", e), +// _ => {} +// } - Ok(()) -} +// Ok(()) +// } -async fn watch_directory(config: CrateConfig, is_err: ErrStatus) -> Result<()> { - // Create a channel to receive the events. - let (watcher_tx, watcher_rx) = async_std::channel::bounded(100); +// async fn watch_directory(config: CrateConfig, is_err: ErrStatus) -> Result<()> { +// // Create a channel to receive the events. +// let (watcher_tx, watcher_rx) = async_std::channel::bounded(100); - // Automatically select the best implementation for your platform. - // You can also access each implementation directly e.g. INotifyWatcher. - let mut watcher: RecommendedWatcher = Watcher::new(move |res| { - async_std::task::block_on(watcher_tx.send(res)); - // send an event - let _ = async_std::task::block_on(watcher_tx.send(res)); - }) - .expect("failed to make watcher"); +// // Automatically select the best implementation for your platform. +// // You can also access each implementation directly e.g. INotifyWatcher. +// let mut watcher: RecommendedWatcher = Watcher::new(move |res| { +// async_std::task::block_on(watcher_tx.send(res)); +// // send an event +// let _ = async_std::task::block_on(watcher_tx.send(res)); +// }) +// .expect("failed to make watcher"); - let src_dir = crate::cargo::crate_root()?; +// let src_dir = crate::cargo::crate_root()?; - // Add a path to be watched. All files and directories at that path and - // below will be monitored for changes. - watcher - .watch(&src_dir.join("src"), RecursiveMode::Recursive) - .expect("Failed to watch dir"); +// // Add a path to be watched. All files and directories at that path and +// // below will be monitored for changes. +// watcher +// .watch(&src_dir.join("src"), RecursiveMode::Recursive) +// .expect("Failed to watch dir"); - match watcher.watch(&src_dir.join("examples"), RecursiveMode::Recursive) { - Ok(_) => {} - Err(e) => log::warn!("Failed to watch examples dir, {:?}", e), - } +// match watcher.watch(&src_dir.join("examples"), RecursiveMode::Recursive) { +// Ok(_) => {} +// Err(e) => log::warn!("Failed to watch examples dir, {:?}", e), +// } - 'run: loop { - match crate::builder::build(&config) { - Ok(_) => { - is_err.store(false, std::sync::atomic::Ordering::Relaxed); - async_std::task::sleep(std::time::Duration::from_millis(500)).await; - } - Err(err) => is_err.store(true, std::sync::atomic::Ordering::Relaxed), - }; +// 'run: loop { +// match crate::builder::build(&config) { +// Ok(_) => { +// is_err.store(false, std::sync::atomic::Ordering::Relaxed); +// async_std::task::sleep(std::time::Duration::from_millis(500)).await; +// } +// Err(err) => is_err.store(true, std::sync::atomic::Ordering::Relaxed), +// }; - let mut msg = None; - loop { - let new_msg = watcher_rx.recv().await.unwrap().unwrap(); - if !watcher_rx.is_empty() { - msg = Some(new_msg); - break; - } - } +// let mut msg = None; +// loop { +// let new_msg = watcher_rx.recv().await.unwrap().unwrap(); +// if !watcher_rx.is_empty() { +// msg = Some(new_msg); +// break; +// } +// } - info!("File updated, rebuilding app"); - } - Ok(()) -} +// info!("File updated, rebuilding app"); +// } +// Ok(()) +// } -async fn launch_server(outdir: PathBuf, is_err: ErrStatus) -> Result<()> { - let _crate_dir = crate::cargo::crate_root()?; - let _workspace_dir = crate::cargo::workspace_root()?; +// async fn launch_server(outdir: PathBuf, is_err: ErrStatus) -> Result<()> { +// let _crate_dir = crate::cargo::crate_root()?; +// let _workspace_dir = crate::cargo::workspace_root()?; - let mut app = tide::with_state(ServerState::new(outdir.to_owned(), is_err)); +// let mut app = tide::with_state(ServerState::new(outdir.to_owned(), is_err)); - let file_path = format!("{}/index.html", outdir.display()); - log::info!("Serving {}", file_path); - let p = outdir.display().to_string(); +// let file_path = format!("{}/index.html", outdir.display()); +// log::info!("Serving {}", file_path); +// let p = outdir.display().to_string(); - app.at("/") - .get(|req: tide::Request| async move { - log::info!("Connected to development server"); - let state = req.state(); +// app.at("/") +// .get(|req: tide::Request| async move { +// log::info!("Connected to development server"); +// let state = req.state(); - match state.is_err.load(std::sync::atomic::Ordering::Relaxed) { - true => { - // - let mut resp = - tide::Body::from_string(format!(include_str!("../err.html"), err = "_")); - resp.set_mime(HTML); +// match state.is_err.load(std::sync::atomic::Ordering::Relaxed) { +// true => { +// // +// let mut resp = +// tide::Body::from_string(format!(include_str!("../err.html"), err = "_")); +// resp.set_mime(HTML); - Ok(resp) - } - false => { - Ok(tide::Body::from_file(state.serv_path.clone().join("index.html")).await?) - } - } - }) - .serve_dir(p)?; - // .serve_file(file_path) - // .unwrap(); +// Ok(resp) +// } +// false => { +// Ok(tide::Body::from_file(state.serv_path.clone().join("index.html")).await?) +// } +// } +// }) +// .serve_dir(p)?; +// // .serve_file(file_path) +// // .unwrap(); - let port = "8080"; - let serve_addr = format!("127.0.0.1:{}", port); +// let port = "8080"; +// let serve_addr = format!("127.0.0.1:{}", port); - info!("App available at http://{}/", serve_addr); - app.listen(serve_addr).await?; - Ok(()) -} +// info!("App available at http://{}/", serve_addr); +// app.listen(serve_addr).await?; +// Ok(()) +// } -/// https://github.com/http-rs/tide/blob/main/examples/state.rs -/// Tide seems to prefer using state instead of injecting into the app closure -/// The app closure needs to be static and -#[derive(Clone)] -struct ServerState { - serv_path: PathBuf, - is_err: ErrStatus, -} +// /// https://github.com/http-rs/tide/blob/main/examples/state.rs +// /// Tide seems to prefer using state instead of injecting into the app closure +// /// The app closure needs to be static and +// #[derive(Clone)] +// struct ServerState { +// serv_path: PathBuf, +// is_err: ErrStatus, +// } -type ErrStatus = Arc; +// type ErrStatus = Arc; -impl ServerState { - fn new(serv_path: PathBuf, is_err: ErrStatus) -> Self { - Self { serv_path, is_err } - } -} +// impl ServerState { +// fn new(serv_path: PathBuf, is_err: ErrStatus) -> Self { +// Self { serv_path, is_err } +// } +// } diff --git a/src/cli/serve/mod.rs b/src/cli/serve/mod.rs index 9695870c0..36c050bd3 100644 --- a/src/cli/serve/mod.rs +++ b/src/cli/serve/mod.rs @@ -1,9 +1,36 @@ -use crate::cfg::ConfigOptsBuild; +use crate::{cfg::ConfigOptsServe, server}; use anyhow::Result; use std::path::PathBuf; +use std::io::Write; use structopt::StructOpt; -/// Build the Rust WASM app and all of its assets. +mod develop; + +/// Run the WASM project on dev-server #[derive(Clone, Debug, StructOpt)] #[structopt(name = "serve")] -pub struct Serve {} +pub struct Serve { + #[structopt(flatten)] + pub serve: ConfigOptsServe, +} + +impl Serve { + pub async fn serve(self) -> anyhow::Result<()> { + + let mut crate_config = crate::CrateConfig::new()?; + // change the relase state. + crate_config.with_release(self.serve.release); + + crate::builder::build(&crate_config).expect("build failed"); + + let serve_html = String::from(include_str!("../../server/serve.html")); + + let mut file = std::fs::File::create(crate_config.out_dir.join("index.html"))?; + file.write_all(serve_html.as_bytes())?; + + // start the develop server + server::startup(crate_config.clone()).await?; + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index ea8393484..d4d3d7953 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod builder; +pub mod server; pub use builder::*; pub mod cargo; @@ -16,4 +17,4 @@ pub use error::*; pub mod logging; pub use logging::*; -pub mod watch; \ No newline at end of file +pub mod watch; diff --git a/src/main.rs b/src/main.rs index 2ec44eb97..a58794783 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,30 +3,28 @@ use structopt::StructOpt; #[tokio::main] async fn main() -> Result<()> { - let args = Cli::from_args(); set_up_logging(); match args.action { Commands::Translate(opts) => { opts.translate(); - } - + } + Commands::Build(opts) => { opts.build(); } - // Commands::Clean(_) => { - // // - // } + // Commands::Clean(_) => { + // // + // } - // Commands::Config(_) => { - // // - // } - - Commands::Serve(_) => { - // - } + // Commands::Config(_) => { + // // + // } + Commands::Serve(opts) => { + opts.serve().await; + } } Ok(()) diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 000000000..2e937dbb8 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,87 @@ +use axum::{ + extract::{ + ws::{Message, WebSocket}, + Extension, TypedHeader, WebSocketUpgrade, + }, + http::StatusCode, + response::IntoResponse, + routing::{get, get_service}, + AddExtensionLayer, Router, +}; +use notify::{watcher, Watcher, DebouncedEvent}; +use std::sync::{mpsc::channel, Arc, Mutex}; +use std::time::Duration; +use tower_http::services::ServeDir; + +use crate::{CrateConfig, builder}; + +struct WsRelodState { + update: bool, +} + +impl WsRelodState { fn change(&mut self) { self.update = !self.update } } + +pub async fn startup(config: CrateConfig) -> anyhow::Result<()> { + + log::info!("Starting development server 🚀"); + + let (tx, rx) = channel(); + + let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap(); + watcher.watch(config.crate_dir.join("src").clone(), notify::RecursiveMode::Recursive).unwrap(); + + let ws_reload_state = Arc::new(Mutex::new(WsRelodState { update: false })); + + let watcher_conf = config.clone(); + let watcher_ws_state = ws_reload_state.clone(); + tokio::spawn(async move { + loop { + if let Ok(v) = rx.recv() { + match v { + DebouncedEvent::Create(_) | DebouncedEvent::Write(_) | + DebouncedEvent::Remove(_) | DebouncedEvent::Rename(_, _) => { + builder::build(&watcher_conf).unwrap(); + watcher_ws_state.lock().unwrap().change(); + }, + _ => {} + } + } + } + }); + + let app = Router::new() + .route("/ws", get(ws_handler)) + .fallback( + get_service(ServeDir::new(config.out_dir)).handle_error( + |error: std::io::Error| async move { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Unhandled internal error: {}", error), + ) + }, + ), + ) + .layer(AddExtensionLayer::new(ws_reload_state.clone())); + + let port = "8080"; + axum::Server::bind(&format!("0.0.0.0:{}", port).parse().unwrap()) + .serve(app.into_make_service()) + .await?; + + Ok(()) +} + +async fn ws_handler( + ws: WebSocketUpgrade, + _: Option>, + Extension(state): Extension>>, +) -> impl IntoResponse { + ws.on_upgrade(|mut socket| async move { + loop { + if state.lock().unwrap().update { + socket.send(Message::Text(String::from("reload"))).await.unwrap(); + state.lock().unwrap().change(); + } + } + }) +} diff --git a/src/server/serve.html b/src/server/serve.html new file mode 100644 index 000000000..98997403b --- /dev/null +++ b/src/server/serve.html @@ -0,0 +1,28 @@ + + + + + Dioxus-CLI Dev Server + + +
+ + + + + + +