Repository: denoland/deno_core
Pull request #340 (2023-11-22 19:45:43)

feat(core): add support for other assertion types to ModuleMap
`ModuleMap` now supports any type of module assertion (including arbitrary strings), however this has not been threaded through beyond this code.



aapoalas: **nitpick:** Shouldn't this be `import attributes`? Or is this still about `import "foo" assert { type: "foo" }`?
mmastrac: @aapoalas In this case it's just allowing for other assertions -- we don't currently allow import assertions with parameters but this will open the door (slightly) for import assertions of other types.
aapoalas: Just as long as we're not adding more features to the deprecated `assert { type: foo }` syntax only expanding the possibilities of the new/fixed `with { type?: foo, ... }` syntax.
mmastrac: @aapoalas Likely this is still a bit of a ways off full support for `with` but this gets us a bit closer.

Patch:
diff --git a/core/modules/map.rs b/core/modules/map.rs
index a0a9820..fa3ff2d 100644
--- a/core/modules/map.rs
+++ b/core/modules/map.rs
@@ -88,6 +88,95 @@ pub(crate) struct ModuleMap {
   pub(crate) dyn_module_evaluate_idle_counter: Cell<u32>,
 }
 
+/// Map of [`ModuleName`] and [`AssertedModuleType`] to a data field.
+struct ModuleNameTypeMap<T> {
+  submaps: Vec<HashMap<ModuleName, T>>,
+  map_index: HashMap<AssertedModuleType, usize>,
+  len: usize,
+}
+
+impl<T> Default for ModuleNameTypeMap<T> {
+  fn default() -> Self {
+    Self {
+      submaps: Default::default(),
+      map_index: Default::default(),
+      len: 0,
+    }
+  }
+}
+
+impl<T> ModuleNameTypeMap<T> {
+  pub fn len(&self) -> usize {
+    self.len
+  }
+
+  fn map_index(&self, ty: &AssertedModuleType) -> Option<usize> {
+    self.map_index.get(ty).copied()
+  }
+
+  pub fn get<Q>(&self, ty: &AssertedModuleType, name: &Q) -> Option<&T>
+  where
+    ModuleName: std::borrow::Borrow<Q>,
+    Q: std::cmp::Eq + std::hash::Hash + ?Sized,
+  {
+    let Some(index) = self.map_index(ty) else {
+      return None;
+    };
+    let Some(map) = self.submaps.get(index) else {
+      return None;
+    };
+    map.get(name)
+  }
+
+  pub fn clear(&mut self) {
+    self.len = 0;
+    self.map_index.clear();
+    self.submaps.clear();
+  }
+
+  pub fn insert(
+    &mut self,
+    module_type: &AssertedModuleType,
+    name: FastString,
+    module: T,
+  ) {
+    let index = match self.map_index(module_type) {
+      Some(index) => index,
+      None => {
+        let index = self.submaps.len();
+        self.map_index.insert(module_type.clone(), index);
+        self.submaps.push(Default::default());
+        index
+      }
+    };
+
+    if self
+      .submaps
+      .get_mut(index)
+      .unwrap()
+      .insert(name, module)
+      .is_none()
+    {
+      self.len += 1;
+    }
+  }
+
+  /// Rather than providing an iterator, we provide a walk method. This is mainly because Rust
+  /// doesn't have generators.
+  pub fn walk(
+    &self,
+    mut f: impl FnMut(usize, &AssertedModuleType, &ModuleName, &T),
+  ) {
+    let mut i = 0;
+    for (ty, value) in self.map_index.iter() {
+      for (key, value) in self.submaps.get(*value).unwrap() {
+        f(i, ty, key, value);
+        i += 1;
+      }
+    }
+  }
+}
+
 #[derive(Default)]
 pub(crate) struct ModuleMapData {
   /// Inverted index from module to index in `info`.
@@ -96,10 +185,8 @@ pub(crate) struct ModuleMapData {
   handles: Vec<v8::Global<v8::Module>>,
   /// The modules we have loaded so far.
   info: Vec<ModuleInfo>,
-  /// [`ModuleName`] to [`SymbolicModule`] for JS/WASM modules.
-  by_name_js: HashMap<ModuleName, SymbolicModule>,
-  /// [`ModuleName`] to [`SymbolicModule`] for JSON modules.
-  by_name_json: HashMap<ModuleName, SymbolicModule>,
+  /// [`ModuleName`] to [`SymbolicModule`] for modules.
+  by_name: ModuleNameTypeMap<SymbolicModule>,
   /// The next ID used for a load.
   next_load_id: ModuleLoadId,
   /// If a main module has been loaded, points to it by index.
@@ -118,25 +205,6 @@ impl ModuleMap {
     id + 1
   }
 
-  /// Walk the currently registered modules (JS, WASM and JSON) along with their index and names.
-  fn walk_modules(
-    &self,
-    mut f: impl FnMut(usize, AssertedModuleType, &ModuleName, &SymbolicModule),
-  ) {
-    let mut i = 0;
-    for module_type in [
-      AssertedModuleType::JavaScriptOrWasm,
-      AssertedModuleType::Json,
-    ] {
-      self.by_name(module_type, |map| {
-        for (name, module) in map.iter() {
-          f(i, module_type, name, module);
-          i += 1;
-        }
-      });
-    }
-  }
-
   #[cfg(debug_assertions)]
   pub(crate) fn assert_all_modules_evaluated(
     &self,
@@ -196,55 +264,51 @@ impl ModuleMap {
         )
         .unwrap();
         requests_arr.set_index(scope, 2 * i as u32, specifier.into());
-
-        let asserted_module_type =
-          v8::Integer::new(scope, request.asserted_module_type as i32);
-        requests_arr.set_index(
-          scope,
-          (2 * i) as u32 + 1,
-          asserted_module_type.into(),
-        );
+        let value = request.asserted_module_type.to_v8(scope);
+        requests_arr.set_index(scope, (2 * i) as u32 + 1, value);
       }
       module_info_arr.set_index(scope, 2, requests_arr.into());
 
-      let module_type = v8::Integer::new(scope, info.module_type as i32);
-      module_info_arr.set_index(scope, 3, module_type.into());
+      let module_type = info.module_type.to_v8(scope);
+      module_info_arr.set_index(scope, 3, module_type);
 
       info_arr.set_index(scope, i as u32, module_info_arr.into());
     }
     array.set_index(scope, 2, info_arr.into());
 
-    let length = self.data.borrow().by_name_js.len()
-      + self.data.borrow().by_name_json.len();
+    let length = self.data.borrow().by_name.len();
     let by_name_array = v8::Array::new(scope, length.try_into().unwrap());
-    self.walk_modules(|i, module_type, name, module| {
-      let arr = v8::Array::new(scope, 3);
-
-      let specifier = name.v8(scope);
-      arr.set_index(scope, 0, specifier.into());
-
-      let asserted_module_type = v8::Integer::new(scope, module_type as i32);
-      arr.set_index(scope, 1, asserted_module_type.into());
-
-      let symbolic_module: v8::Local<v8::Value> = match module {
-        SymbolicModule::Alias(alias) => {
-          let alias = v8::String::new_from_one_byte(
-            scope,
-            alias.as_bytes(),
-            v8::NewStringType::Normal,
-          )
-          .unwrap();
-          alias.into()
-        }
-        SymbolicModule::Mod(id) => {
-          let id = v8::Integer::new(scope, *id as i32);
-          id.into()
-        }
-      };
-      arr.set_index(scope, 2, symbolic_module);
+    self
+      .data
+      .borrow()
+      .by_name
+      .walk(|i, module_type, name, module| {
+        let arr = v8::Array::new(scope, 3);
+
+        let specifier = name.v8(scope);
+        arr.set_index(scope, 0, specifier.into());
+        let value = module_type.to_v8(scope);
+        arr.set_index(scope, 1, value);
+
+        let symbolic_module: v8::Local<v8::Value> = match module {
+          SymbolicModule::Alias(alias) => {
+            let alias = v8::String::new_from_one_byte(
+              scope,
+              alias.as_bytes(),
+              v8::NewStringType::Normal,
+            )
+            .unwrap();
+            alias.into()
+          }
+          SymbolicModule::Mod(id) => {
+            let id = v8::Integer::new(scope, *id as i32);
+            id.into()
+          }
+        };
+        arr.set_index(scope, 2, symbolic_module);
 
-      by_name_array.set_index(scope, i as u32, arr.into());
-    });
+        by_name_array.set_index(scope, i as u32, arr.into());
+      });
     array.set_index(scope, 3, by_name_array.into());
 
     let array_global = v8::Global::new(scope, array);
@@ -321,34 +385,19 @@ impl ModuleMap {
             .get_index(scope, (2 * i) as u32)
             .unwrap()
             .to_rust_string_lossy(scope);
-          let asserted_module_type_no = requests_arr
-            .get_index(scope, (2 * i + 1) as u32)
-            .unwrap()
-            .to_integer(scope)
-            .unwrap()
-            .value();
-          let asserted_module_type = match asserted_module_type_no {
-            0 => AssertedModuleType::JavaScriptOrWasm,
-            1 => AssertedModuleType::Json,
-            _ => unreachable!(),
-          };
+          let value =
+            requests_arr.get_index(scope, (2 * i + 1) as u32).unwrap();
+          let asserted_module_type =
+            AssertedModuleType::try_from_v8(scope, value).unwrap();
           requests.push(ModuleRequest {
             specifier,
             asserted_module_type,
           });
         }
 
-        let module_type_no = module_info_arr
-          .get_index(scope, 3)
-          .unwrap()
-          .to_integer(scope)
-          .unwrap()
-          .value();
-        let module_type = match module_type_no {
-          0 => ModuleType::JavaScript,
-          1 => ModuleType::Json,
-          _ => unreachable!(),
-        };
+        let value = module_info_arr.get_index(scope, 3).unwrap();
+        let module_type =
+          AssertedModuleType::try_from_v8(scope, value).unwrap();
 
         let main = self.data.borrow().main_module_id == Some(id);
         let module_info = ModuleInfo {
@@ -364,8 +413,7 @@ impl ModuleMap {
       self.data.borrow_mut().info = info;
     }
 
-    self.data.borrow_mut().by_name_js.clear();
-    self.data.borrow_mut().by_name_json.clear();
+    self.data.borrow_mut().by_name.clear();
 
     {
       let by_name_arr: v8::Local<v8::Array> =
@@ -409,9 +457,11 @@ impl ModuleMap {
           )
         };
 
-        self.by_name_mut(asserted_module_type, move |map| {
-          map.insert(specifier.into(), val)
-        });
+        self.data.borrow_mut().by_name.insert(
+          &asserted_module_type,
+          specifier.into(),
+          val,
+        );
       }
     }
 
