瀏覽代碼

Merge pull request #1446 from ealmloff/deduplicate-hot-reload-cli

Deduplicate serve code with hot reloading in the CLI crate
Jonathan Kelley 1 年之前
父節點
當前提交
e59a05141e

+ 44 - 69
packages/cli/src/server/desktop/mod.rs

@@ -1,7 +1,7 @@
 use crate::{
 use crate::{
     server::{
     server::{
         output::{print_console_info, PrettierOptions},
         output::{print_console_info, PrettierOptions},
-        setup_file_watcher, setup_file_watcher_hot_reload,
+        setup_file_watcher,
     },
     },
     BuildResult, CrateConfig, Result,
     BuildResult, CrateConfig, Result,
 };
 };
@@ -19,6 +19,8 @@ use tokio::sync::broadcast::{self};
 #[cfg(feature = "plugin")]
 #[cfg(feature = "plugin")]
 use plugin::PluginManager;
 use plugin::PluginManager;
 
 
+use super::HotReloadState;
+
 pub async fn startup(config: CrateConfig) -> Result<()> {
 pub async fn startup(config: CrateConfig) -> Result<()> {
     // ctrl-c shutdown checker
     // ctrl-c shutdown checker
     let _crate_config = config.clone();
     let _crate_config = config.clone();
@@ -28,16 +30,36 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
         std::process::exit(0);
         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::<HtmlCtx>::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(())
     Ok(())
 }
 }
 
 
 /// Start the server without hot reload
 /// Start the server without hot reload
-pub async fn serve_default(config: CrateConfig) -> Result<()> {
+pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>) -> Result<()> {
     let (child, first_build_result) = start_desktop(&config)?;
     let (child, first_build_result) = start_desktop(&config)?;
     let currently_running_child: RwLock<Child> = RwLock::new(child);
     let currently_running_child: RwLock<Child> = RwLock::new(child);
 
 
@@ -59,6 +81,7 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> {
         },
         },
         &config,
         &config,
         None,
         None,
+        hot_reload_state.clone(),
     )
     )
     .await?;
     .await?;
 
 
@@ -73,79 +96,29 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> {
         None,
         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(())
     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::<HtmlCtx>::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") {
     match LocalSocketListener::bind("@dioxusin") {
         Ok(local_socket_stream) => {
         Ok(local_socket_stream) => {
             let aborted = Arc::new(Mutex::new(false));
             let aborted = Arc::new(Mutex::new(false));
+            // States
+            // The open interprocess sockets
+            let channels = Arc::new(Mutex::new(Vec::new()));
 
 
             // listen for connections
             // listen for connections
             std::thread::spawn({
             std::thread::spawn({
-                let file_map = file_map.clone();
+                let file_map = hot_reload_state.file_map.clone();
                 let channels = channels.clone();
                 let channels = channels.clone();
                 let aborted = aborted.clone();
                 let aborted = aborted.clone();
                 let _ = local_socket_stream.set_nonblocking(true);
                 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 {
             while let Ok(template) = hot_reload_rx.recv().await {
                 let channels = &mut *channels.lock().unwrap();
                 let channels = &mut *channels.lock().unwrap();
                 let mut i = 0;
                 let mut i = 0;

+ 74 - 118
packages/cli/src/server/mod.rs

@@ -9,7 +9,7 @@ use std::{
     path::PathBuf,
     path::PathBuf,
     sync::{Arc, Mutex},
     sync::{Arc, Mutex},
 };
 };
-use tokio::sync::broadcast::Sender;
+use tokio::sync::broadcast::{self};
 
 
 mod output;
 mod output;
 use output::*;
 use output::*;
@@ -21,6 +21,7 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
     build_with: F,
     build_with: F,
     config: &CrateConfig,
     config: &CrateConfig,
     web_info: Option<WebServerInfo>,
     web_info: Option<WebServerInfo>,
+    hot_reload: Option<HotReloadState>,
 ) -> Result<RecommendedWatcher> {
 ) -> Result<RecommendedWatcher> {
     let mut last_update_time = chrono::Local::now().timestamp();
     let mut last_update_time = chrono::Local::now().timestamp();
 
 
@@ -38,145 +39,100 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
         let config = watcher_config.clone();
         let config = watcher_config.clone();
         if let Ok(e) = info {
         if let Ok(e) = info {
             if chrono::Local::now().timestamp() > last_update_time {
             if chrono::Local::now().timestamp() > last_update_time {
-                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),
-                }
-            }
-        }
-    })
-    .unwrap();
-
-    for sub_path in allow_watch_path {
-        watcher
-            .watch(
-                &config.crate_dir.join(sub_path),
-                notify::RecursiveMode::Recursive,
-            )
-            .unwrap();
-    }
-    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<F: Fn() -> Result<BuildResult> + Send + 'static>(
-    config: &CrateConfig,
-    hot_reload_tx: Sender<Template<'static>>,
-    file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
-    build_with: F,
-    web_info: Option<WebServerInfo>,
-) -> Result<RecommendedWatcher> {
-    // 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 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<Template<'static>> = Vec::new();
 
 
-    let watcher_config = config.clone();
-    let mut last_update_time = chrono::Local::now().timestamp();
+                    // In hot reload mode, we only need to rebuild if non-rsx code is changed
+                    needs_full_rebuild = false;
 
 
-    let mut watcher = RecommendedWatcher::new(
-        move |evt: notify::Result<notify::Event>| {
-            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<Template<'static>> = Vec::new();
-                    for path in evt.paths.clone() {
+                    for path in &e.paths {
                         // if this is not a rust file, rebuild the whole project
                         // if this is not a rust file, rebuild the whole project
                         if path.extension().and_then(|p| p.to_str()) != Some("rs") {
                         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;
+                            needs_full_rebuild = true;
+                            break;
                         }
                         }
-                        // find changes to the rsx in the file
-                        let mut map = file_map.lock().unwrap();
 
 
-                        match map.update_rsx(&path, &config.crate_dir) {
+                        match rsx_file_map.update_rsx(path, &config.crate_dir) {
                             Ok(UpdateResult::UpdatedRsx(msgs)) => {
                             Ok(UpdateResult::UpdatedRsx(msgs)) => {
                                 messages.extend(msgs);
                                 messages.extend(msgs);
+                                needs_full_rebuild = false;
                             }
                             }
                             Ok(UpdateResult::NeedsRebuild) => {
                             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;
+                                needs_full_rebuild = true;
                             }
                             }
                             Err(err) => {
                             Err(err) => {
                                 log::error!("{}", err);
                                 log::error!("{}", err);
                             }
                             }
                         }
                         }
                     }
                     }
-                    for msg in messages {
-                        let _ = hot_reload_tx.send(msg);
+
+                    if needs_full_rebuild {
+                        // Reset the file map to the new state of the project
+                        let FileMapBuildResult {
+                            map: new_file_map,
+                            errors,
+                        } = FileMap::<HtmlCtx>::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),
                     }
                     }
                 }
                 }
-                last_update_time = chrono::Local::now().timestamp();
             }
             }
-        },
-        notify::Config::default(),
-    )
+        }
+    })
     .unwrap();
     .unwrap();
 
 
     for sub_path in allow_watch_path {
     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);
