Repository: denoland/deno
Pull request: fix(cli/hmr): print compile error with exception details

fix https://github.com/denoland/deno/issues/27577

It can be improved by adding file and line of the error, as it only displays the error information using `get_message_and_description`, example:

`HMR Failed to reload module file:///file.ts: compile error: Uncaught SyntaxError: Cannot use import statement outside a module - undefined.`

Changed files:
filename: cli/cdp.rs
patch: @@ -291,8 +291,10 @@ pub type UnserializableValue = String;
 
 /// <https://chromedevtools.github.io/devtools-protocol/tot/Debugger/#method-setScriptSource>
 #[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
 pub struct SetScriptSourceResponse {
   pub status: Status,
+  pub exception_details: Option<ExceptionDetails>,
 }
 
 #[derive(Debug, Deserialize)]

filename: cli/tools/run/hmr.rs
patch: @@ -19,14 +19,33 @@ use crate::emit::Emitter;
 use crate::util::file_watcher::WatcherCommunicator;
 use crate::util::file_watcher::WatcherRestartMode;
 
-fn explain(status: &cdp::Status) -> &'static str {
-  match status {
-    cdp::Status::Ok => "OK",
-    cdp::Status::CompileError => "compile error",
-    cdp::Status::BlockedByActiveGenerator => "blocked by active generator",
-    cdp::Status::BlockedByActiveFunction => "blocked by active function",
+fn explain(response: &cdp::SetScriptSourceResponse) -> String {
+  match response.status {
+    cdp::Status::Ok => "OK".to_string(),
+    cdp::Status::CompileError => {
+      if let Some(details) = &response.exception_details {
+        let (message, description) = details.get_message_and_description();
+        format!(
+          "compile error: {}{}",
+          message,
+          if description == "undefined" {
+            "".to_string()
+          } else {
+            format!(" - {}", description)
+          }
+        )
+      } else {
+        "compile error: No exception details available".to_string()
+      }
+    }
+    cdp::Status::BlockedByActiveGenerator => {
+      "blocked by active generator".to_string()
+    }
+    cdp::Status::BlockedByActiveFunction => {
+      "blocked by active function".to_string()
+    }
     cdp::Status::BlockedByTopLevelEsModuleChange => {
-      "blocked by top-level ES module change"
+      "blocked by top-level ES module change".to_string()
     }
   }
 }
@@ -154,7 +173,7 @@ impl crate::worker::HmrRunner for HmrRunner {
                 break;
               }
 
-              self.watcher_communicator.print(format!("Failed to reload module {}: {}.", module_url, colors::gray(explain(&result.status))));
+              self.watcher_communicator.print(format!("Failed to reload module {}: {}.", module_url, colors::gray(&explain(&result))));
               if should_retry(&result.status) && tries <= 2 {
                 tries += 1;
                 tokio::time::sleep(std::time::Duration::from_millis(100)).await;

filename: tests/integration/watcher_tests.rs
patch: @@ -1955,3 +1955,56 @@ setInterval(() => {
 
   check_alive_then_kill(child);
 }
+
+#[flaky_test(tokio)]
+async fn run_hmr_compile_error() {
+  let t = TempDir::new();
+  let file_to_watch = t.path().join("file_to_watch.js");
+  file_to_watch.write(
+    r#"
+function foo() {
+  return `<h1>asd1</h1>`;
+}
+
+let i = 0;
+setInterval(() => {
+  console.log(i++, foo());
+}, 100);
+"#,
+  );
+
+  let mut child = util::deno_cmd()
+    .current_dir(t.path())
+    .arg("run")
+    .arg("--watch-hmr")
+    .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("Process started", &mut stderr_lines).await;
+  wait_contains("Finished config loading.", &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;
+
+  // Misspelled `function` on purpose
+  file_to_watch.write(
+    r#"
+function foo() {
+  fnction bar();
+}
+
+let i = 0;
+setInterval(() => {
+  console.log(i++, foo());
+}, 100);
+"#,
+  );
+
+  wait_contains("compile error: Uncaught SyntaxError", &mut stderr_lines).await;
+  check_alive_then_kill(child);
+}