@@ -435,25 +485,26 @@ impl ModuleMap {
   pub(crate) fn get_id(
     &self,
     name: impl AsRef<str>,
-    asserted_module_type: AssertedModuleType,
+    asserted_module_type: impl AsRef<AssertedModuleType>,
   ) -> Option<ModuleId> {
-    self.by_name(asserted_module_type, |map| {
-      let first_symbolic_module = map.get(name.as_ref())?;
-      let mut mod_name = match first_symbolic_module {
-        SymbolicModule::Mod(mod_id) => return Some(*mod_id),
-        SymbolicModule::Alias(target) => target,
-      };
-      loop {
-        let symbolic_module = map.get(mod_name.as_ref())?;
-        match symbolic_module {
-          SymbolicModule::Alias(target) => {
-            debug_assert!(mod_name != target);
-            mod_name = target;
-          }
-          SymbolicModule::Mod(mod_id) => return Some(*mod_id),
+    let map = &self.data.borrow().by_name;
+    let first_symbolic_module =
+      map.get(asserted_module_type.as_ref(), name.as_ref())?;
+    let mut mod_name = match first_symbolic_module {
+      SymbolicModule::Mod(mod_id) => return Some(*mod_id),
+      SymbolicModule::Alias(target) => target,
+    };
+    loop {
+      let symbolic_module =
+        map.get(asserted_module_type.as_ref(), mod_name.as_ref())?;
+      match symbolic_module {
+        SymbolicModule::Alias(target) => {
+          debug_assert!(mod_name != target);
+          mod_name = target;
         }
+        SymbolicModule::Mod(mod_id) => return Some(*mod_id),
       }
-    })
+    }
   }
 
   pub(crate) fn new_json_module(
@@ -738,12 +789,16 @@ impl ModuleMap {
   ) -> ModuleId {
     let mut data = self.data.borrow_mut();
     let id = data.handles.len();
+    let module_type = module_type.into();
     let (name1, name2) = name.into_cheap_copy();
     data.handles_inverted.insert(handle.clone(), id);
     data.handles.push(handle);
     if main {
       data.main_module_id = Some(id);
     }
+    data
+      .by_name
+      .insert(&module_type, name1, SymbolicModule::Mod(id));
     data.info.push(ModuleInfo {
       id,
       main,
@@ -751,10 +806,6 @@ impl ModuleMap {
       requests,
       module_type,
     });
-    drop(data);
-    self.by_name_mut(module_type.into(), |map| {
-      map.insert(name1, SymbolicModule::Mod(id))
-    });
 
     id
   }
@@ -770,50 +821,25 @@ impl ModuleMap {
   fn is_registered(
     &self,
     specifier: impl AsRef<str>,
-    asserted_module_type: AssertedModuleType,
+    asserted_module_type: impl AsRef<AssertedModuleType>,
   ) -> bool {
-    if let Some(id) = self.get_id(specifier.as_ref(), asserted_module_type) {
-      return asserted_module_type
-        == self.data.borrow().info.get(id).unwrap().module_type.into();
-    }
-
-    false
-  }
-
-  pub(crate) fn by_name<T>(
-    &self,
-    asserted_module_type: AssertedModuleType,
-    f: impl FnOnce(&HashMap<ModuleName, SymbolicModule>) -> T,
-  ) -> T {
-    match asserted_module_type {
-      AssertedModuleType::Json => f(&self.data.borrow().by_name_json),
-      AssertedModuleType::JavaScriptOrWasm => f(&self.data.borrow().by_name_js),
-    }
-  }
-
-  pub(crate) fn by_name_mut<T>(
-    &self,
-    asserted_module_type: AssertedModuleType,
-    f: impl FnOnce(&mut HashMap<ModuleName, SymbolicModule>) -> T,
-  ) -> T {
-    match asserted_module_type {
-      AssertedModuleType::Json => f(&mut self.data.borrow_mut().by_name_json),
-      AssertedModuleType::JavaScriptOrWasm => {
-        f(&mut self.data.borrow_mut().by_name_js)
-      }
-    }
+    self
+      .get_id(specifier.as_ref(), asserted_module_type.as_ref())
+      .is_some()
   }
 
   pub(crate) fn alias(
     &self,
     name: FastString,
-    asserted_module_type: AssertedModuleType,
+    asserted_module_type: &AssertedModuleType,
     target: FastString,
   ) {
     debug_assert_ne!(name, target);
-    self.by_name_mut(asserted_module_type, |map| {
-      map.insert(name, SymbolicModule::Alias(target))
-    });
+    self.data.borrow_mut().by_name.insert(
+      asserted_module_type,
+      name,
+      SymbolicModule::Alias(target),
+    );
   }
 
   #[cfg(test)]
@@ -822,9 +848,9 @@ impl ModuleMap {
     name: &str,
     asserted_module_type: AssertedModuleType,
   ) -> bool {
-    self.by_name(asserted_module_type, |map| {
-      matches!(map.get(name), Some(SymbolicModule::Alias(_)))
-    })
+    let map = &self.data.borrow().by_name;
+    let entry = map.get(&asserted_module_type, name);
+    matches!(entry, Some(SymbolicModule::Alias(_)))
   }
 
   pub(crate) fn get_handle(
@@ -863,13 +889,6 @@ impl ModuleMap {
       .map(|info| info.name.as_str().to_owned())
   }
 
-  pub(crate) fn get_module_type_by_id(
-    &self,
-    id: ModuleId,
-  ) -> Option<ModuleType> {
-    self.data.borrow().info.get(id).map(|info| info.module_type)
-  }
-
   pub(crate) async fn load_main(
     module_map_rc: Rc<ModuleMap>,
     specifier: impl AsRef<str>,
@@ -901,7 +920,7 @@ impl ModuleMap {
     let load = RecursiveModuleLoad::dynamic_import(
       specifier,
       referrer,
-      asserted_module_type,
+      asserted_module_type.clone(),
       module_map_rc.clone(),
     );
 
@@ -1339,23 +1358,16 @@ impl ModuleMap {
     assert_eq!(data.handles.len(), modules.len());
     assert_eq!(data.info.len(), modules.len());
     assert_eq!(data.next_load_id as usize, modules.len());
-    drop(data);
-
-    assert_eq!(
-      self.by_name(AssertedModuleType::Json, |map| map.len())
-        + self.by_name(AssertedModuleType::JavaScriptOrWasm, |map| map.len()),
-      modules.len()
-    );
+    assert_eq!(data.by_name.len(), modules.len());
 
     for info in modules {
       let data = self.data.borrow();
       assert!(data.handles.get(info.id).is_some());
       assert_eq!(data.info.get(info.id).unwrap(), info);
-      drop(data);
-      assert!(self.by_name(AssertedModuleType::JavaScriptOrWasm, |map| map
-        .get(&info.name)
-        .unwrap()
-        == &SymbolicModule::Mod(info.id)));
+      assert_eq!(
+        data.by_name.get(&info.module_type, &info.name),
+        Some(&SymbolicModule::Mod(info.id))
+      );
     }
   }
 }
diff --git a/core/modules/mod.rs b/core/modules/mod.rs
index d077c4b..ed50d58 100644
--- a/core/modules/mod.rs
+++ b/core/modules/mod.rs
@@ -11,6 +11,7 @@ use futures::stream::TryStreamExt;
 use log::debug;
 use serde::Deserialize;
 use serde::Serialize;
+use std::borrow::Cow;
 use std::cell::RefCell;
 use std::collections::HashMap;
 use std::collections::HashSet;
@@ -303,7 +304,6 @@ pub(crate) struct RecursiveModuleLoad {
   pub root_module_id: Option<ModuleId>,
   init: LoadInit,
   root_asserted_module_type: Option<AssertedModuleType>,
-  root_module_type: Option<ModuleType>,
   state: LoadState,
   module_map_rc: Rc<ModuleMap>,
   pending: FuturesUnordered<Pin<Box<ModuleLoadFuture>>>,
@@ -349,15 +349,14 @@ impl RecursiveModuleLoad {
   fn new(init: LoadInit, module_map_rc: Rc<ModuleMap>) -> Self {
     let id = module_map_rc.next_load_id();
     let loader = module_map_rc.loader.borrow().clone();
-    let asserted_module_type = match init {
-      LoadInit::DynamicImport(_, _, module_type) => module_type,
+    let asserted_module_type = match &init {
+      LoadInit::DynamicImport(_, _, module_type) => module_type.clone(),
       _ => AssertedModuleType::JavaScriptOrWasm,
     };
     let mut load = Self {
       id,
       root_module_id: None,
       root_asserted_module_type: None,
-      root_module_type: None,
       init,
       state: LoadState::Init,
       module_map_rc: module_map_rc.clone(),
@@ -370,12 +369,10 @@ impl RecursiveModuleLoad {
     // Ignore the error here, let it be hit in `Stream::poll_next()`.
     if let Ok(root_specifier) = load.resolve_root() {
       if let Some(module_id) =
-        module_map_rc.get_id(root_specifier, asserted_module_type)
+        module_map_rc.get_id(root_specifier, &asserted_module_type)
       {
         load.root_module_id = Some(module_id);
         load.root_asserted_module_type = Some(asserted_module_type);
-        load.root_module_type =
-          Some(module_map_rc.get_module_type_by_id(module_id).unwrap());
       }
     }
     load
@@ -463,7 +460,7 @@ impl RecursiveModuleLoad {
         module_url_found.into_cheap_copy();
       self.module_map_rc.alias(
         module_url_specified,
-        expected_asserted_module_type,
+        &expected_asserted_module_type,
         module_url_found1,
       );
       module_url_found2
@@ -473,7 +470,7 @@ impl RecursiveModuleLoad {
 
     let maybe_module_id = self
       .module_map_rc
-      .get_id(&module_url_found, expected_asserted_module_type);
+      .get_id(&module_url_found, &expected_asserted_module_type);
     let module_id = match maybe_module_id {
       Some(id) => {
         debug!(
@@ -498,6 +495,26 @@ impl RecursiveModuleLoad {
       },
     };
 
+    self.register_and_recurse_inner(module_id, module_request);
+
+    // Update `self.state` however applicable.
+    if self.state == LoadState::LoadingRoot {
+      self.root_module_id = Some(module_id);
+      self.root_asserted_module_type = Some(module_source.module_type.into());
+      self.state = LoadState::LoadingImports;
+    }
+    if self.pending.is_empty() {
+      self.state = LoadState::Done;
+    }
+
+    Ok(())
+  }
+
+  fn register_and_recurse_inner(
+    &mut self,
+    module_id: usize,
+    module_request: &ModuleRequest,
+  ) {
     // Recurse the module's imports. There are two cases for each import:
     // 1. If the module is not in the module map, start a new load for it in
     //    `self.pending`. The result of that load should eventually be passed to
@@ -526,7 +543,7 @@ impl RecursiveModuleLoad {
         {
           if let Some(module_id) = self.module_map_rc.get_id(
             module_request.specifier.as_str(),
-            module_request.asserted_module_type,
+            &module_request.asserted_module_type,
           ) {
             already_registered.push_back((module_id, module_request.clone()));
           } else {
@@ -563,18 +580,6 @@ impl RecursiveModuleLoad {
         }
       }
     }
-
-    // Update `self.state` however applicable.
-    if self.state == LoadState::LoadingRoot {
-      self.root_module_id = Some(module_id);
-      self.root_asserted_module_type = Some(module_source.module_type.into());
-      self.state = LoadState::LoadingImports;
-    }
-    if self.pending.is_empty() {
-      self.state = LoadState::Done;
-    }
-
-    Ok(())
   }
 }
 
@@ -594,28 +599,25 @@ impl Stream for RecursiveModuleLoad {
           Ok(url) => url,
           Err(error) => return Poll::Ready(Some(Err(error))),
         };
-        let load_fut = if let Some(_module_id) = inner.root_module_id {
-          // FIXME(bartlomieju): this is very bad
-          // The root module is already in the module map.
-          // TODO(nayeemrmn): In this case we would ideally skip to
-          // `LoadState::LoadingImports` and synchronously recurse the imports
-          // like the bottom of `RecursiveModuleLoad::register_and_recurse()`.
-          // But the module map cannot be borrowed here. Instead fake a load
-          // event so it gets passed to that function and recursed eventually.
-          let asserted_module_type = inner.root_asserted_module_type.unwrap();
-          let module_type = inner.root_module_type.unwrap();
-          let module_request = ModuleRequest {
-            specifier: module_specifier.to_string(),
-            asserted_module_type,
-          };
-          // The code will be discarded, since this module is already in the
-          // module map.
-          let module_source = ModuleSource::new(
-            module_type,
-            Default::default(),
-            &module_specifier,
-          );
-          futures::future::ok(Some((module_request, module_source))).boxed()
+        let asserted_module_type = match &inner.init {
+          LoadInit::DynamicImport(_, _, module_type) => module_type.clone(),
+          _ => AssertedModuleType::JavaScriptOrWasm,
+        };
+        let module_request = ModuleRequest {
+          specifier: module_specifier.to_string(),
+          asserted_module_type,
+        };
+        let load_fut = if let Some(module_id) = inner.root_module_id {
+          // If the inner future is already in the map, we might be done (assuming there are no pending
+          // loads).
+          inner.register_and_recurse_inner(module_id, &module_request);
+          if inner.pending.is_empty() {
+            inner.state = LoadState::Done;
+          } else {
+            inner.state = LoadState::LoadingImports;
+          }
+          // Internally re-poll using the new state to avoid spinning the event loop again.
+          return Self::poll_next(Pin::new(inner), cx);
         } else {
           let maybe_referrer = match inner.init {
             LoadInit::DynamicImport(_, ref referrer, _) => {
@@ -623,14 +625,6 @@ impl Stream for RecursiveModuleLoad {
             }
             _ => None,
           };
-          let asserted_module_type = match inner.init {
-            LoadInit::DynamicImport(_, _, module_type) => module_type,
-            _ => AssertedModuleType::JavaScriptOrWasm,
-          };
-          let module_request = ModuleRequest {
-            specifier: module_specifier.to_string(),
-            asserted_module_type,
-          };
           let loader = inner.loader.clone();
           let is_dynamic_import = inner.is_dynamic_import();
           async move {
@@ -669,11 +663,64 @@ impl Stream for RecursiveModuleLoad {
   }
 }
 
-#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
-#[repr(u32)]
+#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
+#[repr(u8)]
 pub(crate) enum AssertedModuleType {
+  /// JavaScript or WASM.
   JavaScriptOrWasm,
+  /// JSON.
   Json,
+
+  // IMPORTANT: If you add any additional enum values here, you must update `to_v8`` below!
+  /// Non-well-known module type.
+  Other(Cow<'static, str>),
+}
+
+impl AssertedModuleType {
+  pub fn to_v8<'s>(
+    &self,
+    scope: &mut v8::HandleScope<'s>,
+  ) -> v8::Local<'s, v8::Value> {
+    match self {
+      AssertedModuleType::JavaScriptOrWasm => v8::Integer::new(scope, 0).into(),
+      AssertedModuleType::Json => v8::Integer::new(scope, 1).into(),
+      AssertedModuleType::Other(ty) => {
+        v8::String::new(scope, ty).unwrap().into()
+      }
+    }
+  }
+
+  pub fn try_from_v8(
+    scope: &mut v8::HandleScope,
+    value: v8::Local<v8::Value>,
+  ) -> Option<Self> {
+    Some(if let Some(int) = value.to_integer(scope) {
+      match int.int32_value(scope).unwrap_or_default() {
+        0 => AssertedModuleType::JavaScriptOrWasm,
+        1 => AssertedModuleType::Json,
+        _ => return None,
+      }
+    } else if let Ok(str) = v8::Local::<v8::String>::try_from(value) {
+      AssertedModuleType::Other(Cow::Owned(str.to_rust_string_lossy(scope)))
+    } else {
+      return None;
+    })
+  }
+}
+
+impl AsRef<AssertedModuleType> for AssertedModuleType {
+  fn as_ref(&self) -> &AssertedModuleType {
+    self
+  }
+}
+
+impl PartialEq<ModuleType> for AssertedModuleType {
+  fn eq(&self, other: &ModuleType) -> bool {
+    match other {
+      ModuleType::JavaScript => self == &AssertedModuleType::JavaScriptOrWasm,
+      ModuleType::Json => self == &AssertedModuleType::Json,
+    }
+  }
 }
 
 impl From<ModuleType> for AssertedModuleType {
@@ -690,6 +737,7 @@ impl std::fmt::Display for AssertedModuleType {
     match self {
       Self::JavaScriptOrWasm => write!(f, "JavaScriptOrWasm"),
       Self::Json => write!(f, "JSON"),
+      Self::Other(ty) => write!(f, "Other({ty})"),
     }
   }
 }
@@ -711,7 +759,7 @@ pub(crate) struct ModuleInfo {
   pub main: bool,
   pub name: ModuleName,
   pub requests: Vec<ModuleRequest>,
-  pub module_type: ModuleType,
+  pub module_type: AssertedModuleType,
 }
 
 #[derive(Debug)]


Test patch:
diff --git a/core/runtime/tests/snapshot.rs b/core/runtime/tests/snapshot.rs
index c087f53..77c828d 100644
--- a/core/runtime/tests/snapshot.rs
+++ b/core/runtime/tests/snapshot.rs
@@ -3,7 +3,6 @@ use crate::extensions::Op;
 use crate::modules::AssertedModuleType;
 use crate::modules::LoggingModuleLoader;
 use crate::modules::ModuleInfo;
-use crate::modules::ModuleType;
 use crate::*;
 use anyhow::Error;
 use std::borrow::Cow;
@@ -162,7 +161,7 @@ fn es_snapshot() {
         specifier: format!("file:///{prev}.js"),
         asserted_module_type: AssertedModuleType::JavaScriptOrWasm,
       }],
-      module_type: ModuleType::JavaScript,
+      module_type: AssertedModuleType::JavaScriptOrWasm,
     }
   }
 
@@ -199,7 +198,7 @@ fn es_snapshot() {
     main: false,
     name: specifier.into(),
     requests: vec![],
-    module_type: ModuleType::JavaScript,
+    module_type: AssertedModuleType::JavaScriptOrWasm,
   });
 
   modules.extend((1..200).map(|i| create_module(&mut runtime, i, false)));