Repository: denoland/deno_core
Pull request #77 (2023-07-23 02:53:36)

chore: Add some tests for ESM extensions

bartlomieju: @nayeemrmn PTAL

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 15cc96e..74f1061 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -289,7 +289,7 @@ dependencies = [
 
 [[package]]
 name = "deno_core"
-version = "0.195.0"
+version = "0.196.0"
 dependencies = [
  "anyhow",
  "bencher",
@@ -327,7 +327,7 @@ dependencies = [
 
 [[package]]
 name = "deno_ops"
-version = "0.73.0"
+version = "0.74.0"
 dependencies = [
  "deno-proc-macro-rules",
  "lazy-regex",
@@ -1268,7 +1268,7 @@ dependencies = [
 
 [[package]]
 name = "serde_v8"
-version = "0.106.0"
+version = "0.107.0"
 dependencies = [
  "bencher",
  "bytes",
diff --git a/Cargo.toml b/Cargo.toml
index 4c82206..e7c0c00 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,9 +19,9 @@ repository = "https://github.com/denoland/deno"
 v8 = { version = "0.74.3", default-features = false }
 deno_ast = { version = "0.27.0", features = ["transpiling"] }
 
-deno_core = { version = "0.195.0", path = "./core" }
-deno_ops = { version = "0.73.0", path = "./ops" }
-serde_v8 = { version = "0.106.0", path = "./serde_v8" }
+deno_core = { version = "0.196.0", path = "./core" }
+deno_ops = { version = "0.74.0", path = "./ops" }
+serde_v8 = { version = "0.107.0", path = "./serde_v8" }
 
 anyhow = "1.0.57"
 bencher = "0.1"
diff --git a/core/Cargo.toml b/core/Cargo.toml
index e89ef11..9c0956b 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -2,7 +2,7 @@
 
 [package]
 name = "deno_core"
-version = "0.195.0"
+version = "0.196.0"
 authors.workspace = true
 edition.workspace = true
 license.workspace = true
diff --git a/core/runtime/jsrealm.rs b/core/runtime/jsrealm.rs
index 41d8da7..3f467d4 100644
--- a/core/runtime/jsrealm.rs
+++ b/core/runtime/jsrealm.rs
@@ -208,12 +208,13 @@ impl JsRealmInner {
 
   pub(crate) fn check_promise_rejections(
     &self,
-    scope: &mut v8::HandleScope,
+    isolate: &mut v8::Isolate,
   ) -> Result<(), Error> {
     let Some((_, handle)) = self.context_state.borrow_mut().pending_promise_rejections.pop_front() else {
       return Ok(());
     };
 
+    let scope = &mut self.handle_scope(isolate);
     let exception = v8::Local::new(scope, handle);
     let state_rc = JsRuntime::state_from(scope);
     let state = state_rc.borrow();
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index 0ee845b..b125cdc 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -1810,11 +1810,11 @@ impl JsRuntime {
   }
 
   fn check_promise_rejections(&mut self) -> Result<(), Error> {
-    let state = self.inner.state.clone();
-    let scope = &mut self.handle_scope();
-    let state = state.borrow();
+    let state_rc = self.inner.state.clone();
+    let isolate = &mut self.v8_isolate();
+    let state = state_rc.borrow();
     for realm in &state.known_realms {
-      realm.check_promise_rejections(scope)?;
+      realm.check_promise_rejections(isolate)?;
     }
     Ok(())
   }
diff --git a/ops/Cargo.toml b/ops/Cargo.toml
index 8822bd0..a8d247a 100644
--- a/ops/Cargo.toml
+++ b/ops/Cargo.toml
@@ -2,7 +2,7 @@
 
 [package]
 name = "deno_ops"
-version = "0.73.0"
+version = "0.74.0"
 authors.workspace = true
 edition.workspace = true
 license.workspace = true
diff --git a/serde_v8/Cargo.toml b/serde_v8/Cargo.toml
index d74a118..0acd639 100644
--- a/serde_v8/Cargo.toml
+++ b/serde_v8/Cargo.toml
@@ -2,7 +2,7 @@
 
 [package]
 name = "serde_v8"
-version = "0.106.0"
+version = "0.107.0"
 authors.workspace = true
 edition.workspace = true
 license.workspace = true


Test patch:
diff --git a/core/runtime/tests/jsrealm.rs b/core/runtime/tests/jsrealm.rs
index ffd1c85..b517972 100644
--- a/core/runtime/tests/jsrealm.rs
+++ b/core/runtime/tests/jsrealm.rs
@@ -63,6 +63,75 @@ async fn test_set_promise_reject_callback_realms() {
   }
 }
 
+#[test]
+fn test_set_format_exception_callback_realms() {
+  let mut runtime = JsRuntime::new(RuntimeOptions::default());
+  let main_realm = runtime.main_realm();
+  let realm1 = runtime.create_realm(Default::default()).unwrap();
+  let realm2 = runtime.create_realm(Default::default()).unwrap();
+
+  let realm_expectations = &[
+    (&main_realm, "main_realm"),
+    (&realm1, "realm1"),
+    (&realm2, "realm2"),
+  ];
+
+  // Set up format exception callbacks.
+  for (realm, realm_name) in realm_expectations {
+    realm
+      .execute_script(
+        runtime.v8_isolate(),
+        "",
+        format!(
+          r#"
+            Deno.core.ops.op_set_format_exception_callback((error) => {{
+              return `{realm_name} / ${{error}}`;
+            }});
+          "#
+        )
+        .into(),
+      )
+      .unwrap();
+  }
+
+  for (realm, realm_name) in realm_expectations {
+    // Immediate exceptions
+    {
+      let result = realm.execute_script(
+        runtime.v8_isolate(),
+        "",
+        format!("throw new Error('{realm_name}');").into(),
+      );
+      assert!(result.is_err());
+
+      let error = result.unwrap_err().downcast::<error::JsError>().unwrap();
+      assert_eq!(
+        error.exception_message,
+        format!("{realm_name} / Error: {realm_name}")
+      );
+    }
+
+    // Promise rejections
+    {
+      realm
+        .execute_script(
+          runtime.v8_isolate(),
+          "",
+          format!("Promise.reject(new Error('{realm_name}'));").into(),
+        )
+        .unwrap();
+
+      let result = futures::executor::block_on(runtime.run_event_loop(false));
+      assert!(result.is_err());
+      let error = result.unwrap_err().downcast::<error::JsError>().unwrap();
+      assert_eq!(
+        error.exception_message,
+        format!("Uncaught (in promise) {realm_name} / Error: {realm_name}")
+      );
+    }
+  }
+}
+
 #[test]
 fn js_realm_simple() {
   let mut runtime = JsRuntime::new(Default::default());
diff --git a/core/runtime/tests/misc.rs b/core/runtime/tests/misc.rs
index cf91631..cc003d3 100644
--- a/core/runtime/tests/misc.rs
+++ b/core/runtime/tests/misc.rs
@@ -1023,3 +1023,93 @@ async fn test_stalled_tla() {
   assert_eq!(js_error.frames[0].line_number, Some(1));
   assert_eq!(js_error.frames[0].column_number, Some(1));
 }
+
+/// Test that `RuntimeOptions::preserve_snapshotted_modules` also works without
+/// a snapshot.
+#[test]
+fn non_snapshot_preserve_snapshotted_modules() {
+  let extension = Extension::builder("esm_extension")
+    .esm(vec![
+      ExtensionFileSource {
+        specifier: "test:preserved",
+        code: ExtensionFileSourceCode::IncludedInBinary(
+          "export const TEST = 'foo';",
+        ),
+      },
+      ExtensionFileSource {
+        specifier: "test:not-preserved",
+        code: ExtensionFileSourceCode::IncludedInBinary(
+          "import 'test:preserved'; export const TEST = 'bar';",
+        ),
+      },
+    ])
+    .esm_entry_point("test:not-preserved")
+    .build();
+
+  struct Loader;
+
+  impl ModuleLoader for Loader {
+    fn resolve(
+      &self,
+      specifier: &str,
+      referrer: &str,
+      _kind: ResolutionKind,
+    ) -> Result<ModuleSpecifier, Error> {
+      Ok(resolve_import(specifier, referrer)?)
+    }
+
+    fn load(
+      &self,
+      module_specifier: &ModuleSpecifier,
+      _maybe_referrer: Option<&ModuleSpecifier>,
+      _is_dyn_import: bool,
+    ) -> Pin<Box<ModuleSourceFuture>> {
+      assert_eq!(module_specifier.as_str(), "test:not-preserved");
+      futures::future::ready(Err(error::generic_error("Couldn't load module")))
+        .boxed_local()
+    }
+  }
+
+  let mut runtime = JsRuntime::new(RuntimeOptions {
+    module_loader: Some(Rc::new(Loader)),
+    extensions: vec![extension],
+    preserve_snapshotted_modules: Some(&["test:preserved"]),
+    ..Default::default()
+  });
+
+  // We can't import "test:not-preserved"
+  {
+    let dyn_import_promise = runtime
+      .execute_script_static("", "import('test:not-preserved')")
+      .unwrap();
+    let dyn_import_result =
+      futures::executor::block_on(runtime.resolve_value(dyn_import_promise));
+    assert!(dyn_import_result.is_err());
+    assert_eq!(
+      dyn_import_result.err().unwrap().to_string().as_str(),
+      "Uncaught TypeError: Couldn't load module"
+    );
+  }
+
+  // But we can import "test:preserved"
+  {
+    let dyn_import_promise = runtime
+      .execute_script_static(
+        "",
+        "import('test:preserved').then(module => module.TEST)",
+      )
+      .unwrap();
+    let dyn_import_result =
+      futures::executor::block_on(runtime.resolve_value(dyn_import_promise))
+        .unwrap();
+    let scope = &mut runtime.handle_scope();
+    assert!(dyn_import_result.open(scope).is_string());
+    assert_eq!(
+      dyn_import_result
+        .open(scope)
+        .to_rust_string_lossy(scope)
+        .as_str(),
+      "foo"
+    );
+  }
+}
diff --git a/core/runtime/tests/snapshot.rs b/core/runtime/tests/snapshot.rs
index d151b27..bb6745e 100644
--- a/core/runtime/tests/snapshot.rs
+++ b/core/runtime/tests/snapshot.rs
@@ -12,6 +12,7 @@ use crate::modules::SymbolicModule;
 use crate::*;
 use anyhow::Error;
 use deno_ops::op;
+use futures::FutureExt;
 use std::pin::Pin;
 use std::rc::Rc;
 
@@ -320,3 +321,157 @@ fn es_snapshot() {
     assert_eq!(str_, "hello world test");
   }
 }
+
+#[test]
+fn es_snapshot_without_runtime_module_loader() {
+  let startup_data = {
+    let extension = Extension::builder("module_snapshot")
+      .esm(vec![ExtensionFileSource {
+        specifier: "ext:module_snapshot/test.js",
+        code: ExtensionFileSourceCode::IncludedInBinary(
+          "globalThis.TEST = 'foo'; export const TEST = 'bar';",
+        ),
+      }])
+      .esm_entry_point("ext:module_snapshot/test.js")
+      .build();
+
+    let runtime = JsRuntimeForSnapshot::new(
+      RuntimeOptions {
+        extensions: vec![extension],
+        ..Default::default()
+      },
+      Default::default(),
+    );
+
+    runtime.snapshot()
+  };
+
+  let mut runtime = JsRuntime::new(RuntimeOptions {
+    module_loader: None,
+    startup_snapshot: Some(Snapshot::JustCreated(startup_data)),
+    ..Default::default()
+  });
+
+  // Make sure the module was evaluated.
+  {
+    let scope = &mut runtime.handle_scope();
+    let global_test: v8::Local<v8::String> =
+      JsRuntime::eval(scope, "globalThis.TEST").unwrap();
+    assert_eq!(
+      serde_v8::to_utf8(global_test.to_string(scope).unwrap(), scope),
+      String::from("foo"),
+    );
+  }
+
+  // Make sure we can't import the module.
+  let dyn_import_promise = runtime
+    .execute_script_static("", "import('ext:module_snapshot/test.js')")
+    .unwrap();
+  let dyn_import_result =
+    futures::executor::block_on(runtime.resolve_value(dyn_import_promise));
+  assert!(dyn_import_result.is_err());
+  assert_eq!(
+    dyn_import_result.err().unwrap().to_string().as_str(),
+    r#"Uncaught TypeError: Module loading is not supported; attempted to resolve: "ext:module_snapshot/test.js" from """#
+  );
+}
+
+#[test]
+fn preserve_snapshotted_modules() {
+  let startup_data = {
+    let extension = Extension::builder("module_snapshot")
+      .esm(vec![
+        ExtensionFileSource {
+          specifier: "test:preserved",
+          code: ExtensionFileSourceCode::IncludedInBinary(
+            "export const TEST = 'foo';",
+          ),
+        },
+        ExtensionFileSource {
+          specifier: "test:not-preserved",
+          code: ExtensionFileSourceCode::IncludedInBinary(
+            "import 'test:preserved'; export const TEST = 'bar';",
+          ),
+        },
+      ])
+      .esm_entry_point("test:not-preserved")
+      .build();
+
+    let runtime = JsRuntimeForSnapshot::new(
+      RuntimeOptions {
+        extensions: vec![extension],
+        ..Default::default()
+      },
+      Default::default(),
+    );
+
+    runtime.snapshot()
+  };
+
+  struct Loader;
+
+  impl ModuleLoader for Loader {
+    fn resolve(
+      &self,
+      specifier: &str,
+      referrer: &str,
+      _kind: ResolutionKind,
+    ) -> Result<ModuleSpecifier, Error> {
+      Ok(resolve_import(specifier, referrer)?)
+    }
+
+    fn load(
+      &self,
+      module_specifier: &ModuleSpecifier,
+      _maybe_referrer: Option<&ModuleSpecifier>,
+      _is_dyn_import: bool,
+    ) -> Pin<Box<ModuleSourceFuture>> {
+      assert_eq!(module_specifier.as_str(), "test:not-preserved");
+      futures::future::ready(Err(error::generic_error("Couldn't load module")))
+        .boxed_local()
+    }
+  }
+
+  let mut runtime = JsRuntime::new(RuntimeOptions {
+    module_loader: Some(Rc::new(Loader)),
+    startup_snapshot: Some(Snapshot::JustCreated(startup_data)),
+    preserve_snapshotted_modules: Some(&["test:preserved"]),
+    ..Default::default()
+  });
+
+  // We can't import "test:not-preserved"
+  {
+    let dyn_import_promise = runtime
+      .execute_script_static("", "import('test:not-preserved')")
+      .unwrap();
+    let dyn_import_result =
+      futures::executor::block_on(runtime.resolve_value(dyn_import_promise));
+    assert!(dyn_import_result.is_err());
+    assert_eq!(
+      dyn_import_result.err().unwrap().to_string().as_str(),
+      "Uncaught TypeError: Couldn't load module"
+    );
+  }
+
+  // But we can import "test:preserved"
+  {
+    let dyn_import_promise = runtime
+      .execute_script_static(
+        "",
+        "import('test:preserved').then(module => module.TEST)",
+      )
+      .unwrap();
+    let dyn_import_result =
+      futures::executor::block_on(runtime.resolve_value(dyn_import_promise))
+        .unwrap();
+    let scope = &mut runtime.handle_scope();
+    assert!(dyn_import_result.open(scope).is_string());
+    assert_eq!(
+      dyn_import_result
+        .open(scope)
+        .to_rust_string_lossy(scope)
+        .as_str(),
+      "foo"
+    );
+  }
+}