Repository: denoland/deno_core
Pull request #838 (2024-07-23 02:49:39)

feat(core): Use native stack trace prepare callback instead of `Error.prepareStackTrace`
Fixes issues with the `bindings` npm package, as well as `callsites`. 

Moves all error stack trace formatting to rust, allowing us to remove some ops (`op_set_call_site_evals`, `op_apply_source_map`, and some others).

This also moves the stack trace formatting from the `deno` crate to `deno_core` (`format_location` and `format_frame`) this allows us to eliminate some duplication. Previously we had multiple places that were doing formatting that we needed to keep in sync.

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/denoland/deno_core/pull/838?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) Report
Attention: Patch coverage is `76.08200%` with `105 lines` in your changes missing coverage. Please review.
> Project coverage is 81.68%. Comparing base [(`0c7f83e`)](https://app.codecov.io/gh/denoland/deno_core/commit/0c7f83ea8daabe8f541fc93ae97529dc446f5f24?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) to head [(`0dbca7c`)](https://app.codecov.io/gh/denoland/deno_core/commit/0dbca7cccb7cb15b26254c8b418f923b6c1fe918?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland).
> Report is 71 commits behind head on main.

| [Files](https://app.codecov.io/gh/denoland/deno_core/pull/838?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) | Patch % | Lines |
|---|---|---|
| [core/error.rs](https://app.codecov.io/gh/denoland/deno_core/pull/838?src=pr&el=tree&filepath=core%2Ferror.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland#diff-Y29yZS9lcnJvci5ycw==) | 75.52% | [105 Missing :warning: ](https://app.codecov.io/gh/denoland/deno_core/pull/838?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #838      +/-   ##
==========================================
+ Coverage   81.43%   81.68%   +0.24%     
==========================================
  Files          97       98       +1     
  Lines       23877    24805     +928     
==========================================
+ Hits        19445    20261     +816     
- Misses       4432     4544     +112     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/denoland/deno_core/pull/838?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland).

bartlomieju: This should probably be a `feat` - we're changing the output of stack traces and in a (potentially) breaking way.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index affd6ec..bca0e4d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -555,7 +555,7 @@ dependencies = [
 
 [[package]]
 name = "deno_core"
-version = "0.297.0"
+version = "0.298.0"
 dependencies = [
  "anyhow",
  "bencher",
@@ -624,7 +624,7 @@ dependencies = [
 
 [[package]]
 name = "deno_ops"
-version = "0.173.0"
+version = "0.174.0"
 dependencies = [
  "pretty_assertions",
  "prettyplease",
@@ -1841,7 +1841,7 @@ dependencies = [
 
 [[package]]
 name = "serde_v8"
-version = "0.206.0"
+version = "0.207.0"
 dependencies = [
  "bencher",
  "num-bigint",
@@ -2728,9 +2728,9 @@ dependencies = [
 
 [[package]]
 name = "v8"
-version = "0.98.2"
+version = "0.99.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9b8009c63eec162cbcc41aa4db66b81974f641d32a35b1a3198ec2ce4712d6b"
+checksum = "fa3fc0608a78f0c7d4ec88025759cb78c90a29984b48540060355a626ae329c1"
 dependencies = [
  "bindgen",
  "bitflags",
diff --git a/Cargo.toml b/Cargo.toml
index a347aa9..7b9fe09 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,12 +19,12 @@ repository = "https://github.com/denoland/deno_core"
 
 [workspace.dependencies]
 # Local dependencies
-deno_core = { version = "0.297.0", path = "./core" }
-deno_ops = { version = "0.173.0", path = "./ops" }
-serde_v8 = { version = "0.206.0", path = "./serde_v8" }
+deno_core = { version = "0.298.0", path = "./core" }
+deno_ops = { version = "0.174.0", path = "./ops" }
+serde_v8 = { version = "0.207.0", path = "./serde_v8" }
 deno_core_testing = { path = "./testing" }
 
-v8 = { version = "0.98.2", default-features = false }
+v8 = { version = "0.99.0", default-features = false }
 deno_ast = { version = "=0.40.0", features = ["transpiling"] }
 deno_unsync = "0.3.10"
 deno_core_icudata = "0.0.73"
diff --git a/core/02_error.js b/core/02_error.js
deleted file mode 100644
index 1a7c8c3..0000000
--- a/core/02_error.js
+++ /dev/null
@@ -1,178 +0,0 @@
-// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
-"use strict";
-
-((window) => {
-  const core = Deno.core;
-  const {
-    op_format_file_name,
-    op_apply_source_map,
-    op_apply_source_map_filename,
-    op_set_call_site_evals,
-  } = core.ops;
-  const {
-    Error,
-    ArrayPrototypePush,
-    StringPrototypeStartsWith,
-    StringPrototypeEndsWith,
-    Uint8Array,
-    Uint32Array,
-  } = window.__bootstrap.primordials;
-
-  const DATA_URL_ABBREV_THRESHOLD = 150; // keep in sync with ./error.rs
-
-  // Keep in sync with `format_file_name` in ./error.rs
-  function formatFileName(fileName) {
-    if (
-      fileName.startsWith("data:") &&
-      fileName.length > DATA_URL_ABBREV_THRESHOLD
-    ) {
-      return op_format_file_name(fileName);
-    }
-    return fileName;
-  }
-
-  // Keep in sync with `cli/fmt_errors.rs`.
-  function formatLocation(callSite) {
-    if (callSite.isNative) {
-      return "native";
-    }
-    let result = "";
-    if (callSite.fileName) {
-      result += formatFileName(callSite.fileName);
-    } else {
-      if (callSite.isEval) {
-        if (callSite.evalOrigin == null) {
-          throw new Error("assert evalOrigin");
-        }
-        result += `${callSite.evalOrigin}, `;
-      }
-      result += "<anonymous>";
-    }
-    if (callSite.lineNumber !== null) {
-      result += `:${callSite.lineNumber}`;
-      if (callSite.columnNumber !== null) {
-        result += `:${callSite.columnNumber}`;
-      }
-    }
-    return result;
-  }
-
-  // Keep in sync with `cli/fmt_errors.rs`.
-  function formatCallSiteEval(callSite) {
-    let result = "";
-    if (callSite.isAsync) {
-      result += "async ";
-    }
-    if (callSite.isPromiseAll) {
-      result += `Promise.all (index ${callSite.promiseIndex})`;
-      return result;
-    }
-    const isMethodCall = !(callSite.isToplevel || callSite.isConstructor);
-    if (isMethodCall) {
-      if (callSite.functionName) {
-        if (callSite.typeName) {
-          if (
-            !StringPrototypeStartsWith(callSite.functionName, callSite.typeName)
-          ) {
-            result += `${callSite.typeName}.`;
-          }
-        }
-        result += callSite.functionName;
-        if (callSite.methodName) {
-          if (
-            !StringPrototypeEndsWith(callSite.functionName, callSite.methodName)
-          ) {
-            result += ` [as ${callSite.methodName}]`;
-          }
-        }
-      } else {
-        if (callSite.typeName) {
-          result += `${callSite.typeName}.`;
-        }
-        if (callSite.methodName) {
-          result += callSite.methodName;
-        } else {
-          result += "<anonymous>";
-        }
-      }
-    } else if (callSite.isConstructor) {
-      result += "new ";
-      if (callSite.functionName) {
-        result += callSite.functionName;
-      } else {
-        result += "<anonymous>";
-      }
-    } else if (callSite.functionName) {
-      result += callSite.functionName;
-    } else {
-      result += formatLocation(callSite);
-      return result;
-    }
-
-    result += ` (${formatLocation(callSite)})`;
-    return result;
-  }
-
-  const applySourceMapRetBuf = new Uint32Array(2);
-  const applySourceMapRetBufView = new Uint8Array(applySourceMapRetBuf.buffer);
-
-  function prepareStackTrace(error, callSites) {
-    const message = error.message !== undefined ? error.message : "";
-    const name = error.name !== undefined ? error.name : "Error";
-    let stack;
-    if (name != "" && message != "") {
-      stack = `${name}: ${message}`;
-    } else if ((name || message) != "") {
-      stack = name || message;
-    } else {
-      stack = "";
-    }
-    const callSiteEvals = [];
-    for (let i = 0; i < callSites.length; ++i) {
-      const v8CallSite = callSites[i];
-      const callSite = {
-        this: v8CallSite.getThis(),
-        typeName: v8CallSite.getTypeName(),
-        function: v8CallSite.getFunction(),
-        functionName: v8CallSite.getFunctionName(),
-        methodName: v8CallSite.getMethodName(),
-        fileName: v8CallSite.getFileName(),
-        lineNumber: v8CallSite.getLineNumber(),
-        columnNumber: v8CallSite.getColumnNumber(),
-        evalOrigin: v8CallSite.getEvalOrigin(),
-        isToplevel: v8CallSite.isToplevel(),
-        isEval: v8CallSite.isEval(),
-        isNative: v8CallSite.isNative(),
-        isConstructor: v8CallSite.isConstructor(),
-        isAsync: v8CallSite.isAsync(),
-        isPromiseAll: v8CallSite.isPromiseAll(),
-        promiseIndex: v8CallSite.getPromiseIndex(),
-      };
-      let res = 0;
-      if (
-        callSite.fileName !== null && callSite.lineNumber !== null &&
-        callSite.columnNumber !== null
-      ) {
-        res = op_apply_source_map(
-          callSite.fileName,
-          callSite.lineNumber,
-          callSite.columnNumber,
-          applySourceMapRetBufView,
-        );
-      }
-      if (res >= 1) {
-        callSite.lineNumber = applySourceMapRetBuf[0];
-        callSite.columnNumber = applySourceMapRetBuf[1];
-      }
-      if (res >= 2) {
-        callSite.fileName = op_apply_source_map_filename();
-      }
-      ArrayPrototypePush(callSiteEvals, callSite);
-      stack += `\n    at ${formatCallSiteEval(callSite)}`;
-    }
-    op_set_call_site_evals(error, callSiteEvals);
-    return stack;
-  }
-
-  Error.prepareStackTrace = prepareStackTrace;
-})(this);
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 1d78451..2d86c58 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -2,7 +2,7 @@
 
 [package]
 name = "deno_core"
-version = "0.297.0"
+version = "0.298.0"
 authors.workspace = true
 edition.workspace = true
 license.workspace = true
diff --git a/core/error.rs b/core/error.rs
index 7eacc75..79988fd 100644
--- a/core/error.rs
+++ b/core/error.rs
@@ -6,6 +6,7 @@ use std::fmt;
 use std::fmt::Debug;
 use std::fmt::Display;
 use std::fmt::Formatter;
+use std::fmt::Write as _;
 
 use anyhow::Error;
 
@@ -210,6 +211,90 @@ pub struct JsStackFrame {
   pub promise_index: Option<i64>,
 }
 
+/// Applies source map to the given location
+fn apply_source_map<'a>(
+  source_mapper: &mut crate::source_map::SourceMapper,
+  file_name: Cow<'a, str>,
+  line_number: i64,
+  column_number: i64,
+) -> (Cow<'a, str>, i64, i64) {
+  match source_mapper.apply_source_map(
+    &file_name,
+    line_number as u32,
+    column_number as u32,
+  ) {
+    SourceMapApplication::Unchanged => (file_name, line_number, column_number),
+    SourceMapApplication::LineAndColumn {
+      line_number,
+      column_number,
+    } => (file_name, line_number.into(), column_number.into()),
+    SourceMapApplication::LineAndColumnAndFileName {
+      file_name,
+      line_number,
+      column_number,
+    } => (file_name.into(), line_number.into(), column_number.into()),
+  }
+}
+
+/// Parses an eval origin string from V8, returning
+/// the contents before the location,
+/// (the file name, line number, and column number), and
+/// the contents after the location.
+///
+/// # Example
+/// ```ignore
+/// assert_eq!(
+///   parse_eval_origin("eval at foo (bar at (file://a.ts:1:2))"),
+///   Some(("eval at foo (bar at (", ("file://a.ts", 1, 2), "))")),
+/// );
+/// ```
+///
+fn parse_eval_origin(
+  eval_origin: &str,
+) -> Option<(&str, (&str, i64, i64), &str)> {
+  // The eval origin string we get from V8 looks like
+  // `eval at ${function_name} (${origin})`
+  // where origin can be either a file name, like
+  // "eval at foo (file:///path/to/script.ts:1:2)"
+  // or a nested eval origin, like
+  // "eval at foo (eval at bar (file:///path/to/script.ts:1:2))"
+  //
+  let eval_at = "eval at ";
+  // only the innermost eval origin can have location info, so find the last
+  // "eval at", then continue parsing the rest of the string
+  let mut innermost_start = eval_origin.rfind(eval_at)? + eval_at.len();
+  // skip over the function name
+  innermost_start += eval_origin[innermost_start..].find('(')? + 1;
+  if innermost_start >= eval_origin.len() {
+    // malformed
+    return None;
+  }
+
+  // from the right, split by ":" to get the column number, line number, file name
+  // (in that order, since we're iterating from the right). e.g.
+  // eval at foo (eval at bar (file://foo.ts:1:2))
+  //                           ^^^^^^^^^^^^^ ^ ^^^
+  let mut parts = eval_origin[innermost_start..].rsplitn(3, ':');
+  // the part with the column number will include extra stuff, the actual number ends at
+  // the closing paren
+  let column_number_with_rest = parts.next()?;
+  let column_number_end = column_number_with_rest.find(')')?;
+  let column_number = column_number_with_rest[..column_number_end]
+    .parse::<i64>()
+    .ok()?;
+  let line_number = parts.next()?.parse::<i64>().ok()?;
+  let file_name = parts.next()?;
+  // The column number starts after the last occurring ":".
+  let column_start = eval_origin.rfind(':')? + 1;
+  // the innermost origin ends at the end of the column number
+  let innermost_end = column_start + column_number_end;
+  Some((
+    &eval_origin[..innermost_start],
+    (file_name, line_number, column_number),
+    &eval_origin[innermost_end..],
+  ))
+}
+
 impl JsStackFrame {
   pub fn from_location(
     file_name: Option<String>,
@@ -234,6 +319,63 @@ impl JsStackFrame {
     }
   }
 
+  /// Creates a `JsStackFrame` from a `CallSite`` JS object,
+  /// provided by V8.
+  fn from_callsite_object<'s>(
+    scope: &mut v8::HandleScope<'s>,
+    callsite: v8::Local<'s, v8::Object>,
+  ) -> Option<Self> {
+    macro_rules! call {
+      ($key: ident : $t: ty) => {{
+        let res = call_method(scope, callsite, $key, &[])?;
+        let res: $t = serde_v8::from_v8(scope, res).ok()?;
+        res
+      }};
+      ($key: ident) => { call!($key : _) };
+    }
+
+    let state = JsRuntime::state_from(scope);
+    let mut source_mapper = state.source_mapper.borrow_mut();
+    // apply source map
+    let (file_name, line_number, column_number) = match (
+      call!(GET_FILE_NAME : Option<String>),
+      call!(GET_LINE_NUMBER),
+      call!(GET_COLUMN_NUMBER),
+    ) {
+      (Some(f), Some(l), Some(c)) => {
+        let (file_name, line_num, col_num) =
+          apply_source_map(&mut source_mapper, f.into(), l, c);
+        (Some(file_name.into_owned()), Some(line_num), Some(col_num))
+      }
+      (f, l, c) => (f, l, c),
+    };
+
+    // apply source map to the eval origin, if the error originates from `eval`ed code
+    let eval_origin = call!(GET_EVAL_ORIGIN: Option<String>).and_then(|o| {
+      let (before, (file, line, col), after) = parse_eval_origin(&o)?;
+      let (file, line, col) =
+        apply_source_map(&mut source_mapper, file.into(), line, col);
+      Some(format!("{before}{file}:{line}:{col}{after}"))
+    });
+
+    Some(Self {
+      file_name,
+      line_number,
+      column_number,
+      eval_origin,
+      type_name: call!(GET_TYPE_NAME),
+      function_name: call!(GET_FUNCTION_NAME),
+      method_name: call!(GET_METHOD_NAME),
+      is_top_level: call!(IS_TOPLEVEL),
+      is_eval: call!(IS_EVAL),
+      is_native: call!(IS_NATIVE),
+      is_constructor: call!(IS_CONSTRUCTOR),
+      is_async: call!(IS_ASYNC),
+      is_promise_all: call!(IS_PROMISE_ALL),
+      promise_index: call!(GET_PROMISE_INDEX),
+    })
+  }
+
   /// Gets the source mapped stack frame corresponding to the
   /// (script_resource_name, line_number, column_number) from a v8 message.
   /// For non-syntax errors, it should also correspond to the first stack frame.
@@ -244,35 +386,18 @@ impl JsStackFrame {
     let f = message.get_script_resource_name(scope)?;
     let f: v8::Local<v8::String> = f.try_into().ok()?;
     let f = f.to_rust_string_lossy(scope);
-    let l = message.get_line_number(scope)? as u32;
+    let l = message.get_line_number(scope)? as i64;
     // V8's column numbers are 0-based, we want 1-based.
-    let c = message.get_start_column() as u32 + 1;
+    let c = message.get_start_column() as i64 + 1;
     let state = JsRuntime::state_from(scope);
     let mut source_mapper = state.source_mapper.borrow_mut();
-    match source_mapper.apply_source_map(&f, l, c) {
-      SourceMapApplication::Unchanged => Some(JsStackFrame::from_location(
-        Some(f),
-        Some(l.into()),
-        Some(c.into()),
-      )),
-      SourceMapApplication::LineAndColumn {
-        line_number,
-        column_number,
-      } => Some(JsStackFrame::from_location(
-        Some(f),
-        Some(line_number.into()),
-        Some(column_number.into()),
-      )),
-      SourceMapApplication::LineAndColumnAndFileName {
-        file_name,
-        line_number,
-        column_number,
-      } => Some(JsStackFrame::from_location(
-        Some(file_name),
-        Some(line_number.into()),
-        Some(column_number.into()),
-      )),
-    }
+    let (file_name, line_num, col_num) =
+      apply_source_map(&mut source_mapper, f.into(), l, c);
+    Some(JsStackFrame::from_location(
+      Some(file_name.into_owned()),
+      Some(line_num),
+      Some(col_num),
+    ))
   }
 
   pub fn maybe_format_location(&self) -> Option<String> {
@@ -295,6 +420,22 @@ fn get_property<'a>(
   object.get(scope, key.into())
 }
 
+fn call_method<'a, T>(
+  scope: &mut v8::HandleScope<'a>,
+  object: v8::Local<v8::Object>,
+  key: FastStaticString,
+  args: &[v8::Local<'a, v8::Value>],
+) -> Option<v8::Local<'a, T>>
+where
+  v8::Local<'a, T>: TryFrom<v8::Local<'a, v8::Value>>,
+{
+  get_property(scope, object, key)?
+    .try_cast::<v8::Function>()
+    .ok()?
+    .call(scope, object.into(), args)
+    .and_then(|v| v8::Local::try_from(v).ok())
+}
+
 #[derive(Default, serde::Deserialize)]
 pub(crate) struct NativeJsError {
   pub name: Option<String>,
@@ -458,7 +599,16 @@ impl JsError {
 
       // Convert them into Vec<JsStackFrame>
       let mut frames: Vec<JsStackFrame> = match frames_v8 {
-        Some(frames_v8) => serde_v8::from_v8(scope, frames_v8.into()).unwrap(),
+        Some(frames_v8) => {
+          let mut buf = Vec::with_capacity(frames_v8.length() as usize);
+          for i in 0..frames_v8.length() {
+            let callsite = frames_v8.get_index(scope, i).unwrap().cast();
+            buf.push(
+              JsStackFrame::from_callsite_object(scope, callsite).unwrap(),
+            );
+          }
+          buf
+        }
         None => vec![],
       };
       let mut source_line = None;
@@ -780,6 +930,410 @@ pub fn throw_type_error(scope: &mut v8::HandleScope, message: impl AsRef<str>) {
   scope.throw_exception(exception);
 }
 
+v8_static_strings::v8_static_strings! {
+  ERROR = "Error",
+  GET_FILE_NAME = "getFileName",
+  GET_THIS = "getThis",
+  GET_TYPE_NAME = "getTypeName",
+  GET_FUNCTION = "getFunction",
+  GET_FUNCTION_NAME = "getFunctionName",
+  GET_METHOD_NAME = "getMethodName",
+  GET_LINE_NUMBER = "getLineNumber",
+  GET_COLUMN_NUMBER = "getColumnNumber",
+  GET_EVAL_ORIGIN = "getEvalOrigin",
+  IS_TOPLEVEL = "isToplevel",
+  IS_EVAL = "isEval",
+  IS_NATIVE = "isNative",
+  IS_CONSTRUCTOR = "isConstructor",
+  IS_ASYNC = "isAsync",
+  IS_PROMISE_ALL = "isPromiseAll",
+  GET_PROMISE_INDEX = "getPromiseIndex",
+  PREPARE_STACK_TRACE = "prepareStackTrace",
+  ORIGINAL = "_orig",
+}
+
+#[inline(always)]
+pub(crate) fn original_call_site_key<'a>(
+  scope: &mut v8::HandleScope<'a>,
+) -> v8::Local<'a, v8::Private> {
+  let name = ORIGINAL.v8_string(scope);
+  v8::Private::for_api(scope, Some(name))
+}
+
+fn make_patched_callsite<'s>(
+  scope: &mut v8::HandleScope<'s>,
+  callsite: v8::Local<'s, v8::Object>,
+  template: v8::Local<'s, v8::ObjectTemplate>,
+) -> v8::Local<'s, v8::Object> {
+  let out_obj = template.new_instance(scope).unwrap();
+  let orig_key = original_call_site_key(scope);
+  out_obj.set_private(scope, orig_key, callsite.into());
+  out_obj
+}
+
+/// Creates a template for a `Callsite`-like object, with
+/// a patched `getFileName`.
+/// Effectively:
+/// ```js
+/// const kOriginalCallsite = Symbol("_original");
+/// {
+///   [kOriginalCallsite]: originalCallSite,
+///   getLineNumber() {
+///     return this[kOriginalCallsite].getLineNumber();
+///   },
+///   // etc
+///   getFileName() {
+///     const fileName = this[kOriginalCallsite].getFileName();
+///     return fileUrlToPath(fileName);
+///   }
+/// }
+/// ```
+pub(crate) fn make_callsite_template<'s>(
+  scope: &mut v8::HandleScope<'s>,
+) -> v8::Local<'s, v8::ObjectTemplate> {
+  let template = v8::ObjectTemplate::new(scope);
+
+  // effectively
+  // `$field() { return this[kOriginalCallsite].$field() }`
+  macro_rules! make_delegate {
+    ($scope: ident, $template: ident, [$($field: ident),+ $(,)?]) => {
+      $(
+        {
+          let key = $field.v8_string($scope).into();
+          $template.set_with_attr(
+            key,
+            v8::FunctionBuilder::<v8::FunctionTemplate>::new(
+              |scope: &mut v8::HandleScope<'_>,
+              args: v8::FunctionCallbackArguments<'_>,
+              mut rv: v8::ReturnValue<'_>| {
+                let orig_key = original_call_site_key(scope);
+                let orig = args.this().get_private(scope, orig_key).unwrap();
+                let key = $field.v8_string(scope).into();
+                let orig_ret = orig
+                  .cast::<v8::Object>()
+                  .get(scope, key)
+                  .unwrap()
+                  .cast::<v8::Function>()
+                  .call(scope, orig, &[]);
+                rv.set(orig_ret.unwrap_or_else(|| v8::undefined(scope).into()));
+              },
+            )
+            .build($scope)
+            .into(),
+            v8::PropertyAttribute::DONT_DELETE
+            | v8::PropertyAttribute::DONT_ENUM
+            | v8::PropertyAttribute::READ_ONLY,
+          );
+        }
+      )+
+    };
+  }
+
+  // delegate to the original callsite object
+  make_delegate!(
+    scope,
+    template,
+    [
+      // excludes getFileName, which we'll override below
+      GET_THIS,
+      GET_TYPE_NAME,
+      GET_FUNCTION,
+      GET_FUNCTION_NAME,
+      GET_METHOD_NAME,
+      GET_LINE_NUMBER,
+      GET_COLUMN_NUMBER,
+      GET_EVAL_ORIGIN,
+      IS_TOPLEVEL,
+      IS_EVAL,
+      IS_NATIVE,
+      IS_CONSTRUCTOR,
+      IS_ASYNC,
+      IS_PROMISE_ALL,
+      GET_PROMISE_INDEX,
+    ]
+  );
+
+  let get_file_name_key = GET_FILE_NAME.v8_string(scope).into();
+  template.set_with_attr(
+    get_file_name_key,
+    v8::FunctionBuilder::<v8::FunctionTemplate>::new(
+      |scope: &mut v8::HandleScope<'_>,
+       args: v8::FunctionCallbackArguments<'_>,
+       mut rv: v8::ReturnValue<'_>| {
+        // lookup the original call site object
+        let orig_key = original_call_site_key(scope);
+        let orig = args.this().get_private(scope, orig_key).unwrap().cast();
+        // call getFileName
+        let orig_ret =
+          call_method::<v8::String>(scope, orig, GET_FILE_NAME, &[]);
+        if let Some(ret_val) = orig_ret {
+          // strip off `file://`
+          let string = ret_val.to_rust_string_lossy(scope);
+          let file_name = if string.starts_with("file://") {
+            Url::parse(&string)
+              .unwrap()
+              .to_file_path()
+              .unwrap()
+              .to_string_lossy()
+              .into_owned()
+          } else {
+            string
+          };
+          let v8_str =
+            crate::FastString::from(file_name).v8_string(scope).into();
+          rv.set(v8_str);
+        }
+      },
+    )
+    .build(scope)
+    .into(),
+    v8::PropertyAttribute::DONT_DELETE
+      | v8::PropertyAttribute::DONT_ENUM
+      | v8::PropertyAttribute::READ_ONLY,
+  );
+
+  template
+}
+
+// called by V8 whenever a stack trace is created.
+// note that this is called _instead_ of `Error.prepareStackTrace`
+// so we have to explicitly call it to keep user code wrking
+pub fn prepare_stack_trace_callback<'s>(
+  scope: &mut v8::HandleScope<'s>,
+  error: v8::Local<'s, v8::Value>,
+  callsites: v8::Local<'s, v8::Array>,
+) -> v8::Local<'s, v8::Value> {
+  // stash the callsites on the error object. this is used by `JsError::from_exception_inner`
+  // to get more details on an error, because the v8 StackFrame API is missing some info.
+  if let Ok(obj) = error.try_cast::<v8::Object>() {
+    let key = call_site_evals_key(scope);
+    obj.set_private(scope, key, callsites.into());
+  }
+
+  // `globalThis.Error.prepareStackTrace`
+  let global = scope.get_current_context().global(scope);
+  let global_error = get_property(scope, global, ERROR).unwrap().cast();
+  let prepare_fn = get_property(scope, global_error, PREPARE_STACK_TRACE)
+    .and_then(|f| f.try_cast::<v8::Function>().ok());
+
+  if let Some(prepare_fn) = prepare_fn {
+    // User defined `Error.prepareStackTrace`.
+    // Patch the callsites to have file paths, then call
+    // the user's function
+    let len = callsites.length();
+    let mut patched = Vec::with_capacity(len as usize);
+    let template = JsRuntime::state_from(scope)
+      .callsite_template
+      .borrow()
+      .clone()
+      .unwrap();
+    let template = v8::Local::new(scope, template);
+    for i in 0..len {
+      let callsite =
+        callsites.get_index(scope, i).unwrap().cast::<v8::Object>();
+      patched.push(make_patched_callsite(scope, callsite, template).into());
+    }
+    let patched_callsites = v8::Array::new_with_elements(scope, &patched);
+
+    // call the user's `prepareStackTrace` with our patched "callsite" objects
+    let this = global_error.into();
+    let args = &[error, patched_callsites.into()];
+    return prepare_fn
+      .call(scope, this, args)
+      .unwrap_or_else(|| v8::undefined(scope).into());
+  }
+
+  // no user defined `prepareStackTrace`, just call our default
+  format_stack_trace(scope, error, callsites)
+}
+
+fn format_stack_trace<'s>(
+  scope: &mut v8::HandleScope<'s>,
+  error: v8::Local<'s, v8::Value>,
+  callsites: v8::Local<'s, v8::Array>,
+) -> v8::Local<'s, v8::Value> {
+  let mut result = String::new();
+
+  if let Ok(obj) = error.try_cast() {
+    // Write out the error name + message, if any
+    let msg = get_property(scope, obj, v8_static_strings::MESSAGE)
+      .filter(|v| !v.is_undefined())
+      .map(|v| v.to_rust_string_lossy(scope))
+      .unwrap_or_default();
+    let name = get_property(scope, obj, v8_static_strings::NAME)
+      .filter(|v| !v.is_undefined())
+      .map(|v| v.to_rust_string_lossy(scope))
+      .unwrap_or_else(|| "Error".to_string());
+
+    match (!msg.is_empty(), !name.is_empty()) {
+      (true, true) => write!(result, "{}: {}", name, msg).unwrap(),
+      (true, false) => write!(result, "{}", msg).unwrap(),
+      (false, true) => write!(result, "{}", name).unwrap(),
+      (false, false) => {}
+    }
+  }
+
+  // format each stack frame
+  for i in 0..callsites.length() {
+    let callsite = callsites.get_index(scope, i).unwrap().cast::<v8::Object>();
+    let frame = JsStackFrame::from_callsite_object(scope, callsite).unwrap();
+    write!(result, "\n    at {}", format_frame::<NoAnsiColors>(&frame))
+      .unwrap();
+  }
+
+  let result = v8::String::new(scope, &result).unwrap();
+  result.into()
+}
+
+/// Formats an error without using ansi colors.
+pub struct NoAnsiColors;
+
+#[derive(Debug, Clone, Copy)]
+/// Part of an error stack trace
+pub enum ErrorElement {
+  /// An anonymous file or method name
+  Anonymous,
+  /// Text signifying a native stack frame
+  NativeFrame,
+  /// A source line number
+  LineNumber,
+  /// A source column number
+  ColumnNumber,
+  /// The name of a function (or method)
+  FunctionName,
+  /// The name of a source file
+  FileName,
+  /// The origin of an error coming from `eval`ed code
+  EvalOrigin,
+  /// Text signifying a call to `Promise.all`
+  PromiseAll,
+}
+
+/// Applies formatting to various parts of error stack traces.
+///
+/// This is to allow the `deno` crate to reuse `format_frame` and
+/// `format_location` but apply ANSI colors for terminal output,
+/// without adding extra dependencies to `deno_core`.
+pub trait ErrorFormat {
+  fn fmt_element(element: ErrorElement, s: &str) -> Cow<'_, str>;
+}
+
+impl ErrorFormat for NoAnsiColors {
+  fn fmt_element(_element: ErrorElement, s: &str) -> Cow<'_, str> {
+    s.into()
+  }
+}
+
+pub fn format_location<F: ErrorFormat>(frame: &JsStackFrame) -> String {
+  use ErrorElement::*;
+  let _internal = frame
+    .file_name
+    .as_ref()
+    .map(|f| f.starts_with("ext:"))
+    .unwrap_or(false);
+  if frame.is_native {
+    return F::fmt_element(NativeFrame, "native").to_string();
+  }
+  let mut result = String::new();
+  let file_name = frame.file_name.clone().unwrap_or_default();
+  if !file_name.is_empty() {
+    result += &F::fmt_element(FileName, &format_file_name(&file_name))
+  } else {
+    if frame.is_eval {
+      result += &(F::fmt_element(
+        ErrorElement::EvalOrigin,
+        frame.eval_origin.as_ref().unwrap(),
+      )
+      .to_string()
+        + ", ");
+    }
+    result += &F::fmt_element(Anonymous, "<anonymous>");
+  }
+  if let Some(line_number) = frame.line_number {
+    write!(
+      result,
+      ":{}",
+      F::fmt_element(LineNumber, &line_number.to_string())
+    )
+    .unwrap();
+    if let Some(column_number) = frame.column_number {
+      write!(
+        result,
+        ":{}",
+        F::fmt_element(ColumnNumber, &column_number.to_string())
+      )
+      .unwrap();
+    }
+  }
+  result
+}
+
+pub fn format_frame<F: ErrorFormat>(frame: &JsStackFrame) -> String {
+  use ErrorElement::*;
+  let _internal = frame
+    .file_name
+    .as_ref()
+    .map(|f| f.starts_with("ext:"))
+    .unwrap_or(false);
+  let is_method_call =
+    !(frame.is_top_level.unwrap_or_default() || frame.is_constructor);
+  let mut result = String::new();
+  if frame.is_async {
+    result += "async ";
+  }
+  if frame.is_promise_all {
+    result += &F::fmt_element(
+      PromiseAll,
+      &format!(
+        "Promise.all (index {})",
+        frame.promise_index.unwrap_or_default()
+      ),
+    );
+    return result;
+  }
+  if is_method_call {
+    let mut formatted_method = String::new();
+    if let Some(function_name) = &frame.function_name {
+      if let Some(type_name) = &frame.type_name {
+        if !function_name.starts_with(type_name) {
+          write!(formatted_method, "{type_name}.").unwrap();
+        }
+      }
+      formatted_method += function_name;
+      if let Some(method_name) = &frame.method_name {
+        if !function_name.ends_with(method_name) {
+          write!(formatted_method, " [as {method_name}]").unwrap();
+        }
+      }
+    } else {
+      if let Some(type_name) = &frame.type_name {
+        write!(formatted_method, "{type_name}.").unwrap();
+      }
+      if let Some(method_name) = &frame.method_name {
+        formatted_method += method_name
+      } else {
+        formatted_method += "<anonymous>";
+      }
+    }
+    result += F::fmt_element(FunctionName, &formatted_method).as_ref();
+  } else if frame.is_constructor {
+    result += "new ";
+    if let Some(function_name) = &frame.function_name {
+      write!(result, "{}", F::fmt_element(FunctionName, function_name))
+        .unwrap();
+    } else {
+      result += F::fmt_element(Anonymous, "<anonymous>").as_ref();
+    }
+  } else if let Some(function_name) = &frame.function_name {
+    result += F::fmt_element(FunctionName, function_name).as_ref();
+  } else {
+    result += &format_location::<F>(frame);
+    return result;
+  }
+  write!(result, " ({})", format_location::<F>(frame)).unwrap();
+  result
+}
+
 #[cfg(test)]
 mod tests {
   use super::*;
@@ -817,4 +1371,57 @@ mod tests {
       format_file_name("file:///%E6%9D%B1%E4%BA%AC/%F0%9F%A6%95.ts");
     assert_eq!(file_name, "file:///東京/🦕.ts");
   }
+
+  #[test]
+  fn test_parse_eval_origin() {
+    let cases = [
+      (
+        "eval at <anonymous> (file://path.ts:1:2)",
+        Some(("eval at <anonymous> (", ("file://path.ts", 1, 2), ")")),
+      ),
+      (
+        // malformed
+        "eval at (s:1:2",
+        None,
+      ),
+      (
+        // malformed
+        "at ()", None,
+      ),
+      (
+        // url with parens
+        "eval at foo (http://website.zzz/my-script).ts:1:2)",
+        Some((
+          "eval at foo (",
+          ("http://website.zzz/my-script).ts", 1, 2),
+          ")",
+        )),
+      ),
+      (
+        // nested
+        "eval at foo (eval at bar (file://path.ts:1:2))",
+        Some(("eval at foo (eval at bar (", ("file://path.ts", 1, 2), "))")),
+      ),
+    ];
+    for (input, expect) in cases {
+      match expect {
+        Some((
+          expect_before,
+          (expect_file, expect_line, expect_col),
+          expect_after,
+        )) => {
+          let (before, (file_name, line_number, column_number), after) =
+            parse_eval_origin(input).unwrap();
+          assert_eq!(before, expect_before);
+          assert_eq!(file_name, expect_file);
+          assert_eq!(line_number, expect_line);
+          assert_eq!(column_number, expect_col);
+          assert_eq!(after, expect_after);
+        }
+        None => {
+          assert!(parse_eval_origin(input).is_none());
+        }
+      }
+    }
+  }
 }
