From 49a127b31ebada24e5aa84e507a0cb9cf4cf86d4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 9 Sep 2023 16:18:52 -0500 Subject: [PATCH] deduplicate serve code with hot reloading in the CLI crate --- packages/cli/src/server/desktop/mod.rs | 113 +++++------- packages/cli/src/server/mod.rs | 200 +++++++++------------- packages/cli/src/server/web/hot_reload.rs | 16 +- packages/cli/src/server/web/mod.rs | 117 +++---------- packages/core/src/diff.rs | 3 +- 5 files changed, 153 insertions(+), 296 deletions(-) diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index 9b79bdea9..91811857d 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -1,7 +1,7 @@ use crate::{ server::{ output::{print_console_info, PrettierOptions}, - setup_file_watcher, setup_file_watcher_hot_reload, + setup_file_watcher, }, BuildResult, CrateConfig, Result, }; @@ -19,6 +19,8 @@ use tokio::sync::broadcast::{self}; #[cfg(feature = "plugin")] use plugin::PluginManager; +use super::HotReloadState; + pub async fn startup(config: CrateConfig) -> Result<()> { // ctrl-c shutdown checker let _crate_config = config.clone(); @@ -28,16 +30,36 @@ pub async fn startup(config: CrateConfig) -> Result<()> { std::process::exit(0); }); - match config.hot_reload { - true => serve_hot_reload(config).await?, - false => serve_default(config).await?, - } + let hot_reload_state = match config.hot_reload { + true => { + 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 hot_reload_tx = broadcast::channel(100).0; + + clear_paths(); + + Some(HotReloadState { + messages: hot_reload_tx.clone(), + file_map: file_map.clone(), + }) + } + false => None, + }; + + serve(config, hot_reload_state).await?; Ok(()) } /// Start the server without hot reload -pub async fn serve_default(config: CrateConfig) -> Result<()> { +pub async fn serve(config: CrateConfig, hot_reload_state: Option) -> Result<()> { let (child, first_build_result) = start_desktop(&config)?; let currently_running_child: RwLock = RwLock::new(child); @@ -59,6 +81,7 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> { }, &config, None, + hot_reload_state.clone(), ) .await?; @@ -73,79 +96,29 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> { None, ); - std::future::pending::<()>().await; + match hot_reload_state { + Some(hot_reload_state) => { + start_desktop_hot_reload(hot_reload_state).await?; + } + None => { + std::future::pending::<()>().await; + } + } 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 (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. - let _watcher = setup_file_watcher_hot_reload( - &config, - hot_reload_tx, - file_map.clone(), - { - let config = config.clone(); - - let channels = channels.clone(); - move || { - for channel in &mut *channels.lock().unwrap() { - send_msg(HotReloadMsg::Shutdown, channel); - } - Ok(start_desktop(&config)?.1) - } - }, - 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, - ); - - clear_paths(); - +async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> { match LocalSocketListener::bind("@dioxusin") { Ok(local_socket_stream) => { let aborted = Arc::new(Mutex::new(false)); + // States + // The open interprocess sockets + let channels = Arc::new(Mutex::new(Vec::new())); // listen for connections std::thread::spawn({ - let file_map = file_map.clone(); + let file_map = hot_reload_state.file_map.clone(); let channels = channels.clone(); let aborted = aborted.clone(); let _ = local_socket_stream.set_nonblocking(true); @@ -180,6 +153,8 @@ pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> { } }); + let mut hot_reload_rx = hot_reload_state.messages.subscribe(); + 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 19dc44b75..c6073d787 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -9,7 +9,7 @@ use std::{ path::PathBuf, sync::{Arc, Mutex}, }; -use tokio::sync::broadcast::Sender; +use tokio::sync::broadcast::{self}; mod output; use output::*; @@ -21,6 +21,7 @@ async fn setup_file_watcher Result + Send + 'static>( build_with: F, config: &CrateConfig, web_info: Option, + hot_reload: Option, ) -> Result { let mut last_update_time = chrono::Local::now().timestamp(); @@ -38,28 +39,81 @@ async fn setup_file_watcher Result + Send + 'static>( let config = watcher_config.clone(); if let Ok(e) = info { if chrono::Local::now().timestamp() > last_update_time { - match build_with() { - Ok(res) => { - last_update_time = chrono::Local::now().timestamp(); + let mut needs_full_rebuild; + if let Some(hot_reload) = &hot_reload { + // find changes to the rsx in the file + let mut rsx_file_map = hot_reload.file_map.lock().unwrap(); + let mut messages: Vec> = Vec::new(); - #[allow(clippy::redundant_clone)] - print_console_info( - &config, - PrettierOptions { - changed: e.paths.clone(), - warnings: res.warnings, - elapsed_time: res.elapsed_time, - }, - web_info.clone(), - ); + // In hot reload mode, we only need to rebuild if non-rsx code is changed + needs_full_rebuild = false; - #[cfg(feature = "plugin")] - let _ = PluginManager::on_serve_rebuild( - chrono::Local::now().timestamp(), - e.paths, - ); + for path in &e.paths { + // if this is not a rust file, rebuild the whole project + if path.extension().and_then(|p| p.to_str()) != Some("rs") { + needs_full_rebuild = true; + break; + } + + match rsx_file_map.update_rsx(path, &config.crate_dir) { + Ok(UpdateResult::UpdatedRsx(msgs)) => { + messages.extend(msgs); + needs_full_rebuild = false; + } + Ok(UpdateResult::NeedsRebuild) => { + needs_full_rebuild = true; + } + Err(err) => { + log::error!("{}", err); + } + } + } + + if needs_full_rebuild { + // Reset the file map to the new state of the project + let FileMapBuildResult { + map: new_file_map, + errors, + } = FileMap::::create(config.crate_dir.clone()).unwrap(); + + for err in errors { + log::error!("{}", err); + } + + *rsx_file_map = new_file_map; + } else { + for msg in messages { + let _ = hot_reload.messages.send(msg); + } + } + } else { + needs_full_rebuild = true; + } + + if needs_full_rebuild { + match build_with() { + Ok(res) => { + last_update_time = chrono::Local::now().timestamp(); + + #[allow(clippy::redundant_clone)] + print_console_info( + &config, + PrettierOptions { + changed: e.paths.clone(), + warnings: res.warnings, + elapsed_time: res.elapsed_time, + }, + web_info.clone(), + ); + + #[cfg(feature = "plugin")] + let _ = PluginManager::on_serve_rebuild( + chrono::Local::now().timestamp(), + e.paths, + ); + } + Err(e) => log::error!("{}", e), } - Err(e) => log::error!("{}", e), } } } @@ -77,106 +131,8 @@ async fn setup_file_watcher Result + Send + 'static>( Ok(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 Result + Send + 'static>( - config: &CrateConfig, - hot_reload_tx: Sender>, - file_map: Arc>>, - build_with: F, - web_info: Option, -) -> Result { - // file watcher: check file change - let allow_watch_path = config - .dioxus_config - .web - .watcher - .watch_path - .clone() - .unwrap_or_else(|| vec![PathBuf::from("src")]); - - let watcher_config = config.clone(); - let mut last_update_time = chrono::Local::now().timestamp(); - - let mut watcher = RecommendedWatcher::new( - move |evt: notify::Result| { - let config = watcher_config.clone(); - // Give time for the change to take effect before reading the file - std::thread::sleep(std::time::Duration::from_millis(100)); - if chrono::Local::now().timestamp() > last_update_time { - if let Ok(evt) = evt { - let mut messages: Vec> = Vec::new(); - 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_with() { - Ok(res) => { - print_console_info( - &config, - PrettierOptions { - changed: evt.paths, - warnings: res.warnings, - elapsed_time: res.elapsed_time, - }, - web_info.clone(), - ); - } - Err(err) => { - log::error!("{}", err); - } - } - return; - } - // find changes to the rsx in the file - let mut map = file_map.lock().unwrap(); - - match map.update_rsx(&path, &config.crate_dir) { - Ok(UpdateResult::UpdatedRsx(msgs)) => { - messages.extend(msgs); - } - Ok(UpdateResult::NeedsRebuild) => { - match build_with() { - Ok(res) => { - print_console_info( - &config, - PrettierOptions { - changed: evt.paths, - warnings: res.warnings, - elapsed_time: res.elapsed_time, - }, - web_info.clone(), - ); - } - Err(err) => { - log::error!("{}", err); - } - } - return; - } - Err(err) => { - log::error!("{}", err); - } - } - } - for msg in messages { - let _ = hot_reload_tx.send(msg); - } - } - last_update_time = chrono::Local::now().timestamp(); - } - }, - notify::Config::default(), - ) - .unwrap(); - - for sub_path in allow_watch_path { - if let Err(err) = watcher.watch( - &config.crate_dir.join(&sub_path), - notify::RecursiveMode::Recursive, - ) { - log::error!("error watching {sub_path:?}: \n{}", err); - } - } - - Ok(watcher) +#[derive(Clone)] +pub struct HotReloadState { + pub messages: broadcast::Sender>, + pub file_map: Arc>>, } diff --git a/packages/cli/src/server/web/hot_reload.rs b/packages/cli/src/server/web/hot_reload.rs index 49cce38d4..7b47dca4c 100644 --- a/packages/cli/src/server/web/hot_reload.rs +++ b/packages/cli/src/server/web/hot_reload.rs @@ -1,27 +1,15 @@ -use std::sync::{Arc, Mutex}; - use axum::{ extract::{ws::Message, WebSocketUpgrade}, response::IntoResponse, Extension, TypedHeader, }; -use dioxus_core::Template; -use dioxus_html::HtmlCtx; -use dioxus_rsx::hot_reload::FileMap; -use tokio::sync::broadcast; -use crate::CrateConfig; - -pub struct HotReloadState { - pub messages: broadcast::Sender>, - pub file_map: Arc>>, - pub watcher_config: CrateConfig, -} +use crate::server::HotReloadState; pub async fn hot_reload_handler( ws: WebSocketUpgrade, _: Option>, - Extension(state): Extension>, + Extension(state): Extension, ) -> impl IntoResponse { ws.on_upgrade(|mut socket| async move { log::info!("🔥 Hot Reload WebSocket connected"); diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index bb1c00c99..0e1413597 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, + setup_file_watcher, HotReloadState, }, BuildResult, CrateConfig, Result, WebHttpsConfig, }; @@ -58,20 +58,39 @@ pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Res 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?, - } + let hot_reload_state = match config.hot_reload { + true => { + 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 hot_reload_tx = broadcast::channel(100).0; + + Some(HotReloadState { + messages: hot_reload_tx.clone(), + file_map: file_map.clone(), + }) + } + false => None, + }; + + serve(ip, port, config, start_browser, hot_reload_state).await?; Ok(()) } /// Start the server without hot reload -pub async fn serve_default( +pub async fn serve( ip: String, port: u16, config: CrateConfig, start_browser: bool, + hot_reload_state: Option, ) -> Result<()> { let first_build_result = crate::builder::build(&config, true)?; @@ -93,6 +112,7 @@ pub async fn serve_default( ip: ip.clone(), port, }), + hot_reload_state.clone(), ) .await?; @@ -119,88 +139,7 @@ pub async fn serve_default( ); // 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, true)?; - - 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 hot_reload_tx = broadcast::channel(100).0; - - // States - let hot_reload_state = Arc::new(HotReloadState { - messages: hot_reload_tx.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, - { - let config = config.clone(); - let reload_tx = reload_tx.clone(); - move || build(&config, &reload_tx) - }, - 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?; + let router = setup_router(config, ws_reload_state, hot_reload_state).await?; // Start server start_server(port, router, start_browser, rustls_config).await?; @@ -291,7 +230,7 @@ fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, Str async fn setup_router( config: CrateConfig, ws_reload: Arc, - hot_reload: Option>, + hot_reload: Option, ) -> Result { // Setup cors let cors = CorsLayer::new() diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index dd4cf8147..58fceaa48 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -577,8 +577,7 @@ impl<'b> VirtualDom { } // 4. Compute the LIS of this list - let mut lis_sequence = Vec::default(); - lis_sequence.reserve(new_index_to_old_index.len()); + let mut lis_sequence = Vec::with_capacity(new_index_to_old_index.len()); let mut predecessors = vec![0; new_index_to_old_index.len()]; let mut starts = vec![0; new_index_to_old_index.len()];