feat: add dev-server

This commit is contained in:
mrxiaozhuox committed 2022-01-22 21:23:30 +08:00
1 parent 281a075c6c
commit 5e710f6693
11 files changed
+307 -148

No files matched your search

+5 -2
View File
@@ -21,8 +21,6 @@ cargo_toml = "0.10.0"
futures = "0.3.12" futures = "0.3.12"
notify = "4.0.17" notify = "4.0.17"
html_parser = "0.6.2" html_parser = "0.6.2"
tui = { version = "0.16.0", features = ["crossterm"] }
crossterm = "0.22.1"
binary-install = "0.0.2" binary-install = "0.0.2"
convert_case = "0.5.0" convert_case = "0.5.0"
structopt = "0.3.25" structopt = "0.3.25"
@@ -30,6 +28,11 @@ cargo_metadata = "0.14.1"
tokio = { version = "1.15.0", features = ["full"] } tokio = { version = "1.15.0", features = ["full"] }
atty = "0.2.14" 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]] [[bin]]
path = "src/main.rs" path = "src/main.rs"
name = "dioxus" name = "dioxus"
+4 -7
View File
@@ -37,7 +37,6 @@ pub fn build(config: &CrateConfig) -> Result<()> {
.arg("wasm32-unknown-unknown") .arg("wasm32-unknown-unknown")
.stdout(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped()); .stderr(std::process::Stdio::piped());
;
if config.release { if config.release {
cmd.arg("--release"); cmd.arg("--release");
@@ -54,7 +53,7 @@ pub fn build(config: &CrateConfig) -> Result<()> {
let output = child.wait()?; let output = child.wait()?;
if output.success() { if output.success() {
log::info!("Build complete! {:?}", reason); log::info!("Build complete!");
} else { } else {
log::error!("Build failed!"); log::error!("Build failed!");
let mut reason = String::new(); let mut reason = String::new();
@@ -63,7 +62,7 @@ pub fn build(config: &CrateConfig) -> Result<()> {
} }
// [2] Establish the output directory structure // [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 // [3] Bindgen the final binary for use easy linking
let mut bindgen_builder = Bindgen::new(); 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 // [5] Generate the html file with the module name
// TODO: support names via options // TODO: support names via options
log::info!("Writing to '{:#?}' directory...", out_dir); 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(); let copy_options = fs_extra::dir::CopyOptions::new();
if static_dir.is_dir() { if static_dir.is_dir() {
@@ -118,7 +115,7 @@ pub fn build(config: &CrateConfig) -> Result<()> {
Ok(()) Ok(())
} }
fn gen_page(module: &str) -> String { pub fn gen_page(module: &str) -> String {
format!( format!(
r#" r#"
<html> <html>
@@ -132,7 +129,7 @@ fn gen_page(module: &str) -> String {
<!-- Note the usage of `type=module` here as this is an ES6 module --> <!-- Note the usage of `type=module` here as this is an ES6 module -->
<script type="module"> <script type="module">
import init from "{}"; import init from "{}";
init("./wasm/module_bg.wasm"); init("./assets/module_bg.wasm");
</script> </script>
<div id="dioxusroot"> </div> <div id="dioxusroot"> </div>
</body> </body>
+2 -3
View File
@@ -13,8 +13,7 @@ pub struct Build {
impl Build { impl Build {
pub fn build(self) -> anyhow::Result<()> { pub fn build(self) -> anyhow::Result<()> {
let mut crate_config = crate::CrateConfig::new()?;
let mut crate_config = crate::CrateConfig::new()?;
// change the relase state. // change the relase state.
crate_config.with_release(self.build.release); crate_config.with_release(self.build.release);
@@ -23,4 +22,4 @@ impl Build {
Ok(()) Ok(())
} }
} }
+20
View File
@@ -67,6 +67,26 @@ pub struct ConfigOptsBuild {
pub pattern_params: Option<HashMap<String, String>>, pub pattern_params: Option<HashMap<String, String>>,
} }
#[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<PathBuf>,
/// 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<PathBuf>,
/// The public URL from which assets are to be served [default: /]
#[structopt(long, parse(from_str=parse_public_url))]
pub public_url: Option<String>,
}
/// Ensure the given value for `--public-url` is formatted correctly. /// Ensure the given value for `--public-url` is formatted correctly.
pub fn parse_public_url(val: &str) -> String { pub fn parse_public_url(val: &str) -> String {
let prefix = if !val.starts_with('/') { "/" } else { "" }; let prefix = if !val.starts_with('/') { "/" } else { "" };
-1
View File
@@ -35,7 +35,6 @@ pub enum Commands {
Translate(translate::Translate), Translate(translate::Translate),
// /// Build, watch & serve the Rust WASM app and all of its assets. // /// Build, watch & serve the Rust WASM app and all of its assets.
Serve(serve::Serve), Serve(serve::Serve),
// /// Clean output artifacts. // /// Clean output artifacts.
// Clean(clean::Clean), // Clean(clean::Clean),
+118 -118
View File
@@ -1,145 +1,145 @@
use crate::{cli::DevelopOptions, config::CrateConfig, error::Result}; // use crate::{cli::DevelopOptions, config::CrateConfig, error::Result};
use async_std::prelude::FutureExt; // use async_std::prelude::FutureExt;
use log::info; // use log::info;
use notify::{RecommendedWatcher, RecursiveMode, Watcher}; // use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use std::path::PathBuf; // use std::path::PathBuf;
use std::sync::atomic::AtomicBool; // use std::sync::atomic::AtomicBool;
use std::sync::Arc; // use std::sync::Arc;
use tide::http::mime::HTML; // use tide::http::mime::HTML;
use tide::http::Mime; // use tide::http::Mime;
pub struct DevelopState { // pub struct DevelopState {
// // //
reload_on_change: bool, // reload_on_change: bool,
} // }
pub async fn develop(options: DevelopOptions) -> Result<()> { // pub async fn develop(options: DevelopOptions) -> Result<()> {
// // //
log::info!("Starting development server 🚀"); // log::info!("Starting development server 🚀");
let mut cfg = CrateConfig::new()?; // let mut cfg = CrateConfig::new()?;
cfg.with_develop_options(&options); // 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 // // Spawn the server onto a seperate task
// This lets the task progress while we handle file updates // // This lets the task progress while we handle file updates
let server = async_std::task::spawn(launch_server(out_dir, is_err.clone())); // 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())); // let watcher = async_std::task::spawn(watch_directory(cfg.clone(), is_err.clone()));
match server.race(watcher).await { // match server.race(watcher).await {
Err(e) => log::warn!("Error running development server, {:?}", e), // Err(e) => log::warn!("Error running development server, {:?}", e),
_ => {} // _ => {}
} // }
Ok(()) // Ok(())
} // }
async fn watch_directory(config: CrateConfig, is_err: ErrStatus) -> Result<()> { // async fn watch_directory(config: CrateConfig, is_err: ErrStatus) -> Result<()> {
// Create a channel to receive the events. // // Create a channel to receive the events.
let (watcher_tx, watcher_rx) = async_std::channel::bounded(100); // let (watcher_tx, watcher_rx) = async_std::channel::bounded(100);
// Automatically select the best implementation for your platform. // // Automatically select the best implementation for your platform.
// You can also access each implementation directly e.g. INotifyWatcher. // // You can also access each implementation directly e.g. INotifyWatcher.
let mut watcher: RecommendedWatcher = Watcher::new(move |res| { // let mut watcher: RecommendedWatcher = Watcher::new(move |res| {
async_std::task::block_on(watcher_tx.send(res)); // async_std::task::block_on(watcher_tx.send(res));
// send an event // // send an event
let _ = async_std::task::block_on(watcher_tx.send(res)); // let _ = async_std::task::block_on(watcher_tx.send(res));
}) // })
.expect("failed to make watcher"); // .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 // // Add a path to be watched. All files and directories at that path and
// below will be monitored for changes. // // below will be monitored for changes.
watcher // watcher
.watch(&src_dir.join("src"), RecursiveMode::Recursive) // .watch(&src_dir.join("src"), RecursiveMode::Recursive)
.expect("Failed to watch dir"); // .expect("Failed to watch dir");
match watcher.watch(&src_dir.join("examples"), RecursiveMode::Recursive) { // match watcher.watch(&src_dir.join("examples"), RecursiveMode::Recursive) {
Ok(_) => {} // Ok(_) => {}
Err(e) => log::warn!("Failed to watch examples dir, {:?}", e), // Err(e) => log::warn!("Failed to watch examples dir, {:?}", e),
} // }
'run: loop { // 'run: loop {
match crate::builder::build(&config) { // match crate::builder::build(&config) {
Ok(_) => { // Ok(_) => {
is_err.store(false, std::sync::atomic::Ordering::Relaxed); // is_err.store(false, std::sync::atomic::Ordering::Relaxed);
async_std::task::sleep(std::time::Duration::from_millis(500)).await; // async_std::task::sleep(std::time::Duration::from_millis(500)).await;
} // }
Err(err) => is_err.store(true, std::sync::atomic::Ordering::Relaxed), // Err(err) => is_err.store(true, std::sync::atomic::Ordering::Relaxed),
}; // };
let mut msg = None; // let mut msg = None;
loop { // loop {
let new_msg = watcher_rx.recv().await.unwrap().unwrap(); // let new_msg = watcher_rx.recv().await.unwrap().unwrap();
if !watcher_rx.is_empty() { // if !watcher_rx.is_empty() {
msg = Some(new_msg); // msg = Some(new_msg);
break; // break;
} // }
} // }
info!("File updated, rebuilding app"); // info!("File updated, rebuilding app");
} // }
Ok(()) // Ok(())
} // }
async fn launch_server(outdir: PathBuf, is_err: ErrStatus) -> Result<()> { // async fn launch_server(outdir: PathBuf, is_err: ErrStatus) -> Result<()> {
let _crate_dir = crate::cargo::crate_root()?; // let _crate_dir = crate::cargo::crate_root()?;
let _workspace_dir = crate::cargo::workspace_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()); // let file_path = format!("{}/index.html", outdir.display());
log::info!("Serving {}", file_path); // log::info!("Serving {}", file_path);
let p = outdir.display().to_string(); // let p = outdir.display().to_string();
app.at("/") // app.at("/")
.get(|req: tide::Request<ServerState>| async move { // .get(|req: tide::Request<ServerState>| async move {
log::info!("Connected to development server"); // log::info!("Connected to development server");
let state = req.state(); // let state = req.state();
match state.is_err.load(std::sync::atomic::Ordering::Relaxed) { // match state.is_err.load(std::sync::atomic::Ordering::Relaxed) {
true => { // true => {
// // //
let mut resp = // let mut resp =
tide::Body::from_string(format!(include_str!("../err.html"), err = "_")); // tide::Body::from_string(format!(include_str!("../err.html"), err = "_"));
resp.set_mime(HTML); // resp.set_mime(HTML);
Ok(resp) // Ok(resp)
} // }
false => { // false => {
Ok(tide::Body::from_file(state.serv_path.clone().join("index.html")).await?) // Ok(tide::Body::from_file(state.serv_path.clone().join("index.html")).await?)
} // }
} // }
}) // })
.serve_dir(p)?; // .serve_dir(p)?;
// .serve_file(file_path) // // .serve_file(file_path)
// .unwrap(); // // .unwrap();
let port = "8080"; // let port = "8080";
let serve_addr = format!("127.0.0.1:{}", port); // let serve_addr = format!("127.0.0.1:{}", port);
info!("App available at http://{}/", serve_addr); // info!("App available at http://{}/", serve_addr);
app.listen(serve_addr).await?; // app.listen(serve_addr).await?;
Ok(()) // Ok(())
} // }
/// https://github.com/http-rs/tide/blob/main/examples/state.rs // /// https://github.com/http-rs/tide/blob/main/examples/state.rs
/// Tide seems to prefer using state instead of injecting into the app closure // /// Tide seems to prefer using state instead of injecting into the app closure
/// The app closure needs to be static and // /// The app closure needs to be static and
#[derive(Clone)] // #[derive(Clone)]
struct ServerState { // struct ServerState {
serv_path: PathBuf, // serv_path: PathBuf,
is_err: ErrStatus, // is_err: ErrStatus,
} // }
type ErrStatus = Arc<AtomicBool>; // type ErrStatus = Arc<AtomicBool>;
impl ServerState { // impl ServerState {
fn new(serv_path: PathBuf, is_err: ErrStatus) -> Self { // fn new(serv_path: PathBuf, is_err: ErrStatus) -> Self {
Self { serv_path, is_err } // Self { serv_path, is_err }
} // }
} // }
+30 -3
View File
@@ -1,9 +1,36 @@
use crate::cfg::ConfigOptsBuild; use crate::{cfg::ConfigOptsServe, server};
use anyhow::Result; use anyhow::Result;
use std::path::PathBuf; use std::path::PathBuf;
use std::io::Write;
use structopt::StructOpt; 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)] #[derive(Clone, Debug, StructOpt)]
#[structopt(name = "serve")] #[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(())
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
pub mod builder; pub mod builder;
pub mod server;
pub use builder::*; pub use builder::*;
pub mod cargo; pub mod cargo;
@@ -16,4 +17,4 @@ pub use error::*;
pub mod logging; pub mod logging;
pub use logging::*; pub use logging::*;
pub mod watch; pub mod watch;
+11 -13
View File
@@ -3,30 +3,28 @@ use structopt::StructOpt;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let args = Cli::from_args(); let args = Cli::from_args();
set_up_logging(); set_up_logging();
match args.action { match args.action {
Commands::Translate(opts) => { Commands::Translate(opts) => {
opts.translate(); opts.translate();
} }
Commands::Build(opts) => { Commands::Build(opts) => {
opts.build(); opts.build();
} }
// Commands::Clean(_) => { // Commands::Clean(_) => {
// // // //
// } // }
// Commands::Config(_) => { // Commands::Config(_) => {
// // // //
// } // }
Commands::Serve(opts) => {
Commands::Serve(_) => { opts.serve().await;
// }
}
} }
Ok(()) Ok(())
+87
View File
@@ -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<TypedHeader<headers::UserAgent>>,
Extension(state): Extension<Arc<Mutex<WsRelodState>>>,
) -> 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();
}
}
})
}
+28
View File
@@ -0,0 +1,28 @@
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta charset="UTF-8" />
<title>Dioxus-CLI Dev Server</title>
</head>
<body>
<div id="main"></div>
<!-- Note the usage of `type=module` here as this is an ES6 module -->
<script type="module">
import init from "./assets/module.js";
init("./assets/module_bg.wasm");
</script>
<script>
const socket = new WebSocket("ws://localhost:8080/ws");
socket.addEventListener("message", function (event) {
console.log(event);
if (event.data === "reload") {
window.location.reload();
}
});
</script>
</body>
</html>