Repository: denoland/deno_core
Pull request #437 (2024-01-11 21:42:50)

feat: Add JsRuntime::custom_module_evaluation_cb to support arbitrary module types
This commit adds "JsRuntime::custom_module_evaluation_cb" as well as "ModuleType::Other"
that allow embedders to customize and support more module types (like WASM, test or bytes).

If no callback is provided and a "ModuleType::Other" is loaded then an error is returned.

Ref https://github.com/denoland/deno_core/pull/402.


Patch:
diff --git a/core/modules/map.rs b/core/modules/map.rs
index 17df3f5..b5ea9ea 100644
--- a/core/modules/map.rs
+++ b/core/modules/map.rs
@@ -271,7 +271,7 @@ impl ModuleMap {
         module_url_found.into_cheap_copy();
       self.data.borrow_mut().alias(
         module_url_specified,
-        &module_type.into(),
+        &module_type.clone().into(),
         module_url_found1,
       );
       module_url_found2
@@ -279,7 +279,7 @@ impl ModuleMap {
       module_url_specified
     };
 
-    let requested_module_type = RequestedModuleType::from(module_type);
+    let requested_module_type = RequestedModuleType::from(module_type.clone());
     let maybe_module_id = self.get_id(&module_url_found, requested_module_type);
 
     if let Some(module_id) = maybe_module_id {
@@ -290,15 +290,46 @@ impl ModuleMap {
       return Ok(module_id);
     }
 
-    let code = ModuleSource::get_string_source(module_url_found.as_str(), code)
-      .map_err(ModuleError::Other)?;
-    let module_id = match module_type {
+    let module_id = match module_type.clone() {
       ModuleType::JavaScript => {
+        let code =
+          ModuleSource::get_string_source(module_url_found.as_str(), code)
+            .map_err(ModuleError::Other)?;
         self.new_es_module(scope, main, module_url_found, code, dynamic)?
       }
       ModuleType::Json => {
+        let code =
+          ModuleSource::get_string_source(module_url_found.as_str(), code)
+            .map_err(ModuleError::Other)?;
         self.new_json_module(scope, module_url_found, code)?
       }
+      ModuleType::Other(module_type) => {
+        let state = JsRuntime::state_from(scope);
+        let custom_module_evaluation_cb =
+          state.custom_module_evaluation_cb.as_ref();
+
+        let Some(custom_evaluation_cb) = custom_module_evaluation_cb else {
+          return Err(ModuleError::Other(generic_error(format!(
+            "Importing '{}' modules is not supported",
+            module_type
+          ))));
+        };
+
+        let value_global = custom_evaluation_cb(
+          scope,
+          module_type.clone(),
+          &module_url_found,
+          code,
+        )
+        .map_err(ModuleError::Other)?;
+        let value = v8::Local::new(scope, value_global);
+        self.new_synthetic_module(
+          scope,
+          module_url_found,
+          ModuleType::Other(module_type.clone()),
+          value,
+        )?
+      }
     };
     Ok(module_id)
   }
diff --git a/core/modules/mod.rs b/core/modules/mod.rs
index a9b32a0..84214fe 100644
--- a/core/modules/mod.rs
+++ b/core/modules/mod.rs
@@ -121,6 +121,17 @@ pub(crate) fn default_import_meta_resolve_cb(
 pub type ValidateImportAttributesCb =
   Box<dyn Fn(&mut v8::HandleScope, &HashMap<String, String>)>;
 
+/// Callback to validate import attributes. If the validation fails and exception
+/// should be thrown using `scope.throw_exception()`.
+pub type CustomModuleEvaluationCb = Box<
+  dyn Fn(
+    &mut v8::HandleScope,
+    Cow<'_, str>,
+    &FastString,
+    ModuleSourceCode,
+  ) -> Result<v8::Global<v8::Value>, AnyError>,
+>;
+
 #[derive(Debug)]
 pub(crate) enum ImportAttributesKind {
   StaticImport,
@@ -177,15 +188,15 @@ pub(crate) fn get_requested_module_type_from_attributes(
 
 /// A type of module to be executed.
 ///
-/// For non-`JavaScript` modules, this value doesn't tell
-/// how to interpret the module; it is only used to validate
-/// the module against an import assertion (if one is present
-/// in the import statement).
-#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
+/// `deno_core` supports loading and executing JavaScript and JSON modules,
+/// by default, but embedders can customize it further by providing
+/// [`CustomModuleEvaluationCb`].
+#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
 #[repr(u32)]
 pub enum ModuleType {
   JavaScript,
   Json,
+  Other(Cow<'static, str>),
 }
 
 impl std::fmt::Display for ModuleType {
@@ -193,6 +204,7 @@ impl std::fmt::Display for ModuleType {
     match self {
       Self::JavaScript => write!(f, "JavaScript"),
       Self::Json => write!(f, "JSON"),
+      Self::Other(ty) => write!(f, "{}", ty),
     }
   }
 }
@@ -379,11 +391,13 @@ impl AsRef<RequestedModuleType> for RequestedModuleType {
   }
 }
 
+// TODO(bartlomieju): this is questionable. I think we should remove it.
 impl PartialEq<ModuleType> for RequestedModuleType {
   fn eq(&self, other: &ModuleType) -> bool {
     match other {
       ModuleType::JavaScript => self == &RequestedModuleType::None,
       ModuleType::Json => self == &RequestedModuleType::Json,
+      ModuleType::Other(ty) => self == &RequestedModuleType::Other(ty.clone()),
     }
   }
 }
@@ -393,6 +407,7 @@ impl From<ModuleType> for RequestedModuleType {
     match module_type {
       ModuleType::JavaScript => RequestedModuleType::None,
       ModuleType::Json => RequestedModuleType::Json,
+      ModuleType::Other(ty) => RequestedModuleType::Other(ty.clone()),
     }
   }
 }
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index 66d9921..66bfc42 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -17,6 +17,7 @@ use crate::include_js_files;
 use crate::inspector::JsRuntimeInspector;
 use crate::module_specifier::ModuleSpecifier;
 use crate::modules::default_import_meta_resolve_cb;
+use crate::modules::CustomModuleEvaluationCb;
 use crate::modules::ExtModuleLoader;
 use crate::modules::ImportMetaResolveCallback;
 use crate::modules::ModuleCodeString;
@@ -352,6 +353,7 @@ pub struct JsRuntimeState {
   wait_for_inspector_disconnect_callback:
     Option<WaitForInspectorDisconnectCallback>,
   pub(crate) validate_import_attributes_cb: Option<ValidateImportAttributesCb>,
+  pub(crate) custom_module_evaluation_cb: Option<CustomModuleEvaluationCb>,
   waker: Arc<AtomicWaker>,
   /// Accessed through [`JsRuntimeState::with_inspector`].
   inspector: RefCell<Option<Rc<RefCell<JsRuntimeInspector>>>>,
@@ -503,6 +505,10 @@ pub struct RuntimeOptions {
   /// more work can be scheduled from the DevTools.
   pub wait_for_inspector_disconnect_callback:
     Option<WaitForInspectorDisconnectCallback>,
+
+  /// A callback that allows to evaluate a custom type of a module - eg.
+  /// embedders might implement loading WASM or test modules.
+  pub custom_module_evaluation_cb: Option<CustomModuleEvaluationCb>,
 }
 
 impl RuntimeOptions {
@@ -666,6 +672,7 @@ impl JsRuntime {
       inspector: None.into(),
       has_inspector: false.into(),
       validate_import_attributes_cb: options.validate_import_attributes_cb,
+      custom_module_evaluation_cb: options.custom_module_evaluation_cb,
       waker,
     });
 


Test patch:
diff --git a/core/modules/tests.rs b/core/modules/tests.rs
index 120105b..928dba3 100644
--- a/core/modules/tests.rs
+++ b/core/modules/tests.rs
@@ -1,9 +1,11 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
 use crate::ascii_str;
 use crate::error::exception_to_err_result;
+use crate::error::generic_error;
 use crate::modules::loaders::ModuleLoadEventCounts;
 use crate::modules::loaders::TestingModuleLoader;
 use crate::modules::loaders::*;
+use crate::modules::ModuleCodeBytes;
 use crate::modules::ModuleError;
 use crate::modules::ModuleRequest;
 use crate::modules::ModuleSourceCode;
@@ -27,6 +29,7 @@ use deno_ops::op2;
 use futures::future::poll_fn;
 use futures::future::FutureExt;
 use parking_lot::Mutex;
+use std::borrow::Cow;
 use std::cell::RefCell;
 use std::collections::HashMap;
 use std::fmt;
@@ -704,6 +707,126 @@ fn test_validate_import_attributes_callback2() {
   }
 }
 
+#[test]
+fn test_custom_module_type_default() {
+  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::new([])));
+  let mut runtime = JsRuntime::new(RuntimeOptions {
+    module_loader: Some(loader.clone()),
+    ..Default::default()
+  });
+
+  let module_map = runtime.module_map().clone();
+
+  let err = {
+    let scope = &mut runtime.handle_scope();
+    let specifier_a = ascii_str!("file:///a.png");
+    module_map
+      .new_module(
+        scope,
+        true,
+        false,
+        ModuleSource {
+          code: ModuleSourceCode::Bytes(ModuleCodeBytes::Static(&[])),
+          module_url_found: None,
+          module_url_specified: specifier_a,
+          module_type: ModuleType::Other("bytes".into()),
+        },
+      )
+      .unwrap_err()
+  };
+
+  match err {
+    ModuleError::Other(err) => {
+      assert_eq!(
+        err.to_string(),
+        "Importing 'bytes' modules is not supported"
+      );
+    }
+    _ => unreachable!(),
+  };
+}
+
+#[test]
+fn test_custom_module_type_callback() {
+  fn custom_eval_cb(
+    scope: &mut v8::HandleScope,
+    module_type: Cow<'_, str>,
+    _module_name: &FastString,
+    module_code: ModuleSourceCode,
+  ) -> Result<v8::Global<v8::Value>, Error> {
+    if module_type != "bytes" {
+      return Err(generic_error(format!(
+        "Can't load '{}' module",
+        module_type
+      )));
+    }
+
+    let buf = match module_code {
+      ModuleSourceCode::Bytes(buf) => buf.to_vec(),
+      ModuleSourceCode::String(str_) => str_.as_bytes().to_vec(),
+    };
+    let buf_len: usize = buf.len();
+    let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(buf);
+    let backing_store_shared = backing_store.make_shared();
+    let ab = v8::ArrayBuffer::with_backing_store(scope, &backing_store_shared);
+    let uint8_array = v8::Uint8Array::new(scope, ab, 0, buf_len).unwrap();
+    let value: v8::Local<v8::Value> = uint8_array.into();
+    Ok(v8::Global::new(scope, value))
+  }
+
+  let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::new([])));
+  let mut runtime = JsRuntime::new(RuntimeOptions {
+    module_loader: Some(loader.clone()),
+    custom_module_evaluation_cb: Some(Box::new(custom_eval_cb)),
+    ..Default::default()
+  });
+
+  let module_map = runtime.module_map().clone();
+
+  let err = {
+    let scope = &mut runtime.handle_scope();
+    let specifier_a = ascii_str!("file:///a.png");
+    module_map
+      .new_module(
+        scope,
+        true,
+        false,
+        ModuleSource {
+          code: ModuleSourceCode::Bytes(ModuleCodeBytes::Static(&[])),
+          module_url_found: None,
+          module_url_specified: specifier_a,
+          module_type: ModuleType::Other("foo".into()),
+        },
+      )
+      .unwrap_err()
+  };
+
+  match err {
+    ModuleError::Other(err) => {
+      assert_eq!(err.to_string(), "Can't load 'foo' module");
+    }
+    _ => unreachable!(),
+  };
+
+  {
+    let scope = &mut runtime.handle_scope();
+    let specifier_a = ascii_str!("file:///b.png");
+    module_map
+      .new_module(
+        scope,
+        true,
+        false,
+        ModuleSource {
+          code: ModuleSourceCode::Bytes(ModuleCodeBytes::Static(&[])),
+          module_url_found: None,
+          module_url_specified: specifier_a,
+          module_type: ModuleType::Other("bytes".into()),
+        },
+      )
+      .unwrap()
+  };
+}
+
 #[tokio::test]
 async fn dyn_import_err() {
   let loader = Rc::new(TestingModuleLoader::new(StaticModuleLoader::new([])));