summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/args/flags.rs158
-rw-r--r--cli/args/mod.rs12
-rw-r--r--cli/emit.rs20
-rw-r--r--cli/factory.rs14
-rw-r--r--cli/graph_util.rs4
-rw-r--r--cli/standalone/mod.rs3
-rw-r--r--cli/tests/integration/watcher_tests.rs254
-rw-r--r--cli/tools/bench/mod.rs8
-rw-r--r--cli/tools/bundle.rs8
-rw-r--r--cli/tools/fmt.rs7
-rw-r--r--cli/tools/lint.rs5
-rw-r--r--cli/tools/run/hmr/json_types.rs59
-rw-r--r--cli/tools/run/hmr/mod.rs242
-rw-r--r--cli/tools/run/mod.rs (renamed from cli/tools/run.rs)24
-rw-r--r--cli/tools/test/mod.rs8
-rw-r--r--cli/util/file_watcher.rs141
-rw-r--r--cli/worker.rs74
17 files changed, 933 insertions, 108 deletions
diff --git a/cli/args/flags.rs b/cli/args/flags.rs
index 5b411e36b..271a56ac3 100644
--- a/cli/args/flags.rs
+++ b/cli/args/flags.rs
@@ -212,11 +212,13 @@ impl RunFlags {
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct WatchFlags {
+ pub hmr: bool,
pub no_clear_screen: bool,
}
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct WatchFlagsWithPaths {
+ pub hmr: bool,
pub paths: Vec<PathBuf>,
pub no_clear_screen: bool,
}
@@ -1860,6 +1862,7 @@ fn run_subcommand() -> Command {
runtime_args(Command::new("run"), true, true)
.arg(check_arg(false))
.arg(watch_arg(true))
+ .arg(hmr_arg(true))
.arg(no_clear_screen_arg())
.arg(executable_ext_arg())
.arg(
@@ -2728,6 +2731,33 @@ fn seed_arg() -> Arg {
.value_parser(value_parser!(u64))
}
+fn hmr_arg(takes_files: bool) -> Arg {
+ let arg = Arg::new("hmr")
+ .long("unstable-hmr")
+ .help("UNSTABLE: Watch for file changes and hot replace modules")
+ .conflicts_with("watch");
+
+ if takes_files {
+ arg
+ .value_name("FILES")
+ .num_args(0..)
+ .value_parser(value_parser!(PathBuf))
+ .use_value_delimiter(true)
+ .require_equals(true)
+ .long_help(
+ "Watch for file changes and restart process automatically.
+Local files from entry point module graph are watched by default.
+Additional paths might be watched by passing them as arguments to this flag.",
+ )
+ .value_hint(ValueHint::AnyPath)
+ } else {
+ arg.action(ArgAction::SetTrue).long_help(
+ "Watch for file changes and restart process automatically.
+ Only local files from entry point module graph are watched.",
+ )
+ }
+}
+
fn watch_arg(takes_files: bool) -> Arg {
let arg = Arg::new("watch")
.long("watch")
@@ -3849,6 +3879,7 @@ fn reload_arg_validate(urlstr: &str) -> Result<String, String> {
fn watch_arg_parse(matches: &mut ArgMatches) -> Option<WatchFlags> {
if matches.get_flag("watch") {
Some(WatchFlags {
+ hmr: false,
no_clear_screen: matches.get_flag("no-clear-screen"),
})
} else {
@@ -3859,10 +3890,19 @@ fn watch_arg_parse(matches: &mut ArgMatches) -> Option<WatchFlags> {
fn watch_arg_parse_with_paths(
matches: &mut ArgMatches,
) -> Option<WatchFlagsWithPaths> {
+ if let Some(paths) = matches.remove_many::<PathBuf>("watch") {
+ return Some(WatchFlagsWithPaths {
+ paths: paths.collect(),
+ hmr: false,
+ no_clear_screen: matches.get_flag("no-clear-screen"),
+ });
+ }
+
matches
- .remove_many::<PathBuf>("watch")
- .map(|f| WatchFlagsWithPaths {
- paths: f.collect(),
+ .remove_many::<PathBuf>("hmr")
+ .map(|paths| WatchFlagsWithPaths {
+ paths: paths.collect(),
+ hmr: true,
no_clear_screen: matches.get_flag("no-clear-screen"),
})
}
@@ -3980,6 +4020,7 @@ mod tests {
subcommand: DenoSubcommand::Run(RunFlags {
script: "script.ts".to_string(),
watch: Some(WatchFlagsWithPaths {
+ hmr: false,
paths: vec![],
no_clear_screen: false,
}),
@@ -3987,6 +4028,79 @@ mod tests {
..Flags::default()
}
);
+
+ let r = flags_from_vec(svec![
+ "deno",
+ "run",
+ "--watch",
+ "--no-clear-screen",
+ "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: true,
+ }),
+ }),
+ ..Flags::default()
+ }
+ );
+
+ let r = flags_from_vec(svec![
+ "deno",
+ "run",
+ "--unstable-hmr",
+ "--no-clear-screen",
+ "script.ts"
+ ]);
+ let flags = r.unwrap();
+ assert_eq!(
+ flags,
+ Flags {
+ subcommand: DenoSubcommand::Run(RunFlags {
+ script: "script.ts".to_string(),
+ watch: Some(WatchFlagsWithPaths {
+ hmr: true,
+ paths: vec![],
+ no_clear_screen: true,
+ }),
+ }),
+ ..Flags::default()
+ }
+ );
+
+ let r = flags_from_vec(svec![
+ "deno",
+ "run",
+ "--unstable-hmr=foo.txt",
+ "--no-clear-screen",
+ "script.ts"
+ ]);
+ let flags = r.unwrap();
+ assert_eq!(
+ flags,
+ Flags {
+ subcommand: DenoSubcommand::Run(RunFlags {
+ script: "script.ts".to_string(),
+ watch: Some(WatchFlagsWithPaths {
+ hmr: true,
+ paths: vec![PathBuf::from("foo.txt")],
+ no_clear_screen: true,
+ }),
+ }),
+ ..Flags::default()
+ }
+ );
+
+ let r =
+ flags_from_vec(svec!["deno", "run", "--hmr", "--watch", "script.ts"]);
+ assert!(r.is_err());
}
#[test]
@@ -4000,6 +4114,7 @@ mod tests {
subcommand: DenoSubcommand::Run(RunFlags {
script: "script.ts".to_string(),
watch: Some(WatchFlagsWithPaths {
+ hmr: false,
paths: vec![PathBuf::from("file1"), PathBuf::from("file2")],
no_clear_screen: false,
}),
@@ -4026,6 +4141,7 @@ mod tests {
subcommand: DenoSubcommand::Run(RunFlags {
script: "script.ts".to_string(),
watch: Some(WatchFlagsWithPaths {
+ hmr: false,
paths: vec![],
no_clear_screen: true,
})
@@ -4347,9 +4463,7 @@ mod tests {
single_quote: None,
prose_wrap: None,
no_semicolons: None,
- watch: Some(WatchFlags {
- no_clear_screen: false,
- })
+ watch: Some(Default::default()),
}),
ext: Some("ts".to_string()),
..Flags::default()
@@ -4374,6 +4488,7 @@ mod tests {
prose_wrap: None,
no_semicolons: None,
watch: Some(WatchFlags {
+ hmr: false,
no_clear_screen: true,
})
}),
@@ -4405,9 +4520,7 @@ mod tests {
single_quote: None,
prose_wrap: None,
no_semicolons: None,
- watch: Some(WatchFlags {
- no_clear_screen: false,
- })
+ watch: Some(Default::default()),
}),
ext: Some("ts".to_string()),
..Flags::default()
@@ -4461,9 +4574,7 @@ mod tests {
single_quote: None,
prose_wrap: None,
no_semicolons: None,
- watch: Some(WatchFlags {
- no_clear_screen: false,
- })
+ watch: Some(Default::default()),
}),
config_flag: ConfigFlag::Path("deno.jsonc".to_string()),
ext: Some("ts".to_string()),
@@ -4587,9 +4698,7 @@ mod tests {
maybe_rules_exclude: None,
json: false,
compact: false,
- watch: Some(WatchFlags {
- no_clear_screen: false,
- })
+ watch: Some(Default::default()),
}),
..Flags::default()
}
@@ -4621,6 +4730,7 @@ mod tests {
json: false,
compact: false,
watch: Some(WatchFlags {
+ hmr: false,
no_clear_screen: true,
})
}),
@@ -5823,9 +5933,7 @@ mod tests {
subcommand: DenoSubcommand::Bundle(BundleFlags {
source_file: "source.ts".to_string(),
out_file: None,
- watch: Some(WatchFlags {
- no_clear_screen: false,
- }),
+ watch: Some(Default::default()),
}),
type_check_mode: TypeCheckMode::Local,
..Flags::default()
@@ -5849,6 +5957,7 @@ mod tests {
source_file: "source.ts".to_string(),
out_file: None,
watch: Some(WatchFlags {
+ hmr: false,
no_clear_screen: true,
}),
}),
@@ -7017,9 +7126,7 @@ mod tests {
concurrent_jobs: None,
trace_ops: false,
coverage_dir: None,
- watch: Some(WatchFlags {
- no_clear_screen: false,
- }),
+ watch: Some(Default::default()),
reporter: Default::default(),
junit_path: None,
}),
@@ -7049,9 +7156,7 @@ mod tests {
concurrent_jobs: None,
trace_ops: false,
coverage_dir: None,
- watch: Some(WatchFlags {
- no_clear_screen: false,
- }),
+ watch: Some(Default::default()),
reporter: Default::default(),
junit_path: None,
}),
@@ -7084,6 +7189,7 @@ mod tests {
trace_ops: false,
coverage_dir: None,
watch: Some(WatchFlags {
+ hmr: false,
no_clear_screen: true,
}),
reporter: Default::default(),
@@ -7851,9 +7957,7 @@ mod tests {
include: vec![],
ignore: vec![],
},
- watch: Some(WatchFlags {
- no_clear_screen: false,
- }),
+ watch: Some(Default::default()),
}),
no_prompt: true,
type_check_mode: TypeCheckMode::Local,
diff --git a/cli/args/mod.rs b/cli/args/mod.rs
index ab8d6b503..96f4e9a74 100644
--- a/cli/args/mod.rs
+++ b/cli/args/mod.rs
@@ -1130,6 +1130,18 @@ impl CliOptions {
&self.flags.ext
}
+ pub fn has_hmr(&self) -> bool {
+ if let DenoSubcommand::Run(RunFlags {
+ watch: Some(WatchFlagsWithPaths { hmr, .. }),
+ ..
+ }) = &self.flags.subcommand
+ {
+ *hmr
+ } else {
+ false
+ }
+ }
+
/// If the --inspect or --inspect-brk flags are used.
pub fn is_inspecting(&self) -> bool {
self.flags.inspect.is_some()
diff --git a/cli/emit.rs b/cli/emit.rs
index e81d2e83c..8e51c4edd 100644
--- a/cli/emit.rs
+++ b/cli/emit.rs
@@ -101,6 +101,26 @@ impl Emitter {
}
}
+ /// Expects a file URL, panics otherwise.
+ pub async fn load_and_emit_for_hmr(
+ &self,
+ specifier: &ModuleSpecifier,
+ ) -> Result<String, AnyError> {
+ let media_type = MediaType::from_specifier(specifier);
+ let source_code = tokio::fs::read_to_string(
+ ModuleSpecifier::to_file_path(specifier).unwrap(),
+ )
+ .await?;
+ let source_arc: Arc<str> = source_code.into();
+ let parsed_source = self
+ .parsed_source_cache
+ .get_or_parse_module(specifier, source_arc, media_type)?;
+ let mut options = self.emit_options.clone();
+ options.inline_source_map = false;
+ let transpiled_source = parsed_source.transpile(&options)?;
+ Ok(transpiled_source.text)
+ }
+
/// A hashing function that takes the source code and uses the global emit
/// options then generates a string hash which can be stored to
/// determine if the cached emit is valid or not.
diff --git a/cli/factory.rs b/cli/factory.rs
index 9cdd32702..389c4dbe0 100644
--- a/cli/factory.rs
+++ b/cli/factory.rs
@@ -66,7 +66,7 @@ use std::future::Future;
use std::sync::Arc;
pub struct CliFactoryBuilder {
- watcher_communicator: Option<WatcherCommunicator>,
+ watcher_communicator: Option<Arc<WatcherCommunicator>>,
}
impl CliFactoryBuilder {
@@ -86,7 +86,7 @@ impl CliFactoryBuilder {
pub async fn build_from_flags_for_watcher(
mut self,
flags: Flags,
- watcher_communicator: WatcherCommunicator,
+ watcher_communicator: Arc<WatcherCommunicator>,
) -> Result<CliFactory, AnyError> {
self.watcher_communicator = Some(watcher_communicator);
self.build_from_flags(flags).await
@@ -171,7 +171,7 @@ struct CliFactoryServices {
}
pub struct CliFactory {
- watcher_communicator: Option<WatcherCommunicator>,
+ watcher_communicator: Option<Arc<WatcherCommunicator>>,
options: Arc<CliOptions>,
services: CliFactoryServices,
}
@@ -620,6 +620,11 @@ impl CliFactory {
let npm_resolver = self.npm_resolver().await?;
let fs = self.fs();
let cli_node_resolver = self.cli_node_resolver().await?;
+ let maybe_file_watcher_communicator = if self.options.has_hmr() {
+ Some(self.watcher_communicator.clone().unwrap())
+ } else {
+ None
+ };
Ok(CliMainWorkerFactory::new(
StorageKeyResolver::from_options(&self.options),
@@ -643,6 +648,8 @@ impl CliFactory {
)),
self.root_cert_store_provider().clone(),
self.fs().clone(),
+ Some(self.emitter()?.clone()),
+ maybe_file_watcher_communicator,
self.maybe_inspector_server().clone(),
self.maybe_lockfile().clone(),
self.feature_checker().clone(),
@@ -659,6 +666,7 @@ impl CliFactory {
coverage_dir: self.options.coverage_dir(),
enable_testing_features: self.options.enable_testing_features(),
has_node_modules_dir: self.options.has_node_modules_dir(),
+ hmr: self.options.has_hmr(),
inspect_brk: self.options.inspect_brk().is_some(),
inspect_wait: self.options.inspect_wait().is_some(),
is_inspecting: self.options.is_inspecting(),
diff --git a/cli/graph_util.rs b/cli/graph_util.rs
index 2f5fd40fd..f2713a9db 100644
--- a/cli/graph_util.rs
+++ b/cli/graph_util.rs
@@ -681,12 +681,12 @@ impl<'a> ModuleGraphUpdatePermit<'a> {
#[derive(Clone, Debug)]
pub struct FileWatcherReporter {
- watcher_communicator: WatcherCommunicator,
+ watcher_communicator: Arc<WatcherCommunicator>,
file_paths: Arc<Mutex<Vec<PathBuf>>>,
}
impl FileWatcherReporter {
- pub fn new(watcher_communicator: WatcherCommunicator) -> Self {
+ pub fn new(watcher_communicator: Arc<WatcherCommunicator>) -> Self {
Self {
watcher_communicator,
file_paths: Default::default(),
diff --git a/cli/standalone/mod.rs b/cli/standalone/mod.rs
index 612ae9eed..803655b9a 100644
--- a/cli/standalone/mod.rs
+++ b/cli/standalone/mod.rs
@@ -446,6 +446,8 @@ pub async fn run(
fs,
None,
None,
+ None,
+ None,
feature_checker,
CliMainWorkerOptions {
argv: metadata.argv,
@@ -453,6 +455,7 @@ pub async fn run(
coverage_dir: None,
enable_testing_features: false,
has_node_modules_dir,
+ hmr: false,
inspect_brk: false,
inspect_wait: false,
is_inspecting: false,
diff --git a/cli/tests/integration/watcher_tests.rs b/cli/tests/integration/watcher_tests.rs
index 1ee8a45e0..0defaa69e 100644
--- a/cli/tests/integration/watcher_tests.rs
+++ b/cli/tests/integration/watcher_tests.rs
@@ -1645,3 +1645,257 @@ async fn run_watch_inspect() {
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");
+ file_to_watch.write(
+ r#"
+globalThis.state = { i: 0 };
+
+function bar() {
+ globalThis.state.i = 0;
+ console.log("got request", globalThis.state.i);
+}
+
+function handler(_req) {
+ bar();
+ return new Response("Hello world!");
+}
+
+Deno.serve({ port: 11111 }, handler);
+console.log("Listening...")
+ "#,
+ );
+
+ let mut child = util::deno_cmd()
+ .current_dir(util::testdata_path())
+ .arg("run")
+ .arg("--unstable-hmr")
+ .arg("--allow-net")
+ .arg("-L")
+ .arg("debug")
+ .arg(&file_to_watch)
+ .env("NO_COLOR", "1")
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped())
+ .spawn()
+ .unwrap();
+ let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
+ wait_contains("Process started", &mut stderr_lines).await;
+ wait_contains("No package.json file found", &mut stderr_lines).await;
+
+ wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
+ wait_contains("Listening...", &mut stdout_lines).await;
+
+ file_to_watch.write(
+ r#"
+globalThis.state = { i: 0 };
+
+function bar() {
+ globalThis.state.i = 0;
+ console.log("got request1", globalThis.state.i);
+}
+
+function handler(_req) {
+ bar();
+ return new Response("Hello world!");
+}
+
+Deno.serve({ port: 11111 }, handler);
+console.log("Listening...")
+ "#,
+ );
+
+ wait_contains("Failed to reload module", &mut stderr_lines).await;
+ wait_contains("File change detected", &mut stderr_lines).await;
+
+ check_alive_then_kill(child);
+}
+
+#[tokio::test]
+async fn run_hmr_jsx() {
+ let t = TempDir::new();
+ let file_to_watch = t.path().join("file_to_watch.js");
+ file_to_watch.write(
+ r#"
+import { foo } from "./foo.jsx";
+
+let i = 0;
+setInterval(() => {
+ console.log(i++, foo());
+}, 100);
+"#,
+ );
+ let file_to_watch2 = t.path().join("foo.jsx");
+ file_to_watch2.write(
+ r#"
+export function foo() {
+ return `<h1>Hello</h1>`;
+}
+"#,
+ );
+
+ let mut child = util::deno_cmd()
+ .current_dir(util::testdata_path())
+ .arg("run")
+ .arg("--unstable-hmr")
+ .arg("-L")
+ .arg("debug")
+ .arg(&file_to_watch)
+ .env("NO_COLOR", "1")
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped())
+ .spawn()
+ .unwrap();
+ let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
+ wait_contains("Process started", &mut stderr_lines).await;
+ wait_contains("No package.json file found", &mut stderr_lines).await;
+
+ wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
+ wait_contains("5 <h1>Hello</h1>", &mut stdout_lines).await;
+
+ file_to_watch2.write(
+ r#"
+export function foo() {
+ return `<h1>Hello world</h1>`;
+}
+ "#,
+ );
+
+ wait_contains("Replaced changed module", &mut stderr_lines).await;
+ wait_contains("<h1>Hello world</h1>", &mut stdout_lines).await;
+
+ check_alive_then_kill(child);
+}
+
+#[tokio::test]
+async fn run_hmr_uncaught_error() {
+ let t = TempDir::new();
+ let file_to_watch = t.path().join("file_to_watch.js");
+ file_to_watch.write(
+ r#"
+import { foo } from "./foo.jsx";
+
+let i = 0;
+setInterval(() => {
+ console.log(i++, foo());
+}, 100);
+"#,
+ );
+ let file_to_watch2 = t.path().join("foo.jsx");
+ file_to_watch2.write(
+ r#"
+export function foo() {
+ setTimeout(() => {
+ throw new Error("fail");
+ });
+ return `<h1>asd1</h1>`;
+}
+"#,
+ );
+
+ let mut child = util::deno_cmd()
+ .current_dir(util::testdata_path())
+ .arg("run")
+ .arg("--unstable-hmr")
+ .arg("-L")
+ .arg("debug")
+ .arg(&file_to_watch)
+ .env("NO_COLOR", "1")
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped())
+ .spawn()
+ .unwrap();
+ let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
+ wait_contains("Process started", &mut stderr_lines).await;
+ wait_contains("No package.json file found", &mut stderr_lines).await;
+
+ wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
+ wait_contains("<h1>asd1</h1>", &mut stdout_lines).await;
+ wait_contains("fail", &mut stderr_lines).await;
+
+ file_to_watch2.write(
+ r#"
+export function foo() {
+ return `<h1>asd2</h1>`;
+}
+ "#,
+ );
+
+ wait_contains("Process failed", &mut stderr_lines).await;
+ wait_contains("File change detected", &mut stderr_lines).await;
+ wait_contains("<h1>asd2</h1>", &mut stdout_lines).await;
+
+ check_alive_then_kill(child);
+}
+
+#[tokio::test]
+async fn run_hmr_unhandled_rejection() {
+ let t = TempDir::new();
+ let file_to_watch = t.path().join("file_to_watch.js");
+ file_to_watch.write(
+ r#"
+import { foo } from "./foo.jsx";
+
+// deno-lint-ignore require-await
+async function rejection() {
+ throw new Error("boom!");
+}
+
+let i = 0;
+setInterval(() => {
+ if (i == 3) {
+ rejection();
+ }
+ console.log(i++, foo());
+}, 100);
+"#,
+ );
+ let file_to_watch2 = t.path().join("foo.jsx");
+ file_to_watch2.write(
+ r#"
+export function foo() {
+ return `<h1>asd1</h1>`;
+}
+"#,
+ );
+
+ let mut child = util::deno_cmd()
+ .current_dir(util::testdata_path())
+ .arg("run")
+ .arg("--unstable-hmr")
+ .arg("-L")
+ .arg("debug")
+ .arg(&file_to_watch)
+ .env("NO_COLOR", "1")
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped())
+ .spawn()
+ .unwrap();
+ let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
+ wait_contains("Process started", &mut stderr_lines).await;
+ wait_contains("No package.json file found", &mut stderr_lines).await;
+
+ wait_for_watcher("file_to_watch.js", &mut stderr_lines).await;
+ wait_contains("2 <h1>asd1</h1>", &mut stdout_lines).await;
+ wait_contains("boom", &mut stderr_lines).await;
+
+ file_to_watch.write(
+ r#"
+import { foo } from "./foo.jsx";
+
+let i = 0;
+setInterval(() => {
+ console.log(i++, foo());
+}, 100);
+ "#,
+ );
+
+ wait_contains("Process failed", &mut stderr_lines).await;
+ wait_contains("File change detected", &mut stderr_lines).await;
+ wait_contains("<h1>asd1</h1>", &mut stdout_lines).await;
+
+ check_alive_then_kill(child);
+}
diff --git a/cli/tools/bench/mod.rs b/cli/tools/bench/mod.rs
index eb400442e..70551a767 100644
--- a/cli/tools/bench/mod.rs
+++ b/cli/tools/bench/mod.rs
@@ -409,14 +409,14 @@ pub async fn run_benchmarks_with_watch(
) -> Result<(), AnyError> {
file_watcher::watch_func(
flags,
- file_watcher::PrintConfig {
- job_name: "Bench".to_string(),
- clear_screen: bench_flags
+ file_watcher::PrintConfig::new(
+ "Bench",
+ bench_flags
.watch
.as_ref()
.map(|w| !w.no_clear_screen)
.unwrap_or(true),
- },
+ ),
move |flags, watcher_communicator, changed_paths| {
let bench_flags = bench_flags.clone();
Ok(async move {
diff --git a/cli/tools/bundle.rs b/cli/tools/bundle.rs
index b36ff023a..0946c728b 100644
--- a/cli/tools/bundle.rs
+++ b/cli/tools/bundle.rs
@@ -31,10 +31,10 @@ pub async fn bundle(
if let Some(watch_flags) = &bundle_flags.watch {
util::file_watcher::watch_func(
flags,
- util::file_watcher::PrintConfig {
- job_name: "Bundle".to_string(),
- clear_screen: !watch_flags.no_clear_screen,
- },
+ util::file_watcher::PrintConfig::new(
+ "Bundle",
+ !watch_flags.no_clear_screen,
+ ),
move |flags, watcher_communicator, _changed_paths| {
let bundle_flags = bundle_flags.clone();
Ok(async move {
diff --git a/cli/tools/fmt.rs b/cli/tools/fmt.rs
index 92facc7ec..5c47b5497 100644
--- a/cli/tools/fmt.rs
+++ b/cli/tools/fmt.rs
@@ -64,10 +64,7 @@ pub async fn format(flags: Flags, fmt_flags: FmtFlags) -> Result<(), AnyError> {
if let Some(watch_flags) = &fmt_flags.watch {
file_watcher::watch_func(
flags,
- file_watcher::PrintConfig {
- job_name: "Fmt".to_string(),
- clear_screen: !watch_flags.no_clear_screen,
- },
+ file_watcher::PrintConfig::new("Fmt", !watch_flags.no_clear_screen),
move |flags, watcher_communicator, changed_paths| {
let fmt_flags = fmt_flags.clone();
Ok(async move {
@@ -82,7 +79,7 @@ pub async fn format(flags: Flags, fmt_flags: FmtFlags) -> Result<(), AnyError> {
Ok(files)
}
})?;
- _ = watcher_communicator.watch_paths(files.clone());
+ let _ = watcher_communicator.watch_paths(files.clone());
let refmt_files = if let Some(paths) = changed_paths {
if fmt_options.check {
// check all files on any changed (https://github.com/denoland/deno/issues/12446)
diff --git a/cli/tools/lint.rs b/cli/tools/lint.rs
index b7f4a3f0d..5b9387eb1 100644
--- a/cli/tools/lint.rs
+++ b/cli/tools/lint.rs
@@ -59,10 +59,7 @@ pub async fn lint(flags: Flags, lint_flags: LintFlags) -> Result<(), AnyError> {
}
file_watcher::watch_func(
flags,
- file_watcher::PrintConfig {
- job_name: "Lint".to_string(),
- clear_screen: !watch_flags.no_clear_screen,
- },
+ file_watcher::PrintConfig::new("Lint", !watch_flags.no_clear_screen),
move |flags, watcher_communicator, changed_paths| {
let lint_flags = lint_flags.clone();
Ok(async move {
diff --git a/cli/tools/run/hmr/json_types.rs b/cli/tools/run/hmr/json_types.rs
new file mode 100644
index 000000000..3ac80344b
--- /dev/null
+++ b/cli/tools/run/hmr/json_types.rs
@@ -0,0 +1,59 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+// TODO(bartlomieju): this code should be factored out to `cli/cdp.rs` along
+// with code in `cli/tools/repl/` and `cli/tools/coverage/`. These are all
+// Chrome Devtools Protocol message types.
+
+use deno_core::serde_json::Value;
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize)]
+pub struct RpcNotification {
+ pub method: String,
+ pub params: Value,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct SetScriptSourceReturnObject {
+ pub status: Status,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ScriptParsed {
+ pub script_id: String,
+ pub url: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub enum Status {
+ Ok,
+ CompileError,
+ BlockedByActiveGenerator,
+ BlockedByActiveFunction,
+ BlockedByTopLevelEsModuleChange,
+}
+
+impl Status {
+ pub(crate) fn explain(&self) -> &'static str {
+ match self {
+ Status::Ok => "OK",
+ Status::CompileError => "compile error",
+ Status::BlockedByActiveGenerator => "blocked by active generator",
+ Status::BlockedByActiveFunction => "blocked by active function",
+ Status::BlockedByTopLevelEsModuleChange => {
+ "blocked by top-level ES module change"
+ }
+ }
+ }
+
+ pub(crate) fn should_retry(&self) -> bool {
+ match self {
+ Status::Ok => false,
+ Status::CompileError => false,
+ Status::BlockedByActiveGenerator => true,
+ Status::BlockedByActiveFunction => true,
+ Status::BlockedByTopLevelEsModuleChange => false,
+ }
+ }
+}
diff --git a/cli/tools/run/hmr/mod.rs b/cli/tools/run/hmr/mod.rs
new file mode 100644
index 000000000..1a5772307
--- /dev/null
+++ b/cli/tools/run/hmr/mod.rs
@@ -0,0 +1,242 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+use crate::emit::Emitter;
+use crate::util::file_watcher::WatcherCommunicator;
+use crate::util::file_watcher::WatcherRestartMode;
+use deno_core::error::generic_error;
+use deno_core::error::AnyError;
+use deno_core::futures::StreamExt;
+use deno_core::serde_json::json;
+use deno_core::serde_json::{self};
+use deno_core::url::Url;
+use deno_core::LocalInspectorSession;
+use deno_runtime::colors;
+use std::collections::HashMap;
+use std::path::PathBuf;
+use std::sync::Arc;
+use tokio::select;
+
+mod json_types;
+
+use json_types::RpcNotification;
+use json_types::ScriptParsed;
+use json_types::SetScriptSourceReturnObject;
+use json_types::Status;
+
+/// This structure is responsible for providing Hot Module Replacement
+/// functionality.
+///
+/// It communicates with V8 inspector over a local session and waits for
+/// notifications about changed files from the `FileWatcher`.
+///
+/// Upon receiving such notification, the runner decides if the changed
+/// path should be handled the `FileWatcher` itself (as if we were running
+/// in `--watch` mode), or if the path is eligible to be hot replaced in the
+/// current program.
+///
+/// Even if the runner decides that a path will be hot-replaced, the V8 isolate
+/// can refuse to perform hot replacement, eg. a top-level variable/function
+/// of an ES module cannot be hot-replaced. In such situation the runner will
+/// force a full restart of a program by notifying the `FileWatcher`.
+pub struct HmrRunner {
+ session: LocalInspectorSession,
+ watcher_communicator: Arc<WatcherCommunicator>,
+ script_ids: HashMap<String, String>,
+ emitter: Arc<Emitter>,
+}
+
+impl HmrRunner {
+ pub fn new(
+ emitter: Arc<Emitter>,
+ session: LocalInspectorSession,
+ watcher_communicator: Arc<WatcherCommunicator>,
+ ) -> Self {
+ Self {
+ session,
+ emitter,
+ watcher_communicator,
+ script_ids: HashMap::new(),
+ }
+ }
+
+ // TODO(bartlomieju): this code is duplicated in `cli/tools/coverage/mod.rs`
+ pub async fn start(&mut self) -> Result<(), AnyError> {
+ self.enable_debugger().await
+ }
+
+ // TODO(bartlomieju): this code is duplicated in `cli/tools/coverage/mod.rs`
+ pub async fn stop(&mut self) -> Result<(), AnyError> {
+ self
+ .watcher_communicator
+ .change_restart_mode(WatcherRestartMode::Automatic);
+ self.disable_debugger().await
+ }
+
+ // TODO(bartlomieju): this code is duplicated in `cli/tools/coverage/mod.rs`
+ async fn enable_debugger(&mut self) -> Result<(), AnyError> {
+ self
+ .session
+ .post_message::<()>("Debugger.enable", None)
+ .await?;
+ self
+ .session
+ .post_message::<()>("Runtime.enable", None)
+ .await?;
+ Ok(())
+ }
+
+ // TODO(bartlomieju): this code is duplicated in `cli/tools/coverage/mod.rs`
+ async fn disable_debugger(&mut self) -> Result<(), AnyError> {
+ self
+ .session
+ .post_message::<()>("Debugger.disable", None)
+ .await?;
+ self
+ .session
+ .post_message::<()>("Runtime.disable", None)
+ .await?;
+ Ok(())
+ }
+
+ async fn set_script_source(
+ &mut self,
+ script_id: &str,
+ source: &str,
+ ) -> Result<SetScriptSourceReturnObject, AnyError> {
+ let result = self
+ .session
+ .post_message(
+ "Debugger.setScriptSource",
+ Some(json!({
+ "scriptId": script_id,
+ "scriptSource": source,
+ "allowTopFrameEditing": true,
+ })),
+ )
+ .await?;
+
+ Ok(serde_json::from_value::<SetScriptSourceReturnObject>(
+ result,
+ )?)
+ }
+
+ async fn dispatch_hmr_event(
+ &mut self,
+ script_id: &str,
+ ) -> Result<(), AnyError> {
+ let expr = format!(
+ "dispatchEvent(new CustomEvent(\"hmr\", {{ detail: {{ path: \"{}\" }} }}));",
+ script_id
+ );
+
+ let _result = self
+ .session
+ .post_message(
+ "Runtime.evaluate",
+ Some(json!({
+ "expression": expr,
+ "contextId": Some(1),
+ })),
+ )
+ .await?;
+
+ Ok(())
+ }
+
+ pub async fn run(&mut self) -> Result<(), AnyError> {
+ self
+ .watcher_communicator
+ .change_restart_mode(WatcherRestartMode::Manual);
+ let mut session_rx = self.session.take_notification_rx();
+ loop {
+ select! {
+ biased;
+ Some(notification) = session_rx.next() => {
+ let notification = serde_json::from_value::<RpcNotification>(notification)?;
+ // TODO(bartlomieju): this is not great... and the code is duplicated with the REPL.
+ if notification.method == "Runtime.exceptionThrown" {
+ let params = notification.params;
+ let exception_details = params.get("exceptionDetails").unwrap().as_object().unwrap();
+ let text = exception_details.get("text").unwrap().as_str().unwrap();
+ let exception = exception_details.get("exception").unwrap().as_object().unwrap();
+ let description = exception.get("description").and_then(|d| d.as_str()).unwrap_or("undefined");
+ break Err(generic_error(format!("{text} {description}")));
+ } else if notification.method == "Debugger.scriptParsed" {
+ let params = serde_json::from_value::<ScriptParsed>(notification.params)?;
+ if params.url.starts_with("file://") {
+ let file_url = Url::parse(&params.url).unwrap();
+ let file_path = file_url.to_file_path().unwrap();
+ if let Ok(canonicalized_file_path) = file_path.canonicalize() {
+ let canonicalized_file_url = Url::from_file_path(canonicalized_file_path).unwrap();
+ self.script_ids.insert(canonicalized_file_url.to_string(), params.script_id);
+ }
+ }
+ }
+ }
+ changed_paths = self.watcher_communicator.watch_for_changed_paths() => {
+ let changed_paths = changed_paths?;
+
+ let Some(changed_paths) = changed_paths else {
+ let _ = self.watcher_communicator.force_restart();
+ continue;
+ };
+
+ let filtered_paths: Vec<PathBuf> = changed_paths.into_iter().filter(|p| p.extension().map_or(false, |ext| {
+ let ext_str = ext.to_str().unwrap();
+ matches!(ext_str, "js" | "ts" | "jsx" | "tsx")
+ })).collect();
+
+ // If after filtering there are no paths it means it's either a file
+ // we can't HMR or an external file that was passed explicitly to
+ // `--unstable-hmr=<file>` path.
+ if filtered_paths.is_empty() {
+ let _ = self.watcher_communicator.force_restart();
+ continue;
+ }
+
+ for path in filtered_paths {
+ let Some(path_str) = path.to_str() else {
+ let _ = self.watcher_communicator.force_restart();
+ continue;
+ };
+ let Ok(module_url) = Url::from_file_path(path_str) else {
+ let _ = self.watcher_communicator.force_restart();
+ continue;
+ };
+
+ let Some(id) = self.script_ids.get(module_url.as_str()).cloned() else {
+ let _ = self.watcher_communicator.force_restart();
+ continue;
+ };
+
+ let source_code = self.emitter.load_and_emit_for_hmr(
+ &module_url
+ ).await?;
+
+ let mut tries = 1;
+ loop {
+ let result = self.set_script_source(&id, source_code.as_str()).await?;
+
+ if matches!(result.status, Status::Ok) {
+ self.dispatch_hmr_event(module_url.as_str()).await?;
+ self.watcher_communicator.print(format!("Replaced changed module {}", module_url.as_str()));
+ break;
+ }
+
+ self.watcher_communicator.print(format!("Failed to reload module {}: {}.", module_url, colors::gray(result.status.explain())));
+ if result.status.should_retry() && tries <= 2 {
+ tries += 1;
+ tokio::time::sleep(std::time::Duration::from_millis(100)).await;
+ continue;
+ }
+
+ let _ = self.watcher_communicator.force_restart();
+ break;
+ }
+ }
+ }
+ _ = self.session.receive_from_v8_session() => {}
+ }
+ }
+ }
+}
diff --git a/cli/tools/run.rs b/cli/tools/run/mod.rs
index 80e80577e..119129b1b 100644
--- a/cli/tools/run.rs
+++ b/cli/tools/run/mod.rs
@@ -15,6 +15,9 @@ use crate::factory::CliFactory;
use crate::factory::CliFactoryBuilder;
use crate::file_fetcher::File;
use crate::util;
+use crate::util::file_watcher::WatcherRestartMode;
+
+pub mod hmr;
pub async fn run_script(
flags: Flags,
@@ -104,12 +107,14 @@ async fn run_with_watch(
flags: Flags,
watch_flags: WatchFlagsWithPaths,
) -> Result<i32, AnyError> {
- util::file_watcher::watch_func(
+ util::file_watcher::watch_recv(
flags,
- util::file_watcher::PrintConfig {
- job_name: "Process".to_string(),
- clear_screen: !watch_flags.no_clear_screen,
- },
+ util::file_watcher::PrintConfig::new_with_banner(
+ if watch_flags.hmr { "HMR" } else { "Watcher" },
+ "Process",
+ !watch_flags.no_clear_screen,
+ ),
+ WatcherRestartMode::Automatic,
move |flags, watcher_communicator, _changed_paths| {
Ok(async move {
let factory = CliFactoryBuilder::new()
@@ -125,12 +130,17 @@ async fn run_with_watch(
let permissions = PermissionsContainer::new(Permissions::from_options(
&cli_options.permissions_options(),
)?);
- let worker = factory
+ let mut worker = factory
.create_cli_main_worker_factory()
.await?
.create_main_worker(main_module, permissions)
.await?;
- worker.run_for_watcher().await?;
+
+ if watch_flags.hmr {
+ worker.run().await?;
+ } else {
+ worker.run_for_watcher().await?;
+ }
Ok(())
})
diff --git a/cli/tools/test/mod.rs b/cli/tools/test/mod.rs
index 8e29ba2cb..5e34e345f 100644
--- a/cli/tools/test/mod.rs
+++ b/cli/tools/test/mod.rs
@@ -1205,14 +1205,14 @@ pub async fn run_tests_with_watch(
file_watcher::watch_func(
flags,
- file_watcher::PrintConfig {
- job_name: "Test".to_string(),
- clear_screen: test_flags
+ file_watcher::PrintConfig::new(
+ "Test",
+ test_flags
.watch
.as_ref()
.map(|w| !w.no_clear_screen)
.unwrap_or(true),
- },
+ ),
move |flags, watcher_communicator, changed_paths| {
let test_flags = test_flags.clone();
Ok(async move {
diff --git a/cli/util/file_watcher.rs b/cli/util/file_watcher.rs
index 8d6b4e8fb..5a316139c 100644
--- a/cli/util/file_watcher.rs
+++ b/cli/util/file_watcher.rs
@@ -8,6 +8,7 @@ use deno_core::error::AnyError;
use deno_core::error::JsError;
use deno_core::futures::Future;
use deno_core::futures::FutureExt;
+use deno_core::parking_lot::Mutex;
use deno_runtime::fmt_errors::format_js_error;
use log::info;
use notify::event::Event as NotifyEvent;
@@ -16,9 +17,11 @@ use notify::Error as NotifyError;
use notify::RecommendedWatcher;
use notify::RecursiveMode;
use notify::Watcher;
+use std::cell::RefCell;
use std::collections::HashSet;
use std::io::IsTerminal;
use std::path::PathBuf;
+use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use tokio::select;
@@ -91,20 +94,49 @@ where
}
pub struct PrintConfig {
- /// printing watcher status to terminal.
- pub job_name: String,
- /// determine whether to clear the terminal screen; applicable to TTY environments only.
- pub clear_screen: bool,
+ banner: &'static str,
+ /// Printing watcher status to terminal.
+ job_name: &'static str,
+ /// Determine whether to clear the terminal screen; applicable to TTY environments only.
+ clear_screen: bool,
}
-fn create_print_after_restart_fn(clear_screen: bool) -> impl Fn() {
+impl PrintConfig {
+ /// By default `PrintConfig` uses "Watcher" as a banner name that will
+ /// be printed in color. If you need to customize it, use
+ /// `PrintConfig::new_with_banner` instead.
+ pub fn new(job_name: &'static str, clear_screen: bool) -> Self {
+ Self {
+ banner: "Watcher",
+ job_name,
+ clear_screen,
+ }
+ }
+
+ pub fn new_with_banner(
+ banner: &'static str,
+ job_name: &'static str,
+ clear_screen: bool,
+ ) -> Self {
+ Self {
+ banner,
+ job_name,
+ clear_screen,
+ }
+ }
+}
+
+fn create_print_after_restart_fn(
+ banner: &'static str,
+ clear_screen: bool,
+) -> impl Fn() {
move || {
if clear_screen && std::io::stderr().is_terminal() {
eprint!("{CLEAR_SCREEN}");
}
info!(
"{} File change detected! Restarting!",
- colors::intense_blue("Watcher"),
+ colors::intense_blue(banner),
);
}
}
@@ -120,22 +152,38 @@ pub struct WatcherCommunicator {
/// Send a message to force a restart.
restart_tx: tokio::sync::mpsc::UnboundedSender<()>,
-}
-impl Clone for WatcherCommunicator {
- fn clone(&self) -> Self {
- Self {
- paths_to_watch_tx: self.paths_to_watch_tx.clone(),
- changed_paths_rx: self.changed_paths_rx.resubscribe(),
- restart_tx: self.restart_tx.clone(),
- }
- }
+ restart_mode: Mutex<WatcherRestartMode>,
+
+ banner: String,
}
impl WatcherCommunicator {
pub fn watch_paths(&self, paths: Vec<PathBuf>) -> Result<(), AnyError> {
self.paths_to_watch_tx.send(paths).map_err(AnyError::from)
}
+
+ pub fn force_restart(&self) -> Result<(), AnyError> {
+ // Change back to automatic mode, so that HMR can set up watching
+ // from scratch.
+ *self.restart_mode.lock() = WatcherRestartMode::Automatic;
+ self.restart_tx.send(()).map_err(AnyError::from)
+ }
+
+ pub async fn watch_for_changed_paths(
+ &self,
+ ) -> Result<Option<Vec<PathBuf>>, AnyError> {
+ let mut rx = self.changed_paths_rx.resubscribe();
+ rx.recv().await.map_err(AnyError::from)
+ }
+
+ pub fn change_restart_mode(&self, restart_mode: WatcherRestartMode) {
+ *self.restart_mode.lock() = restart_mode;
+ }
+
+ pub fn print(&self, msg: String) {
+ log::info!("{} {}", self.banner, msg);
+ }
}
/// Creates a file watcher.
@@ -151,7 +199,7 @@ pub async fn watch_func<O, F>(
where
O: FnMut(
Flags,
- WatcherCommunicator,
+ Arc<WatcherCommunicator>,
Option<Vec<PathBuf>>,
) -> Result<F, AnyError>,
F: Future<Output = Result<(), AnyError>>,
@@ -173,9 +221,7 @@ pub enum WatcherRestartMode {
Automatic,
/// When a file path changes the caller will trigger a restart, using
- /// `WatcherCommunicator.restart_tx`.
- // TODO(bartlomieju): this mode will be used in a follow up PR
- #[allow(dead_code)]
+ /// `WatcherInterface.restart_tx`.
Manual,
}
@@ -193,7 +239,7 @@ pub async fn watch_recv<O, F>(
where
O: FnMut(
Flags,
- WatcherCommunicator,
+ Arc<WatcherCommunicator>,
Option<Vec<PathBuf>>,
) -> Result<F, AnyError>,
F: Future<Output = Result<(), AnyError>>,
@@ -206,19 +252,42 @@ where
DebouncedReceiver::new_with_sender();
let PrintConfig {
+ banner,
job_name,
clear_screen,
} = print_config;
- let print_after_restart = create_print_after_restart_fn(clear_screen);
- let watcher_communicator = WatcherCommunicator {
+ let print_after_restart = create_print_after_restart_fn(banner, clear_screen);
+ let watcher_communicator = Arc::new(WatcherCommunicator {
paths_to_watch_tx: paths_to_watch_tx.clone(),
changed_paths_rx: changed_paths_rx.resubscribe(),
restart_tx: restart_tx.clone(),
- };
- info!("{} {} started.", colors::intense_blue("Watcher"), job_name,);
+ restart_mode: Mutex::new(restart_mode),
+ banner: colors::intense_blue(banner).to_string(),
+ });
+ info!("{} {} started.", colors::intense_blue(banner), job_name);
+
+ let changed_paths = Rc::new(RefCell::new(None));
+ let changed_paths_ = changed_paths.clone();
+ let watcher_ = watcher_communicator.clone();
+
+ deno_core::unsync::spawn(async move {
+ loop {
+ let received_changed_paths = watcher_receiver.recv().await;
+ *changed_paths_.borrow_mut() = received_changed_paths.clone();
+
+ match *watcher_.restart_mode.lock() {
+ WatcherRestartMode::Automatic => {
+ let _ = restart_tx.send(());
+ }
+ WatcherRestartMode::Manual => {
+ // TODO(bartlomieju): should we fail on sending changed paths?
+ let _ = changed_paths_tx.send(received_changed_paths);
+ }
+ }
+ }
+ });
- let mut changed_paths = None;
loop {
// We may need to give the runtime a tick to settle, as cancellations may need to propagate
// to tasks. We choose yielding 10 times to the runtime as a decent heuristic. If watch tests
@@ -239,7 +308,7 @@ where
let operation_future = error_handler(operation(
flags.clone(),
watcher_communicator.clone(),
- changed_paths.take(),
+ changed_paths.borrow_mut().take(),
)?);
// don't reload dependencies after the first run
@@ -251,26 +320,12 @@ where
print_after_restart();
continue;
},
- received_changed_paths = watcher_receiver.recv() => {
- changed_paths = received_changed_paths.clone();
-
- match restart_mode {
- WatcherRestartMode::Automatic => {
- print_after_restart();
- continue;
- },
- WatcherRestartMode::Manual => {
- // TODO(bartlomieju): should we fail on sending changed paths?
- let _ = changed_paths_tx.send(received_changed_paths);
- }
- }
- },
success = operation_future => {
consume_paths_to_watch(&mut watcher, &mut paths_to_watch_rx);
// TODO(bartlomieju): print exit code here?
info!(
"{} {} {}. Restarting on file change...",
- colors::intense_blue("Watcher"),
+ colors::intense_blue(banner),
job_name,
if success {
"finished"
@@ -280,7 +335,6 @@ where
);
},
};
-
let receiver_future = async {
loop {
let maybe_paths = paths_to_watch_rx.recv().await;
@@ -293,9 +347,8 @@ where
// watched paths has changed.
select! {
_ = receiver_future => {},
- received_changed_paths = watcher_receiver.recv() => {
+ _ = restart_rx.recv() => {
print_after_restart();
- changed_paths = received_changed_paths;
continue;
},
};
diff --git a/cli/worker.rs b/cli/worker.rs
index d8738d492..58bd96642 100644
--- a/cli/worker.rs
+++ b/cli/worker.rs
@@ -43,15 +43,20 @@ use deno_runtime::BootstrapOptions;
use deno_runtime::WorkerLogLevel;
use deno_semver::npm::NpmPackageReqReference;
use deno_semver::package::PackageReqReference;
+use tokio::select;
use crate::args::package_json::PackageJsonDeps;
use crate::args::StorageKeyResolver;
+use crate::emit::Emitter;
use crate::errors;
use crate::npm::CliNpmResolver;
use crate::ops;
use crate::tools;
use crate::tools::coverage::CoverageCollector;
+use crate::tools::run::hmr::HmrRunner;
use crate::util::checksum;
+use crate::util::file_watcher::WatcherCommunicator;
+use crate::util::file_watcher::WatcherRestartMode;
use crate::version;
pub trait ModuleLoaderFactory: Send + Sync {
@@ -83,6 +88,7 @@ pub struct CliMainWorkerOptions {
pub coverage_dir: Option<String>,
pub enable_testing_features: bool,
pub has_node_modules_dir: bool,
+ pub hmr: bool,
pub inspect_brk: bool,
pub inspect_wait: bool,
pub is_inspecting: bool,
@@ -108,6 +114,8 @@ struct SharedWorkerState {
module_loader_factory: Box<dyn ModuleLoaderFactory>,
root_cert_store_provider: Arc<dyn RootCertStoreProvider>,
fs: Arc<dyn deno_fs::FileSystem>,
+ emitter: Option<Arc<Emitter>>,
+ maybe_file_watcher_communicator: Option<Arc<WatcherCommunicator>>,
maybe_inspector_server: Option<Arc<InspectorServer>>,
maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
feature_checker: Arc<FeatureChecker>,
@@ -137,6 +145,8 @@ impl CliMainWorker {
pub async fn run(&mut self) -> Result<i32, AnyError> {
let mut maybe_coverage_collector =
self.maybe_setup_coverage_collector().await?;
+ let mut maybe_hmr_runner = self.maybe_setup_hmr_runner().await?;
+
log::debug!("main_module {}", self.main_module);
if self.is_main_cjs {
@@ -153,10 +163,34 @@ impl CliMainWorker {
self.worker.dispatch_load_event(located_script_name!())?;
loop {
- self
- .worker
- .run_event_loop(maybe_coverage_collector.is_none())
- .await?;
+ if let Some(hmr_runner) = maybe_hmr_runner.as_mut() {
+ let watcher_communicator =
+ self.shared.maybe_file_watcher_communicator.clone().unwrap();
+
+ let hmr_future = hmr_runner.run().boxed_local();
+ let event_loop_future = self.worker.run_event_loop(false).boxed_local();
+
+ let result;
+ select! {
+ hmr_result = hmr_future => {
+ result = hmr_result;
+ },
+ event_loop_result = event_loop_future => {
+ result = event_loop_result;
+ }
+ }
+ if let Err(e) = result {
+ watcher_communicator
+ .change_restart_mode(WatcherRestartMode::Automatic);
+ return Err(e);
+ }
+ } else {
+ self
+ .worker
+ .run_event_loop(maybe_coverage_collector.is_none())
+ .await?;
+ }
+
if !self
.worker
.dispatch_beforeunload_event(located_script_name!())?
@@ -173,6 +207,12 @@ impl CliMainWorker {
.with_event_loop(coverage_collector.stop_collecting().boxed_local())
.await?;
}
+ if let Some(hmr_runner) = maybe_hmr_runner.as_mut() {
+ self
+ .worker
+ .with_event_loop(hmr_runner.stop().boxed_local())
+ .await?;
+ }
Ok(self.worker.exit_code())
}
@@ -287,6 +327,28 @@ impl CliMainWorker {
}
}
+ pub async fn maybe_setup_hmr_runner(
+ &mut self,
+ ) -> Result<Option<HmrRunner>, AnyError> {
+ if !self.shared.options.hmr {
+ return Ok(None);
+ }
+
+ let watcher_communicator =
+ self.shared.maybe_file_watcher_communicator.clone().unwrap();
+ let emitter = self.shared.emitter.clone().unwrap();
+
+ let session = self.worker.create_inspector_session().await;
+ let mut hmr_runner = HmrRunner::new(emitter, session, watcher_communicator);
+
+ self
+ .worker
+ .with_event_loop(hmr_runner.start().boxed_local())
+ .await?;
+
+ Ok(Some(hmr_runner))
+ }
+
pub fn execute_script_static(
&mut self,
name: &'static str,
@@ -313,6 +375,8 @@ impl CliMainWorkerFactory {
module_loader_factory: Box<dyn ModuleLoaderFactory>,
root_cert_store_provider: Arc<dyn RootCertStoreProvider>,
fs: Arc<dyn deno_fs::FileSystem>,
+ emitter: Option<Arc<Emitter>>,
+ maybe_file_watcher_communicator: Option<Arc<WatcherCommunicator>>,
maybe_inspector_server: Option<Arc<InspectorServer>>,
maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
feature_checker: Arc<FeatureChecker>,
@@ -330,7 +394,9 @@ impl CliMainWorkerFactory {
compiled_wasm_module_store: Default::default(),
module_loader_factory,
root_cert_store_provider,
+ emitter,
fs,
+ maybe_file_watcher_communicator,
maybe_inspector_server,
maybe_lockfile,
feature_checker,