mirror of
https://github.com/denoland/deno.git
synced 2024-12-26 00:59:24 -05:00
feat: add --watch-exclude
flag (#21935)
This PR introduces the ability to exclude certain paths from the file watcher in Deno. This is particularly useful when running scripts in watch mode, as it allows developers to prevent unnecessary restarts when changes are made to files that do not affect the running script, or when executing scripts that generate new files which results in an infinite restart loop. --------- Co-authored-by: David Sherret <dsherret@gmail.com>
This commit is contained in:
parent
d31f2307ee
commit
08d5d32dfc
3 changed files with 273 additions and 9 deletions
|
@ -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,
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue