Repository: denoland/deno
Pull request: feat: Add suggestion for packages using Node-API addons

This commit adds a suggestion with information and hint how
to resolve situation when user tries to run an npm package
with Node-API addons using global cache (which is currently not supported).

![Screenshot 2024-10-01 at 22 29 40](https://github.com/user-attachments/assets/b1b022df-afeb-47eb-9f78-eb9e476c655a)


Closes https://github.com/denoland/deno/issues/25974

Changed files:
filename: cli/main.rs
patch: @@ -392,6 +392,31 @@ fn get_suggestions_for_terminal_errors(e: &JsError) -> Vec<FixSuggestion> {
           "Run again with `--unstable-webgpu` flag to enable this API.",
         ),
       ];
+    // Try to capture errors like:
+    // ```
+    // Uncaught Error: Cannot find module '../build/Release/canvas.node'
+    // Require stack:
+    // - /.../deno/npm/registry.npmjs.org/canvas/2.11.2/lib/bindings.js
+    // - /.../.cache/deno/npm/registry.npmjs.org/canvas/2.11.2/lib/canvas.js
+    // ```
+    } else if msg.contains("Cannot find module")
+      && msg.contains("Require stack")
+      && msg.contains(".node'")
+    {
+      return vec![
+        FixSuggestion::info_multiline(
+          &[
+            "Trying to execute an npm package using Node-API addons,",
+            "these packages require local `node_modules` directory to be present."
+          ]
+        ),
+        FixSuggestion::hint_multiline(
+          &[
+            "Add `\"nodeModulesDir\": \"auto\" option to `deno.json`, and then run",
+            "`deno install --allow-scripts=npm:<package> --entrypoint <script>` to setup `node_modules` directory."
+          ]
+        )
+      ];
     }
   }
 

filename: runtime/fmt_errors.rs
patch: @@ -26,24 +26,44 @@ enum FixSuggestionKind {
   Hint,
 }
 
+#[derive(Debug)]
+enum FixSuggestionMessage<'a> {
+  Single(&'a str),
+  Multiline(&'a [&'a str]),
+}
+
 #[derive(Debug)]
 pub struct FixSuggestion<'a> {
   kind: FixSuggestionKind,
-  message: &'a str,
+  message: FixSuggestionMessage<'a>,
 }
 
 impl<'a> FixSuggestion<'a> {
   pub fn info(message: &'a str) -> Self {
     Self {
       kind: FixSuggestionKind::Info,
-      message,
+      message: FixSuggestionMessage::Single(message),
+    }
+  }
+
+  pub fn info_multiline(messages: &'a [&'a str]) -> Self {
+    Self {
+      kind: FixSuggestionKind::Info,
+      message: FixSuggestionMessage::Multiline(messages),
     }
   }
 
   pub fn hint(message: &'a str) -> Self {
     Self {
       kind: FixSuggestionKind::Hint,
-      message,
+      message: FixSuggestionMessage::Single(message),
+    }
+  }
+
+  pub fn hint_multiline(messages: &'a [&'a str]) -> Self {
+    Self {
+      kind: FixSuggestionKind::Hint,
+      message: FixSuggestionMessage::Multiline(messages),
     }
   }
 }
@@ -238,7 +258,21 @@ fn format_js_error_inner(
         FixSuggestionKind::Hint => write!(s, "{} ", cyan("hint:")).unwrap(),
         FixSuggestionKind::Info => write!(s, "{} ", yellow("info:")).unwrap(),
       };
-      write!(s, "{}", suggestion.message).unwrap();
+      match suggestion.message {
+        FixSuggestionMessage::Single(msg) => {
+          write!(s, "{}", msg).unwrap();
+        }
+        FixSuggestionMessage::Multiline(messages) => {
+          for (idx, message) in messages.iter().enumerate() {
+            if idx != 0 {
+              writeln!(s).unwrap();
+              write!(s, "          ").unwrap();
+            }
+            write!(s, "{}", message).unwrap();
+          }
+        }
+      }
+
       if index != (suggestions.len() - 1) {
         writeln!(s).unwrap();
       }