-        }
+        watcher
+            .watch(
+                &config.crate_dir.join(sub_path),
+                notify::RecursiveMode::Recursive,
+            )
+            .unwrap();
     }
     }
-
     Ok(watcher)
     Ok(watcher)
 }
 }
+
+#[derive(Clone)]
+pub struct HotReloadState {
+    pub messages: broadcast::Sender<Template<'static>>,
+    pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
+}

+ 2 - 14
packages/cli/src/server/web/hot_reload.rs

@@ -1,27 +1,15 @@
-use std::sync::{Arc, Mutex};
-
 use axum::{
 use axum::{
     extract::{ws::Message, WebSocketUpgrade},
     extract::{ws::Message, WebSocketUpgrade},
     response::IntoResponse,
     response::IntoResponse,
     Extension, TypedHeader,
     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<Template<'static>>,
-    pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
-    pub watcher_config: CrateConfig,
-}
+use crate::server::HotReloadState;
 
 
 pub async fn hot_reload_handler(
 pub async fn hot_reload_handler(
     ws: WebSocketUpgrade,
     ws: WebSocketUpgrade,
     _: Option<TypedHeader<headers::UserAgent>>,
     _: Option<TypedHeader<headers::UserAgent>>,
-    Extension(state): Extension<Arc<HotReloadState>>,
+    Extension(state): Extension<HotReloadState>,
 ) -> impl IntoResponse {
 ) -> impl IntoResponse {
     ws.on_upgrade(|mut socket| async move {
     ws.on_upgrade(|mut socket| async move {
         log::info!("🔥 Hot Reload WebSocket connected");
         log::info!("🔥 Hot Reload WebSocket connected");

+ 28 - 89
packages/cli/src/server/web/mod.rs

@@ -3,7 +3,7 @@ use crate::{
     serve::Serve,
     serve::Serve,
     server::{
     server::{
         output::{print_console_info, PrettierOptions, WebServerInfo},
         output::{print_console_info, PrettierOptions, WebServerInfo},
-        setup_file_watcher, setup_file_watcher_hot_reload,
+        setup_file_watcher, HotReloadState,
     },
     },
     BuildResult, CrateConfig, Result, WebHttpsConfig,
     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"));
     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::<HtmlCtx>::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(())
     Ok(())
 }
 }
 
 
 /// Start the server without hot reload
 /// Start the server without hot reload
-pub async fn serve_default(
+pub async fn serve(
     ip: String,
     ip: String,
     port: u16,
     port: u16,
     config: CrateConfig,
     config: CrateConfig,
     start_browser: bool,
     start_browser: bool,
+    hot_reload_state: Option<HotReloadState>,
 ) -> Result<()> {
 ) -> Result<()> {
     let first_build_result = crate::builder::build(&config, true)?;
     let first_build_result = crate::builder::build(&config, true)?;
 
 
@@ -93,6 +112,7 @@ pub async fn serve_default(
             ip: ip.clone(),
             ip: ip.clone(),
             port,
             port,
         }),
         }),
+        hot_reload_state.clone(),
     )
     )
     .await?;
     .await?;
 
 
@@ -119,88 +139,7 @@ pub async fn serve_default(
     );
     );
 
 
     // Router
     // 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::<HtmlCtx>::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
     start_server(port, router, start_browser, rustls_config).await?;
     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(
 async fn setup_router(
     config: CrateConfig,
     config: CrateConfig,
     ws_reload: Arc<WsReloadState>,
     ws_reload: Arc<WsReloadState>,
-    hot_reload: Option<Arc<HotReloadState>>,
+    hot_reload: Option<HotReloadState>,
 ) -> Result<Router> {
 ) -> Result<Router> {
     // Setup cors
     // Setup cors
     let cors = CorsLayer::new()
     let cors = CorsLayer::new()

+ 1 - 2
packages/core/src/diff.rs

@@ -583,8 +583,7 @@ impl<'b> VirtualDom {
         }
         }
 
 
         // 4. Compute the LIS of this list
         // 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 predecessors = vec![0; new_index_to_old_index.len()];
         let mut starts = vec![0; new_index_to_old_index.len()];
         let mut starts = vec![0; new_index_to_old_index.len()];