diff options
Diffstat (limited to 'cli/file_watcher.rs')
-rw-r--r-- | cli/file_watcher.rs | 143 |
1 files changed, 125 insertions, 18 deletions
diff --git a/cli/file_watcher.rs b/cli/file_watcher.rs index 1730e6472..c76e29219 100644 --- a/cli/file_watcher.rs +++ b/cli/file_watcher.rs @@ -4,7 +4,7 @@ use crate::colors; use core::task::{Context, Poll}; use deno_core::error::AnyError; use deno_core::futures::stream::{Stream, StreamExt}; -use deno_core::futures::Future; +use deno_core::futures::{Future, FutureExt}; use notify::event::Event as NotifyEvent; use notify::event::EventKind; use notify::Config; @@ -18,22 +18,21 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::select; -use tokio::time::{interval, Interval}; +use tokio::time::{delay_for, Delay}; const DEBOUNCE_INTERVAL_MS: Duration = Duration::from_millis(200); -// TODO(bartlomieju): rename -type WatchFuture = Pin<Box<dyn Future<Output = Result<(), AnyError>>>>; +type FileWatcherFuture<T> = Pin<Box<dyn Future<Output = Result<T, AnyError>>>>; struct Debounce { - interval: Interval, + delay: Delay, event_detected: Arc<AtomicBool>, } impl Debounce { fn new() -> Self { Self { - interval: interval(DEBOUNCE_INTERVAL_MS), + delay: delay_for(DEBOUNCE_INTERVAL_MS), event_detected: Arc::new(AtomicBool::new(false)), } } @@ -53,13 +52,18 @@ impl Stream for Debounce { inner.event_detected.store(false, Ordering::Relaxed); Poll::Ready(Some(())) } else { - let _ = inner.interval.poll_tick(cx); - Poll::Pending + match inner.delay.poll_unpin(cx) { + Poll::Ready(_) => { + inner.delay = delay_for(DEBOUNCE_INTERVAL_MS); + Poll::Pending + } + Poll::Pending => Poll::Pending, + } } } } -async fn error_handler(watch_future: WatchFuture) { +async fn error_handler(watch_future: FileWatcherFuture<()>) { let result = watch_future.await; if let Err(err) = result { let msg = format!("{}: {}", colors::red_bold("error"), err.to_string(),); @@ -67,19 +71,37 @@ async fn error_handler(watch_future: WatchFuture) { } } -pub async fn watch_func<F>( - paths: &[PathBuf], - closure: F, +/// This function adds watcher functionality to subcommands like `fmt` or `lint`. +/// The difference from [`watch_func_with_module_resolution`] is that this doesn't depend on +/// [`ModuleGraph`]. +/// +/// - `target_resolver` is used for resolving file paths to be watched at every restarting of the watcher. The +/// return value of this closure will then be passed to `operation` as an argument. +/// +/// - `operation` is the actual operation we want to run every time the watcher detects file +/// changes. For example, in the case where we would like to apply `fmt`, then `operation` would +/// have the logic for it like calling `format_source_files`. +/// +/// - `job_name` is just used for printing watcher status to terminal. +/// +/// Note that the watcher will stop working if `target_resolver` fails at some point. +/// +/// [`ModuleGraph`]: crate::module_graph::Graph +pub async fn watch_func<F, G>( + target_resolver: F, + operation: G, + job_name: &str, ) -> Result<(), AnyError> where - F: Fn() -> WatchFuture, + F: Fn() -> Result<Vec<PathBuf>, AnyError>, + G: Fn(Vec<PathBuf>) -> FileWatcherFuture<()>, { let mut debounce = Debounce::new(); - // This binding is required for the watcher to work properly without being dropped. - let _watcher = new_watcher(paths, &debounce)?; loop { - let func = error_handler(closure()); + let paths = target_resolver()?; + let _watcher = new_watcher(&paths, &debounce)?; + let func = error_handler(operation(paths)); let mut is_file_changed = false; select! { _ = debounce.next() => { @@ -90,11 +112,95 @@ where ); }, _ = func => {}, + }; + + if !is_file_changed { + info!( + "{} {} finished! Restarting on file change...", + colors::intense_blue("Watcher"), + job_name, + ); + debounce.next().await; + info!( + "{} File change detected! Restarting!", + colors::intense_blue("Watcher"), + ); + } + } +} + +/// This function adds watcher functionality to subcommands like `run` or `bundle`. +/// The difference from [`watch_func`] is that this does depend on [`ModuleGraph`]. +/// +/// - `module_resolver` is used for both resolving file paths to be watched at every restarting +/// of the watcher and building [`ModuleGraph`] or [`ModuleSpecifier`] which will then be passed +/// to `operation`. +/// +/// - `operation` is the actual operation we want to run every time the watcher detects file +/// changes. For example, in the case where we would like to bundle, then `operation` would +/// have the logic for it like doing bundle with the help of [`ModuleGraph`]. +/// +/// - `job_name` is just used for printing watcher status to terminal. +/// +/// Note that the watcher will try to continue watching files using the previously resolved +/// data if `module_resolver` fails at some point, which means the watcher won't work at all +/// if `module_resolver` fails at the first attempt. +/// +/// [`ModuleGraph`]: crate::module_graph::Graph +/// [`ModuleSpecifier`]: deno_core::ModuleSpecifier +pub async fn watch_func_with_module_resolution<F, G, T>( + module_resolver: F, + operation: G, + job_name: &str, +) -> Result<(), AnyError> +where + F: Fn() -> FileWatcherFuture<(Vec<PathBuf>, T)>, + G: Fn(T) -> FileWatcherFuture<()>, + T: Clone, +{ + let mut debounce = Debounce::new(); + // Store previous data. If module resolution fails at some point, the watcher will try to + // continue watching files using these data. + let mut paths = None; + let mut module = None; + + loop { + match module_resolver().await { + Ok((next_paths, next_module)) => { + paths = Some(next_paths); + module = Some(next_module); + } + Err(e) => { + // If at least one of `paths` and `module` is `None`, the watcher cannot decide which files + // should be watched. So return the error immediately without watching anything. + if paths.is_none() || module.is_none() { + return Err(e); + } + } } + // These `unwrap`s never cause panic since `None` is already checked above. + let cur_paths = paths.clone().unwrap(); + let cur_module = module.clone().unwrap(); + + let _watcher = new_watcher(&cur_paths, &debounce)?; + let func = error_handler(operation(cur_module)); + let mut is_file_changed = false; + select! { + _ = debounce.next() => { + is_file_changed = true; + info!( + "{} File change detected! Restarting!", + colors::intense_blue("Watcher"), + ); + }, + _ = func => {}, + }; + if !is_file_changed { info!( - "{} Process terminated! Restarting on file change...", + "{} {} finished! Restarting on file change...", colors::intense_blue("Watcher"), + job_name, ); debounce.next().await; info!( @@ -125,7 +231,8 @@ fn new_watcher( watcher.configure(Config::PreciseEvents(true)).unwrap(); for path in paths { - watcher.watch(path, RecursiveMode::NonRecursive)?; + // Ignore any error e.g. `PathNotFound` + let _ = watcher.watch(path, RecursiveMode::NonRecursive); } Ok(watcher) |