diff --git a/core/ops_builtin.rs b/core/ops_builtin.rs
index e410796..32764ad 100644
--- a/core/ops_builtin.rs
+++ b/core/ops_builtin.rs
@@ -117,9 +117,6 @@ builtin_ops! {
   ops_builtin_v8::op_destructure_error,
   ops_builtin_v8::op_dispatch_exception,
   ops_builtin_v8::op_op_names,
-  ops_builtin_v8::op_apply_source_map,
-  ops_builtin_v8::op_apply_source_map_filename,
-  ops_builtin_v8::op_set_call_site_evals,
   ops_builtin_v8::op_current_user_call_site,
   ops_builtin_v8::op_set_format_exception_callback,
   ops_builtin_v8::op_event_loop_has_more_work,
diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs
index e02fe13..5263556 100644
--- a/core/ops_builtin_v8.rs
+++ b/core/ops_builtin_v8.rs
@@ -1,5 +1,4 @@
 // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
-use crate::error::call_site_evals_key;
 use crate::error::custom_error;
 use crate::error::is_instance_of_error;
 use crate::error::range_error;
@@ -1040,74 +1039,6 @@ fn write_line_and_col_to_ret_buf(
   ret_buf[4..8].copy_from_slice(&column_number.to_le_bytes());
 }
 
-// Returns:
-// 0: no source mapping performed, use original location
-// 1: mapped line and column, but not file name. new line and column are in
-//    ret_buf, use original file name.
-// 2: mapped line, column, and file name. new line, column, and file name are in
-//    ret_buf. retrieve file name by calling `op_apply_source_map_filename`
-//    immediately after this op returns.
-#[op2(fast)]
-#[smi]
-pub fn op_apply_source_map(
-  state: &JsRuntimeState,
-  #[string] file_name: &str,
-  #[smi] line_number: u32,
-  #[smi] column_number: u32,
-  #[buffer] ret_buf: &mut [u8],
-) -> Result<u8, Error> {
-  if ret_buf.len() != 8 {
-    return Err(type_error("retBuf must be 8 bytes"));
-  }
-  let mut source_mapper = state.source_mapper.borrow_mut();
-  let application =
-    source_mapper.apply_source_map(file_name, line_number, column_number);
-  match application {
-    SourceMapApplication::Unchanged => Ok(0),
-    SourceMapApplication::LineAndColumn {
-      line_number,
-      column_number,
-    } => {
-      write_line_and_col_to_ret_buf(ret_buf, line_number, column_number);
-      Ok(1)
-    }
-    SourceMapApplication::LineAndColumnAndFileName {
-      line_number,
-      column_number,
-      file_name,
-    } => {
-      write_line_and_col_to_ret_buf(ret_buf, line_number, column_number);
-      source_mapper.stashed_file_name.replace(file_name);
-      Ok(2)
-    }
-  }
-}
-
-// Call to retrieve the stashed file name from a previous call to
-// `op_apply_source_map` that returned `2`.
-#[op2]
-#[string]
-pub fn op_apply_source_map_filename(
-  state: &JsRuntimeState,
-) -> Result<String, Error> {
-  state
-    .source_mapper
-    .borrow_mut()
-    .stashed_file_name
-    .take()
-    .ok_or_else(|| type_error("No stashed file name"))
-}
-
-#[op2]
-pub fn op_set_call_site_evals(
-  scope: &mut v8::HandleScope,
-  exception: v8::Local<v8::Object>,
-  value: v8::Local<v8::Value>,
-) {
-  let key = call_site_evals_key(scope);
-  assert!(exception.set_private(scope, key, value).unwrap())
-}
-
 #[op2]
 #[string]
 pub fn op_current_user_call_site(
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index 6c680e1..6d40d1e 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -325,10 +325,8 @@ pub(crate) static CONTEXT_SETUP_SOURCES: [InternalSourceFile; 2] = [
 
 /// These files are executed when we start setting up extensions. They rely
 /// on ops being already fully set up.
-pub(crate) static BUILTIN_SOURCES: [InternalSourceFile; 2] = [
-  internal_source_file!("01_core.js"),
-  internal_source_file!("02_error.js"),
-];
+pub(crate) static BUILTIN_SOURCES: [InternalSourceFile; 1] =
+  [internal_source_file!("01_core.js")];
 
 /// Executed after `BUILTIN_SOURCES` are executed. Provides a thin ES module
 /// that exports `core`, `internals` and `primordials` objects.
@@ -436,6 +434,7 @@ pub struct JsRuntimeState {
   pub(crate) eval_context_code_cache_ready_cb:
     Option<EvalContextCodeCacheReadyCb>,
   pub(crate) cppgc_template: RefCell<Option<v8::Global<v8::FunctionTemplate>>>,
+  pub(crate) callsite_template: RefCell<Option<v8::Global<v8::ObjectTemplate>>>,
   waker: Arc<AtomicWaker>,
   /// Accessed through [`JsRuntimeState::with_inspector`].
   inspector: RefCell<Option<Rc<RefCell<JsRuntimeInspector>>>>,
@@ -745,6 +744,7 @@ impl JsRuntime {
       inspector: None.into(),
       has_inspector: false.into(),
       cppgc_template: None.into(),
+      callsite_template: None.into(),
     });
 
     // ...now we're moving on to ops; set them up, create `OpCtx` for each op
@@ -902,6 +902,12 @@ impl JsRuntime {
     let scope = &mut context_scope;
     let context = v8::Local::new(scope, &main_context);
 
+    let callsite_template = crate::error::make_callsite_template(scope);
+    state_rc
+      .callsite_template
+      .borrow_mut()
+      .replace(v8::Global::new(scope, callsite_template));
+
     // ...followed by creation of `Deno.core` namespace, as well as internal
     // infrastructure to provide JavaScript bindings for ops...
     if init_mode == InitMode::New {
diff --git a/core/runtime/setup.rs b/core/runtime/setup.rs
index 7e935ae..dd44e4a 100644
--- a/core/runtime/setup.rs
+++ b/core/runtime/setup.rs
@@ -149,6 +149,9 @@ pub fn create_isolate(
 
   isolate.set_capture_stack_trace_for_uncaught_exceptions(true, 10);
   isolate.set_promise_reject_callback(bindings::promise_reject_callback);
+  isolate.set_prepare_stack_trace_callback(
+    crate::error::prepare_stack_trace_callback,
+  );
   isolate.set_host_initialize_import_meta_object_callback(
     bindings::host_initialize_import_meta_object_callback,
   );
diff --git a/core/runtime/v8_static_strings.rs b/core/runtime/v8_static_strings.rs
index 9e94214..0fad47f 100644
--- a/core/runtime/v8_static_strings.rs
+++ b/core/runtime/v8_static_strings.rs
@@ -7,6 +7,8 @@ macro_rules! v8_static_strings {
   };
 }
 
+pub(crate) use v8_static_strings;
+
 v8_static_strings!(
   BUILD_CUSTOM_ERROR = "buildCustomError",
   CALL_CONSOLE = "callConsole",
diff --git a/core/source_map.rs b/core/source_map.rs
index 4da0027..3e0dedf 100644
--- a/core/source_map.rs
+++ b/core/source_map.rs
@@ -54,9 +54,6 @@ pub struct SourceMapper {
   getter: Option<Rc<dyn SourceMapGetter>>,
 
   ext_source_maps: HashMap<ModuleName, SourceMapData>,
-  // This is not the right place for this, but it's the easiest way to make
-  // op_apply_source_map a fast op. This stashing should happen in #[op2].
-  pub(crate) stashed_file_name: Option<String>,
 }
 
 impl SourceMapper {
@@ -70,7 +67,6 @@ impl SourceMapper {
       ext_source_maps: Default::default(),
       loader,
       getter,
-      stashed_file_name: Default::default(),
     }
   }
 
diff --git a/ops/Cargo.toml b/ops/Cargo.toml
index 84c6df2..48cb024 100644
--- a/ops/Cargo.toml
+++ b/ops/Cargo.toml
@@ -2,7 +2,7 @@
 
 [package]
 name = "deno_ops"
-version = "0.173.0"
+version = "0.174.0"
 authors.workspace = true
 edition.workspace = true
 license.workspace = true
diff --git a/serde_v8/Cargo.toml b/serde_v8/Cargo.toml
index dadb80e..f2e8ba6 100644
--- a/serde_v8/Cargo.toml
+++ b/serde_v8/Cargo.toml
@@ -2,7 +2,7 @@
 
 [package]
 name = "serde_v8"
-version = "0.206.0"
+version = "0.207.0"
 authors.workspace = true
 edition.workspace = true
 license.workspace = true


Test patch:
diff --git a/testing/integration/error_eval_stack/error_eval_stack.out b/testing/integration/error_eval_stack/error_eval_stack.out
new file mode 100644
index 0000000..8c46c06
--- /dev/null
+++ b/testing/integration/error_eval_stack/error_eval_stack.out
@@ -0,0 +1,6 @@
+[ERR] Error
+[ERR]     at eval (eval at <anonymous> (eval at <anonymous> (eval at foo (test:///integration/error_eval_stack/error_eval_stack.ts:5:3))), <anonymous>:1:7)
+[ERR]     at eval (eval at <anonymous> (eval at foo (test:///integration/error_eval_stack/error_eval_stack.ts:5:3)), <anonymous>:1:1)
+[ERR]     at eval (eval at foo (test:///integration/error_eval_stack/error_eval_stack.ts:5:3), <anonymous>:1:1)
+[ERR]     at foo (test:///integration/error_eval_stack/error_eval_stack.ts:5:3)
+[ERR]     at test:///integration/error_eval_stack/error_eval_stack.ts:8:1
diff --git a/testing/integration/error_eval_stack/error_eval_stack.ts b/testing/integration/error_eval_stack/error_eval_stack.ts
new file mode 100644
index 0000000..be67ab0
--- /dev/null
+++ b/testing/integration/error_eval_stack/error_eval_stack.ts
@@ -0,0 +1,8 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+// FAIL
+
+function foo() {
+  eval(`eval("eval('throw new Error()')")`);
+}
+
+foo();
diff --git a/testing/lib.rs b/testing/lib.rs
index 5a6d683..6314e80 100644
--- a/testing/lib.rs
+++ b/testing/lib.rs
@@ -59,6 +59,7 @@ integration_test!(
   error_async_stack,
   error_rejection_catch,
   error_rejection_order,
+  error_eval_stack,
   error_ext_stack,
   error_with_stack,
   error_without_stack,