Skip to content

Commit cfddb71

Browse files
authored
feat(cli): add deno watch subcommand (#35301)
Adds a `deno watch <file>` subcommand that aliases `deno run --watch-hmr <file>`. Watch-with-hot-module-replacement is one of the more useful development workflows, but it was only reachable through a long flag on `deno run`. A dedicated subcommand makes it short to type and discoverable from `deno --help`. Internally `watch` reuses the existing `run` argument set and parsing path, only defaulting the watch configuration to HMR when no explicit watch flag is given, so all `run` flags (including `--watch-hmr=<path>`, `--watch-exclude`, and `--no-clear-screen`) continue to work and the behavior stays identical to `deno run --watch-hmr`.
1 parent 3c3a651 commit cfddb71

4 files changed

Lines changed: 157 additions & 3 deletions

File tree

cli/args/flags.rs

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,8 @@ static DENO_HELP: &str = cstr!(
956956
<p(245)>deno run main.ts | deno run --allow-net=google.com main.ts | deno main.ts</>
957957
<g>serve</> Run a server
958958
<p(245)>deno serve main.ts</>
959+
<g>watch</> Run a program, watching for changes and hot-replacing modules
960+
<p(245)>deno watch main.ts</>
959961
<g>task</> Run a task defined in the configuration file
960962
<p(245)>deno task dev</>
961963
<g>repl</> Start an interactive Read-Eval-Print Loop (REPL) for Deno
@@ -1303,7 +1305,8 @@ pub fn flags_from_vec_with_initial_cwd(
13031305
"lsp" => lsp_parse(&mut flags, &mut m),
13041306
"outdated" => outdated_parse(&mut flags, &mut m, false)?,
13051307
"repl" => repl_parse(&mut flags, &mut m)?,
1306-
"run" => run_parse(&mut flags, &mut m, app, false)?,
1308+
"run" => run_parse(&mut flags, &mut m, app, false, false)?,
1309+
"watch" => run_parse(&mut flags, &mut m, app, false, true)?,
13071310
"serve" => serve_parse(&mut flags, &mut m, app)?,
13081311
"task" => task_parse(&mut flags, &mut m, app)?,
13091312
"test" => test_parse(&mut flags, &mut m)?,
@@ -1334,7 +1337,7 @@ pub fn flags_from_vec_with_initial_cwd(
13341337
});
13351338

13361339
if has_non_globals || matches.contains_id("script_arg") {
1337-
run_parse(&mut flags, &mut matches, app, true)?;
1340+
run_parse(&mut flags, &mut matches, app, true, false)?;
13381341
} else {
13391342
handle_repl_flags(
13401343
&mut flags,
@@ -1548,6 +1551,7 @@ pub fn clap_root() -> Command {
15481551
.global(true),
15491552
)
15501553
.subcommand(run_subcommand())
1554+
.subcommand(watch_subcommand())
15511555
.subcommand(serve_subcommand())
15521556
.defer(|cmd| {
15531557
let cmd = cmd
@@ -4083,6 +4087,18 @@ Specifying the filename '-' to read the file from stdin.
40834087
<y>Read more:</> <c>https://docs.deno.com/go/run</>"), UnstableArgsConfig::ResolutionAndRuntime), false)
40844088
}
40854089

4090+
fn watch_subcommand() -> Command {
4091+
run_args(command("watch", cstr!("Run a JavaScript or TypeScript program, watching for file changes and hot-replacing modules.
4092+
4093+
This is an alias for <c>deno run --watch-hmr</>. The process restarts if hot replacement fails.
4094+
<p(245)>deno watch main.ts</>
4095+
4096+
Local files from the entry point module graph are watched by default. Additional paths can be passed with <c>--watch-hmr</>:
4097+
<p(245)>deno watch --watch-hmr=./templates main.ts</>
4098+
4099+
<y>Read more:</> <c>https://docs.deno.com/go/run</>"), UnstableArgsConfig::ResolutionAndRuntime), false)
4100+
}
4101+
40864102
fn serve_host_validator(host: &str) -> Result<String, String> {
40874103
if Url::parse(&format!("internal://{host}:9999")).is_ok() {
40884104
Ok(host.to_owned())
@@ -7725,6 +7741,7 @@ fn run_parse(
77257741
matches: &mut ArgMatches,
77267742
app: Command,
77277743
bare: bool,
7744+
force_hmr: bool,
77287745
) -> clap::error::Result<()> {
77297746
runtime_args_parse(flags, matches, true, true, true)?;
77307747
ext_arg_parse(flags, matches);
@@ -7748,7 +7765,30 @@ fn run_parse(
77487765
Some(mut script_arg) => {
77497766
let script = script_arg.next().unwrap();
77507767
flags.argv.extend(script_arg);
7751-
let watch = watch_arg_parse_with_paths(matches)?;
7768+
let mut watch = watch_arg_parse_with_paths(matches)?;
7769+
// `deno watch` is an alias for `deno run --watch-hmr`, so enable hot
7770+
// module replacement watching by default when no explicit watch flag was
7771+
// passed.
7772+
if force_hmr {
7773+
match &mut watch {
7774+
Some(watch) => watch.hmr = true,
7775+
None => {
7776+
watch = Some(WatchFlagsWithPaths {
7777+
hmr: true,
7778+
paths: vec![],
7779+
no_clear_screen: matches.get_flag("no-clear-screen"),
7780+
exclude: matches
7781+
.remove_many::<String>("watch-exclude")
7782+
.map(|f| {
7783+
f.flat_map(flat_escape_split_commas)
7784+
.collect::<Result<_, _>>()
7785+
})
7786+
.transpose()?
7787+
.unwrap_or_default(),
7788+
});
7789+
}
7790+
}
7791+
}
77527792
if script == "-" && watch.is_some() {
77537793
return Err(clap::Error::raw(
77547794
clap::error::ErrorKind::ArgumentConflict,
@@ -9186,6 +9226,94 @@ mod tests {
91869226
assert!(r.is_err());
91879227
}
91889228

9229+
#[test]
9230+
fn watch_subcommand() {
9231+
// `deno watch script.ts` is an alias for `deno run --watch-hmr script.ts`.
9232+
let r = flags_from_vec(svec!["deno", "watch", "script.ts"]);
9233+
let flags = r.unwrap();
9234+
assert_eq!(
9235+
flags,
9236+
Flags {
9237+
subcommand: DenoSubcommand::Run(RunFlags {
9238+
script: "script.ts".to_string(),
9239+
watch: Some(WatchFlagsWithPaths {
9240+
hmr: true,
9241+
paths: vec![],
9242+
no_clear_screen: false,
9243+
exclude: vec![],
9244+
}),
9245+
bare: false,
9246+
coverage_dir: None,
9247+
print_task_list: false,
9248+
}),
9249+
code_cache_enabled: true,
9250+
..Flags::default()
9251+
}
9252+
);
9253+
9254+
// Additional watched paths and watch options are still respected.
9255+
let r = flags_from_vec(svec![
9256+
"deno",
9257+
"watch",
9258+
"--watch-hmr=foo.txt",
9259+
"--no-clear-screen",
9260+
"--watch-exclude=bar.txt",
9261+
"script.ts"
9262+
]);
9263+
let flags = r.unwrap();
9264+
assert_eq!(
9265+
flags,
9266+
Flags {
9267+
subcommand: DenoSubcommand::Run(RunFlags {
9268+
script: "script.ts".to_string(),
9269+
watch: Some(WatchFlagsWithPaths {
9270+
hmr: true,
9271+
paths: vec![String::from("foo.txt")],
9272+
no_clear_screen: true,
9273+
exclude: vec![String::from("bar.txt")],
9274+
}),
9275+
bare: false,
9276+
coverage_dir: None,
9277+
print_task_list: false,
9278+
}),
9279+
code_cache_enabled: true,
9280+
..Flags::default()
9281+
}
9282+
);
9283+
9284+
// `--watch-exclude` is honored even without an explicit `--watch-hmr` flag.
9285+
let r = flags_from_vec(svec![
9286+
"deno",
9287+
"watch",
9288+
"--watch-exclude=bar.txt",
9289+
"script.ts"
9290+
]);
9291+
let flags = r.unwrap();
9292+
assert_eq!(
9293+
flags,
9294+
Flags {
9295+
subcommand: DenoSubcommand::Run(RunFlags {
9296+
script: "script.ts".to_string(),
9297+
watch: Some(WatchFlagsWithPaths {
9298+
hmr: true,
9299+
paths: vec![],
9300+
no_clear_screen: false,
9301+
exclude: vec![String::from("bar.txt")],
9302+
}),
9303+
bare: false,
9304+
coverage_dir: None,
9305+
print_task_list: false,
9306+
}),
9307+
code_cache_enabled: true,
9308+
..Flags::default()
9309+
}
9310+
);
9311+
9312+
// Reading from stdin while watching is not supported.
9313+
let r = flags_from_vec(svec!["deno", "watch", "-"]);
9314+
assert!(r.is_err());
9315+
}
9316+
91899317
#[test]
91909318
fn run_watch_with_external() {
91919319
let r = flags_from_vec(svec!["deno", "--watch=file1,file2", "script.ts"]);

tests/specs/watch/__test__.jsonc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"tests": {
3+
// `deno watch` is an alias for `deno run --watch-hmr`, so its help text
4+
// points at the same behavior.
5+
"help": {
6+
"args": "watch --help",
7+
"output": "help.out"
8+
},
9+
// Watching while reading the program from stdin is not supported, same as
10+
// `deno run --watch-hmr -`.
11+
"stdin_conflict": {
12+
"args": "watch -",
13+
"exitCode": 1,
14+
"output": "stdin_conflict.out"
15+
}
16+
}
17+
}

tests/specs/watch/help.out

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Run a JavaScript or TypeScript program, watching for file changes and hot-replacing modules.
2+
3+
This is an alias for deno run --watch-hmr. The process restarts if hot replacement fails.
4+
deno watch main.ts
5+
[WILDCARD]
6+
Usage: deno watch [OPTIONS] [SCRIPT_ARG]...
7+
[WILDCARD]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
error: --watch and --watch-hmr cannot be used while reading from stdin.
2+
[WILDCARD]

0 commit comments

Comments
 (0)