summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/args/flags.rs210
-rw-r--r--cli/util/file_watcher.rs33
-rw-r--r--tests/integration/watcher_tests.rs39
3 files changed, 273 insertions, 9 deletions
diff --git a/cli/args/flags.rs b/cli/args/flags.rs
index 1bd30bd68..804e766a2 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<String>,
}
#[derive(Clone, Default, Debug, Eq, PartialEq)]
@@ -256,6 +260,7 @@ pub struct WatchFlagsWithPaths {
pub hmr: bool,
pub paths: Vec<String>,
pub no_clear_screen: bool,
+ pub exclude: Vec<String>,
}
#[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<PathOrPatternSet, AnyError> {
+ 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<WatchFlags> {
Some(WatchFlags {
hmr: false,
no_clear_screen: matches.get_flag("no-clear-screen"),
+ exclude: matches
+ .remove_many::<String>("watch-exclude")
+ .map(|f| f.collect::<Vec<String>>())
+ .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::<String>("watch-exclude")
+ .map(|f| f.collect::<Vec<String>>())
+ .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::<String>("watch-exclude")
+ .map(|f| f.collect::<Vec<String>>())
+ .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 33b764bbf..50ae7c233 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, AnyError>,
F: Future<Output = Result<(), AnyError>>,
{
+ 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<Vec<PathBuf>>,
+ 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 6a2cab08a..6d18ad2f4 100644
--- a/tests/integration/watcher_tests.rs
+++ b/tests/integration/watcher_tests.rs
@@ -1614,6 +1614,45 @@ async fn run_watch_inspect() {
}
#[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();
let file_to_watch = t.path().join("file_to_watch.js");