diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 1bd30bd683..804e766a20 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -9,7 +9,10 @@ use clap::ArgMatches; use clap::ColorChoice; use clap::Command; use clap::ValueHint; +use deno_config::glob::PathOrPatternSet; use deno_config::ConfigFlag; +use deno_core::anyhow::Context; +use deno_core::error::AnyError; use deno_core::resolve_url_or_path; use deno_core::url::Url; use deno_graph::GraphKind; @@ -249,6 +252,7 @@ impl RunFlags { pub struct WatchFlags { pub hmr: bool, pub no_clear_screen: bool, + pub exclude: Vec, } #[derive(Clone, Default, Debug, Eq, PartialEq)] @@ -256,6 +260,7 @@ pub struct WatchFlagsWithPaths { pub hmr: bool, pub paths: Vec, pub no_clear_screen: bool, + pub exclude: Vec, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -831,6 +836,69 @@ impl Flags { self.allow_ffi = Some(vec![]); self.allow_hrtime = true; } + + pub fn resolve_watch_exclude_set( + &self, + ) -> Result { + if let DenoSubcommand::Run(RunFlags { + watch: + Some(WatchFlagsWithPaths { + exclude: excluded_paths, + .. + }), + .. + }) + | DenoSubcommand::Bundle(BundleFlags { + watch: + Some(WatchFlags { + exclude: excluded_paths, + .. + }), + .. + }) + | DenoSubcommand::Bench(BenchFlags { + watch: + Some(WatchFlags { + exclude: excluded_paths, + .. + }), + .. + }) + | DenoSubcommand::Test(TestFlags { + watch: + Some(WatchFlags { + exclude: excluded_paths, + .. + }), + .. + }) + | DenoSubcommand::Lint(LintFlags { + watch: + Some(WatchFlags { + exclude: excluded_paths, + .. + }), + .. + }) + | DenoSubcommand::Fmt(FmtFlags { + watch: + Some(WatchFlags { + exclude: excluded_paths, + .. + }), + .. + }) = &self.subcommand + { + let cwd = std::env::current_dir()?; + PathOrPatternSet::from_exclude_relative_path_or_patterns( + &cwd, + excluded_paths, + ) + .context("Failed resolving watch exclude patterns.") + } else { + Ok(PathOrPatternSet::default()) + } + } } static ENV_VARIABLES_HELP: &str = color_print::cstr!( @@ -1211,6 +1279,7 @@ glob {*_,*.,}bench.{js,mjs,ts,mts,jsx,tsx}: .action(ArgAction::SetTrue), ) .arg(watch_arg(false)) + .arg(watch_exclude_arg()) .arg(no_clear_screen_arg()) .arg(script_arg().last(true)) .arg(env_file_arg()) @@ -1243,6 +1312,7 @@ If no output file is given, the output is written to standard output: ) .arg(Arg::new("out_file").value_hint(ValueHint::FilePath)) .arg(watch_arg(false)) + .arg(watch_exclude_arg()) .arg(no_clear_screen_arg()) .arg(executable_ext_arg()) }) @@ -1726,6 +1796,7 @@ Ignore formatting a file by adding an ignore comment at the top of the file: .value_hint(ValueHint::AnyPath), ) .arg(watch_arg(false)) + .arg(watch_exclude_arg()) .arg(no_clear_screen_arg()) .arg( Arg::new("use-tabs") @@ -2095,6 +2166,7 @@ Ignore linting a file by adding an ignore comment at the top of the file: .value_hint(ValueHint::AnyPath), ) .arg(watch_arg(false)) + .arg(watch_exclude_arg()) .arg(no_clear_screen_arg()) }) } @@ -2126,6 +2198,7 @@ fn run_subcommand() -> Command { runtime_args(Command::new("run"), true, true) .arg(check_arg(false)) .arg(watch_arg(true)) + .arg(watch_exclude_arg()) .arg(hmr_arg(true)) .arg(no_clear_screen_arg()) .arg(executable_ext_arg()) @@ -2308,6 +2381,7 @@ Directory arguments are expanded to all contained files matching the glob .conflicts_with("no-run") .conflicts_with("coverage"), ) + .arg(watch_exclude_arg()) .arg(no_clear_screen_arg()) .arg(script_arg().last(true)) .arg( @@ -3120,6 +3194,18 @@ fn no_clear_screen_arg() -> Arg { .help("Do not clear terminal screen when under watch mode") } +fn watch_exclude_arg() -> Arg { + Arg::new("watch-exclude") + .long("watch-exclude") + .help("Exclude provided files/patterns from watch mode") + .value_name("FILES") + .num_args(0..) + .value_parser(value_parser!(String)) + .use_value_delimiter(true) + .require_equals(true) + .value_hint(ValueHint::AnyPath) +} + fn no_check_arg() -> Arg { Arg::new("no-check") .num_args(0..=1) @@ -4263,6 +4349,10 @@ fn watch_arg_parse(matches: &mut ArgMatches) -> Option { Some(WatchFlags { hmr: false, no_clear_screen: matches.get_flag("no-clear-screen"), + exclude: matches + .remove_many::("watch-exclude") + .map(|f| f.collect::>()) + .unwrap_or_default(), }) } else { None @@ -4277,6 +4367,10 @@ fn watch_arg_parse_with_paths( paths: paths.collect(), hmr: false, no_clear_screen: matches.get_flag("no-clear-screen"), + exclude: matches + .remove_many::("watch-exclude") + .map(|f| f.collect::>()) + .unwrap_or_default(), }); } @@ -4286,6 +4380,10 @@ fn watch_arg_parse_with_paths( paths: paths.collect(), hmr: true, no_clear_screen: matches.get_flag("no-clear-screen"), + exclude: matches + .remove_many::("watch-exclude") + .map(|f| f.collect::>()) + .unwrap_or_default(), }) } @@ -4424,6 +4522,7 @@ mod tests { hmr: false, paths: vec![], no_clear_screen: false, + exclude: vec![], }), }), ..Flags::default() @@ -4447,6 +4546,7 @@ mod tests { hmr: false, paths: vec![], no_clear_screen: true, + exclude: vec![], }), }), ..Flags::default() @@ -4470,6 +4570,7 @@ mod tests { hmr: true, paths: vec![], no_clear_screen: true, + exclude: vec![], }), }), ..Flags::default() @@ -4493,6 +4594,7 @@ mod tests { hmr: true, paths: vec![String::from("foo.txt")], no_clear_screen: true, + exclude: vec![], }), }), ..Flags::default() @@ -4518,6 +4620,7 @@ mod tests { hmr: false, paths: vec![String::from("file1"), String::from("file2")], no_clear_screen: false, + exclude: vec![], }), }), ..Flags::default() @@ -4545,6 +4648,109 @@ mod tests { hmr: false, paths: vec![], no_clear_screen: true, + exclude: vec![], + }), + }), + ..Flags::default() + } + ); + } + + #[test] + fn run_watch_with_excluded_paths() { + let r = flags_from_vec(svec!( + "deno", + "run", + "--watch", + "--watch-exclude=foo", + "script.ts" + )); + + let flags = r.unwrap(); + assert_eq!( + flags, + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + script: "script.ts".to_string(), + watch: Some(WatchFlagsWithPaths { + hmr: false, + paths: vec![], + no_clear_screen: false, + exclude: vec![String::from("foo")], + }), + }), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec!( + "deno", + "run", + "--watch=foo", + "--watch-exclude=bar", + "script.ts" + )); + let flags = r.unwrap(); + assert_eq!( + flags, + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + script: "script.ts".to_string(), + watch: Some(WatchFlagsWithPaths { + hmr: false, + paths: vec![String::from("foo")], + no_clear_screen: false, + exclude: vec![String::from("bar")], + }), + }), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec![ + "deno", + "run", + "--watch", + "--watch-exclude=foo,bar", + "script.ts" + ]); + + let flags = r.unwrap(); + assert_eq!( + flags, + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + script: "script.ts".to_string(), + watch: Some(WatchFlagsWithPaths { + hmr: false, + paths: vec![], + no_clear_screen: false, + exclude: vec![String::from("foo"), String::from("bar")], + }), + }), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec![ + "deno", + "run", + "--watch=foo,bar", + "--watch-exclude=baz,qux", + "script.ts" + ]); + + let flags = r.unwrap(); + assert_eq!( + flags, + Flags { + subcommand: DenoSubcommand::Run(RunFlags { + script: "script.ts".to_string(), + watch: Some(WatchFlagsWithPaths { + hmr: false, + paths: vec![String::from("foo"), String::from("bar")], + no_clear_screen: false, + exclude: vec![String::from("baz"), String::from("qux"),], }), }), ..Flags::default() @@ -4876,6 +5082,7 @@ mod tests { watch: Some(WatchFlags { hmr: false, no_clear_screen: true, + exclude: vec![], }) }), ext: Some("ts".to_string()), @@ -5112,6 +5319,7 @@ mod tests { watch: Some(WatchFlags { hmr: false, no_clear_screen: true, + exclude: vec![], }) }), ..Flags::default() @@ -6350,6 +6558,7 @@ mod tests { watch: Some(WatchFlags { hmr: false, no_clear_screen: true, + exclude: vec![], }), }), type_check_mode: TypeCheckMode::Local, @@ -7624,6 +7833,7 @@ mod tests { watch: Some(WatchFlags { hmr: false, no_clear_screen: true, + exclude: vec![], }), reporter: Default::default(), junit_path: None, diff --git a/cli/util/file_watcher.rs b/cli/util/file_watcher.rs index 33b764bbf2..50ae7c2339 100644 --- a/cli/util/file_watcher.rs +++ b/cli/util/file_watcher.rs @@ -4,6 +4,7 @@ use crate::args::Flags; use crate::colors; use crate::util::fs::canonicalize_path; +use deno_config::glob::PathOrPatternSet; use deno_core::error::AnyError; use deno_core::error::JsError; use deno_core::futures::Future; @@ -244,6 +245,7 @@ where ) -> Result, F: Future>, { + let exclude_set = flags.resolve_watch_exclude_set()?; let (paths_to_watch_tx, mut paths_to_watch_rx) = tokio::sync::mpsc::unbounded_channel(); let (restart_tx, mut restart_rx) = tokio::sync::mpsc::unbounded_channel(); @@ -297,12 +299,12 @@ where } let mut watcher = new_watcher(watcher_sender.clone())?; - consume_paths_to_watch(&mut watcher, &mut paths_to_watch_rx); + consume_paths_to_watch(&mut watcher, &mut paths_to_watch_rx, &exclude_set); let receiver_future = async { loop { let maybe_paths = paths_to_watch_rx.recv().await; - add_paths_to_watcher(&mut watcher, &maybe_paths.unwrap()); + add_paths_to_watcher(&mut watcher, &maybe_paths.unwrap(), &exclude_set); } }; let operation_future = error_handler(operation( @@ -321,7 +323,7 @@ where continue; }, success = operation_future => { - consume_paths_to_watch(&mut watcher, &mut paths_to_watch_rx); + consume_paths_to_watch(&mut watcher, &mut paths_to_watch_rx, &exclude_set); // TODO(bartlomieju): print exit code here? info!( "{} {} {}. Restarting on file change...", @@ -334,11 +336,11 @@ where } ); }, - }; + } let receiver_future = async { loop { let maybe_paths = paths_to_watch_rx.recv().await; - add_paths_to_watcher(&mut watcher, &maybe_paths.unwrap()); + add_paths_to_watcher(&mut watcher, &maybe_paths.unwrap(), &exclude_set); } }; @@ -351,7 +353,7 @@ where print_after_restart(); continue; }, - }; + } } } @@ -376,28 +378,41 @@ fn new_watcher( .iter() .filter_map(|path| canonicalize_path(path).ok()) .collect(); + sender.send(paths).unwrap(); }, Default::default(), )?) } -fn add_paths_to_watcher(watcher: &mut RecommendedWatcher, paths: &[PathBuf]) { +fn add_paths_to_watcher( + watcher: &mut RecommendedWatcher, + paths: &[PathBuf], + paths_to_exclude: &PathOrPatternSet, +) { // Ignore any error e.g. `PathNotFound` + let mut watched_paths = Vec::new(); + for path in paths { + if paths_to_exclude.matches_path(path) { + continue; + } + + watched_paths.push(path.clone()); let _ = watcher.watch(path, RecursiveMode::Recursive); } - log::debug!("Watching paths: {:?}", paths); + log::debug!("Watching paths: {:?}", watched_paths); } fn consume_paths_to_watch( watcher: &mut RecommendedWatcher, receiver: &mut UnboundedReceiver>, + exclude_set: &PathOrPatternSet, ) { loop { match receiver.try_recv() { Ok(paths) => { - add_paths_to_watcher(watcher, &paths); + add_paths_to_watcher(watcher, &paths, exclude_set); } Err(e) => match e { mpsc::error::TryRecvError::Empty => { diff --git a/tests/integration/watcher_tests.rs b/tests/integration/watcher_tests.rs index 6a2cab08a5..6d18ad2f4b 100644 --- a/tests/integration/watcher_tests.rs +++ b/tests/integration/watcher_tests.rs @@ -1613,6 +1613,45 @@ async fn run_watch_inspect() { check_alive_then_kill(child); } +#[tokio::test] +async fn run_watch_with_excluded_paths() { + let t = TempDir::new(); + + let file_to_exclude = t.path().join("file_to_exclude.js"); + file_to_exclude.write("export const foo = 0;"); + + let file_to_watch = t.path().join("file_to_watch.js"); + file_to_watch + .write("import { foo } from './file_to_exclude.js'; console.log(foo);"); + + let mjs_file_to_exclude = t.path().join("file_to_exclude.mjs"); + mjs_file_to_exclude.write("export const foo = 0;"); + + let mut child = util::deno_cmd() + .current_dir(util::testdata_path()) + .arg("run") + .arg("--watch") + .arg("--watch-exclude=file_to_exclude.js,*.mjs") + .arg("-L") + .arg("debug") + .arg(&file_to_watch) + .env("NO_COLOR", "1") + .piped_output() + .spawn() + .unwrap(); + let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child); + + wait_contains("0", &mut stdout_lines).await; + wait_for_watcher("file_to_watch.js", &mut stderr_lines).await; + + // Confirm that restarting doesn't occurs when a excluded file is updated + file_to_exclude.write("export const foo = 42;"); + mjs_file_to_exclude.write("export const foo = 42;"); + + wait_contains("finished", &mut stderr_lines).await; + check_alive_then_kill(child); +} + #[tokio::test] async fn run_hmr_server() { let t = TempDir::new();