From 0c5025ffa0c1a094fe33345e95b7a2694516297e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 18 Jul 2023 10:24:29 -0700 Subject: [PATCH 1/6] WIP desktop serve hot reload --- packages/cli/Cargo.toml | 2 + packages/cli/src/builder.rs | 7 +- packages/cli/src/cli/autoformat.rs | 8 +- packages/cli/src/cli/build.rs | 10 +- packages/cli/src/cli/cfg.rs | 21 +- packages/cli/src/cli/serve.rs | 44 +- packages/cli/src/config.rs | 6 +- packages/cli/src/server/desktop/mod.rs | 255 +++++++++ packages/cli/src/server/mod.rs | 498 ++---------------- packages/cli/src/server/output.rs | 54 +- .../cli/src/server/{ => web}/hot_reload.rs | 0 packages/cli/src/server/web/mod.rs | 475 +++++++++++++++++ packages/cli/src/server/{ => web}/proxy.rs | 0 13 files changed, 865 insertions(+), 515 deletions(-) create mode 100644 packages/cli/src/server/desktop/mod.rs rename packages/cli/src/server/{ => web}/hot_reload.rs (100%) create mode 100644 packages/cli/src/server/web/mod.rs rename packages/cli/src/server/{ => web}/proxy.rs (100%) diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index c947baa63..cc78a5d28 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -81,6 +81,8 @@ rsx-rosetta = { workspace = true } dioxus-rsx = { workspace = true } dioxus-html = { workspace = true, features = ["hot-reload-context"] } dioxus-core = { workspace = true, features = ["serialize"] } +dioxus-hot-reload = { workspace = true } +interprocess-docfix = { version = "1.2.2" } [features] default = [] diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 6f50fc49d..57794b728 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -244,7 +244,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { }) } -pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<()> { +pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result { log::info!("🚅 Running build [Desktop] command..."); let ignore_files = build_assets(config)?; @@ -365,7 +365,10 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<()> { ); } - Ok(()) + Ok(BuildResult { + warnings: vec![], + elapsed_time: 0, + }) } fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { diff --git a/packages/cli/src/cli/autoformat.rs b/packages/cli/src/cli/autoformat.rs index fbc34402b..ea2c27148 100644 --- a/packages/cli/src/cli/autoformat.rs +++ b/packages/cli/src/cli/autoformat.rs @@ -136,12 +136,16 @@ async fn autoformat_project(check: bool) -> Result<()> { } fn collect_rs_files(folder: &Path, files: &mut Vec) { - let Ok(folder) = folder.read_dir() else { return }; + let Ok(folder) = folder.read_dir() else { + return; + }; // load the gitignore for entry in folder { - let Ok(entry) = entry else { continue; }; + let Ok(entry) = entry else { + continue; + }; let path = entry.path(); diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 148a4ab76..29edf301b 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -1,3 +1,4 @@ +use crate::cfg::Platform; #[cfg(feature = "plugin")] use crate::plugin::PluginManager; @@ -42,16 +43,13 @@ impl Build { #[cfg(feature = "plugin")] let _ = PluginManager::on_build_start(&crate_config, &platform); - match platform.as_str() { - "web" => { + match platform { + Platform::Web => { crate::builder::build(&crate_config, false)?; } - "desktop" => { + Platform::Desktop => { crate::builder::build_desktop(&crate_config, false)?; } - _ => { - return custom_error!("Unsupported platform target."); - } } let temp = gen_page(&crate_config.dioxus_config, false); diff --git a/packages/cli/src/cli/cfg.rs b/packages/cli/src/cli/cfg.rs index 30ca89ce9..edc32f346 100644 --- a/packages/cli/src/cli/cfg.rs +++ b/packages/cli/src/cli/cfg.rs @@ -1,3 +1,6 @@ +use clap::ValueEnum; +use serde::Serialize; + use super::*; /// Config options for the build system. @@ -26,8 +29,8 @@ pub struct ConfigOptsBuild { pub profile: Option, /// Build platform: support Web & Desktop [default: "default_platform"] - #[clap(long)] - pub platform: Option, + #[clap(long, value_enum)] + pub platform: Option, /// Space separated list of features to activate #[clap(long)] @@ -69,8 +72,8 @@ pub struct ConfigOptsServe { pub profile: Option, /// Build platform: support Web & Desktop [default: "default_platform"] - #[clap(long)] - pub platform: Option, + #[clap(long, value_enum)] + pub platform: Option, /// Build with hot reloading rsx [default: false] #[clap(long)] @@ -88,6 +91,16 @@ pub struct ConfigOptsServe { pub features: Option>, } +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)] +pub enum Platform { + #[clap(name = "web")] + #[serde(rename = "web")] + Web, + #[clap(name = "desktop")] + #[serde(rename = "desktop")] + Desktop, +} + /// 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/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index e50eb0f38..89b9eaaa0 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -47,33 +47,33 @@ impl Serve { .clone() }); - if platform.as_str() == "desktop" { - crate::builder::build_desktop(&crate_config, true)?; + match platform { + cfg::Platform::Web => { + // generate dev-index page + Serve::regen_dev_page(&crate_config)?; - match &crate_config.executable { - crate::ExecutableType::Binary(name) - | crate::ExecutableType::Lib(name) - | crate::ExecutableType::Example(name) => { - let mut file = crate_config.out_dir.join(name); - if cfg!(windows) { - file.set_extension("exe"); + // start the develop server + server::web::startup(self.serve.port, crate_config.clone(), self.serve.open) + .await?; + } + cfg::Platform::Desktop => { + crate::builder::build_desktop(&crate_config, true)?; + + match &crate_config.executable { + crate::ExecutableType::Binary(name) + | crate::ExecutableType::Lib(name) + | crate::ExecutableType::Example(name) => { + let mut file = crate_config.out_dir.join(name); + if cfg!(windows) { + file.set_extension("exe"); + } + Command::new(file.to_str().unwrap()) + .stdout(Stdio::inherit()) + .output()?; } - Command::new(file.to_str().unwrap()) - .stdout(Stdio::inherit()) - .output()?; } } - return Ok(()); - } else if platform != "web" { - return custom_error!("Unsupported platform target."); } - - // generate dev-index page - Serve::regen_dev_page(&crate_config)?; - - // start the develop server - server::startup(self.serve.port, crate_config.clone(), self.serve.open).await?; - Ok(()) } diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 05aca0d35..e46b4f0fe 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -1,4 +1,4 @@ -use crate::error::Result; +use crate::{cfg::Platform, error::Result}; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, @@ -73,7 +73,7 @@ impl Default for DioxusConfig { Self { application: ApplicationConfig { name: "dioxus".into(), - default_platform: "web".to_string(), + default_platform: Platform::Web, out_dir: Some(PathBuf::from("dist")), asset_dir: Some(PathBuf::from("public")), @@ -115,7 +115,7 @@ impl Default for DioxusConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApplicationConfig { pub name: String, - pub default_platform: String, + pub default_platform: Platform, pub out_dir: Option, pub asset_dir: Option, diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs new file mode 100644 index 000000000..0a0c0f35f --- /dev/null +++ b/packages/cli/src/server/desktop/mod.rs @@ -0,0 +1,255 @@ +use crate::{ + builder, + serve::Serve, + server::{ + output::{print_console_info, PrettierOptions, WebServerInfo}, + setup_file_watcher, setup_file_watcher_hot_reload, BuildManager, + }, + BuildResult, CrateConfig, Result, +}; +use axum::{ + body::{Full, HttpBody}, + extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade}, + http::{ + header::{HeaderName, HeaderValue}, + Method, Response, StatusCode, + }, + response::IntoResponse, + routing::{get, get_service}, + Router, +}; +use axum_server::tls_rustls::RustlsConfig; +use cargo_metadata::diagnostic::Diagnostic; +use dioxus_core::Template; +use dioxus_hot_reload::HotReloadMsg; +use dioxus_html::HtmlCtx; +use dioxus_rsx::hot_reload::*; +use interprocess_docfix::local_socket::LocalSocketListener; +use notify::{RecommendedWatcher, Watcher}; +use std::{ + net::UdpSocket, + path::PathBuf, + process::{Command, Stdio}, + sync::{Arc, Mutex}, +}; +use tokio::sync::broadcast::{self, Sender}; +use tower::ServiceBuilder; +use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody}; +use tower_http::{ + cors::{Any, CorsLayer}, + ServiceBuilderExt, +}; + +#[cfg(feature = "plugin")] +use plugin::PluginManager; + +struct WsReloadState { + update: broadcast::Sender<()>, +} + +pub async fn startup(config: CrateConfig) -> Result<()> { + // ctrl-c shutdown checker + let _crate_config = config.clone(); + let _ = ctrlc::set_handler(move || { + #[cfg(feature = "plugin")] + let _ = PluginManager::on_serve_shutdown(&_crate_config); + std::process::exit(0); + }); + + match config.hot_reload { + true => serve_hot_reload(config).await?, + false => serve_default(config).await?, + } + + Ok(()) +} + +/// Start the server without hot reload +pub async fn serve_default(config: CrateConfig) -> Result<()> { + let first_build_result = start_desktop(&config)?; + + log::info!("🚀 Starting development server..."); + + // We got to own watcher so that it exists for the duration of serve + // Otherwise full reload won't work. + let _watcher = setup_file_watcher(crate::cfg::Platform::Desktop, &config, None, None).await?; + + // Print serve info + print_console_info( + &config, + PrettierOptions { + changed: vec![], + warnings: first_build_result.warnings, + elapsed_time: first_build_result.elapsed_time, + }, + None, + ); + + Ok(()) +} + +/// Start the server without hot reload + +/// Start dx serve with hot reload +pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> { + let first_build_result = start_desktop(&config)?; + + println!("🚀 Starting development server..."); + + // Setup hot reload + let FileMapBuildResult { map, errors } = + FileMap::::create(config.crate_dir.clone()).unwrap(); + + println!("🚀 Starting development server..."); + + for err in errors { + log::error!("{}", err); + } + + let file_map = Arc::new(Mutex::new(map)); + let build_manager = Arc::new(BuildManager { + platform: crate::cfg::Platform::Web, + config: config.clone(), + reload_tx: None, + }); + + let (hot_reload_tx, mut hot_reload_rx) = broadcast::channel(100); + + // States + // Setup file watcher + // We got to own watcher so that it exists for the duration of serve + // Otherwise hot reload won't work. + let _watcher = setup_file_watcher_hot_reload( + &config, + hot_reload_tx, + file_map.clone(), + build_manager, + None, + ) + .await?; + + // Print serve info + print_console_info( + &config, + PrettierOptions { + changed: vec![], + warnings: first_build_result.warnings, + elapsed_time: first_build_result.elapsed_time, + }, + None, + ); + + #[cfg(target_os = "macos")] + { + // On unix, if you force quit the application, it can leave the file socket open + // This will cause the local socket listener to fail to open + // We check if the file socket is already open from an old session and then delete it + let paths = ["./dioxusin", "./@dioxusin"]; + for path in paths { + let path = PathBuf::from(path); + if path.exists() { + let _ = std::fs::remove_file(path); + } + } + } + + let channels = Arc::new(Mutex::new(Vec::new())); + + match LocalSocketListener::bind("@dioxusin") { + Ok(local_socket_stream) => { + let aborted = Arc::new(Mutex::new(false)); + + // listen for connections + std::thread::spawn({ + let file_map = file_map.clone(); + let channels = channels.clone(); + let aborted = aborted.clone(); + let _ = local_socket_stream.set_nonblocking(true); + move || { + loop { + if let Ok(mut connection) = local_socket_stream.accept() { + // send any templates than have changed before the socket connected + let templates: Vec<_> = { + file_map + .lock() + .unwrap() + .map + .values() + .filter_map(|(_, template_slot)| *template_slot) + .collect() + }; + for template in templates { + if !send_msg( + HotReloadMsg::UpdateTemplate(template), + &mut connection, + ) { + continue; + } + } + channels.lock().unwrap().push(connection); + println!("Connected to hot reloading 🚀"); + } + if *aborted.lock().unwrap() { + break; + } + } + } + }); + + // for channel in &mut *channels.lock().unwrap() { + // send_msg(HotReloadMsg::Shutdown, channel); + // } + + while let Ok(template) = hot_reload_rx.recv().await { + let channels = &mut *channels.lock().unwrap(); + let mut i = 0; + while i < channels.len() { + let channel = &mut channels[i]; + if send_msg(HotReloadMsg::UpdateTemplate(template), channel) { + i += 1; + } else { + channels.remove(i); + } + } + } + } + Err(error) => println!("failed to connect to hot reloading\n{error}"), + } + + Ok(()) +} + +fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool { + if let Ok(msg) = serde_json::to_string(&msg) { + if channel.write_all(msg.as_bytes()).is_err() { + return false; + } + if channel.write_all(&[b'\n']).is_err() { + return false; + } + true + } else { + false + } +} + +pub fn start_desktop(config: &CrateConfig) -> Result { + // Run the desktop application + let result = crate::builder::build_desktop(config, true)?; + + match &config.executable { + crate::ExecutableType::Binary(name) + | crate::ExecutableType::Lib(name) + | crate::ExecutableType::Example(name) => { + let mut file = config.out_dir.join(name); + if cfg!(windows) { + file.set_extension("exe"); + } + Command::new(file.to_str().unwrap()) + .stdout(Stdio::inherit()) + .spawn()?; + } + } + + Ok(result) +} diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index 3bd434005..389f5000b 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -1,4 +1,7 @@ -use crate::{builder, serve::Serve, BuildResult, CrateConfig, Result}; +use crate::{ + builder, cfg::Platform, serve::Serve, server::desktop::start_desktop, BuildResult, CrateConfig, + Result, +}; use axum::{ body::{Full, HttpBody}, extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade}, @@ -19,7 +22,7 @@ use notify::{RecommendedWatcher, Watcher}; use std::{ net::UdpSocket, path::PathBuf, - process::Command, + process::{Command, Stdio}, sync::{Arc, Mutex}, }; use tokio::sync::broadcast::{self, Sender}; @@ -30,416 +33,20 @@ use tower_http::{ ServiceBuilderExt, }; -#[cfg(feature = "plugin")] -use plugin::PluginManager; - -mod proxy; - -mod hot_reload; -use hot_reload::*; - mod output; use output::*; - -pub struct BuildManager { - config: CrateConfig, - reload_tx: broadcast::Sender<()>, -} - -impl BuildManager { - fn rebuild(&self) -> Result { - log::info!("🪁 Rebuild project"); - let result = builder::build(&self.config, true)?; - // change the websocket reload state to true; - // the page will auto-reload. - if self - .config - .dioxus_config - .web - .watcher - .reload_html - .unwrap_or(false) - { - let _ = Serve::regen_dev_page(&self.config); - } - let _ = self.reload_tx.send(()); - Ok(result) - } -} - -struct WsReloadState { - update: broadcast::Sender<()>, -} - -pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> { - // ctrl-c shutdown checker - let _crate_config = config.clone(); - let _ = ctrlc::set_handler(move || { - #[cfg(feature = "plugin")] - let _ = PluginManager::on_serve_shutdown(&_crate_config); - std::process::exit(0); - }); - - let ip = get_ip().unwrap_or(String::from("0.0.0.0")); - - match config.hot_reload { - true => serve_hot_reload(ip, port, config, start_browser).await?, - false => serve_default(ip, port, config, start_browser).await?, - } - - Ok(()) -} - -/// Start the server without hot reload -pub async fn serve_default( - ip: String, - port: u16, - config: CrateConfig, - start_browser: bool, -) -> Result<()> { - let first_build_result = crate::builder::build(&config, false)?; - - log::info!("🚀 Starting development server..."); - - // WS Reload Watching - let (reload_tx, _) = broadcast::channel(100); - - // We got to own watcher so that it exists for the duration of serve - // Otherwise full reload won't work. - let _watcher = setup_file_watcher(&config, port, ip.clone(), reload_tx.clone()).await?; - - let ws_reload_state = Arc::new(WsReloadState { - update: reload_tx.clone(), - }); - - // HTTPS - // Before console info so it can stop if mkcert isn't installed or fails - let rustls_config = get_rustls(&config).await?; - - // Print serve info - print_console_info( - &ip, - port, - &config, - PrettierOptions { - changed: vec![], - warnings: first_build_result.warnings, - elapsed_time: first_build_result.elapsed_time, - }, - ); - - // Router - let router = setup_router(config, ws_reload_state, None).await?; - - // Start server - start_server(port, router, start_browser, rustls_config).await?; - - Ok(()) -} - -/// Start dx serve with hot reload -pub async fn serve_hot_reload( - ip: String, - port: u16, - config: CrateConfig, - start_browser: bool, -) -> Result<()> { - let first_build_result = crate::builder::build(&config, false)?; - - log::info!("🚀 Starting development server..."); - - // Setup hot reload - let (reload_tx, _) = broadcast::channel(100); - let FileMapBuildResult { map, errors } = - FileMap::::create(config.crate_dir.clone()).unwrap(); - - for err in errors { - log::error!("{}", err); - } - - let file_map = Arc::new(Mutex::new(map)); - let build_manager = Arc::new(BuildManager { - config: config.clone(), - reload_tx: reload_tx.clone(), - }); - - let hot_reload_tx = broadcast::channel(100).0; - - // States - let hot_reload_state = Arc::new(HotReloadState { - messages: hot_reload_tx.clone(), - build_manager: build_manager.clone(), - file_map: file_map.clone(), - watcher_config: config.clone(), - }); - - let ws_reload_state = Arc::new(WsReloadState { - update: reload_tx.clone(), - }); - - // Setup file watcher - // We got to own watcher so that it exists for the duration of serve - // Otherwise hot reload won't work. - let _watcher = setup_file_watcher_hot_reload( - &config, - port, - ip.clone(), - hot_reload_tx, - file_map, - build_manager, - ) - .await?; - - // HTTPS - // Before console info so it can stop if mkcert isn't installed or fails - let rustls_config = get_rustls(&config).await?; - - // Print serve info - print_console_info( - &ip, - port, - &config, - PrettierOptions { - changed: vec![], - warnings: first_build_result.warnings, - elapsed_time: first_build_result.elapsed_time, - }, - ); - - // Router - let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?; - - // Start server - start_server(port, router, start_browser, rustls_config).await?; - - Ok(()) -} - -const DEFAULT_KEY_PATH: &str = "ssl/key.pem"; -const DEFAULT_CERT_PATH: &str = "ssl/cert.pem"; - -/// Returns an enum of rustls config and a bool if mkcert isn't installed -async fn get_rustls(config: &CrateConfig) -> Result> { - let web_config = &config.dioxus_config.web.https; - if web_config.enabled != Some(true) { - return Ok(None); - } - - let (cert_path, key_path) = match web_config.mkcert { - // mkcert, use it - Some(true) => { - // Get paths to store certs, otherwise use ssl/item.pem - let key_path = web_config - .key_path - .clone() - .unwrap_or(DEFAULT_KEY_PATH.to_string()); - - let cert_path = web_config - .cert_path - .clone() - .unwrap_or(DEFAULT_CERT_PATH.to_string()); - - // Create ssl directory if using defaults - if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH { - _ = fs::create_dir("ssl"); - } - - let cmd = Command::new("mkcert") - .args([ - "-install", - "-key-file", - &key_path, - "-cert-file", - &cert_path, - "localhost", - "::1", - "127.0.0.1", - ]) - .spawn(); - - match cmd { - Err(e) => { - match e.kind() { - io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."), - e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()), - }; - return Err("failed to generate mkcert certificates".into()); - } - Ok(mut cmd) => { - cmd.wait()?; - } - } - - (cert_path, key_path) - } - // not mkcert - Some(false) => { - // get paths to cert & key - if let (Some(key), Some(cert)) = - (web_config.key_path.clone(), web_config.cert_path.clone()) - { - (cert, key) - } else { - // missing cert or key - return Err("https is enabled but cert or key path is missing".into()); - } - } - // other - _ => return Ok(None), - }; - - Ok(Some( - RustlsConfig::from_pem_file(cert_path, key_path).await?, - )) -} - -/// Sets up and returns a router -async fn setup_router( - config: CrateConfig, - ws_reload: Arc, - hot_reload: Option>, -) -> Result { - // Setup cors - let cors = CorsLayer::new() - // allow `GET` and `POST` when accessing the resource - .allow_methods([Method::GET, Method::POST]) - // allow requests from any origin - .allow_origin(Any) - .allow_headers(Any); - - let (coep, coop) = if config.cross_origin_policy { - ( - HeaderValue::from_static("require-corp"), - HeaderValue::from_static("same-origin"), - ) - } else { - ( - HeaderValue::from_static("unsafe-none"), - HeaderValue::from_static("unsafe-none"), - ) - }; - - // Create file service - let file_service_config = config.clone(); - let file_service = ServiceBuilder::new() - .override_response_header( - HeaderName::from_static("cross-origin-embedder-policy"), - coep, - ) - .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop) - .and_then( - move |response: Response| async move { - let response = if file_service_config - .dioxus_config - .web - .watcher - .index_on_404 - .unwrap_or(false) - && response.status() == StatusCode::NOT_FOUND - { - let body = Full::from( - // TODO: Cache/memoize this. - std::fs::read_to_string( - file_service_config - .crate_dir - .join(file_service_config.out_dir) - .join("index.html"), - ) - .ok() - .unwrap(), - ) - .map_err(|err| match err {}) - .boxed(); - Response::builder() - .status(StatusCode::OK) - .body(body) - .unwrap() - } else { - response.map(|body| body.boxed()) - }; - Ok(response) - }, - ) - .service(ServeDir::new(config.crate_dir.join(&config.out_dir))); - - // Setup websocket - let mut router = Router::new().route("/_dioxus/ws", get(ws_handler)); - - // Setup proxy - for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() { - router = proxy::add_proxy(router, &proxy_config)?; - } - - // Route file service - router = router.fallback(get_service(file_service).handle_error( - |error: std::io::Error| async move { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Unhandled internal error: {}", error), - ) - }, - )); - - // Setup routes - router = router - .route("/_dioxus/hot_reload", get(hot_reload_handler)) - .layer(cors) - .layer(Extension(ws_reload)); - - if let Some(hot_reload) = hot_reload { - router = router.layer(Extension(hot_reload)) - } - - Ok(router) -} - -/// Starts dx serve with no hot reload -async fn start_server( - port: u16, - router: Router, - start_browser: bool, - rustls: Option, -) -> Result<()> { - // If plugins, call on_serve_start event - #[cfg(feature = "plugin")] - PluginManager::on_serve_start(&config)?; - - // Parse address - let addr = format!("0.0.0.0:{}", port).parse().unwrap(); - - // Open the browser - if start_browser { - match rustls { - Some(_) => _ = open::that(format!("https://{}", addr)), - None => _ = open::that(format!("http://{}", addr)), - } - } - - // Start the server with or without rustls - match rustls { - Some(rustls) => { - axum_server::bind_rustls(addr, rustls) - .serve(router.into_make_service()) - .await? - } - None => { - axum::Server::bind(&addr) - .serve(router.into_make_service()) - .await? - } - } - - Ok(()) -} +pub mod desktop; +pub mod web; /// Sets up a file watcher async fn setup_file_watcher( + platform: Platform, config: &CrateConfig, - port: u16, - watcher_ip: String, - reload_tx: Sender<()>, + reload_tx: Option>, + web_info: Option, ) -> Result { let build_manager = BuildManager { + platform, config: config.clone(), reload_tx, }; @@ -466,14 +73,13 @@ async fn setup_file_watcher( #[allow(clippy::redundant_clone)] print_console_info( - &watcher_ip, - port, &config, PrettierOptions { changed: e.paths.clone(), warnings: res.warnings, elapsed_time: res.elapsed_time, }, + web_info.clone(), ); #[cfg(feature = "plugin")] @@ -504,11 +110,10 @@ async fn setup_file_watcher( /// Sets up a file watcher with hot reload async fn setup_file_watcher_hot_reload( config: &CrateConfig, - port: u16, - watcher_ip: String, hot_reload_tx: Sender>, file_map: Arc>>, build_manager: Arc, + web_info: Option, ) -> Result { // file watcher: check file change let allow_watch_path = config @@ -536,14 +141,13 @@ async fn setup_file_watcher_hot_reload( match build_manager.rebuild() { Ok(res) => { print_console_info( - &watcher_ip, - port, &config, PrettierOptions { changed: evt.paths, warnings: res.warnings, elapsed_time: res.elapsed_time, }, + web_info.clone(), ); } Err(err) => { @@ -563,14 +167,13 @@ async fn setup_file_watcher_hot_reload( match build_manager.rebuild() { Ok(res) => { print_console_info( - &watcher_ip, - port, &config, PrettierOptions { changed: evt.paths, warnings: res.warnings, elapsed_time: res.elapsed_time, }, + web_info.clone(), ); } Err(err) => { @@ -607,49 +210,34 @@ async fn setup_file_watcher_hot_reload( Ok(watcher) } -/// Get the network ip -fn get_ip() -> Option { - let socket = match UdpSocket::bind("0.0.0.0:0") { - Ok(s) => s, - Err(_) => return None, - }; +pub struct BuildManager { + platform: Platform, + config: CrateConfig, + reload_tx: Option>, +} - match socket.connect("8.8.8.8:80") { - Ok(()) => (), - Err(_) => return None, - }; - - match socket.local_addr() { - Ok(addr) => Some(addr.ip().to_string()), - Err(_) => None, +impl BuildManager { + fn rebuild(&self) -> Result { + log::info!("🪁 Rebuild project"); + match self.platform { + Platform::Web => { + let result = builder::build(&self.config, true)?; + // change the websocket reload state to true; + // the page will auto-reload. + if self + .config + .dioxus_config + .web + .watcher + .reload_html + .unwrap_or(false) + { + let _ = Serve::regen_dev_page(&self.config); + } + let _ = self.reload_tx.as_ref().map(|tx| tx.send(())); + Ok(result) + } + Platform::Desktop => start_desktop(&self.config), + } } } - -/// Handle websockets -async fn ws_handler( - ws: WebSocketUpgrade, - _: Option>, - Extension(state): Extension>, -) -> impl IntoResponse { - ws.on_upgrade(|mut socket| async move { - let mut rx = state.update.subscribe(); - let reload_watcher = tokio::spawn(async move { - loop { - rx.recv().await.unwrap(); - // ignore the error - if socket - .send(Message::Text(String::from("reload"))) - .await - .is_err() - { - break; - } - - // flush the errors after recompling - rx = rx.resubscribe(); - } - }); - - reload_watcher.await.unwrap(); - }) -} diff --git a/packages/cli/src/server/output.rs b/packages/cli/src/server/output.rs index 4c6e1f3b2..ce0f55064 100644 --- a/packages/cli/src/server/output.rs +++ b/packages/cli/src/server/output.rs @@ -11,7 +11,17 @@ pub struct PrettierOptions { pub elapsed_time: u128, } -pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: PrettierOptions) { +#[derive(Debug, Clone)] +pub struct WebServerInfo { + pub ip: String, + pub port: u16, +} + +pub fn print_console_info( + config: &CrateConfig, + options: PrettierOptions, + web_info: Option, +) { if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") { "cls" } else { @@ -70,26 +80,28 @@ pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: ); } - if config.dioxus_config.web.https.enabled == Some(true) { - println!( - "\t> Local : {}", - format!("https://localhost:{}/", port).blue() - ); - println!( - "\t> Network : {}", - format!("https://{}:{}/", ip, port).blue() - ); - println!("\t> HTTPS : {}", "Enabled".to_string().green()); - } else { - println!( - "\t> Local : {}", - format!("http://localhost:{}/", port).blue() - ); - println!( - "\t> Network : {}", - format!("http://{}:{}/", ip, port).blue() - ); - println!("\t> HTTPS : {}", "Disabled".to_string().red()); + if let Some(WebServerInfo { ip, port }) = web_info { + if config.dioxus_config.web.https.enabled == Some(true) { + println!( + "\t> Local : {}", + format!("https://localhost:{}/", port).blue() + ); + println!( + "\t> Network : {}", + format!("https://{}:{}/", ip, port).blue() + ); + println!("\t> HTTPS : {}", "Enabled".to_string().green()); + } else { + println!( + "\t> Local : {}", + format!("http://localhost:{}/", port).blue() + ); + println!( + "\t> Network : {}", + format!("http://{}:{}/", ip, port).blue() + ); + println!("\t> HTTPS : {}", "Disabled".to_string().red()); + } } println!(); println!("\t> Profile : {}", profile.green()); diff --git a/packages/cli/src/server/hot_reload.rs b/packages/cli/src/server/web/hot_reload.rs similarity index 100% rename from packages/cli/src/server/hot_reload.rs rename to packages/cli/src/server/web/hot_reload.rs diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs new file mode 100644 index 000000000..c7661f028 --- /dev/null +++ b/packages/cli/src/server/web/mod.rs @@ -0,0 +1,475 @@ +use crate::{ + builder, + serve::Serve, + server::{ + output::{print_console_info, PrettierOptions, WebServerInfo}, + setup_file_watcher, setup_file_watcher_hot_reload, BuildManager, + }, + BuildResult, CrateConfig, Result, +}; +use axum::{ + body::{Full, HttpBody}, + extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade}, + http::{ + header::{HeaderName, HeaderValue}, + Method, Response, StatusCode, + }, + response::IntoResponse, + routing::{get, get_service}, + Router, +}; +use axum_server::tls_rustls::RustlsConfig; +use cargo_metadata::diagnostic::Diagnostic; +use dioxus_core::Template; +use dioxus_html::HtmlCtx; +use dioxus_rsx::hot_reload::*; +use notify::{RecommendedWatcher, Watcher}; +use std::{ + net::UdpSocket, + path::PathBuf, + process::Command, + sync::{Arc, Mutex}, +}; +use tokio::sync::broadcast::{self, Sender}; +use tower::ServiceBuilder; +use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody}; +use tower_http::{ + cors::{Any, CorsLayer}, + ServiceBuilderExt, +}; + +#[cfg(feature = "plugin")] +use plugin::PluginManager; + +mod proxy; + +mod hot_reload; +use hot_reload::*; + +struct WsReloadState { + update: broadcast::Sender<()>, +} + +pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> { + // ctrl-c shutdown checker + let _crate_config = config.clone(); + let _ = ctrlc::set_handler(move || { + #[cfg(feature = "plugin")] + let _ = PluginManager::on_serve_shutdown(&_crate_config); + std::process::exit(0); + }); + + let ip = get_ip().unwrap_or(String::from("0.0.0.0")); + + match config.hot_reload { + true => serve_hot_reload(ip, port, config, start_browser).await?, + false => serve_default(ip, port, config, start_browser).await?, + } + + Ok(()) +} + +/// Start the server without hot reload +pub async fn serve_default( + ip: String, + port: u16, + config: CrateConfig, + start_browser: bool, +) -> Result<()> { + let first_build_result = crate::builder::build(&config, false)?; + + log::info!("🚀 Starting development server..."); + + // WS Reload Watching + let (reload_tx, _) = broadcast::channel(100); + + // We got to own watcher so that it exists for the duration of serve + // Otherwise full reload won't work. + let _watcher = setup_file_watcher( + crate::cfg::Platform::Web, + &config, + Some(reload_tx.clone()), + Some(WebServerInfo { + ip: ip.clone(), + port, + }), + ) + .await?; + + let ws_reload_state = Arc::new(WsReloadState { + update: reload_tx.clone(), + }); + + // HTTPS + // Before console info so it can stop if mkcert isn't installed or fails + let rustls_config = get_rustls(&config).await?; + + // Print serve info + print_console_info( + &config, + PrettierOptions { + changed: vec![], + warnings: first_build_result.warnings, + elapsed_time: first_build_result.elapsed_time, + }, + Some(crate::server::output::WebServerInfo { + ip: ip.clone(), + port, + }), + ); + + // Router + let router = setup_router(config, ws_reload_state, None).await?; + + // Start server + start_server(port, router, start_browser, rustls_config).await?; + + Ok(()) +} + +/// Start dx serve with hot reload +pub async fn serve_hot_reload( + ip: String, + port: u16, + config: CrateConfig, + start_browser: bool, +) -> Result<()> { + let first_build_result = crate::builder::build(&config, false)?; + + log::info!("🚀 Starting development server..."); + + // Setup hot reload + let (reload_tx, _) = broadcast::channel(100); + let FileMapBuildResult { map, errors } = + FileMap::::create(config.crate_dir.clone()).unwrap(); + + for err in errors { + log::error!("{}", err); + } + + let file_map = Arc::new(Mutex::new(map)); + let build_manager = Arc::new(BuildManager { + platform: crate::cfg::Platform::Web, + config: config.clone(), + reload_tx: Some(reload_tx.clone()), + }); + + let hot_reload_tx = broadcast::channel(100).0; + + // States + let hot_reload_state = Arc::new(HotReloadState { + messages: hot_reload_tx.clone(), + build_manager: build_manager.clone(), + file_map: file_map.clone(), + watcher_config: config.clone(), + }); + + let ws_reload_state = Arc::new(WsReloadState { + update: reload_tx.clone(), + }); + + // Setup file watcher + // We got to own watcher so that it exists for the duration of serve + // Otherwise hot reload won't work. + let _watcher = setup_file_watcher_hot_reload( + &config, + hot_reload_tx, + file_map, + build_manager, + Some(WebServerInfo { + ip: ip.clone(), + port, + }), + ) + .await?; + + // HTTPS + // Before console info so it can stop if mkcert isn't installed or fails + let rustls_config = get_rustls(&config).await?; + + // Print serve info + print_console_info( + &config, + PrettierOptions { + changed: vec![], + warnings: first_build_result.warnings, + elapsed_time: first_build_result.elapsed_time, + }, + Some(WebServerInfo { + ip: ip.clone(), + port, + }), + ); + + // Router + let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?; + + // Start server + start_server(port, router, start_browser, rustls_config).await?; + + Ok(()) +} + +const DEFAULT_KEY_PATH: &str = "ssl/key.pem"; +const DEFAULT_CERT_PATH: &str = "ssl/cert.pem"; + +/// Returns an enum of rustls config and a bool if mkcert isn't installed +async fn get_rustls(config: &CrateConfig) -> Result> { + let web_config = &config.dioxus_config.web.https; + if web_config.enabled != Some(true) { + return Ok(None); + } + + let (cert_path, key_path) = match web_config.mkcert { + // mkcert, use it + Some(true) => { + // Get paths to store certs, otherwise use ssl/item.pem + let key_path = web_config + .key_path + .clone() + .unwrap_or(DEFAULT_KEY_PATH.to_string()); + + let cert_path = web_config + .cert_path + .clone() + .unwrap_or(DEFAULT_CERT_PATH.to_string()); + + // Create ssl directory if using defaults + if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH { + _ = fs::create_dir("ssl"); + } + + let cmd = Command::new("mkcert") + .args([ + "-install", + "-key-file", + &key_path, + "-cert-file", + &cert_path, + "localhost", + "::1", + "127.0.0.1", + ]) + .spawn(); + + match cmd { + Err(e) => { + match e.kind() { + io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."), + e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()), + }; + return Err("failed to generate mkcert certificates".into()); + } + Ok(mut cmd) => { + cmd.wait()?; + } + } + + (cert_path, key_path) + } + // not mkcert + Some(false) => { + // get paths to cert & key + if let (Some(key), Some(cert)) = + (web_config.key_path.clone(), web_config.cert_path.clone()) + { + (cert, key) + } else { + // missing cert or key + return Err("https is enabled but cert or key path is missing".into()); + } + } + // other + _ => return Ok(None), + }; + + Ok(Some( + RustlsConfig::from_pem_file(cert_path, key_path).await?, + )) +} + +/// Sets up and returns a router +async fn setup_router( + config: CrateConfig, + ws_reload: Arc, + hot_reload: Option>, +) -> Result { + // Setup cors + let cors = CorsLayer::new() + // allow `GET` and `POST` when accessing the resource + .allow_methods([Method::GET, Method::POST]) + // allow requests from any origin + .allow_origin(Any) + .allow_headers(Any); + + let (coep, coop) = if config.cross_origin_policy { + ( + HeaderValue::from_static("require-corp"), + HeaderValue::from_static("same-origin"), + ) + } else { + ( + HeaderValue::from_static("unsafe-none"), + HeaderValue::from_static("unsafe-none"), + ) + }; + + // Create file service + let file_service_config = config.clone(); + let file_service = ServiceBuilder::new() + .override_response_header( + HeaderName::from_static("cross-origin-embedder-policy"), + coep, + ) + .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop) + .and_then( + move |response: Response| async move { + let response = if file_service_config + .dioxus_config + .web + .watcher + .index_on_404 + .unwrap_or(false) + && response.status() == StatusCode::NOT_FOUND + { + let body = Full::from( + // TODO: Cache/memoize this. + std::fs::read_to_string( + file_service_config + .crate_dir + .join(file_service_config.out_dir) + .join("index.html"), + ) + .ok() + .unwrap(), + ) + .map_err(|err| match err {}) + .boxed(); + Response::builder() + .status(StatusCode::OK) + .body(body) + .unwrap() + } else { + response.map(|body| body.boxed()) + }; + Ok(response) + }, + ) + .service(ServeDir::new(config.crate_dir.join(&config.out_dir))); + + // Setup websocket + let mut router = Router::new().route("/_dioxus/ws", get(ws_handler)); + + // Setup proxy + for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() { + router = proxy::add_proxy(router, &proxy_config)?; + } + + // Route file service + router = router.fallback(get_service(file_service).handle_error( + |error: std::io::Error| async move { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Unhandled internal error: {}", error), + ) + }, + )); + + // Setup routes + router = router + .route("/_dioxus/hot_reload", get(hot_reload_handler)) + .layer(cors) + .layer(Extension(ws_reload)); + + if let Some(hot_reload) = hot_reload { + router = router.layer(Extension(hot_reload)) + } + + Ok(router) +} + +/// Starts dx serve with no hot reload +async fn start_server( + port: u16, + router: Router, + start_browser: bool, + rustls: Option, +) -> Result<()> { + // If plugins, call on_serve_start event + #[cfg(feature = "plugin")] + PluginManager::on_serve_start(&config)?; + + // Parse address + let addr = format!("0.0.0.0:{}", port).parse().unwrap(); + + // Open the browser + if start_browser { + match rustls { + Some(_) => _ = open::that(format!("https://{}", addr)), + None => _ = open::that(format!("http://{}", addr)), + } + } + + // Start the server with or without rustls + match rustls { + Some(rustls) => { + axum_server::bind_rustls(addr, rustls) + .serve(router.into_make_service()) + .await? + } + None => { + axum::Server::bind(&addr) + .serve(router.into_make_service()) + .await? + } + } + + Ok(()) +} + +/// Get the network ip +fn get_ip() -> Option { + let socket = match UdpSocket::bind("0.0.0.0:0") { + Ok(s) => s, + Err(_) => return None, + }; + + match socket.connect("8.8.8.8:80") { + Ok(()) => (), + Err(_) => return None, + }; + + match socket.local_addr() { + Ok(addr) => Some(addr.ip().to_string()), + Err(_) => None, + } +} + +/// Handle websockets +async fn ws_handler( + ws: WebSocketUpgrade, + _: Option>, + Extension(state): Extension>, +) -> impl IntoResponse { + ws.on_upgrade(|mut socket| async move { + let mut rx = state.update.subscribe(); + let reload_watcher = tokio::spawn(async move { + loop { + rx.recv().await.unwrap(); + // ignore the error + if socket + .send(Message::Text(String::from("reload"))) + .await + .is_err() + { + break; + } + + // flush the errors after recompling + rx = rx.resubscribe(); + } + }); + + reload_watcher.await.unwrap(); + }) +} diff --git a/packages/cli/src/server/proxy.rs b/packages/cli/src/server/web/proxy.rs similarity index 100% rename from packages/cli/src/server/proxy.rs rename to packages/cli/src/server/web/proxy.rs From 2c1c147828e6a17bcd368021098c45aa3ece7541 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 18 Jul 2023 11:55:56 -0700 Subject: [PATCH 2/6] fix desktop hot reloading --- examples/simple_desktop.rs | 2 +- packages/cli/src/builder.rs | 1 + packages/cli/src/cli/serve.rs | 16 +----- packages/cli/src/server/desktop/mod.rs | 38 ++++++++----- packages/cli/src/server/mod.rs | 53 +++---------------- packages/cli/src/server/web/hot_reload.rs | 2 - packages/cli/src/server/web/mod.rs | 38 +++++++++---- .../fullstack/src/adapters/salvo_adapter.rs | 2 +- packages/html/src/events/form.rs | 7 ++- packages/rink/src/widgets/button.rs | 16 ++++-- packages/rink/src/widgets/checkbox.rs | 20 +++++-- packages/rink/src/widgets/input.rs | 4 +- packages/rink/src/widgets/slider.rs | 20 +++++-- packages/rink/src/widgets/text_like.rs | 24 ++++++--- packages/router/examples/simple_routes.rs | 2 +- 15 files changed, 130 insertions(+), 115 deletions(-) diff --git a/examples/simple_desktop.rs b/examples/simple_desktop.rs index 74bfb28c6..6c8bf8d72 100644 --- a/examples/simple_desktop.rs +++ b/examples/simple_desktop.rs @@ -48,7 +48,7 @@ fn BlogList(cx: Scope) -> Element { fn BlogPost(cx: Scope) -> Element { let Some(id) = use_route(cx).segment("id") else { - return cx.render(rsx! { div { "No blog post id" } }) + return cx.render(rsx! { div { "No blog post id" } }); }; log::debug!("rendering blog post {}", id); diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 57794b728..1cd6d787e 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -364,6 +364,7 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result { - crate::builder::build_desktop(&crate_config, true)?; - - match &crate_config.executable { - crate::ExecutableType::Binary(name) - | crate::ExecutableType::Lib(name) - | crate::ExecutableType::Example(name) => { - let mut file = crate_config.out_dir.join(name); - if cfg!(windows) { - file.set_extension("exe"); - } - Command::new(file.to_str().unwrap()) - .stdout(Stdio::inherit()) - .output()?; - } - } + server::desktop::startup(crate_config.clone()).await?; } } Ok(()) diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index 0a0c0f35f..6a2c57539 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -3,7 +3,7 @@ use crate::{ serve::Serve, server::{ output::{print_console_info, PrettierOptions, WebServerInfo}, - setup_file_watcher, setup_file_watcher_hot_reload, BuildManager, + setup_file_watcher, setup_file_watcher_hot_reload, }, BuildResult, CrateConfig, Result, }; @@ -72,7 +72,15 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> { // We got to own watcher so that it exists for the duration of serve // Otherwise full reload won't work. - let _watcher = setup_file_watcher(crate::cfg::Platform::Desktop, &config, None, None).await?; + let _watcher = setup_file_watcher( + { + let config = config.clone(); + move || start_desktop(&config) + }, + &config, + None, + ) + .await?; // Print serve info print_console_info( @@ -107,15 +115,13 @@ pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> { } let file_map = Arc::new(Mutex::new(map)); - let build_manager = Arc::new(BuildManager { - platform: crate::cfg::Platform::Web, - config: config.clone(), - reload_tx: None, - }); let (hot_reload_tx, mut hot_reload_rx) = broadcast::channel(100); // States + // The open interprocess sockets + let channels = Arc::new(Mutex::new(Vec::new())); + // Setup file watcher // We got to own watcher so that it exists for the duration of serve // Otherwise hot reload won't work. @@ -123,7 +129,17 @@ pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> { &config, hot_reload_tx, file_map.clone(), - build_manager, + { + let config = config.clone(); + + let channels = channels.clone(); + move || { + for channel in &mut *channels.lock().unwrap() { + send_msg(HotReloadMsg::Shutdown, channel); + } + start_desktop(&config) + } + }, None, ) .await?; @@ -153,8 +169,6 @@ pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> { } } - let channels = Arc::new(Mutex::new(Vec::new())); - match LocalSocketListener::bind("@dioxusin") { Ok(local_socket_stream) => { let aborted = Arc::new(Mutex::new(false)); @@ -196,10 +210,6 @@ pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> { } }); - // for channel in &mut *channels.lock().unwrap() { - // send_msg(HotReloadMsg::Shutdown, channel); - // } - while let Ok(template) = hot_reload_rx.recv().await { let channels = &mut *channels.lock().unwrap(); let mut i = 0; diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index 389f5000b..8c421f269 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -39,18 +39,11 @@ pub mod desktop; pub mod web; /// Sets up a file watcher -async fn setup_file_watcher( - platform: Platform, +async fn setup_file_watcher Result + Send + 'static>( + build_with: F, config: &CrateConfig, - reload_tx: Option>, web_info: Option, ) -> Result { - let build_manager = BuildManager { - platform, - config: config.clone(), - reload_tx, - }; - let mut last_update_time = chrono::Local::now().timestamp(); // file watcher: check file change @@ -67,7 +60,7 @@ async fn setup_file_watcher( let config = watcher_config.clone(); if let Ok(e) = info { if chrono::Local::now().timestamp() > last_update_time { - match build_manager.rebuild() { + match build_with() { Ok(res) => { last_update_time = chrono::Local::now().timestamp(); @@ -108,11 +101,11 @@ async fn setup_file_watcher( // Todo: reduce duplication and merge with setup_file_watcher() /// Sets up a file watcher with hot reload -async fn setup_file_watcher_hot_reload( +async fn setup_file_watcher_hot_reload Result + Send + 'static>( config: &CrateConfig, hot_reload_tx: Sender>, file_map: Arc>>, - build_manager: Arc, + build_with: F, web_info: Option, ) -> Result { // file watcher: check file change @@ -138,7 +131,7 @@ async fn setup_file_watcher_hot_reload( for path in evt.paths.clone() { // if this is not a rust file, rebuild the whole project if path.extension().and_then(|p| p.to_str()) != Some("rs") { - match build_manager.rebuild() { + match build_with() { Ok(res) => { print_console_info( &config, @@ -164,7 +157,7 @@ async fn setup_file_watcher_hot_reload( messages.extend(msgs); } Ok(UpdateResult::NeedsRebuild) => { - match build_manager.rebuild() { + match build_with() { Ok(res) => { print_console_info( &config, @@ -209,35 +202,3 @@ async fn setup_file_watcher_hot_reload( Ok(watcher) } - -pub struct BuildManager { - platform: Platform, - config: CrateConfig, - reload_tx: Option>, -} - -impl BuildManager { - fn rebuild(&self) -> Result { - log::info!("🪁 Rebuild project"); - match self.platform { - Platform::Web => { - let result = builder::build(&self.config, true)?; - // change the websocket reload state to true; - // the page will auto-reload. - if self - .config - .dioxus_config - .web - .watcher - .reload_html - .unwrap_or(false) - { - let _ = Serve::regen_dev_page(&self.config); - } - let _ = self.reload_tx.as_ref().map(|tx| tx.send(())); - Ok(result) - } - Platform::Desktop => start_desktop(&self.config), - } - } -} diff --git a/packages/cli/src/server/web/hot_reload.rs b/packages/cli/src/server/web/hot_reload.rs index 167e712af..49cce38d4 100644 --- a/packages/cli/src/server/web/hot_reload.rs +++ b/packages/cli/src/server/web/hot_reload.rs @@ -10,12 +10,10 @@ use dioxus_html::HtmlCtx; use dioxus_rsx::hot_reload::FileMap; use tokio::sync::broadcast; -use super::BuildManager; use crate::CrateConfig; pub struct HotReloadState { pub messages: broadcast::Sender>, - pub build_manager: Arc, pub file_map: Arc>>, pub watcher_config: CrateConfig, } diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index c7661f028..e6a429788 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -3,7 +3,7 @@ use crate::{ serve::Serve, server::{ output::{print_console_info, PrettierOptions, WebServerInfo}, - setup_file_watcher, setup_file_watcher_hot_reload, BuildManager, + setup_file_watcher, setup_file_watcher_hot_reload, }, BuildResult, CrateConfig, Result, }; @@ -86,9 +86,12 @@ pub async fn serve_default( // We got to own watcher so that it exists for the duration of serve // Otherwise full reload won't work. let _watcher = setup_file_watcher( - crate::cfg::Platform::Web, + { + let config = config.clone(); + let reload_tx = reload_tx.clone(); + move || build(&config, &reload_tx) + }, &config, - Some(reload_tx.clone()), Some(WebServerInfo { ip: ip.clone(), port, @@ -148,18 +151,12 @@ pub async fn serve_hot_reload( } let file_map = Arc::new(Mutex::new(map)); - let build_manager = Arc::new(BuildManager { - platform: crate::cfg::Platform::Web, - config: config.clone(), - reload_tx: Some(reload_tx.clone()), - }); let hot_reload_tx = broadcast::channel(100).0; // States let hot_reload_state = Arc::new(HotReloadState { messages: hot_reload_tx.clone(), - build_manager: build_manager.clone(), file_map: file_map.clone(), watcher_config: config.clone(), }); @@ -175,7 +172,11 @@ pub async fn serve_hot_reload( &config, hot_reload_tx, file_map, - build_manager, + { + let config = config.clone(); + let reload_tx = reload_tx.clone(); + move || build(&config, &reload_tx) + }, Some(WebServerInfo { ip: ip.clone(), port, @@ -473,3 +474,20 @@ async fn ws_handler( reload_watcher.await.unwrap(); }) } + +fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result { + let result = builder::build(&config, true)?; + // change the websocket reload state to true; + // the page will auto-reload. + if config + .dioxus_config + .web + .watcher + .reload_html + .unwrap_or(false) + { + let _ = Serve::regen_dev_page(&config); + } + let _ = reload_tx.send(()); + Ok(result) +} diff --git a/packages/fullstack/src/adapters/salvo_adapter.rs b/packages/fullstack/src/adapters/salvo_adapter.rs index 59b7c36ea..a31c1ea67 100644 --- a/packages/fullstack/src/adapters/salvo_adapter.rs +++ b/packages/fullstack/src/adapters/salvo_adapter.rs @@ -378,7 +378,7 @@ impl ServerFnHandler { .to_vec() .into(); let body = hyper::body::to_bytes(req.body_mut().unwrap()).await; - let Ok(body)=body else { + let Ok(body) = body else { handle_error(body.err().unwrap(), res); return; }; diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 113c765c5..1fe59a640 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -56,10 +56,9 @@ where { use serde::Deserialize; - let Ok(file_engine) = - SerializedFileEngine::deserialize(deserializer) else{ - return Ok(None); - }; + let Ok(file_engine) = SerializedFileEngine::deserialize(deserializer) else { + return Ok(None); + }; let file_engine = std::sync::Arc::new(file_engine); Ok(Some(file_engine)) diff --git a/packages/rink/src/widgets/button.rs b/packages/rink/src/widgets/button.rs index e4d3754f1..9018a3e9a 100644 --- a/packages/rink/src/widgets/button.rs +++ b/packages/rink/src/widgets/button.rs @@ -82,7 +82,9 @@ impl Button { fn write_value(&self, rdom: &mut RealDom) { if let Some(mut text) = rdom.get_mut(self.text_id) { let node_type = text.node_type_mut(); - let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Text(mut text) = node_type else { + panic!("input must be an element") + }; *text.text_mut() = self.value.clone(); } } @@ -111,7 +113,9 @@ impl CustomElement for Button { fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { let node_type = root.node_type(); - let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + let NodeType::Element(el) = &*node_type else { + panic!("input must be an element") + }; let value = el .attributes @@ -146,7 +150,9 @@ impl CustomElement for Button { AttributeMask::All => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; self.update_value_attr(&el); self.update_size_attr(&mut el); } @@ -155,7 +161,9 @@ impl CustomElement for Button { AttributeMask::Some(attrs) => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; if attrs.contains("width") || attrs.contains("height") { self.update_size_attr(&mut el); } diff --git a/packages/rink/src/widgets/checkbox.rs b/packages/rink/src/widgets/checkbox.rs index 24b3f6244..fab809ed2 100644 --- a/packages/rink/src/widgets/checkbox.rs +++ b/packages/rink/src/widgets/checkbox.rs @@ -94,14 +94,18 @@ impl CheckBox { fn write_value(&self, mut root: NodeMut) { let single_char = { let node_type = root.node_type_mut(); - let NodeTypeMut::Element( el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(el) = node_type else { + panic!("input must be an element") + }; Self::width(&el) == "1px" || Self::height(&el) == "1px" }; let rdom = root.real_dom_mut(); if let Some(mut text) = rdom.get_mut(self.text_id) { let node_type = text.node_type_mut(); - let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Text(mut text) = node_type else { + panic!("input must be an element") + }; let value = if single_char { if self.checked { "☑" @@ -156,7 +160,9 @@ impl CustomElement for CheckBox { fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { let node_type = root.node_type(); - let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + let NodeType::Element(el) = &*node_type else { + panic!("input must be an element") + }; let value = el .attributes @@ -197,7 +203,9 @@ impl CustomElement for CheckBox { AttributeMask::All => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; self.update_value_attr(&el); self.update_size_attr(&mut el); self.update_checked_attr(&el); @@ -207,7 +215,9 @@ impl CustomElement for CheckBox { AttributeMask::Some(attrs) => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; if attrs.contains("width") || attrs.contains("height") { self.update_size_attr(&mut el); } diff --git a/packages/rink/src/widgets/input.rs b/packages/rink/src/widgets/input.rs index c6a3e56b9..c1fb563d7 100644 --- a/packages/rink/src/widgets/input.rs +++ b/packages/rink/src/widgets/input.rs @@ -56,7 +56,9 @@ impl CustomElement for Input { } let node_type = root.node_type(); - let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + let NodeType::Element(el) = &*node_type else { + panic!("input must be an element") + }; let input_type = el .attributes .get(&OwnedAttributeDiscription { diff --git a/packages/rink/src/widgets/slider.rs b/packages/rink/src/widgets/slider.rs index f97c12bf6..7080b6c12 100644 --- a/packages/rink/src/widgets/slider.rs +++ b/packages/rink/src/widgets/slider.rs @@ -163,7 +163,9 @@ impl Slider { if let Some(mut div) = rdom.get_mut(self.pre_cursor_div) { let node_type = div.node_type_mut(); - let NodeTypeMut::Element(mut element) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut element) = node_type else { + panic!("input must be an element") + }; element.set_attribute( OwnedAttributeDiscription { name: "width".to_string(), @@ -175,7 +177,9 @@ impl Slider { if let Some(mut div) = rdom.get_mut(self.post_cursor_div) { let node_type = div.node_type_mut(); - let NodeTypeMut::Element(mut element) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut element) = node_type else { + panic!("input must be an element") + }; element.set_attribute( OwnedAttributeDiscription { name: "width".to_string(), @@ -259,7 +263,9 @@ impl CustomElement for Slider { fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { let node_type = root.node_type(); - let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + let NodeType::Element(el) = &*node_type else { + panic!("input must be an element") + }; let value = el.attributes.get(&OwnedAttributeDiscription { name: "value".to_string(), @@ -390,7 +396,9 @@ impl CustomElement for Slider { AttributeMask::All => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; self.update_value_attr(&el); self.update_size_attr(&mut el); self.update_max_attr(&el); @@ -403,7 +411,9 @@ impl CustomElement for Slider { AttributeMask::Some(attrs) => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; if attrs.contains("width") || attrs.contains("height") { self.update_size_attr(&mut el); } diff --git a/packages/rink/src/widgets/text_like.rs b/packages/rink/src/widgets/text_like.rs index b5874c776..6521073d0 100644 --- a/packages/rink/src/widgets/text_like.rs +++ b/packages/rink/src/widgets/text_like.rs @@ -143,19 +143,25 @@ impl TextLike { if let Some(mut text) = rdom.get_mut(self.pre_cursor_text) { let node_type = text.node_type_mut(); - let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Text(mut text) = node_type else { + panic!("input must be an element") + }; *text.text_mut() = self.controller.display_text(text_before_first_cursor); } if let Some(mut text) = rdom.get_mut(self.highlighted_text) { let node_type = text.node_type_mut(); - let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Text(mut text) = node_type else { + panic!("input must be an element") + }; *text.text_mut() = self.controller.display_text(text_highlighted); } if let Some(mut text) = rdom.get_mut(self.post_cursor_text) { let node_type = text.node_type_mut(); - let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Text(mut text) = node_type else { + panic!("input must be an element") + }; *text.text_mut() = self.controller.display_text(text_after_second_cursor); } @@ -288,7 +294,9 @@ impl CustomElement for fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { let node_type = root.node_type(); - let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + let NodeType::Element(el) = &*node_type else { + panic!("input must be an element") + }; let value = el .attributes @@ -370,7 +378,9 @@ impl CustomElement for AttributeMask::All => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; self.update_value_attr(&el); self.update_size_attr(&mut el); self.update_max_width_attr(&el); @@ -381,7 +391,9 @@ impl CustomElement for AttributeMask::Some(attrs) => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; if attrs.contains("width") || attrs.contains("height") { self.update_size_attr(&mut el); } diff --git a/packages/router/examples/simple_routes.rs b/packages/router/examples/simple_routes.rs index 9b94c7a9d..8c1a6fc76 100644 --- a/packages/router/examples/simple_routes.rs +++ b/packages/router/examples/simple_routes.rs @@ -38,7 +38,7 @@ fn BlogList(cx: Scope) -> Element { fn BlogPost(cx: Scope) -> Element { let Some(id) = use_route(cx).segment("id") else { - return cx.render(rsx! { div { "No blog post id" } }) + return cx.render(rsx! { div { "No blog post id" } }); }; log::trace!("rendering blog post {}", id); From 98e2b4acbd9d8a1852e130b61b7e5b8e5941f0c9 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 18 Jul 2023 12:40:47 -0700 Subject: [PATCH 3/6] fix normal desktop serve --- packages/cli/src/builder.rs | 165 ++++++++++++------------- packages/cli/src/server/desktop/mod.rs | 32 +++-- 2 files changed, 99 insertions(+), 98 deletions(-) diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 1cd6d787e..0455de93f 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -247,128 +247,121 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result { log::info!("🚅 Running build [Desktop] command..."); + let t_start = std::time::Instant::now(); let ignore_files = build_assets(config)?; - let mut cmd = Command::new("cargo"); - cmd.current_dir(&config.crate_dir) + let mut cmd = subprocess::Exec::cmd("cargo") + .cwd(&config.crate_dir) .arg("build") - .stdout(std::process::Stdio::inherit()) - .stderr(std::process::Stdio::inherit()); + .arg("--message-format=json"); if config.release { - cmd.arg("--release"); + cmd = cmd.arg("--release"); } if config.verbose { - cmd.arg("--verbose"); + cmd = cmd.arg("--verbose"); } if config.custom_profile.is_some() { let custom_profile = config.custom_profile.as_ref().unwrap(); - cmd.arg("--profile"); - cmd.arg(custom_profile); + cmd = cmd.arg("--profile").arg(custom_profile); } if config.features.is_some() { let features_str = config.features.as_ref().unwrap().join(" "); - cmd.arg("--features"); - cmd.arg(features_str); + cmd = cmd.arg("--features").arg(features_str); } - match &config.executable { + let cmd = match &config.executable { crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name), crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name), crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name), }; - let output = cmd.output()?; + let warning_messages = prettier_build(cmd)?; - if !output.status.success() { - return Err(Error::BuildFailed("Program build failed.".into())); - } + let release_type = match config.release { + true => "release", + false => "debug", + }; - if output.status.success() { - let release_type = match config.release { - true => "release", - false => "debug", - }; - - let file_name: String; - let mut res_path = match &config.executable { - crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => { - file_name = name.clone(); - config.target_dir.join(release_type).join(name) - } - crate::ExecutableType::Example(name) => { - file_name = name.clone(); - config - .target_dir - .join(release_type) - .join("examples") - .join(name) - } - }; - - let target_file = if cfg!(windows) { - res_path.set_extension("exe"); - format!("{}.exe", &file_name) - } else { - file_name - }; - - if !config.out_dir.is_dir() { - create_dir_all(&config.out_dir)?; + let file_name: String; + let mut res_path = match &config.executable { + crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => { + file_name = name.clone(); + config.target_dir.join(release_type).join(name) } - copy(res_path, &config.out_dir.join(target_file))?; + crate::ExecutableType::Example(name) => { + file_name = name.clone(); + config + .target_dir + .join(release_type) + .join("examples") + .join(name) + } + }; - // this code will copy all public file to the output dir - if config.asset_dir.is_dir() { - let copy_options = fs_extra::dir::CopyOptions { - overwrite: true, - skip_exist: false, - buffer_size: 64000, - copy_inside: false, - content_only: false, - depth: 0, - }; + let target_file = if cfg!(windows) { + res_path.set_extension("exe"); + format!("{}.exe", &file_name) + } else { + file_name + }; - for entry in std::fs::read_dir(&config.asset_dir)? { - let path = entry?.path(); - if path.is_file() { - std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?; - } else { - match fs_extra::dir::copy(&path, &config.out_dir, ©_options) { - Ok(_) => {} - Err(e) => { - log::warn!("Error copying dir: {}", e); - } + if !config.out_dir.is_dir() { + create_dir_all(&config.out_dir)?; + } + copy(res_path, &config.out_dir.join(target_file))?; + + // this code will copy all public file to the output dir + if config.asset_dir.is_dir() { + let copy_options = fs_extra::dir::CopyOptions { + overwrite: true, + skip_exist: false, + buffer_size: 64000, + copy_inside: false, + content_only: false, + depth: 0, + }; + + for entry in std::fs::read_dir(&config.asset_dir)? { + let path = entry?.path(); + if path.is_file() { + std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?; + } else { + match fs_extra::dir::copy(&path, &config.out_dir, ©_options) { + Ok(_) => {} + Err(e) => { + log::warn!("Error copying dir: {}", e); } - for ignore in &ignore_files { - let ignore = ignore.strip_prefix(&config.asset_dir).unwrap(); - let ignore = config.out_dir.join(ignore); - if ignore.is_file() { - std::fs::remove_file(ignore)?; - } + } + for ignore in &ignore_files { + let ignore = ignore.strip_prefix(&config.asset_dir).unwrap(); + let ignore = config.out_dir.join(ignore); + if ignore.is_file() { + std::fs::remove_file(ignore)?; } } } } - - log::info!( - "🚩 Build completed: [./{}]", - config - .dioxus_config - .application - .out_dir - .clone() - .unwrap_or_else(|| PathBuf::from("dist")) - .display() - ); } + + log::info!( + "🚩 Build completed: [./{}]", + config + .dioxus_config + .application + .out_dir + .clone() + .unwrap_or_else(|| PathBuf::from("dist")) + .display() + ); + println!("build desktop done"); Ok(BuildResult { - warnings: vec![], - elapsed_time: 0, + warnings: warning_messages, + elapsed_time: (t_start - std::time::Instant::now()).as_millis(), }) } diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index 6a2c57539..bc0d0ef82 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -29,8 +29,8 @@ use notify::{RecommendedWatcher, Watcher}; use std::{ net::UdpSocket, path::PathBuf, - process::{Command, Stdio}, - sync::{Arc, Mutex}, + process::{Child, Command, Stdio}, + sync::{Arc, Mutex, RwLock}, }; use tokio::sync::broadcast::{self, Sender}; use tower::ServiceBuilder; @@ -66,7 +66,8 @@ pub async fn startup(config: CrateConfig) -> Result<()> { /// Start the server without hot reload pub async fn serve_default(config: CrateConfig) -> Result<()> { - let first_build_result = start_desktop(&config)?; + let (child, first_build_result) = start_desktop(&config)?; + let currently_running_child: RwLock = RwLock::new(child); log::info!("🚀 Starting development server..."); @@ -75,7 +76,14 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> { let _watcher = setup_file_watcher( { let config = config.clone(); - move || start_desktop(&config) + + move || { + let mut current_child = currently_running_child.write().unwrap(); + current_child.kill()?; + let (child, result) = start_desktop(&config)?; + *current_child = child; + Ok(result) + } }, &config, None, @@ -93,6 +101,8 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> { None, ); + std::future::pending::<()>().await; + Ok(()) } @@ -100,7 +110,7 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> { /// Start dx serve with hot reload pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> { - let first_build_result = start_desktop(&config)?; + let (_, first_build_result) = start_desktop(&config)?; println!("🚀 Starting development server..."); @@ -137,7 +147,7 @@ pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> { for channel in &mut *channels.lock().unwrap() { send_msg(HotReloadMsg::Shutdown, channel); } - start_desktop(&config) + Ok(start_desktop(&config)?.1) } }, None, @@ -243,7 +253,7 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool { } } -pub fn start_desktop(config: &CrateConfig) -> Result { +pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> { // Run the desktop application let result = crate::builder::build_desktop(config, true)?; @@ -255,11 +265,9 @@ pub fn start_desktop(config: &CrateConfig) -> Result { if cfg!(windows) { file.set_extension("exe"); } - Command::new(file.to_str().unwrap()) - .stdout(Stdio::inherit()) - .spawn()?; + let child = Command::new(file.to_str().unwrap()).spawn()?; + + Ok((child, result)) } } - - Ok(result) } From 4a0869de7b3fb08925460515fdb17a00a117c1f2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 18 Jul 2023 12:41:48 -0700 Subject: [PATCH 4/6] cargo fix --- packages/cli/src/builder.rs | 1 - packages/cli/src/cli/serve.rs | 7 +----- packages/cli/src/server/desktop/mod.rs | 35 +++----------------------- packages/cli/src/server/mod.rs | 28 +++------------------ packages/cli/src/server/web/mod.rs | 5 +--- 5 files changed, 9 insertions(+), 67 deletions(-) diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 0455de93f..c935db106 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -12,7 +12,6 @@ use std::{ io::Read, panic, path::PathBuf, - process::Command, time::Duration, }; use wasm_bindgen_cli_support::Bindgen; diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index 1be19cb3b..a5d69c7cc 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -1,10 +1,5 @@ use super::*; -use std::{ - fs::create_dir_all, - io::Write, - path::PathBuf, - process::{Command, Stdio}, -}; +use std::{fs::create_dir_all, io::Write, path::PathBuf}; /// Run the WASM project on dev-server #[derive(Clone, Debug, Parser)] diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index bc0d0ef82..93bd4e9e9 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -1,52 +1,25 @@ use crate::{ - builder, - serve::Serve, server::{ - output::{print_console_info, PrettierOptions, WebServerInfo}, + output::{print_console_info, PrettierOptions}, setup_file_watcher, setup_file_watcher_hot_reload, }, BuildResult, CrateConfig, Result, }; -use axum::{ - body::{Full, HttpBody}, - extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade}, - http::{ - header::{HeaderName, HeaderValue}, - Method, Response, StatusCode, - }, - response::IntoResponse, - routing::{get, get_service}, - Router, -}; -use axum_server::tls_rustls::RustlsConfig; -use cargo_metadata::diagnostic::Diagnostic; -use dioxus_core::Template; + use dioxus_hot_reload::HotReloadMsg; use dioxus_html::HtmlCtx; use dioxus_rsx::hot_reload::*; use interprocess_docfix::local_socket::LocalSocketListener; -use notify::{RecommendedWatcher, Watcher}; use std::{ - net::UdpSocket, path::PathBuf, - process::{Child, Command, Stdio}, + process::{Child, Command}, sync::{Arc, Mutex, RwLock}, }; -use tokio::sync::broadcast::{self, Sender}; -use tower::ServiceBuilder; -use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody}; -use tower_http::{ - cors::{Any, CorsLayer}, - ServiceBuilderExt, -}; +use tokio::sync::broadcast::{self}; #[cfg(feature = "plugin")] use plugin::PluginManager; -struct WsReloadState { - update: broadcast::Sender<()>, -} - pub async fn startup(config: CrateConfig) -> Result<()> { // ctrl-c shutdown checker let _crate_config = config.clone(); diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index 8c421f269..19dc44b75 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -1,37 +1,15 @@ -use crate::{ - builder, cfg::Platform, serve::Serve, server::desktop::start_desktop, BuildResult, CrateConfig, - Result, -}; -use axum::{ - body::{Full, HttpBody}, - extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade}, - http::{ - header::{HeaderName, HeaderValue}, - Method, Response, StatusCode, - }, - response::IntoResponse, - routing::{get, get_service}, - Router, -}; -use axum_server::tls_rustls::RustlsConfig; +use crate::{BuildResult, CrateConfig, Result}; + use cargo_metadata::diagnostic::Diagnostic; use dioxus_core::Template; use dioxus_html::HtmlCtx; use dioxus_rsx::hot_reload::*; use notify::{RecommendedWatcher, Watcher}; use std::{ - net::UdpSocket, path::PathBuf, - process::{Command, Stdio}, sync::{Arc, Mutex}, }; -use tokio::sync::broadcast::{self, Sender}; -use tower::ServiceBuilder; -use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody}; -use tower_http::{ - cors::{Any, CorsLayer}, - ServiceBuilderExt, -}; +use tokio::sync::broadcast::Sender; mod output; use output::*; diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index e6a429788..28ffa28c0 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -19,14 +19,11 @@ use axum::{ Router, }; use axum_server::tls_rustls::RustlsConfig; -use cargo_metadata::diagnostic::Diagnostic; -use dioxus_core::Template; + use dioxus_html::HtmlCtx; use dioxus_rsx::hot_reload::*; -use notify::{RecommendedWatcher, Watcher}; use std::{ net::UdpSocket, - path::PathBuf, process::Command, sync::{Arc, Mutex}, }; From f3b3c0698b2a33a309e35e2fea2d5a3777264a0d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 18 Jul 2023 12:43:02 -0700 Subject: [PATCH 5/6] fix clippy --- packages/cli/src/cli/build.rs | 11 ++++------- packages/cli/src/cli/serve.rs | 11 ++++------- packages/cli/src/server/web/mod.rs | 4 ++-- packages/core/src/scopes.rs | 4 ++-- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 29edf301b..3053a6e5a 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -32,13 +32,10 @@ impl Build { crate_config.set_features(self.build.features.unwrap()); } - let platform = self.build.platform.unwrap_or_else(|| { - crate_config - .dioxus_config - .application - .default_platform - .clone() - }); + let platform = self + .build + .platform + .unwrap_or(crate_config.dioxus_config.application.default_platform); #[cfg(feature = "plugin")] let _ = PluginManager::on_build_start(&crate_config, &platform); diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index a5d69c7cc..433d611b7 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -34,13 +34,10 @@ impl Serve { // Subdirectories don't work with the server crate_config.dioxus_config.web.app.base_path = None; - let platform = self.serve.platform.unwrap_or_else(|| { - crate_config - .dioxus_config - .application - .default_platform - .clone() - }); + let platform = self + .serve + .platform + .unwrap_or(crate_config.dioxus_config.application.default_platform); match platform { cfg::Platform::Web => { diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index 28ffa28c0..e08d2297a 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -473,7 +473,7 @@ async fn ws_handler( } fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result { - let result = builder::build(&config, true)?; + let result = builder::build(config, true)?; // change the websocket reload state to true; // the page will auto-reload. if config @@ -483,7 +483,7 @@ fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result { .reload_html .unwrap_or(false) { - let _ = Serve::regen_dev_page(&config); + let _ = Serve::regen_dev_page(config); } let _ = reload_tx.send(()); Ok(result) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index ff94c456d..87e894ef3 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -688,13 +688,13 @@ impl<'src> ScopeState { raw_ref.downcast_mut::() }) .expect( - r###" + r#" Unable to retrieve the hook that was initialized at this index. Consult the `rules of hooks` to understand how to use hooks properly. You likely used the hook in a conditional. Hooks rely on consistent ordering between renders. Functions prefixed with "use" should never be called conditionally. - "###, + "#, ) } } From 90d23714da373cb824a0dd9674b4b746966b3069 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 19 Jul 2023 13:44:38 -0700 Subject: [PATCH 6/6] Fix clippy around pathbuf --- packages/cli/src/server/desktop/mod.rs | 30 ++++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index 93bd4e9e9..9b79bdea9 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -11,7 +11,6 @@ use dioxus_html::HtmlCtx; use dioxus_rsx::hot_reload::*; use interprocess_docfix::local_socket::LocalSocketListener; use std::{ - path::PathBuf, process::{Child, Command}, sync::{Arc, Mutex, RwLock}, }; @@ -138,19 +137,7 @@ pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> { None, ); - #[cfg(target_os = "macos")] - { - // On unix, if you force quit the application, it can leave the file socket open - // This will cause the local socket listener to fail to open - // We check if the file socket is already open from an old session and then delete it - let paths = ["./dioxusin", "./@dioxusin"]; - for path in paths { - let path = PathBuf::from(path); - if path.exists() { - let _ = std::fs::remove_file(path); - } - } - } + clear_paths(); match LocalSocketListener::bind("@dioxusin") { Ok(local_socket_stream) => { @@ -212,6 +199,21 @@ pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> { Ok(()) } +fn clear_paths() { + if cfg!(target_os = "macos") { + // On unix, if you force quit the application, it can leave the file socket open + // This will cause the local socket listener to fail to open + // We check if the file socket is already open from an old session and then delete it + let paths = ["./dioxusin", "./@dioxusin"]; + for path in paths { + let path = std::path::PathBuf::from(path); + if path.exists() { + let _ = std::fs::remove_file(path); + } + } + } +} + fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool { if let Ok(msg) = serde_json::to_string(&msg) { if channel.write_all(msg.as_bytes()).is_err() {