summaryrefslogtreecommitdiff
path: root/cli/tools
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tools')
-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
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(&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 {