1use color_print::cformat;
4use color_print::cstr;
5use deno_core::error::format_frame;
6use deno_core::error::JsError;
7use deno_terminal::colors;
8use std::fmt::Write as _;
9
10#[derive(Debug, Clone)]
11struct ErrorReference<'a> {
12 from: &'a JsError,
13 to: &'a JsError,
14}
15
16#[derive(Debug, Clone)]
17struct IndexedErrorReference<'a> {
18 reference: ErrorReference<'a>,
19 index: usize,
20}
21
22#[derive(Debug)]
23enum FixSuggestionKind {
24 Info,
25 Hint,
26 Docs,
27}
28
29#[derive(Debug)]
30enum FixSuggestionMessage<'a> {
31 Single(&'a str),
32 Multiline(&'a [&'a str]),
33}
34
35#[derive(Debug)]
36pub struct FixSuggestion<'a> {
37 kind: FixSuggestionKind,
38 message: FixSuggestionMessage<'a>,
39}
40
41impl<'a> FixSuggestion<'a> {
42 pub fn info(message: &'a str) -> Self {
43 Self {
44 kind: FixSuggestionKind::Info,
45 message: FixSuggestionMessage::Single(message),
46 }
47 }
48
49 pub fn info_multiline(messages: &'a [&'a str]) -> Self {
50 Self {
51 kind: FixSuggestionKind::Info,
52 message: FixSuggestionMessage::Multiline(messages),
53 }
54 }
55
56 pub fn hint(message: &'a str) -> Self {
57 Self {
58 kind: FixSuggestionKind::Hint,
59 message: FixSuggestionMessage::Single(message),
60 }
61 }
62
63 pub fn hint_multiline(messages: &'a [&'a str]) -> Self {
64 Self {
65 kind: FixSuggestionKind::Hint,
66 message: FixSuggestionMessage::Multiline(messages),
67 }
68 }
69
70 pub fn docs(url: &'a str) -> Self {
71 Self {
72 kind: FixSuggestionKind::Docs,
73 message: FixSuggestionMessage::Single(url),
74 }
75 }
76}
77
78struct AnsiColors;
79
80impl deno_core::error::ErrorFormat for AnsiColors {
81 fn fmt_element(
82 element: deno_core::error::ErrorElement,
83 s: &str,
84 ) -> std::borrow::Cow<'_, str> {
85 use deno_core::error::ErrorElement::*;
86 match element {
87 Anonymous | NativeFrame | FileName | EvalOrigin => {
88 colors::cyan(s).to_string().into()
89 }
90 LineNumber | ColumnNumber => colors::yellow(s).to_string().into(),
91 FunctionName | PromiseAll => colors::italic_bold(s).to_string().into(),
92 }
93 }
94}
95
96fn format_maybe_source_line(
99 source_line: Option<&str>,
100 column_number: Option<i64>,
101 is_error: bool,
102 level: usize,
103) -> String {
104 if source_line.is_none() || column_number.is_none() {
105 return "".to_string();
106 }
107
108 let source_line = source_line.unwrap();
109 if source_line.is_empty() {
112 return "".to_string();
113 }
114 if source_line.contains("Couldn't format source line: ") {
115 return format!("\n{source_line}");
116 }
117
118 let mut s = String::new();
119 let column_number = column_number.unwrap();
120
121 if column_number as usize > source_line.len() {
122 return format!(
123 "\n{} Couldn't format source line: Column {} is out of bounds (source may have changed at runtime)",
124 colors::yellow("Warning"), column_number,
125 );
126 }
127
128 for _i in 0..(column_number - 1) {
129 if source_line.chars().nth(_i as usize).unwrap() == '\t' {
130 s.push('\t');
131 } else {
132 s.push(' ');
133 }
134 }
135 s.push('^');
136 let color_underline = if is_error {
137 colors::red(&s).to_string()
138 } else {
139 colors::cyan(&s).to_string()
140 };
141
142 let indent = format!("{:indent$}", "", indent = level);
143
144 format!("\n{indent}{source_line}\n{indent}{color_underline}")
145}
146
147fn find_recursive_cause(js_error: &JsError) -> Option<ErrorReference> {
148 let mut history = Vec::<&JsError>::new();
149
150 let mut current_error: &JsError = js_error;
151
152 while let Some(cause) = ¤t_error.cause {
153 history.push(current_error);
154
155 if let Some(seen) = history.iter().find(|&el| cause.is_same_error(el)) {
156 return Some(ErrorReference {
157 from: current_error,
158 to: seen,
159 });
160 } else {
161 current_error = cause;
162 }
163 }
164
165 None
166}
167
168fn format_aggregated_error(
169 aggregated_errors: &Vec<JsError>,
170 circular_reference_index: usize,
171) -> String {
172 let mut s = String::new();
173 let mut nested_circular_reference_index = circular_reference_index;
174
175 for js_error in aggregated_errors {
176 let aggregated_circular = find_recursive_cause(js_error);
177 if aggregated_circular.is_some() {
178 nested_circular_reference_index += 1;
179 }
180 let error_string = format_js_error_inner(
181 js_error,
182 aggregated_circular.map(|reference| IndexedErrorReference {
183 reference,
184 index: nested_circular_reference_index,
185 }),
186 false,
187 vec![],
188 );
189
190 for line in error_string.trim_start_matches("Uncaught ").lines() {
191 write!(s, "\n {line}").unwrap();
192 }
193 }
194
195 s
196}
197
198fn format_js_error_inner(
199 js_error: &JsError,
200 circular: Option<IndexedErrorReference>,
201 include_source_code: bool,
202 suggestions: Vec<FixSuggestion>,
203) -> String {
204 let mut s = String::new();
205
206 s.push_str(&js_error.exception_message);
207
208 if let Some(circular) = &circular {
209 if js_error.is_same_error(circular.reference.to) {
210 write!(s, " {}", colors::cyan(format!("<ref *{}>", circular.index)))
211 .unwrap();
212 }
213 }
214
215 if let Some(aggregated) = &js_error.aggregated {
216 let aggregated_message = format_aggregated_error(
217 aggregated,
218 circular
219 .as_ref()
220 .map(|circular| circular.index)
221 .unwrap_or(0),
222 );
223 s.push_str(&aggregated_message);
224 }
225
226 let column_number = js_error
227 .source_line_frame_index
228 .and_then(|i| js_error.frames.get(i).unwrap().column_number);
229 s.push_str(&format_maybe_source_line(
230 if include_source_code {
231 js_error.source_line.as_deref()
232 } else {
233 None
234 },
235 column_number,
236 true,
237 0,
238 ));
239 for frame in &js_error.frames {
240 write!(s, "\n at {}", format_frame::<AnsiColors>(frame)).unwrap();
241 }
242 if let Some(cause) = &js_error.cause {
243 let is_caused_by_circular = circular
244 .as_ref()
245 .map(|circular| js_error.is_same_error(circular.reference.from))
246 .unwrap_or(false);
247
248 let error_string = if is_caused_by_circular {
249 colors::cyan(format!("[Circular *{}]", circular.unwrap().index))
250 .to_string()
251 } else {
252 format_js_error_inner(cause, circular, false, vec![])
253 };
254
255 write!(
256 s,
257 "\nCaused by: {}",
258 error_string.trim_start_matches("Uncaught ")
259 )
260 .unwrap();
261 }
262 if !suggestions.is_empty() {
263 write!(s, "\n\n").unwrap();
264 for (index, suggestion) in suggestions.iter().enumerate() {
265 write!(s, " ").unwrap();
266 match suggestion.kind {
267 FixSuggestionKind::Hint => {
268 write!(s, "{} ", colors::cyan("hint:")).unwrap()
269 }
270 FixSuggestionKind::Info => {
271 write!(s, "{} ", colors::yellow("info:")).unwrap()
272 }
273 FixSuggestionKind::Docs => {
274 write!(s, "{} ", colors::green("docs:")).unwrap()
275 }
276 };
277 match suggestion.message {
278 FixSuggestionMessage::Single(msg) => {
279 if matches!(suggestion.kind, FixSuggestionKind::Docs) {
280 write!(s, "{}", cformat!("<u>{}</>", msg)).unwrap();
281 } else {
282 write!(s, "{}", msg).unwrap();
283 }
284 }
285 FixSuggestionMessage::Multiline(messages) => {
286 for (idx, message) in messages.iter().enumerate() {
287 if idx != 0 {
288 writeln!(s).unwrap();
289 write!(s, " ").unwrap();
290 }
291 write!(s, "{}", message).unwrap();
292 }
293 }
294 }
295
296 if index != (suggestions.len() - 1) {
297 writeln!(s).unwrap();
298 }
299 }
300 }
301
302 s
303}
304
305fn get_suggestions_for_terminal_errors(e: &JsError) -> Vec<FixSuggestion> {
306 if let Some(msg) = &e.message {
307 if msg.contains("module is not defined")
308 || msg.contains("exports is not defined")
309 || msg.contains("require is not defined")
310 {
311 return vec![
312 FixSuggestion::info_multiline(&[
313 cstr!("Deno supports CommonJS modules in <u>.cjs</> files, or when the closest"),
314 cstr!("<u>package.json</> has a <i>\"type\": \"commonjs\"</> option.")
315 ]),
316 FixSuggestion::hint_multiline(&[
317 "Rewrite this module to ESM,",
318 cstr!("or change the file extension to <u>.cjs</u>,"),
319 cstr!("or add <u>package.json</> next to the file with <i>\"type\": \"commonjs\"</> option,"),
320 cstr!("or pass <i>--unstable-detect-cjs</> flag to detect CommonJS when loading."),
321 ]),
322 FixSuggestion::docs("https://docs.deno.com/go/commonjs"),
323 ];
324 } else if msg.contains("__filename is not defined") {
325 return vec![
326 FixSuggestion::info(cstr!(
327 "<u>__filename</> global is not available in ES modules."
328 )),
329 FixSuggestion::hint(cstr!("Use <u>import.meta.filename</> instead.")),
330 ];
331 } else if msg.contains("__dirname is not defined") {
332 return vec![
333 FixSuggestion::info(cstr!(
334 "<u>__dirname</> global is not available in ES modules."
335 )),
336 FixSuggestion::hint(cstr!("Use <u>import.meta.dirname</> instead.")),
337 ];
338 } else if msg.contains("Buffer is not defined") {
339 return vec![
340 FixSuggestion::info(cstr!(
341 "<u>Buffer</> is not available in the global scope in Deno."
342 )),
343 FixSuggestion::hint_multiline(&[
344 cstr!("Import it explicitly with <u>import { Buffer } from \"node:buffer\";</>,"),
345 cstr!("or run again with <u>--unstable-node-globals</> flag to add this global."),
346 ]),
347 ];
348 } else if msg.contains("clearImmediate is not defined") {
349 return vec![
350 FixSuggestion::info(cstr!(
351 "<u>clearImmediate</> is not available in the global scope in Deno."
352 )),
353 FixSuggestion::hint_multiline(&[
354 cstr!("Import it explicitly with <u>import { clearImmediate } from \"node:timers\";</>,"),
355 cstr!("or run again with <u>--unstable-node-globals</> flag to add this global."),
356 ]),
357 ];
358 } else if msg.contains("setImmediate is not defined") {
359 return vec![
360 FixSuggestion::info(cstr!(
361 "<u>setImmediate</> is not available in the global scope in Deno."
362 )),
363 FixSuggestion::hint_multiline(
364 &[cstr!("Import it explicitly with <u>import { setImmediate } from \"node:timers\";</>,"),
365 cstr!("or run again with <u>--unstable-node-globals</> flag to add this global."),
366 ]),
367 ];
368 } else if msg.contains("global is not defined") {
369 return vec![
370 FixSuggestion::info(cstr!(
371 "<u>global</> is not available in the global scope in Deno."
372 )),
373 FixSuggestion::hint_multiline(&[
374 cstr!("Use <u>globalThis</> instead, or assign <u>globalThis.global = globalThis</>,"),
375 cstr!("or run again with <u>--unstable-node-globals</> flag to add this global."),
376 ]),
377 ];
378 } else if msg.contains("openKv is not a function") {
379 return vec![
380 FixSuggestion::info("Deno.openKv() is an unstable API."),
381 FixSuggestion::hint(
382 "Run again with `--unstable-kv` flag to enable this API.",
383 ),
384 ];
385 } else if msg.contains("cron is not a function") {
386 return vec![
387 FixSuggestion::info("Deno.cron() is an unstable API."),
388 FixSuggestion::hint(
389 "Run again with `--unstable-cron` flag to enable this API.",
390 ),
391 ];
392 } else if msg.contains("WebSocketStream is not defined") {
393 return vec![
394 FixSuggestion::info("new WebSocketStream() is an unstable API."),
395 FixSuggestion::hint(
396 "Run again with `--unstable-net` flag to enable this API.",
397 ),
398 ];
399 } else if msg.contains("Temporal is not defined") {
400 return vec![
401 FixSuggestion::info("Temporal is an unstable API."),
402 FixSuggestion::hint(
403 "Run again with `--unstable-temporal` flag to enable this API.",
404 ),
405 ];
406 } else if msg.contains("BroadcastChannel is not defined") {
407 return vec![
408 FixSuggestion::info("BroadcastChannel is an unstable API."),
409 FixSuggestion::hint(
410 "Run again with `--unstable-broadcast-channel` flag to enable this API.",
411 ),
412 ];
413 } else if msg.contains("window is not defined") {
414 return vec![
415 FixSuggestion::info("window global is not available in Deno 2."),
416 FixSuggestion::hint("Replace `window` with `globalThis`."),
417 ];
418 } else if msg.contains("UnsafeWindowSurface is not a constructor") {
419 return vec![
420 FixSuggestion::info("Deno.UnsafeWindowSurface is an unstable API."),
421 FixSuggestion::hint(
422 "Run again with `--unstable-webgpu` flag to enable this API.",
423 ),
424 ];
425 } else if msg.contains("Cannot find module")
433 && msg.contains("Require stack")
434 && msg.contains(".node'")
435 {
436 return vec![
437 FixSuggestion::info_multiline(
438 &[
439 "Trying to execute an npm package using Node-API addons,",
440 "these packages require local `node_modules` directory to be present."
441 ]
442 ),
443 FixSuggestion::hint_multiline(
444 &[
445 "Add `\"nodeModulesDir\": \"auto\" option to `deno.json`, and then run",
446 "`deno install --allow-scripts=npm:<package> --entrypoint <script>` to setup `node_modules` directory."
447 ]
448 )
449 ];
450 } else if msg.contains("document is not defined") {
451 return vec![
452 FixSuggestion::info(cstr!(
453 "<u>document</> global is not available in Deno."
454 )),
455 FixSuggestion::hint_multiline(&[
456 cstr!("Use a library like <u>happy-dom</>, <u>deno_dom</>, <u>linkedom</> or <u>JSDom</>"),
457 cstr!("and setup the <u>document</> global according to the library documentation."),
458 ]),
459 ];
460 }
461 }
462
463 vec![]
464}
465
466pub fn format_js_error(js_error: &JsError) -> String {
468 let circular =
469 find_recursive_cause(js_error).map(|reference| IndexedErrorReference {
470 reference,
471 index: 1,
472 });
473 let suggestions = get_suggestions_for_terminal_errors(js_error);
474 format_js_error_inner(js_error, circular, true, suggestions)
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480 use test_util::strip_ansi_codes;
481
482 #[test]
483 fn test_format_none_source_line() {
484 let actual = format_maybe_source_line(None, None, false, 0);
485 assert_eq!(actual, "");
486 }
487
488 #[test]
489 fn test_format_some_source_line() {
490 let actual =
491 format_maybe_source_line(Some("console.log('foo');"), Some(9), true, 0);
492 assert_eq!(
493 strip_ansi_codes(&actual),
494 "\nconsole.log(\'foo\');\n ^"
495 );
496 }
497}