diff options
author | Bartek IwaĆczuk <biwanczuk@gmail.com> | 2023-10-31 01:25:58 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-31 01:25:58 +0100 |
commit | 1713df13524ad20c6bd0413bcf4aa57adc3f9735 (patch) | |
tree | a558239a7f483cab10b83305b333d5ef9657bd3e /cli/util/file_watcher.rs | |
parent | 48c5c3a3fb2f43716528db8915b36e55c411d94f (diff) |
feat: deno run --unstable-hmr (#20876)
This commit adds `--unstable-hmr` flag, that enabled Hot Module Replacement.
This flag works like `--watch` and accepts the same arguments. If
HMR is not possible the process will be restarted instead.
Currently HMR is only supported in `deno run` subcommand.
Upon HMR a `CustomEvent("hmr")` will be dispatched that contains
information which file was changed in its `details` property.
---------
Co-authored-by: Valentin Anger <syrupthinker@gryphno.de>
Co-authored-by: David Sherret <dsherret@gmail.com>
Diffstat (limited to 'cli/util/file_watcher.rs')
-rw-r--r-- | cli/util/file_watcher.rs | 141 |
1 files changed, 97 insertions, 44 deletions
diff --git a/cli/util/file_watcher.rs b/cli/util/file_watcher.rs index 8d6b4e8fb..5a316139c 100644 --- a/cli/util/file_watcher.rs +++ b/cli/util/file_watcher.rs @@ -8,6 +8,7 @@ use deno_core::error::AnyError; use deno_core::error::JsError; use deno_core::futures::Future; use deno_core::futures::FutureExt; +use deno_core::parking_lot::Mutex; use deno_runtime::fmt_errors::format_js_error; use log::info; use notify::event::Event as NotifyEvent; @@ -16,9 +17,11 @@ use notify::Error as NotifyError; use notify::RecommendedWatcher; use notify::RecursiveMode; use notify::Watcher; +use std::cell::RefCell; use std::collections::HashSet; use std::io::IsTerminal; use std::path::PathBuf; +use std::rc::Rc; use std::sync::Arc; use std::time::Duration; use tokio::select; @@ -91,20 +94,49 @@ where } pub struct PrintConfig { - /// printing watcher status to terminal. - pub job_name: String, - /// determine whether to clear the terminal screen; applicable to TTY environments only. - pub clear_screen: bool, + banner: &'static str, + /// Printing watcher status to terminal. + job_name: &'static str, + /// Determine whether to clear the terminal screen; applicable to TTY environments only. + clear_screen: bool, } -fn create_print_after_restart_fn(clear_screen: bool) -> impl Fn() { +impl PrintConfig { + /// By default `PrintConfig` uses "Watcher" as a banner name that will + /// be printed in color. If you need to customize it, use + /// `PrintConfig::new_with_banner` instead. + pub fn new(job_name: &'static str, clear_screen: bool) -> Self { + Self { + banner: "Watcher", + job_name, + clear_screen, + } + } + + pub fn new_with_banner( + banner: &'static str, + job_name: &'static str, + clear_screen: bool, + ) -> Self { + Self { + banner, + job_name, + clear_screen, + } + } +} + +fn create_print_after_restart_fn( + banner: &'static str, + clear_screen: bool, +) -> impl Fn() { move || { if clear_screen && std::io::stderr().is_terminal() { eprint!("{CLEAR_SCREEN}"); } info!( "{} File change detected! Restarting!", - colors::intense_blue("Watcher"), + colors::intense_blue(banner), ); } } @@ -120,22 +152,38 @@ pub struct WatcherCommunicator { /// Send a message to force a restart. restart_tx: tokio::sync::mpsc::UnboundedSender<()>, -} -impl Clone for WatcherCommunicator { - fn clone(&self) -> Self { - Self { - paths_to_watch_tx: self.paths_to_watch_tx.clone(), - changed_paths_rx: self.changed_paths_rx.resubscribe(), - restart_tx: self.restart_tx.clone(), - } - } + restart_mode: Mutex<WatcherRestartMode>, + + banner: String, } impl WatcherCommunicator { pub fn watch_paths(&self, paths: Vec<PathBuf>) -> Result<(), AnyError> { self.paths_to_watch_tx.send(paths).map_err(AnyError::from) } + + pub fn force_restart(&self) -> Result<(), AnyError> { + // Change back to automatic mode, so that HMR can set up watching + // from scratch. + *self.restart_mode.lock() = WatcherRestartMode::Automatic; + self.restart_tx.send(()).map_err(AnyError::from) + } + + pub async fn watch_for_changed_paths( + &self, + ) -> Result<Option<Vec<PathBuf>>, AnyError> { + let mut rx = self.changed_paths_rx.resubscribe(); + rx.recv().await.map_err(AnyError::from) + } + + pub fn change_restart_mode(&self, restart_mode: WatcherRestartMode) { + *self.restart_mode.lock() = restart_mode; + } + + pub fn print(&self, msg: String) { + log::info!("{} {}", self.banner, msg); + } } /// Creates a file watcher. @@ -151,7 +199,7 @@ pub async fn watch_func<O, F>( where O: FnMut( Flags, - WatcherCommunicator, + Arc<WatcherCommunicator>, Option<Vec<PathBuf>>, ) -> Result<F, AnyError>, F: Future<Output = Result<(), AnyError>>, @@ -173,9 +221,7 @@ pub enum WatcherRestartMode { Automatic, /// When a file path changes the caller will trigger a restart, using - /// `WatcherCommunicator.restart_tx`. - // TODO(bartlomieju): this mode will be used in a follow up PR - #[allow(dead_code)] + /// `WatcherInterface.restart_tx`. Manual, } @@ -193,7 +239,7 @@ pub async fn watch_recv<O, F>( where O: FnMut( Flags, - WatcherCommunicator, + Arc<WatcherCommunicator>, Option<Vec<PathBuf>>, ) -> Result<F, AnyError>, F: Future<Output = Result<(), AnyError>>, @@ -206,19 +252,42 @@ where DebouncedReceiver::new_with_sender(); let PrintConfig { + banner, job_name, clear_screen, } = print_config; - let print_after_restart = create_print_after_restart_fn(clear_screen); - let watcher_communicator = WatcherCommunicator { + let print_after_restart = create_print_after_restart_fn(banner, clear_screen); + let watcher_communicator = Arc::new(WatcherCommunicator { paths_to_watch_tx: paths_to_watch_tx.clone(), changed_paths_rx: changed_paths_rx.resubscribe(), restart_tx: restart_tx.clone(), - }; - info!("{} {} started.", colors::intense_blue("Watcher"), job_name,); + restart_mode: Mutex::new(restart_mode), + banner: colors::intense_blue(banner).to_string(), + }); + info!("{} {} started.", colors::intense_blue(banner), job_name); + + let changed_paths = Rc::new(RefCell::new(None)); + let changed_paths_ = changed_paths.clone(); + let watcher_ = watcher_communicator.clone(); + + deno_core::unsync::spawn(async move { + loop { + let received_changed_paths = watcher_receiver.recv().await; + *changed_paths_.borrow_mut() = received_changed_paths.clone(); + + match *watcher_.restart_mode.lock() { + WatcherRestartMode::Automatic => { + let _ = restart_tx.send(()); + } + WatcherRestartMode::Manual => { + // TODO(bartlomieju): should we fail on sending changed paths? + let _ = changed_paths_tx.send(received_changed_paths); + } + } + } + }); - let mut changed_paths = None; loop { // We may need to give the runtime a tick to settle, as cancellations may need to propagate // to tasks. We choose yielding 10 times to the runtime as a decent heuristic. If watch tests @@ -239,7 +308,7 @@ where let operation_future = error_handler(operation( flags.clone(), watcher_communicator.clone(), - changed_paths.take(), + changed_paths.borrow_mut().take(), )?); // don't reload dependencies after the first run @@ -251,26 +320,12 @@ where print_after_restart(); continue; }, - received_changed_paths = watcher_receiver.recv() => { - changed_paths = received_changed_paths.clone(); - - match restart_mode { - WatcherRestartMode::Automatic => { - print_after_restart(); - continue; - }, - WatcherRestartMode::Manual => { - // TODO(bartlomieju): should we fail on sending changed paths? - let _ = changed_paths_tx.send(received_changed_paths); - } - } - }, success = operation_future => { consume_paths_to_watch(&mut watcher, &mut paths_to_watch_rx); // TODO(bartlomieju): print exit code here? info!( "{} {} {}. Restarting on file change...", - colors::intense_blue("Watcher"), + colors::intense_blue(banner), job_name, if success { "finished" @@ -280,7 +335,6 @@ where ); }, }; - let receiver_future = async { loop { let maybe_paths = paths_to_watch_rx.recv().await; @@ -293,9 +347,8 @@ where // watched paths has changed. select! { _ = receiver_future => {}, - received_changed_paths = watcher_receiver.recv() => { + _ = restart_rx.recv() => { print_after_restart(); - changed_paths = received_changed_paths; continue; }, }; |