Skip to content
This repository was archived by the owner on Jul 7, 2025. It is now read-only.

Commit d861951

Browse files
authored
fix: error stack traces in transpiled TypeScript (#740)
* fix: error stack traces in transpiled TypeScript Example code: ```ts interface User { name: string; email: string; } const err: Error = new Error("boom!"); throw err; ``` Deno output: ``` error: Uncaught (in promise) Error: boom! const err: Error = new Error("boom!"); throw err; ^ at file:///project/x.ts:6:20 Zinnia before this change - notice the missing `: Error` text and wrong source code line & column numbers: ``` error: Uncaught (in promise) Error: boom! const err = new Error("boom!"); ^ at file:///project/x.ts:1:13 ``` Zinnia after this change (same output as from Deno): ``` error: Uncaught (in promise) Error: boom! const err: Error = new Error("boom!"); throw err; ^ at file:///project/x.ts:6:20 ``` --------- Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
1 parent 0419b15 commit d861951

File tree

4 files changed

+82
-15
lines changed

4 files changed

+82
-15
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/runtime/vendored
22
/runtime/js/vendored
33
target
4+
/runtime/tests/js/typescript_fixtures/typescript_stack_trace.ts
45

56
# Let's keep LICENSE.md in the same formatting as we use in other PL repositories
67
LICENSE.md

runtime/module_loader.rs

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use std::borrow::Cow;
2+
use std::cell::RefCell;
13
use std::collections::HashMap;
24
use std::path::{Path, PathBuf};
3-
use std::sync::{Arc, RwLock};
5+
use std::rc::Rc;
46

57
use deno_ast::{MediaType, ParseParams};
68
use deno_core::anyhow::anyhow;
@@ -21,7 +23,9 @@ use deno_core::anyhow::Result;
2123
pub struct ZinniaModuleLoader {
2224
module_root: Option<PathBuf>,
2325
// Cache mapping file_name to source_code
24-
code_cache: Arc<RwLock<HashMap<String, String>>>,
26+
code_cache: Rc<RefCell<HashMap<String, String>>>,
27+
// Cache mapping module_specifier string to source_map bytes
28+
source_maps: Rc<RefCell<HashMap<String, Vec<u8>>>>,
2529
}
2630

2731
impl ZinniaModuleLoader {
@@ -34,7 +38,8 @@ impl ZinniaModuleLoader {
3438

3539
Ok(Self {
3640
module_root,
37-
code_cache: Arc::new(RwLock::new(HashMap::new())),
41+
code_cache: Rc::new(RefCell::new(HashMap::new())),
42+
source_maps: Rc::new(RefCell::new(HashMap::new())),
3843
})
3944
}
4045
}
@@ -79,6 +84,7 @@ impl ModuleLoader for ZinniaModuleLoader {
7984
let module_root = self.module_root.clone();
8085
let maybe_referrer = maybe_referrer.cloned();
8186
let code_cache = self.code_cache.clone();
87+
let source_maps = self.source_maps.clone();
8288
let module_load = async move {
8389
let spec_str = module_specifier.as_str();
8490

@@ -188,6 +194,10 @@ impl ModuleLoader for ZinniaModuleLoader {
188194

189195
let code = read_file_to_string(&module_path).await?;
190196

197+
code_cache
198+
.borrow_mut()
199+
.insert(spec_str.to_string(), code.clone());
200+
191201
let code = if should_transpile {
192202
let parsed = deno_ast::parse_module(ParseParams {
193203
specifier: module_specifier.clone(),
@@ -198,30 +208,34 @@ impl ModuleLoader for ZinniaModuleLoader {
198208
maybe_syntax: None,
199209
})
200210
.map_err(JsErrorBox::from_err)?;
201-
parsed
211+
let res = parsed
202212
.transpile(
203213
&deno_ast::TranspileOptions {
204214
imports_not_used_as_values: deno_ast::ImportsNotUsedAsValues::Error,
205215
verbatim_module_syntax: true,
206216
..Default::default()
207217
},
208218
&Default::default(),
209-
&Default::default(),
219+
&deno_ast::EmitOptions {
220+
source_map: deno_ast::SourceMapOption::Separate,
221+
inline_sources: true,
222+
..Default::default()
223+
},
210224
)
211225
.map_err(JsErrorBox::from_err)?
212-
.into_source()
213-
.text
226+
.into_source();
227+
228+
if let Some(source_map) = res.source_map {
229+
source_maps
230+
.borrow_mut()
231+
.insert(module_specifier.to_string(), source_map.into_bytes());
232+
}
233+
234+
res.text
214235
} else {
215236
code
216237
};
217238

218-
code_cache
219-
.write()
220-
.map_err(|_| {
221-
JsErrorBox::generic("Unexpected internal error: code_cache lock was poisoned")
222-
})?
223-
.insert(spec_str.to_string(), code.clone());
224-
225239
let sandboxed_module_specifier = ModuleSpecifier::from_file_path(&sandboxed_path)
226240
.map_err(|_| {
227241
let msg = format!(
@@ -245,9 +259,16 @@ impl ModuleLoader for ZinniaModuleLoader {
245259
ModuleLoadResponse::Async(module_load.boxed_local())
246260
}
247261

262+
fn get_source_map(&self, specifier: &str) -> Option<Cow<[u8]>> {
263+
self.source_maps
264+
.borrow()
265+
.get(specifier)
266+
.map(|v| v.clone().into())
267+
}
268+
248269
fn get_source_mapped_source_line(&self, file_name: &str, line_number: usize) -> Option<String> {
249270
log::debug!("get_source_mapped_source_line {file_name}:{line_number}");
250-
let code_cache = self.code_cache.read().ok()?;
271+
let code_cache = self.code_cache.borrow();
251272
let code = code_cache.get(file_name)?;
252273

253274
// Based on Deno cli/module_loader.rs
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// This TypeScript-only block of code is removed during transpilation. A naive solution that removes
2+
// the TypeScript code instead of replacing it with whitespace and does not apply source maps to error
3+
// stack traces will lead to incorrect line numbers in error stack traces.
4+
interface User {
5+
name: string;
6+
email: string;
7+
}
8+
9+
// The part `: Error` changes the source column number
10+
// between the TypeScript original and the transpiled code.
11+
//
12+
// Throw the error so that the test can verify source code line & column numbers
13+
// in the stack trace frames but also the source code of the line throwing the exception.
14+
const error: Error = new Error(); throw error;

runtime/tests/runtime_integration_tests.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::rc::Rc;
88

99
use anyhow::{anyhow, Context};
1010
use deno_core::ModuleSpecifier;
11+
use zinnia_runtime::fmt_errors::format_js_error;
1112
use zinnia_runtime::{any_and_jserrorbox_downcast_ref, CoreError, RecordingReporter};
1213
use zinnia_runtime::{anyhow, deno_core, run_js_module, AnyError, BootstrapOptions};
1314

@@ -94,6 +95,36 @@ js_tests!(ipfs_retrieval_tests);
9495
test_runner_tests!(passing_tests);
9596
test_runner_tests!(failing_tests expect_failure);
9697

98+
#[tokio::test]
99+
async fn typescript_stack_trace_test() -> Result<(), AnyError> {
100+
let (_, run_error) = run_js_test_file("typescript_fixtures/typescript_stack_trace.ts").await?;
101+
let error = run_error.ok_or_else(|| {
102+
anyhow!("The script was expected to throw an error. Success was reported instead.")
103+
})?;
104+
105+
if let Some(CoreError::Js(e)) = any_and_jserrorbox_downcast_ref::<CoreError>(&error) {
106+
let actual_error = format_js_error(e);
107+
// Strip ANSI codes (colors, styles)
108+
let actual_error = console_static_text::ansi::strip_ansi_codes(&actual_error);
109+
// Replace current working directory in stack trace file paths with a fixed placeholder
110+
let cwd_url = ModuleSpecifier::from_file_path(std::env::current_dir().unwrap()).unwrap();
111+
let actual_error = actual_error.replace(cwd_url.as_str(), "file:///project-root");
112+
// Normalize line endings to Unix style (LF only)
113+
let actual_error = actual_error.replace("\r\n", "\n");
114+
115+
let expected_error = r#"
116+
Uncaught (in promise) Error
117+
const error: Error = new Error(); throw error;
118+
^
119+
at file:///project-root/tests/js/typescript_fixtures/typescript_stack_trace.ts:14:22
120+
"#;
121+
assert_eq!(actual_error.trim(), expected_error.trim());
122+
} else {
123+
panic!("The script threw unexpected error: {}", error);
124+
}
125+
Ok(())
126+
}
127+
97128
#[tokio::test]
98129
async fn source_code_paths_when_no_module_root() -> Result<(), AnyError> {
99130
let (activities, run_error) =

0 commit comments

Comments
 (0)