/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/web/console/error_fmt.rs
948 строк
27 KB
Divy Srivastava
perf(ext/web): port console/inspect to Rust (cppgc object wrap) (#35087)
14 июн 2026, 15:26
Не верифицирован
14 июн 2026, 15:26
d400a15
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. //! Error formatting ported from `01_console.js` (`formatError`, //! `getStackString`, `improveStack`, stack-frame collapsing, cwd marking). use deno_core::v8; use super::inspect::*; /// `ObjectPrototypeToString(value)`. fn object_proto_to_string<'s>( scope: &mut v8::PinScope<'s, '_>, value: v8::Local<'s, v8::Value>, ) -> String { let tag: Option<String> = match v8::Local::<v8::Object>::try_from(value) { Ok(obj) => { let sym = v8::Symbol::get_to_string_tag(scope); v8::tc_scope!(tc, scope); match obj.get(tc, sym.into()) { Some(v) if v.is_string() => Some(v.to_rust_string_lossy(tc)), _ => None, } } Err(_) => None, }; let builtin = if let Some(tag) = tag { tag } else if value.is_array() { "Array".to_string() } else if value.is_function() { "Function".to_string() } else if value.is_native_error() { "Error".to_string() } else if value.is_date() { "Date".to_string() } else if value.is_reg_exp() { "RegExp".to_string() } else { "Object".to_string() }; format!("[object {builtin}]") } /// `ErrorPrototypeToString(error)`; exceptions from `name`/`message` getters /// propagate. fn error_proto_to_string<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, error: v8::Local<'s, v8::Object>, ) -> R<String> { let name_val = js_get_str(scope, error, "name")?; let name = if name_val.is_undefined() { "Error".to_string() } else { let s = { v8::tc_scope!(tc, scope); match name_val.to_string(tc) { Some(s) => Ok(s.to_rust_string_lossy(tc)), None => { let exc = tc.exception(); Err(grab_err(tc, exc)) } } }; s? }; let message_val = js_get_str(scope, error, "message")?; let message = if message_val.is_undefined() { String::new() } else { let s = { v8::tc_scope!(tc, scope); match message_val.to_string(tc) { Some(s) => Ok(s.to_rust_string_lossy(tc)), None => { let exc = tc.exception(); Err(grab_err(tc, exc)) } } }; s? }; if name.is_empty() { return Ok(message); } if message.is_empty() { return Ok(name); } Ok(format!("{name}: {message}")) } /// `getStackString(ctx, error)`. pub fn get_stack_string<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, intr: &Intrinsics<'_>, ctx: &mut Ctx<'s>, error: v8::Local<'s, v8::Object>, ) -> R<String> { let stack = { v8::tc_scope!(tc, scope); let key = v8_str(tc, "stack"); // If `stack` is a getter that throws, ignore the error. obj_get_swallow(tc, error, key.into()) }; if let Some(stack) = stack { if stack.boolean_value(scope) { if stack.is_string() { return Ok(stack.to_rust_string_lossy(scope)); } ctx.seen.push(error.into()); ctx.indentation_lvl += 4; let result = format_value(scope, intr, ctx, stack, f64::NAN, false)?; ctx.indentation_lvl -= 4; ctx.seen.pop(); let prefix = error_proto_to_string(scope, error)?; return Ok(format!("{prefix}\n {result}")); } } error_proto_to_string(scope, error) } fn obj_get_swallow<'s>( tc: &mut v8::PinScope<'s, '_>, obj: v8::Local<'s, v8::Object>, key: v8::Local<'s, v8::Value>, ) -> Option<v8::Local<'s, v8::Value>> { obj.get(tc, key) } /// `improveStack(stack, constructor, name, tag)`. `name` is the raw value of /// `err.name` (may be a non-string). fn improve_stack( mut stack: String, constructor: Option<&str>, name: &NameInfo, tag: &str, ) -> String { // let len = name.length; let len: Option<usize> = name.utf16_len; if !name.is_string { // stack = replace(stack, `${name}`, `${name} [${prefix - last char}]`) let prefix = get_prefix(constructor, tag, "Error", ""); let prefix_trimmed = &prefix[..prefix.len().saturating_sub(1)]; let replacement = format!("{} [{}]", name.display, prefix_trimmed); if let Some(pos) = stack.find(&name.display) { stack = format!( "{}{}{}", &stack[..pos], replacement, &stack[pos + name.display.len()..] ); } } let name_check = name.is_string && name.display.ends_with("Error") && stack.starts_with(&name.display) && (stack.len() == name.display.len() || stack[name.display.len()..].starts_with(':') || stack[name.display.len()..].starts_with('\n')); if constructor.is_none() || name_check { let mut fallback = "Error".to_string(); let mut len = len; if constructor.is_none() { let start = match_null_proto_error_start(&stack); fallback = start.clone().unwrap_or_default(); len = Some(fallback.encode_utf16().count()); if fallback.is_empty() { fallback = "Error".to_string(); } } let prefix_full = get_prefix(constructor, tag, &fallback, ""); let prefix = &prefix_full[..prefix_full.len().saturating_sub(1)]; // JS: `if (name !== prefix)` -- a non-string name never equals a string. if name.display != prefix || !name.is_string { if prefix.contains(&name.display) { if len == Some(0) { stack = format!("{prefix}: {stack}"); } else { // Non-string name: `StringPrototypeSlice(stack, undefined)` is // the full string (byte_len 0). let byte_len = match len { Some(l) => utf16_to_byte_len(&stack, l), None => 0, }; stack = format!("{prefix}{}", &stack[byte_len..]); } } else { let byte_len = match len { Some(l) => utf16_to_byte_len(&stack, l), None => 0, }; stack = format!("{prefix} [{}]{}", name.display, &stack[byte_len..]); } } } stack } /// `^([A-Z][a-z_ A-Z0-9[\]()-]+)(?::|\n {4}at)` or `^([a-z_A-Z0-9-]*Error)$` fn match_null_proto_error_start(stack: &str) -> Option<String> { // First pattern. let chars: Vec<char> = stack.chars().collect(); if let Some(first) = chars.first() { if first.is_ascii_uppercase() { let mut end = 1; while end < chars.len() { let c = chars[end]; if c.is_ascii_lowercase() || c == '_' || c == ' ' || c.is_ascii_uppercase() || c.is_ascii_digit() || matches!(c, '[' | ']' | '(' | ')' | '-') { end += 1; } else { break; } } // The matched group needs at least 2 chars ([A-Z] + one more), then a // `:` or `\n at` must follow. The `+` is greedy with backtracking: // find the longest end where the suffix matches. let mut e = end; while e >= 2 { let rest: String = chars[e..].iter().collect(); if rest.starts_with(':') || rest.starts_with("\n at") { return Some(chars[..e].iter().collect()); } e -= 1; } } } // Second pattern: entire stack is `[a-z_A-Z0-9-]*Error`. if stack.ends_with("Error") && stack .chars() .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') { return Some(stack.to_string()); } None } fn utf16_to_byte_len(s: &str, utf16_len: usize) -> usize { let mut count = 0usize; for (byte_idx, c) in s.char_indices() { if count >= utf16_len { return byte_idx; } count += c.len_utf16(); } s.len() } struct NameInfo { display: String, is_string: bool, utf16_len: Option<usize>, } /// `getDuplicateErrorFrameRanges(frames)`. fn get_duplicate_error_frame_ranges(frames: &[String]) -> Vec<usize> { let mut result: Vec<usize> = Vec::new(); let mut line_to_positions: Vec<(usize, Vec<usize>)> = Vec::new(); let mut index_of: std::collections::HashMap<&str, usize> = std::collections::HashMap::new(); for (i, frame) in frames.iter().enumerate() { match index_of.get(frame.as_str()) { Some(&idx) => line_to_positions[idx].1.push(i), None => { index_of.insert(frame.as_str(), line_to_positions.len()); line_to_positions.push((i, vec![i])); } } } let minimum_duplicate_range = 3usize; if frames.len() - line_to_positions.len() <= minimum_duplicate_range { return result; } let mut i = 0usize; while i + minimum_duplicate_range < frames.len() { let positions = &line_to_positions[*index_of.get(frames[i].as_str()).unwrap()].1; if positions.len() == 1 || *positions.last().unwrap() == i { i += 1; continue; } let current = positions.iter().position(|&p| p == i).unwrap() + 1; if current == positions.len() { i += 1; continue; } let mut range = positions[positions.len() - 1] - i; if range < minimum_duplicate_range { i += 1; continue; } let mut extra_steps: Option<Vec<usize>> = None; if current + 1 < positions.len() { // GCD of candidate distances. let mut gcd_range = 0usize; let mut steps: Vec<usize> = Vec::new(); for &p in &positions[current..] { let mut distance = p - i; while distance != 0 { let remainder = if distance == 0 { 0 } else { gcd_range % distance }; if gcd_range != 0 { if !steps.contains(&gcd_range) { steps.push(gcd_range); } } gcd_range = distance; distance = remainder; } if gcd_range == 1 { break; } } range = gcd_range; steps.retain(|&s| s != range); if !steps.is_empty() { extra_steps = Some(steps); } } let mut max_range = range; let mut max_duplicates = 0usize; let mut duplicate_ranges = 0usize; let mut next_start = i + range; loop { let mut equal_frames = 0usize; for j in 0..range { if frames.get(i + j) != frames.get(next_start + j) { break; } equal_frames += 1; } if equal_frames != range { let has_extra = extra_steps.as_ref().map(|s| !s.is_empty()).unwrap_or(false); if !has_extra { break; } if duplicate_ranges != 0 && max_range * max_duplicates < range * duplicate_ranges { max_range = range; max_duplicates = duplicate_ranges; } range = extra_steps.as_mut().unwrap().pop().unwrap(); next_start = i; duplicate_ranges = 0; next_start += range; continue; } duplicate_ranges += 1; next_start += range; } if max_duplicates != 0 && max_range * max_duplicates >= range * duplicate_ranges { range = max_range; duplicate_ranges = max_duplicates; } if duplicate_ranges * range >= 3 { result.push(i + range); result.push(range); result.push(duplicate_ranges); i += range * (duplicate_ranges + 1) - 1; } i += 1; } result } /// `identicalSequenceRange(a, b)`. fn identical_sequence_range(a: &[String], b: &[String]) -> (usize, usize) { if a.len() < 4 { return (0, 0); } for i in 0..a.len() - 3 { if let Some(pos) = b.iter().position(|x| *x == a[i]) { let rest = b.len() - pos; if rest > 3 { let mut len = 1usize; let max_len = (a.len() - i).min(rest); while max_len > len && a[i + len] == b[pos + len] { len += 1; } if len > 3 { return (len, i); } } } } (0, 0) } /// `getStackFrames(ctx, err, stack)`. fn get_stack_frames<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, intr: &Intrinsics<'_>, ctx: &mut Ctx<'s>, err: v8::Local<'s, v8::Object>, stack: &str, ) -> R<Vec<String>> { let mut frames: Vec<String> = stack.split('\n').map(|s| s.to_string()).collect(); let cause = { v8::tc_scope!(tc, scope); let key = v8_str(tc, "cause"); err.get(tc, key.into()) }; // Remove stack frames identical to frames in cause. if let Some(cause) = cause { let is_error = cause.is_native_error() || is_error_instance(scope, intr, cause); if !cause.is_null_or_undefined() && is_error { if let Ok(cause_obj) = v8::Local::<v8::Object>::try_from(cause) { let cause_stack = get_stack_string(scope, intr, ctx, cause_obj)?; if let Some(cause_stack_start) = cause_stack.find("\n at") { let cause_frames: Vec<String> = cause_stack[cause_stack_start + 1..] .split('\n') .map(|s| s.to_string()) .collect(); let (len, offset) = identical_sequence_range(&frames, &cause_frames); if len > 0 { let skipped = len - 2; let msg = format!(" ... {skipped} lines matching cause stack trace ..."); let styled = ctx.stylize(scope, &msg, "undefined")?; let end = (offset + 1 + skipped).min(frames.len()); frames.splice(offset + 1..end, [styled]); } } } } } // Remove recursive repetitive stack frames in long stacks. if frames.len() > 10 { let ranges = get_duplicate_error_frame_ranges(&frames); let mut i = ranges.len() as i64 - 3; while i >= 0 { let idx = i as usize; let offset = ranges[idx]; let length = ranges[idx + 1]; let duplicate_ranges = ranges[idx + 2]; let msg = format!( " ... collapsed {} duplicate lines matching above {}", length * duplicate_ranges, if duplicate_ranges > 1 { format!("{length} lines {duplicate_ranges} times...") } else { "lines ...".to_string() } ); let styled = ctx.stylize(scope, &msg, "undefined")?; let end = (offset + length * duplicate_ranges).min(frames.len()); frames.splice(offset..end, [styled]); i -= 3; } } Ok(frames) } fn is_error_instance<'s>( scope: &mut v8::PinScope<'s, '_>, intr: &Intrinsics<'_>, value: v8::Local<'s, v8::Value>, ) -> bool { let error_prototype = intr.error_prototype(scope); is_prototype_of(scope, error_prototype.into(), value) } /// `markNodeModules(ctx, line)`. fn mark_node_modules<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, ctx: &mut Ctx<'s>, line: &str, ) -> R<String> { let mut temp_line = String::new(); let mut last_pos = 0usize; let mut search_from = 0usize; loop { let Some(rel) = line.get(search_from..).and_then(|s| s.find("node_modules")) else { break; }; let node_module_position = search_from + rel; let separator = if node_module_position == 0 { None } else { line[..node_module_position].chars().last() }; let after = line[node_module_position + 12..].chars().next(); let sep_ok = matches!(separator, Some('/') | Some('\\')); let after_ok = matches!(after, Some('/') | Some('\\')); if !sep_ok || !after_ok { search_from = node_module_position + 1; continue; } let separator = separator.unwrap(); let module_start = node_module_position + 13; // include trailing separator temp_line.push_str(&line[last_pos..module_start]); let mut module_end = line[module_start..] .find(separator) .map(|p| module_start + p); if line[module_start..].starts_with('@') { // Namespaced modules have an extra slash: @namespace/package if let Some(me) = module_end { module_end = line[me + 1..].find(separator).map(|p| me + 1 + p); } } let Some(module_end) = module_end else { // JS would compute indexOf == -1 and slice(start, -1)-ish chaos; // practically module_end is always found in real frames. Bail out. temp_line.push_str(&line[module_start..]); last_pos = line.len(); break; }; let node_module = &line[module_start..module_end]; temp_line.push_str(&ctx.stylize(scope, node_module, "module")?); last_pos = module_end; search_from = module_end; } if last_pos != 0 { Ok(format!("{temp_line}{}", &line[last_pos..])) } else { Ok(line.to_string()) } } /// `markCwd(ctx, line, workingDirectory)`. fn mark_cwd<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, ctx: &mut Ctx<'s>, line: &str, working_directory: &str, ) -> R<String> { let Some(mut cwd_start_pos) = line.find(working_directory) else { return Ok(line.to_string()); }; let mut temp_line = String::new(); let mut cwd_length = working_directory.len(); // `.get` returns None (rather than panicking) when `cwd_start_pos - 7` lands // mid-character, e.g. when a multibyte char precedes the match. if cwd_start_pos >= 7 && line.get(cwd_start_pos - 7..cwd_start_pos) == Some("file://") { cwd_length += 7; cwd_start_pos -= 7; } let start = if cwd_start_pos > 0 && line[..cwd_start_pos].ends_with('(') { cwd_start_pos - 1 } else { cwd_start_pos }; let strip_close = start != cwd_start_pos && line.ends_with(')'); let end = if strip_close { line.len() - 1 } else { line.len() }; // Extend one character past the matched cwd to include the path separator. // The old JS sliced UTF-16 and added one unit; in UTF-8 the following byte // can be mid-character, so extend by the whole next char's byte length to // stay on a char boundary (`cwd_start_pos + cwd_length` is always one). let after_cwd = cwd_start_pos + cwd_length; let working_directory_end_pos = after_cwd + line[after_cwd..].chars().next().map_or(0, |c| c.len_utf8()); let cwd_slice = &line[start..working_directory_end_pos]; temp_line.push_str(&line[..start]); temp_line.push_str(&ctx.stylize(scope, cwd_slice, "undefined")?); if working_directory_end_pos < end { temp_line.push_str(&line[working_directory_end_pos..end]); } if strip_close { temp_line.push_str(&ctx.stylize(scope, ")", "undefined")?); } Ok(temp_line) } // --- frame regexes ----------------------------------------------------- fn frame_after_at(line: &str) -> Option<&str> { line.strip_prefix(" at ") } /// `^ {4}at (?:[^/\\(]+ \(|)<prefix>.+:\d+:\d+\)?$` fn match_module_frame<'a>(line: &'a str, prefix: &str) -> Option<&'a str> { let rest = frame_after_at(line)?; // Optional "funcName (" where funcName has no '/', '\' or '('. let body = if let Some(paren) = rest.find(" (") { let func = &rest[..paren]; if !func.is_empty() && !func.contains('/') && !func.contains('\\') && !func.contains('(') { &rest[paren + 2..] } else { rest } } else { rest }; let spec = body.strip_prefix(prefix)?; // `.+:\d+:\d+\)?$` let spec = spec.strip_suffix(')').unwrap_or(spec); // Trailing :col let (rest1, col) = spec.rsplit_once(':')?; if col.is_empty() || !col.bytes().all(|b| b.is_ascii_digit()) { return None; } let (module_path, lineno) = rest1.rsplit_once(':')?; if lineno.is_empty() || !lineno.bytes().all(|b| b.is_ascii_digit()) { return None; } if module_path.is_empty() { return None; } Some(module_path) } fn is_core_module_frame(line: &str) -> bool { match_module_frame(line, "node:").is_some() } fn is_ext_module_frame(line: &str) -> bool { match_module_frame(line, "ext:").is_some() } /// `^ {4}at (?:__node_internal_\S+|eventLoopTick|denoErrorToNodeError|__drainNextTickAndMacrotasks) ` fn is_filtered_ext_frame(line: &str) -> bool { let Some(rest) = frame_after_at(line) else { return false; }; for name in [ "eventLoopTick", "denoErrorToNodeError", "__drainNextTickAndMacrotasks", ] { if rest.starts_with(name) && rest[name.len()..].starts_with(' ') { return true; } } if let Some(after) = rest.strip_prefix("__node_internal_") { if let Some(space) = after.find(' ') { if space > 0 { return true; } } } false } // --- formatError -------------------------------------------------------- /// `formatError(err, constructor, tag, ctx, keys)`. pub fn format_error<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, intr: &Intrinsics<'_>, ctx: &mut Ctx<'s>, err: v8::Local<'s, v8::Object>, constructor: Option<&str>, tag: &str, keys: &mut Vec<v8::Local<'s, v8::Value>>, ) -> R<String> { let mut stack = match get_stack_string(scope, intr, ctx, err) { Ok(s) => s, Err(_) => return Ok(object_proto_to_string(scope, err.into())), }; let mut message: Option<v8::Local<'s, v8::Value>> = None; let mut message_is_getter_that_throws = false; { v8::tc_scope!(tc, scope); let key = v8_str(tc, "message"); match err.get(tc, key.into()) { Some(v) => message = Some(v), None => message_is_getter_that_throws = true, } } let mut name: Option<v8::Local<'s, v8::Value>> = None; let mut name_is_getter_that_throws = false; { v8::tc_scope!(tc, scope); let key = v8_str(tc, "name"); match err.get(tc, key.into()) { Some(v) => name = Some(v), None => name_is_getter_that_throws = true, } } let key_string = |scope: &mut v8::PinScope<'s, '_>, key: &v8::Local<'s, v8::Value>| -> Option<String> { if key.is_string() { Some(key.to_rust_string_lossy(scope)) } else { None } }; if !ctx.show_hidden && !keys.is_empty() { // Remove "stack". if let Some(pos) = keys .iter() .position(|k| key_string(scope, k).as_deref() == Some("stack")) { keys.remove(pos); } if !message_is_getter_that_throws { let msg_val = message.unwrap_or_else(|| v8::undefined(scope).into()); let msg_str = if msg_val.is_string() { Some(msg_val.to_rust_string_lossy(scope)) } else { None }; if let Some(pos) = keys .iter() .position(|k| key_string(scope, k).as_deref() == Some("message")) { // Only hide if it's a string and part of the original stack. let hide = match &msg_str { None => true, Some(m) => stack.contains(m.as_str()), }; if hide { keys.remove(pos); } } } if !name_is_getter_that_throws { let name_val = name.unwrap_or_else(|| v8::undefined(scope).into()); let name_str = if name_val.is_string() { Some(name_val.to_rust_string_lossy(scope)) } else { None }; if let Some(pos) = keys .iter() .position(|k| key_string(scope, k).as_deref() == Some("name")) { let hide = match &name_str { None => true, Some(n) => stack.contains(n.as_str()), }; if hide { keys.remove(pos); } } } } // name ??= "Error"; let name_info = { let name_val = if name_is_getter_that_throws { None } else { name }; match name_val { Some(v) if !v.is_null_or_undefined() => { let display = v.to_rust_string_lossy(scope); let is_string = v.is_string(); let utf16_len = if is_string { Some(display.encode_utf16().count()) } else { None }; NameInfo { display, is_string, utf16_len, } } _ => NameInfo { display: "Error".to_string(), is_string: true, utf16_len: Some(5), }, } }; // Push "cause" if present and not already listed. let has_cause = { v8::tc_scope!(tc, scope); let key = v8_str(tc, "cause"); err.has(tc, key.into()).unwrap_or(false) }; if has_cause { let already = keys .iter() .any(|k| key_string(scope, k).as_deref() == Some("cause")); if keys.is_empty() || !already { let cause_key = v8_str(scope, "cause"); keys.push(cause_key.into()); } } // Print errors aggregated into AggregateError. { let errors = { v8::tc_scope!(tc, scope); let key = v8_str(tc, "errors"); err.get(tc, key.into()) }; if let Some(errors) = errors { if errors.is_array() { let already = keys .iter() .any(|k| key_string(scope, k).as_deref() == Some("errors")); if keys.is_empty() || !already { let errors_key = v8_str(scope, "errors"); keys.push(errors_key.into()); } } } } stack = improve_stack(stack, constructor, &name_info, tag); // Ignore the error message if it's contained in the stack. let mut pos = 0usize; if let Some(message) = message { if !message_is_getter_that_throws && message.is_string() { let msg = message.to_rust_string_lossy(scope); if !msg.is_empty() { if let Some(p) = stack.find(&msg) { pos = p + msg.len(); } } } } // Wrap the error in brackets in case it has no stack trace. let stack_start = stack .get(pos..) .and_then(|s| s.find("\n at")) .map(|p| pos + p); match stack_start { None => { stack = format!("[{stack}]"); } Some(stack_start) => { let mut new_stack = stack[..stack_start].to_string(); let stack_frame_part = &stack[stack_start + 1..]; let lines = get_stack_frames(scope, intr, ctx, err, stack_frame_part)?; if ctx.colors { // Highlight userland code and node modules. let working_directory = { let undef: v8::Local<v8::Value> = v8::undefined(scope).into(); let ret = { let f = intr.get_cwd(scope); js_call(scope, f, undef, &[]) }?; if ret.is_string() { Some(ret.to_rust_string_lossy(scope)) } else { None } }; let mut esm_working_directory: Option<String> = None; for line in &lines { if is_filtered_ext_frame(line) && is_ext_module_frame(line) { continue; } // Frames in node:* / ext:* modules are builtin frames. if is_core_module_frame(line) || is_ext_module_frame(line) { let styled = ctx.stylize(scope, line, "undefined")?; new_stack.push_str(&format!("\n{styled}")); } else { new_stack.push('\n'); let mut line = mark_node_modules(scope, ctx, line)?; if let Some(wd) = &working_directory { let new_line = mark_cwd(scope, ctx, &line, wd)?; if new_line == line { if esm_working_directory.is_none() { esm_working_directory = Some(path_to_file_url_href(wd)); } line = mark_cwd( scope, ctx, &line, esm_working_directory.as_ref().unwrap(), )?; } else { line = new_line; } } new_stack.push_str(&line); } } } else { for line in &lines { if is_filtered_ext_frame(line) && is_ext_module_frame(line) { continue; } new_stack.push_str(&format!("\n{line}")); } } stack = new_stack; } } // The message and the stack have to be indented as well. if ctx.indentation_lvl != 0 { let indentation = " ".repeat(ctx.indentation_lvl); stack = stack.replace('\n', &format!("\n{indentation}")); } Ok(stack) } fn path_to_file_url_href(path: &str) -> String { match deno_core::url::Url::from_file_path(path) { Ok(url) => url.to_string(), Err(_) => String::new(), } }