diff options
Diffstat (limited to 'cli/tools')
-rw-r--r-- | cli/tools/bench/mod.rs | 8 | ||||
-rw-r--r-- | cli/tools/bundle.rs | 8 | ||||
-rw-r--r-- | cli/tools/fmt.rs | 7 | ||||
-rw-r--r-- | cli/tools/lint.rs | 5 | ||||
-rw-r--r-- | cli/tools/run/hmr/json_types.rs | 59 | ||||
-rw-r--r-- | cli/tools/run/hmr/mod.rs | 242 | ||||
-rw-r--r-- | cli/tools/run/mod.rs (renamed from cli/tools/run.rs) | 24 | ||||
-rw-r--r-- | cli/tools/test/mod.rs | 8 |
8 files changed, 333 insertions, 28 deletions
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(¶ms.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 { |