2024-01-01 14:58:21 -05:00
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2019-06-20 12:07:01 +10:00
//! This mod provides DenoError to unify errors across Deno.
2024-10-22 21:22:26 +01:00
use color_print ::cformat ;
2024-10-15 22:51:39 +01:00
use color_print ::cstr ;
2024-07-26 08:08:15 +01:00
use deno_core ::error ::format_frame ;
2022-05-03 19:45:57 +02:00
use deno_core ::error ::JsError ;
2024-10-22 21:22:26 +01:00
use deno_terminal ::colors ;
2022-07-01 15:28:06 +02:00
use std ::fmt ::Write as _ ;
2019-06-20 12:07:01 +10:00
2022-10-26 15:37:45 +02:00
#[ derive(Debug, Clone) ]
struct ErrorReference < ' a > {
from : & ' a JsError ,
to : & ' a JsError ,
}
#[ derive(Debug, Clone) ]
struct IndexedErrorReference < ' a > {
reference : ErrorReference < ' a > ,
index : usize ,
}
2024-09-05 13:49:07 +01:00
#[ derive(Debug) ]
enum FixSuggestionKind {
Info ,
Hint ,
2024-10-22 21:22:26 +01:00
Docs ,
2024-09-05 13:49:07 +01:00
}
2024-10-01 22:49:32 +01:00
#[ derive(Debug) ]
enum FixSuggestionMessage < ' a > {
Single ( & ' a str ) ,
Multiline ( & ' a [ & ' a str ] ) ,
}
2024-09-05 13:49:07 +01:00
#[ derive(Debug) ]
pub struct FixSuggestion < ' a > {
kind : FixSuggestionKind ,
2024-10-01 22:49:32 +01:00
message : FixSuggestionMessage < ' a > ,
2024-09-05 13:49:07 +01:00
}
impl < ' a > FixSuggestion < ' a > {
pub fn info ( message : & ' a str ) -> Self {
Self {
kind : FixSuggestionKind ::Info ,
2024-10-01 22:49:32 +01:00
message : FixSuggestionMessage ::Single ( message ) ,
}
}
pub fn info_multiline ( messages : & ' a [ & ' a str ] ) -> Self {
Self {
kind : FixSuggestionKind ::Info ,
message : FixSuggestionMessage ::Multiline ( messages ) ,
2024-09-05 13:49:07 +01:00
}
}
pub fn hint ( message : & ' a str ) -> Self {
Self {
kind : FixSuggestionKind ::Hint ,
2024-10-01 22:49:32 +01:00
message : FixSuggestionMessage ::Single ( message ) ,
}
}
pub fn hint_multiline ( messages : & ' a [ & ' a str ] ) -> Self {
Self {
kind : FixSuggestionKind ::Hint ,
message : FixSuggestionMessage ::Multiline ( messages ) ,
2024-09-05 13:49:07 +01:00
}
}
2024-10-22 21:22:26 +01:00
pub fn docs ( url : & ' a str ) -> Self {
Self {
kind : FixSuggestionKind ::Docs ,
message : FixSuggestionMessage ::Single ( url ) ,
}
}
2024-09-05 13:49:07 +01:00
}
2024-07-26 08:08:15 +01:00
struct AnsiColors ;
impl deno_core ::error ::ErrorFormat for AnsiColors {
fn fmt_element (
element : deno_core ::error ::ErrorElement ,
s : & str ,
) -> std ::borrow ::Cow < '_ , str > {
use deno_core ::error ::ErrorElement ::* ;
match element {
Anonymous | NativeFrame | FileName | EvalOrigin = > {
2024-10-22 21:22:26 +01:00
colors ::cyan ( s ) . to_string ( ) . into ( )
2020-09-22 18:01:30 +01:00
}
2024-10-22 21:22:26 +01:00
LineNumber | ColumnNumber = > colors ::yellow ( s ) . to_string ( ) . into ( ) ,
FunctionName | PromiseAll = > colors ::italic_bold ( s ) . to_string ( ) . into ( ) ,
2020-09-22 18:01:30 +01:00
}
}
2020-06-08 14:06:20 +02:00
}
2019-06-20 12:07:01 +10:00
/// Take an optional source line and associated information to format it into
/// a pretty printed version of that line.
2020-04-20 20:39:02 +01:00
fn format_maybe_source_line (
2020-06-29 14:17:37 +02:00
source_line : Option < & str > ,
2022-04-15 15:08:09 +01:00
column_number : Option < i64 > ,
2019-06-20 12:07:01 +10:00
is_error : bool ,
level : usize ,
) -> String {
2022-04-15 15:08:09 +01:00
if source_line . is_none ( ) | | column_number . is_none ( ) {
2019-06-20 12:07:01 +10:00
return " " . to_string ( ) ;
}
2020-04-20 20:39:02 +01:00
let source_line = source_line . unwrap ( ) ;
2019-06-20 12:07:01 +10:00
// sometimes source_line gets set with an empty string, which then outputs
2020-03-24 20:53:48 -07:00
// an empty source line when displayed, so need just short circuit here.
2022-06-20 13:42:20 +01:00
if source_line . is_empty ( ) {
2019-06-20 12:07:01 +10:00
return " " . to_string ( ) ;
}
2021-10-18 17:05:36 +01:00
if source_line . contains ( " Couldn't format source line: " ) {
2023-01-27 10:43:16 -05:00
return format! ( " \n {source_line} " ) ;
2021-10-18 17:05:36 +01:00
}
2019-06-20 12:07:01 +10:00
let mut s = String ::new ( ) ;
2022-04-15 15:08:09 +01:00
let column_number = column_number . unwrap ( ) ;
2021-10-18 17:05:36 +01:00
2022-04-15 15:08:09 +01:00
if column_number as usize > source_line . len ( ) {
2021-10-18 17:05:36 +01:00
return format! (
" \n {} Couldn't format source line: Column {} is out of bounds (source may have changed at runtime) " ,
2024-10-22 21:22:26 +01:00
colors ::yellow ( " Warning " ) , column_number ,
2021-10-18 17:05:36 +01:00
) ;
}
2022-04-15 15:08:09 +01:00
for _i in 0 .. ( column_number - 1 ) {
2020-05-02 01:03:54 +08:00
if source_line . chars ( ) . nth ( _i as usize ) . unwrap ( ) = = '\t' {
s . push ( '\t' ) ;
} else {
s . push ( ' ' ) ;
}
2020-04-13 15:54:16 +01:00
}
2022-04-15 15:08:09 +01:00
s . push ( '^' ) ;
2019-06-20 12:07:01 +10:00
let color_underline = if is_error {
2024-10-22 21:22:26 +01:00
colors ::red ( & s ) . to_string ( )
2019-06-20 12:07:01 +10:00
} else {
2024-10-22 21:22:26 +01:00
colors ::cyan ( & s ) . to_string ( )
2019-06-20 12:07:01 +10:00
} ;
let indent = format! ( " {:indent$} " , " " , indent = level ) ;
2023-01-27 10:43:16 -05:00
format! ( " \n {indent} {source_line} \n {indent} {color_underline} " )
2019-06-20 12:07:01 +10:00
}
2022-10-26 15:37:45 +02:00
fn find_recursive_cause ( js_error : & JsError ) -> Option < ErrorReference > {
let mut history = Vec ::< & JsError > ::new ( ) ;
let mut current_error : & JsError = js_error ;
while let Some ( cause ) = & current_error . cause {
history . push ( current_error ) ;
2023-12-06 17:02:52 -07:00
if let Some ( seen ) = history . iter ( ) . find ( | & el | cause . is_same_error ( el ) ) {
2022-10-26 15:37:45 +02:00
return Some ( ErrorReference {
from : current_error ,
2022-11-17 22:59:10 -03:00
to : seen ,
2022-10-26 15:37:45 +02:00
} ) ;
} else {
current_error = cause ;
}
}
None
}
fn format_aggregated_error (
aggregated_errors : & Vec < JsError > ,
circular_reference_index : usize ,
) -> String {
2022-04-16 16:12:26 +02:00
let mut s = String ::new ( ) ;
2022-10-26 15:37:45 +02:00
let mut nested_circular_reference_index = circular_reference_index ;
for js_error in aggregated_errors {
let aggregated_circular = find_recursive_cause ( js_error ) ;
if aggregated_circular . is_some ( ) {
nested_circular_reference_index + = 1 ;
}
let error_string = format_js_error_inner (
js_error ,
aggregated_circular . map ( | reference | IndexedErrorReference {
reference ,
index : nested_circular_reference_index ,
} ) ,
false ,
2024-09-05 13:49:07 +01:00
vec! [ ] ,
2022-10-26 15:37:45 +02:00
) ;
for line in error_string . trim_start_matches ( " Uncaught " ) . lines ( ) {
2023-01-27 10:43:16 -05:00
write! ( s , " \n {line} " ) . unwrap ( ) ;
2022-10-26 15:37:45 +02:00
}
}
s
}
fn format_js_error_inner (
js_error : & JsError ,
circular : Option < IndexedErrorReference > ,
include_source_code : bool ,
2024-09-05 13:49:07 +01:00
suggestions : Vec < FixSuggestion > ,
2022-10-26 15:37:45 +02:00
) -> String {
let mut s = String ::new ( ) ;
2022-04-16 16:12:26 +02:00
s . push_str ( & js_error . exception_message ) ;
2022-10-26 15:37:45 +02:00
if let Some ( circular ) = & circular {
2023-12-06 17:02:52 -07:00
if js_error . is_same_error ( circular . reference . to ) {
2024-10-22 21:22:26 +01:00
write! ( s , " {} " , colors ::cyan ( format! ( " <ref * {} > " , circular . index ) ) )
. unwrap ( ) ;
2022-04-16 16:12:26 +02:00
}
}
2022-10-26 15:37:45 +02:00
if let Some ( aggregated ) = & js_error . aggregated {
let aggregated_message = format_aggregated_error (
aggregated ,
2023-03-15 17:46:36 -04:00
circular
. as_ref ( )
. map ( | circular | circular . index )
. unwrap_or ( 0 ) ,
2022-10-26 15:37:45 +02:00
) ;
s . push_str ( & aggregated_message ) ;
}
2022-04-16 16:12:26 +02:00
let column_number = js_error
. source_line_frame_index
. and_then ( | i | js_error . frames . get ( i ) . unwrap ( ) . column_number ) ;
s . push_str ( & format_maybe_source_line (
2022-10-26 15:37:45 +02:00
if include_source_code {
2022-04-16 16:12:26 +02:00
js_error . source_line . as_deref ( )
2022-10-26 15:37:45 +02:00
} else {
None
2022-04-16 16:12:26 +02:00
} ,
column_number ,
true ,
0 ,
) ) ;
for frame in & js_error . frames {
2024-07-26 08:08:15 +01:00
write! ( s , " \n at {} " , format_frame ::< AnsiColors > ( frame ) ) . unwrap ( ) ;
2022-04-16 16:12:26 +02:00
}
if let Some ( cause ) = & js_error . cause {
2023-03-15 17:46:36 -04:00
let is_caused_by_circular = circular
. as_ref ( )
2023-12-06 17:02:52 -07:00
. map ( | circular | js_error . is_same_error ( circular . reference . from ) )
2023-03-15 17:46:36 -04:00
. unwrap_or ( false ) ;
2022-10-26 15:37:45 +02:00
let error_string = if is_caused_by_circular {
2024-10-22 21:22:26 +01:00
colors ::cyan ( format! ( " [Circular * {} ] " , circular . unwrap ( ) . index ) )
. to_string ( )
2022-10-26 15:37:45 +02:00
} else {
2024-09-05 13:49:07 +01:00
format_js_error_inner ( cause , circular , false , vec! [ ] )
2022-10-26 15:37:45 +02:00
} ;
2022-07-01 15:28:06 +02:00
write! (
s ,
2022-04-16 16:12:26 +02:00
" \n Caused by: {} " ,
error_string . trim_start_matches ( " Uncaught " )
2022-07-01 15:28:06 +02:00
)
. unwrap ( ) ;
2022-04-16 16:12:26 +02:00
}
2024-09-05 13:49:07 +01:00
if ! suggestions . is_empty ( ) {
write! ( s , " \n \n " ) . unwrap ( ) ;
for ( index , suggestion ) in suggestions . iter ( ) . enumerate ( ) {
write! ( s , " " ) . unwrap ( ) ;
match suggestion . kind {
2024-10-22 21:22:26 +01:00
FixSuggestionKind ::Hint = > {
write! ( s , " {} " , colors ::cyan ( " hint: " ) ) . unwrap ( )
}
FixSuggestionKind ::Info = > {
write! ( s , " {} " , colors ::yellow ( " info: " ) ) . unwrap ( )
}
FixSuggestionKind ::Docs = > {
write! ( s , " {} " , colors ::green ( " docs: " ) ) . unwrap ( )
}
2024-09-05 13:49:07 +01:00
} ;
2024-10-01 22:49:32 +01:00
match suggestion . message {
FixSuggestionMessage ::Single ( msg ) = > {
2024-10-22 21:22:26 +01:00
if matches! ( suggestion . kind , FixSuggestionKind ::Docs ) {
write! ( s , " {} " , cformat! ( " <u>{}</> " , msg ) ) . unwrap ( ) ;
} else {
write! ( s , " {} " , msg ) . unwrap ( ) ;
}
2024-10-01 22:49:32 +01:00
}
FixSuggestionMessage ::Multiline ( messages ) = > {
for ( idx , message ) in messages . iter ( ) . enumerate ( ) {
if idx ! = 0 {
writeln! ( s ) . unwrap ( ) ;
write! ( s , " " ) . unwrap ( ) ;
}
write! ( s , " {} " , message ) . unwrap ( ) ;
}
}
}
2024-09-05 13:49:07 +01:00
if index ! = ( suggestions . len ( ) - 1 ) {
writeln! ( s ) . unwrap ( ) ;
}
}
}
2022-04-16 16:12:26 +02:00
s
}
2024-10-15 17:59:28 +01:00
fn get_suggestions_for_terminal_errors ( e : & JsError ) -> Vec < FixSuggestion > {
if let Some ( msg ) = & e . message {
if msg . contains ( " module is not defined " )
| | msg . contains ( " exports is not defined " )
2024-10-16 00:25:24 +01:00
| | msg . contains ( " require is not defined " )
2024-10-15 17:59:28 +01:00
{
return vec! [
2024-10-16 00:25:24 +01:00
FixSuggestion ::info_multiline ( & [
cstr! ( " Deno supports CommonJS modules in <u>.cjs</> files, or when there's a <u>package.json</> " ) ,
cstr! ( " with <i> \" type \" : \" commonjs \" </> option and <i>--unstable-detect-cjs</> flag is used. " )
] ) ,
FixSuggestion ::hint_multiline ( & [
" Rewrite this module to ESM, " ,
cstr! ( " or change the file extension to <u>.cjs</u>, " ) ,
cstr! ( " or add <u>package.json</> next to the file with <i> \" type \" : \" commonjs \" </> option " ) ,
cstr! ( " and pass <i>--unstable-detect-cjs</> flag. " ) ,
] ) ,
2024-10-22 21:22:26 +01:00
FixSuggestion ::docs ( " https://docs.deno.com/go/commonjs " ) ,
2024-10-15 17:59:28 +01:00
] ;
} else if msg . contains ( " openKv is not a function " ) {
return vec! [
FixSuggestion ::info ( " Deno.openKv() is an unstable API. " ) ,
FixSuggestion ::hint (
" Run again with `--unstable-kv` flag to enable this API. " ,
) ,
] ;
} else if msg . contains ( " cron is not a function " ) {
return vec! [
FixSuggestion ::info ( " Deno.cron() is an unstable API. " ) ,
FixSuggestion ::hint (
" Run again with `--unstable-cron` flag to enable this API. " ,
) ,
] ;
} else if msg . contains ( " WebSocketStream is not defined " ) {
return vec! [
FixSuggestion ::info ( " new WebSocketStream() is an unstable API. " ) ,
FixSuggestion ::hint (
" Run again with `--unstable-net` flag to enable this API. " ,
) ,
] ;
} else if msg . contains ( " Temporal is not defined " ) {
return vec! [
FixSuggestion ::info ( " Temporal is an unstable API. " ) ,
FixSuggestion ::hint (
" Run again with `--unstable-temporal` flag to enable this API. " ,
) ,
] ;
} else if msg . contains ( " BroadcastChannel is not defined " ) {
return vec! [
FixSuggestion ::info ( " BroadcastChannel is an unstable API. " ) ,
FixSuggestion ::hint (
" Run again with `--unstable-broadcast-channel` flag to enable this API. " ,
) ,
] ;
} else if msg . contains ( " window is not defined " ) {
return vec! [
FixSuggestion ::info ( " window global is not available in Deno 2. " ) ,
FixSuggestion ::hint ( " Replace `window` with `globalThis`. " ) ,
] ;
} else if msg . contains ( " UnsafeWindowSurface is not a constructor " ) {
return vec! [
FixSuggestion ::info ( " Deno.UnsafeWindowSurface is an unstable API. " ) ,
FixSuggestion ::hint (
" 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. "
]
)
] ;
2024-10-15 22:51:39 +01:00
} else if msg . contains ( " document is not defined " ) {
return vec! [
FixSuggestion ::info ( cstr! (
" <u>document</> global is not available in Deno. "
) ) ,
FixSuggestion ::hint_multiline ( & [
cstr! ( " Use a library like <u>happy-dom</>, <u>deno_dom</>, <u>linkedom</> or <u>JSDom</> " ) ,
cstr! ( " and setup the <u>document</> global according to the library documentation. " ) ,
] ) ,
] ;
2024-10-15 17:59:28 +01:00
}
2024-11-26 10:36:10 +09:00
// See: ext/canvas/01_image.js
else if msg . contains ( " The MIME type of source image is not specified " ) {
return vec! [
FixSuggestion ::hint_multiline ( & [
cstr! ( " When you want to get a <u>Blob</> from <u>fetch</>, make sure to go through a file server that returns the appropriate content-type response header, " ) ,
cstr! ( " and specify the URL to the file server like <u>await(await fetch('http://localhost:8000/sample.png').blob()</>. " ) ,
cstr! ( " Alternatively, if you are reading a local file using <u>Deno.readFile</> etc., " ) ,
cstr! ( " set the appropriate MIME type like <u>new Blob([await Deno.readFile('sample.png')], { type: 'image/png' })</>. " )
] ) ,
] ;
}
// See: ext/canvas/01_image.js
else if msg . contains ( " The MIME type " )
& & msg . contains ( " of source image is not a supported format " )
{
return vec! [
FixSuggestion ::info (
" The following MIME types are supported. "
) ,
FixSuggestion ::docs ( " https://mimesniff.spec.whatwg.org/#image-type-pattern-matching-algorithm " ) ,
] ;
}
2024-10-15 17:59:28 +01:00
}
2022-10-26 15:37:45 +02:00
2024-10-15 17:59:28 +01:00
vec! [ ]
2024-09-05 13:49:07 +01:00
}
2024-10-15 17:59:28 +01:00
/// Format a [`JsError`] for terminal output.
pub fn format_js_error ( js_error : & JsError ) -> String {
2024-09-05 13:49:07 +01:00
let circular =
find_recursive_cause ( js_error ) . map ( | reference | IndexedErrorReference {
reference ,
index : 1 ,
} ) ;
2024-10-15 17:59:28 +01:00
let suggestions = get_suggestions_for_terminal_errors ( js_error ) ;
2024-09-05 13:49:07 +01:00
format_js_error_inner ( js_error , circular , true , suggestions )
2020-01-18 00:43:53 +01:00
}
2019-06-20 12:07:01 +10:00
#[ cfg(test) ]
mod tests {
use super ::* ;
2021-09-12 12:04:17 -04:00
use test_util ::strip_ansi_codes ;
2019-06-20 12:07:01 +10:00
#[ test ]
fn test_format_none_source_line ( ) {
2022-04-15 15:08:09 +01:00
let actual = format_maybe_source_line ( None , None , false , 0 ) ;
2019-06-20 12:07:01 +10:00
assert_eq! ( actual , " " ) ;
}
#[ test ]
fn test_format_some_source_line ( ) {
2022-04-15 15:08:09 +01:00
let actual =
format_maybe_source_line ( Some ( " console.log('foo'); " ) , Some ( 9 ) , true , 0 ) ;
2019-06-20 12:07:01 +10:00
assert_eq! (
strip_ansi_codes ( & actual ) ,
2022-04-15 15:08:09 +01:00
" \n console.log( \' foo \' ); \n ^ "
2019-06-20 12:07:01 +10:00
) ;
}
}