Repository: denoland/deno_core
Pull request #84 (2023-07-25 16:07:28)

refactor(core): Introduce a StaticModuleLoader
Adds the following test utilities to remove a lot of boilerplate from the deno_core tests:

 - `StaticModuleLoader` to provide a set of static files that don't require the filesystem
 - `CountingModuleLoader` to test resolve/prepare/load counts
 - `LoggingModuleLoader` to log a list of load attempts



Patch:
diff --git a/core/fast_string.rs b/core/fast_string.rs
index 95dfb49..e9ba321 100644
--- a/core/fast_string.rs
+++ b/core/fast_string.rs
@@ -83,6 +83,16 @@ impl FastString {
     }
   }
 
+  /// If this [`FastString`] is cheaply cloneable, returns a clone.
+  pub fn try_clone(&self) -> Option<Self> {
+    match self {
+      Self::Static(s) => Some(Self::Static(s)),
+      Self::StaticAscii(s) => Some(Self::StaticAscii(s)),
+      Self::Arc(s) => Some(Self::Arc(s.clone())),
+      Self::Owned(_s) => None,
+    }
+  }
+
   pub const fn try_static_ascii(&self) -> Option<&'static [u8]> {
     match self {
       Self::StaticAscii(s) => Some(s.as_bytes()),
diff --git a/core/lib.rs b/core/lib.rs
index 8f276bc..8d75dbd 100644
--- a/core/lib.rs
+++ b/core/lib.rs
@@ -92,6 +92,7 @@ pub use crate::modules::ModuleSourceFuture;
 pub use crate::modules::ModuleType;
 pub use crate::modules::NoopModuleLoader;
 pub use crate::modules::ResolutionKind;
+pub use crate::modules::StaticModuleLoader;
 pub use crate::normalize_path::normalize_path;
 pub use crate::ops::OpCall;
 pub use crate::ops::OpError;
diff --git a/core/modules/loaders.rs b/core/modules/loaders.rs
index bc64556..c19e014 100644
--- a/core/modules/loaders.rs
+++ b/core/modules/loaders.rs
@@ -10,8 +10,10 @@ use crate::modules::ModuleType;
 use crate::modules::ResolutionKind;
 use crate::resolve_import;
 use crate::Extension;
+
 use anyhow::anyhow;
 use anyhow::Error;
+use futures::future::ready;
 use futures::future::FutureExt;
 use std::cell::RefCell;
 use std::collections::HashMap;
@@ -26,10 +28,10 @@ pub trait ModuleLoader {
   /// algorithm described here:
   /// <https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier>
   ///
-  /// `is_main` can be used to resolve from current working directory or
+  /// [`ResolutionKind::MainModule`] can be used to resolve from current working directory or
   /// apply import map for child imports.
   ///
-  /// `is_dyn_import` can be used to check permissions or deny
+  /// [`ResolutionKind::DynamicImport`] can be used to check permissions or deny
   /// dynamic imports altogether.
   fn resolve(
     &self,
@@ -78,9 +80,7 @@ impl ModuleLoader for NoopModuleLoader {
     referrer: &str,
     _kind: ResolutionKind,
   ) -> Result<ModuleSpecifier, Error> {
-    Err(generic_error(
-      format!("Module loading is not supported; attempted to resolve: \"{specifier}\" from \"{referrer}\"")
-    ))
+    Ok(resolve_import(specifier, referrer)?)
   }
 
   fn load(
@@ -89,9 +89,12 @@ impl ModuleLoader for NoopModuleLoader {
     maybe_referrer: Option<&ModuleSpecifier>,
     _is_dyn_import: bool,
   ) -> Pin<Box<ModuleSourceFuture>> {
+    let maybe_referrer = maybe_referrer
+      .map(|s| s.as_str())
+      .unwrap_or("(no referrer)");
     let err = generic_error(
       format!(
-        "Module loading is not supported; attempted to load: \"{module_specifier}\" from \"{maybe_referrer:?}\"",
+        "Module loading is not supported; attempted to load: \"{module_specifier}\" from \"{maybe_referrer}\"",
       )
     );
     async move { Err(err) }.boxed_local()
@@ -251,3 +254,200 @@ impl ModuleLoader for FsModuleLoader {
     futures::future::ready(load(module_specifier)).boxed_local()
   }
 }
+
+/// A module loader that you can pre-load a number of modules into and resolve from. Useful for testing and
+/// embedding situations where the filesystem and snapshot systems are not usable or a good fit.
+pub struct StaticModuleLoader {
+  map: HashMap<ModuleSpecifier, ModuleCode>,
+}
+
+impl StaticModuleLoader {
+  /// Create a new [`StaticModuleLoader`] from an `Iterator` of specifiers and code.
+  pub fn new(
+    from: impl IntoIterator<Item = (ModuleSpecifier, ModuleCode)>,
+  ) -> Self {
+    Self {
+      map: HashMap::from_iter(
+        from
+          .into_iter()
+          .map(|(url, code)| (url, code.into_cheap_copy().0)),
+      ),
+    }
+  }
+
+  /// Create a new [`StaticModuleLoader`] from a single code item.
+  pub fn with(specifier: ModuleSpecifier, code: ModuleCode) -> Self {
+    Self::new([(specifier, code)])
+  }
+}
+
+impl ModuleLoader for StaticModuleLoader {
+  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>> {
+    let res = if let Some(code) = self.map.get(module_specifier) {
+      Ok(ModuleSource::new(
+        ModuleType::JavaScript,
+        code.try_clone().unwrap(),
+        module_specifier,
+      ))
+    } else {
+      Err(generic_error("Module not found"))
+    };
+    ready(res).boxed_local()
+  }
+}
+
+/// Annotates a `ModuleLoader` with a log of all `load()` calls.
+#[cfg(test)]
+pub struct LoggingModuleLoader<L: ModuleLoader> {
+  loader: L,
+  log: RefCell<Vec<ModuleSpecifier>>,
+}
+
+#[cfg(test)]
+impl<L: ModuleLoader> LoggingModuleLoader<L> {
+  pub fn new(loader: L) -> Self {
+    Self {
+      loader,
+      log: RefCell::new(vec![]),
+    }
+  }
+
+  pub fn log(&self) -> Vec<ModuleSpecifier> {
+    self.log.borrow().clone()
+  }
+}
+
+#[cfg(test)]
+impl<L: ModuleLoader> ModuleLoader for LoggingModuleLoader<L> {
+  fn resolve(
+    &self,
+    specifier: &str,
+    referrer: &str,
+    kind: ResolutionKind,
+  ) -> Result<ModuleSpecifier, Error> {
+    self.loader.resolve(specifier, referrer, kind)
+  }
+
+  fn prepare_load(
+    &self,
+    module_specifier: &ModuleSpecifier,
+    maybe_referrer: Option<String>,
+    is_dyn_import: bool,
+  ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
+    self
+      .loader
+      .prepare_load(module_specifier, maybe_referrer, is_dyn_import)
+  }
+
+  fn load(
+    &self,
+    module_specifier: &ModuleSpecifier,
+    maybe_referrer: Option<&ModuleSpecifier>,
+    is_dyn_import: bool,
+  ) -> Pin<Box<ModuleSourceFuture>> {
+    self.log.borrow_mut().push(module_specifier.clone());
+    self
+      .loader
+      .load(module_specifier, maybe_referrer, is_dyn_import)
+  }
+}
+
+/// Annotates a `ModuleLoader` with a count of all `resolve()`, `prepare()`, and `load()` calls.
+#[cfg(test)]
+pub struct CountingModuleLoader<L: ModuleLoader> {
+  loader: L,
+  load_count: std::cell::Cell<usize>,
+  prepare_count: std::cell::Cell<usize>,
+  resolve_count: std::cell::Cell<usize>,
+}
+
+#[cfg(test)]
+#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
+pub struct ModuleLoadEventCounts {
+  pub resolve: usize,
+  pub prepare: usize,
+  pub load: usize,
+}
+
+#[cfg(test)]
+impl ModuleLoadEventCounts {
+  pub fn new(resolve: usize, prepare: usize, load: usize) -> Self {
+    Self {
+      resolve,
+      prepare,
+      load,
+    }
+  }
+}
+
+#[cfg(test)]
+impl<L: ModuleLoader> CountingModuleLoader<L> {
+  pub fn new(loader: L) -> Self {
+    Self {
+      loader,
+      load_count: Default::default(),
+      prepare_count: Default::default(),
+      resolve_count: Default::default(),
+    }
+  }
+
+  /// Retrieve the current module load event counts.
+  pub fn counts(&self) -> ModuleLoadEventCounts {
+    ModuleLoadEventCounts {
+      load: self.load_count.get(),
+      prepare: self.prepare_count.get(),
+      resolve: self.resolve_count.get(),
+    }
+  }
+}
+
+#[cfg(test)]
+impl<L: ModuleLoader> ModuleLoader for CountingModuleLoader<L> {
+  fn load(
+    &self,
+    module_specifier: &ModuleSpecifier,
+    maybe_referrer: Option<&ModuleSpecifier>,
+    is_dyn_import: bool,
+  ) -> Pin<Box<ModuleSourceFuture>> {
+    self.load_count.set(self.load_count.get() + 1);
+    self
+      .loader
+      .load(module_specifier, maybe_referrer, is_dyn_import)
+  }
+
+  fn prepare_load(
+    &self,
+    module_specifier: &ModuleSpecifier,
+    maybe_referrer: Option<String>,
+    is_dyn_import: bool,
+  ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
+    self.prepare_count.set(self.prepare_count.get() + 1);
+    self
+      .loader
+      .prepare_load(module_specifier, maybe_referrer, is_dyn_import)
+  }
+
+  fn resolve(
+    &self,
+    specifier: &str,
+    referrer: &str,
+    kind: ResolutionKind,
+  ) -> Result<ModuleSpecifier, Error> {
+    self.resolve_count.set(self.resolve_count.get() + 1);
+    self.loader.resolve(specifier, referrer, kind)
+  }
+}
diff --git a/core/modules/mod.rs b/core/modules/mod.rs
index dd7323d..2af22dd 100644
--- a/core/modules/mod.rs
+++ b/core/modules/mod.rs
@@ -27,11 +27,19 @@ mod map;
 #[cfg(test)]
 mod tests;
 
+#[cfg(test)]
+pub use loaders::CountingModuleLoader;
+#[cfg(test)]
+pub use loaders::LoggingModuleLoader;
+#[cfg(test)]
+pub use loaders::ModuleLoadEventCounts;
+
 pub(crate) use loaders::ExtModuleLoader;
 pub use loaders::ExtModuleLoaderCb;
 pub use loaders::FsModuleLoader;
 pub use loaders::ModuleLoader;
 pub use loaders::NoopModuleLoader;
+pub use loaders::StaticModuleLoader;
 pub(crate) use map::ModuleMap;
 #[cfg(test)]
 pub(crate) use map::SymbolicModule;
diff --git a/core/runtime/jsrealm.rs b/core/runtime/jsrealm.rs
index 3f467d4..d263d28 100644
--- a/core/runtime/jsrealm.rs
+++ b/core/runtime/jsrealm.rs
@@ -885,7 +885,6 @@ impl JsRealm {
       .borrow_mut()
       .pending_mod_evaluate
       .take();
-
     if maybe_module_evaluation.is_none() {
       return;
     }
@@ -897,7 +896,6 @@ impl JsRealm {
     let promise_global = module_evaluation.promise.clone().unwrap();
     let promise = promise_global.open(scope);
     let promise_state = promise.state();
-
     match promise_state {
       v8::PromiseState::Pending => {
         // NOTE: `poll_event_loop` will decide if
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index b125cdc..15541c9 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -110,6 +110,7 @@ impl<T> DerefMut for ManuallyDropRc<T> {
 /// control dropping more closely here using ManuallyDrop.
 pub(crate) struct InnerIsolateState {
   will_snapshot: bool,
+  main_realm: Option<JsRealm>,
   pub(crate) state: ManuallyDropRc<RefCell<JsRuntimeState>>,
   v8_isolate: ManuallyDrop<v8::OwnedIsolate>,
 }
@@ -139,6 +140,7 @@ impl InnerIsolateState {
     // the runtime.
     _ = unsafe { Rc::from_raw(state_ptr as *const RefCell<JsRuntimeState>) };
 
+    self.main_realm.take();
     self.state.borrow_mut().destroy_all_realms();
 
     debug_assert_eq!(Rc::strong_count(&self.state), 1);
@@ -283,7 +285,6 @@ pub type CompiledWasmModuleStore = CrossIsolateStore<v8::CompiledWasmModule>;
 /// Internal state for JsRuntime which is stored in one of v8::Isolate's
 /// embedder slots.
 pub struct JsRuntimeState {
-  main_realm: Option<JsRealm>,
   known_realms: Vec<JsRealmInner>,
   pub(crate) has_tick_scheduled: bool,
   /// A counter used to delay our dynamic import deadlock detection by one spin
@@ -305,7 +306,6 @@ pub struct JsRuntimeState {
 
 impl JsRuntimeState {
   pub(crate) fn destroy_all_realms(&mut self) {
-    self.main_realm.take();
     for realm in self.known_realms.drain(..) {
       realm.destroy()
     }
@@ -573,7 +573,6 @@ impl JsRuntime {
       dispatched_exception: None,
       // Some fields are initialized later after isolate is created
       inspector: None,
-      main_realm: None,
       known_realms: Vec::with_capacity(1),
     }));
 
@@ -709,10 +708,11 @@ impl JsRuntime {
         true,
       );
       let mut state = state_rc.borrow_mut();
-      state.main_realm = Some(JsRealm::new(main_realm.clone()));
       state.inspector = inspector;
       state.known_realms.push(main_realm);
     }
+    let main_realm =
+      JsRealm::new(state_rc.borrow().known_realms.get(0).unwrap().clone());
     scope.set_data(
       STATE_DATA_OFFSET,
       Rc::into_raw(state_rc.clone()) as *mut c_void,
@@ -723,6 +723,7 @@ impl JsRuntime {
     let mut js_runtime = JsRuntime {
       inner: InnerIsolateState {
         will_snapshot,
+        main_realm: Some(main_realm),
         state: ManuallyDropRc(ManuallyDrop::new(state_rc)),
         v8_isolate: ManuallyDrop::new(isolate),
       },
@@ -784,8 +785,7 @@ impl JsRuntime {
 
   #[inline]
   pub fn main_realm(&mut self) -> JsRealm {
-    let state = self.inner.state.borrow();
-    state.main_realm.clone().unwrap()
+    self.inner.main_realm.as_ref().unwrap().clone()
   }
 
   /// Returns the extensions that this runtime is using (including internal ones).
@@ -1399,28 +1399,53 @@ impl JsRuntime {
       loop {
         let mut has_evaluated = false;
 
-        let known_realms = self.inner.state.borrow().known_realms.clone();
-        let isolate = self.v8_isolate();
-        for inner_realm in known_realms {
-          let realm = JsRealm::new(inner_realm);
-
+        let state = self.inner.state.borrow();
+        if state.known_realms.len() == 1 {
+          drop(state);
           // Try and resolve as many dynamic imports in each realm as possible
           // before moving to the next.
+          let realm = self.inner.main_realm.as_ref().unwrap();
           loop {
-            let poll_imports = realm.prepare_dyn_imports(isolate, cx)?;
+            let poll_imports =
+              realm.prepare_dyn_imports(&mut self.inner.v8_isolate, cx)?;
             assert!(poll_imports.is_ready());
 
-            let poll_imports = realm.poll_dyn_imports(isolate, cx)?;
+            let poll_imports =
+              realm.poll_dyn_imports(&mut self.inner.v8_isolate, cx)?;
             assert!(poll_imports.is_ready());
 
-            if realm.evaluate_dyn_imports(isolate) {
+            if realm.evaluate_dyn_imports(&mut self.inner.v8_isolate) {
               has_evaluated = true;
             } else {
               break;
             }
           }
+        } else {
+          // TODO(bartlomieju|mmastrac): Remove cloning in the runtime loop
+          let realms = state.known_realms.clone();
+          drop(state);
+          for inner_realm in realms {
+            let realm = JsRealm::new(inner_realm);
+
+            // Try and resolve as many dynamic imports in each realm as possible
+            // before moving to the next.
+            loop {
+              let poll_imports =
+                realm.prepare_dyn_imports(&mut self.inner.v8_isolate, cx)?;
+              assert!(poll_imports.is_ready());
+
+              let poll_imports =
+                realm.poll_dyn_imports(&mut self.inner.v8_isolate, cx)?;
+              assert!(poll_imports.is_ready());
+
+              if realm.evaluate_dyn_imports(&mut self.inner.v8_isolate) {
+                has_evaluated = true;
+              } else {
+                break;
+              }
+            }
+          }
         }
-
         if !has_evaluated {
           break;
         }
@@ -1447,10 +1472,19 @@ impl JsRuntime {
 
     // Top level module
     {
-      let known_realms = self.inner.state.borrow().known_realms.clone();
-      for inner_realm in known_realms {
-        let realm = JsRealm::new(inner_realm);
+      let state = self.inner.state.borrow();
+      if state.known_realms.len() == 1 {
+        drop(state);
+        let realm = self.inner.main_realm.as_ref().unwrap();
         realm.evaluate_pending_module(&mut self.inner.v8_isolate);
+      } else {
+        // TODO(bartlomieju|mmastrac): Remove cloning in the runtime loop
+        let realms = state.known_realms.clone();
+        drop(state);
+        for inner_realm in realms {
+          let realm = JsRealm::new(inner_realm);
+          realm.evaluate_pending_module(&mut self.inner.v8_isolate);
+        }
       }
     }
 


Test patch:
diff --git a/core/modules/tests.rs b/core/modules/tests.rs
index d9aad41..b65e54e 100644
--- a/core/modules/tests.rs
+++ b/core/modules/tests.rs
@@ -1,5 +1,7 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
 use crate::ascii_str;
+use crate::modules::loaders::CountingModuleLoader;
+use crate::modules::loaders::ModuleLoadEventCounts;
 use crate::resolve_import;
 use crate::runtime::JsRuntime;
 use crate::runtime::JsRuntimeForSnapshot;
@@ -11,11 +13,10 @@ use futures::future::FutureExt;
 use parking_lot::Mutex;
 use std::fmt;
 use std::future::Future;
-use std::io;
-use std::path::PathBuf;
 use std::sync::atomic::AtomicUsize;
 use std::sync::atomic::Ordering;
 use std::sync::Arc;
+use url::Url;
 
 use super::*;
 
@@ -298,38 +299,7 @@ fn test_recursive_load() {
 
 #[test]
 fn test_mods() {
-  #[derive(Default)]
-  struct ModsLoader {
-    pub count: Arc<AtomicUsize>,
-  }
-
-  impl ModuleLoader for ModsLoader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      self.count.fetch_add(1, Ordering::Relaxed);
-      assert_eq!(specifier, "./b.js");
-      assert_eq!(referrer, "file:///a.js");
-      let s = resolve_import(specifier, referrer).unwrap();
-      Ok(s)
-    }
-
-    fn load(
-      &self,
-      _module_specifier: &ModuleSpecifier,
-      _maybe_referrer: Option<&ModuleSpecifier>,
-      _is_dyn_import: bool,
-    ) -> Pin<Box<ModuleSourceFuture>> {
-      unreachable!()
-    }
-  }
-
-  let loader = Rc::new(ModsLoader::default());
-
-  let resolve_count = loader.count.clone();
+  let loader = Rc::new(CountingModuleLoader::new(NoopModuleLoader));
   static DISPATCH_COUNT: AtomicUsize = AtomicUsize::new(0);
 
   #[op]
@@ -343,7 +313,7 @@ fn test_mods() {
 
   let mut runtime = JsRuntime::new(RuntimeOptions {
     extensions: vec![test_ext::init_ops()],
-    module_loader: Some(loader),
+    module_loader: Some(loader.clone()),
     ..Default::default()
   });
 
@@ -411,7 +381,7 @@ fn test_mods() {
 
   runtime.instantiate_module(mod_b).unwrap();
   assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 0);
-  assert_eq!(resolve_count.load(Ordering::SeqCst), 1);
+  assert_eq!(loader.counts(), ModuleLoadEventCounts::new(1, 0, 0));
 
   runtime.instantiate_module(mod_a).unwrap();
   assert_eq!(DISPATCH_COUNT.load(Ordering::Relaxed), 0);
@@ -423,41 +393,9 @@ fn test_mods() {
 
 #[test]
 fn test_json_module() {
-  #[derive(Default)]
-  struct ModsLoader {
-    pub count: Arc<AtomicUsize>,
-  }
-
-  impl ModuleLoader for ModsLoader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      self.count.fetch_add(1, Ordering::Relaxed);
-      assert_eq!(specifier, "./b.json");
-      assert_eq!(referrer, "file:///a.js");
-      let s = resolve_import(specifier, referrer).unwrap();
-      Ok(s)
-    }
-
-    fn load(
-      &self,
-      _module_specifier: &ModuleSpecifier,
-      _maybe_referrer: Option<&ModuleSpecifier>,
-      _is_dyn_import: bool,
-    ) -> Pin<Box<ModuleSourceFuture>> {
-      unreachable!()
-    }
-  }
-
-  let loader = Rc::new(ModsLoader::default());
-
-  let resolve_count = loader.count.clone();
-
+  let loader = Rc::new(CountingModuleLoader::new(StaticModuleLoader::new([])));
   let mut runtime = JsRuntime::new(RuntimeOptions {
-    module_loader: Some(loader),
+    module_loader: Some(loader.clone()),
     ..Default::default()
   });
 
@@ -518,7 +456,7 @@ fn test_json_module() {
   };
 
   runtime.instantiate_module(mod_b).unwrap();
-  assert_eq!(resolve_count.load(Ordering::SeqCst), 1);
+  assert_eq!(loader.counts(), ModuleLoadEventCounts::new(1, 0, 0));
 
   runtime.instantiate_module(mod_a).unwrap();
 
@@ -529,39 +467,9 @@ fn test_json_module() {
 
 #[tokio::test]
 async fn dyn_import_err() {
-  #[derive(Clone, Default)]
-  struct DynImportErrLoader {
-    pub count: Arc<AtomicUsize>,
-  }
-
-  impl ModuleLoader for DynImportErrLoader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      self.count.fetch_add(1, Ordering::Relaxed);
-      assert_eq!(specifier, "/foo.js");
-      assert_eq!(referrer, "file:///dyn_import2.js");
-      let s = resolve_import(specifier, referrer).unwrap();
-      Ok(s)
-    }
-
-    fn load(
-      &self,
-      _module_specifier: &ModuleSpecifier,
-      _maybe_referrer: Option<&ModuleSpecifier>,
-      _is_dyn_import: bool,
-    ) -> Pin<Box<ModuleSourceFuture>> {
-      async { Err(io::Error::from(io::ErrorKind::NotFound).into()) }.boxed()
-    }
-  }
-
-  let loader = Rc::new(DynImportErrLoader::default());
-  let count = loader.count.clone();
+  let loader = Rc::new(CountingModuleLoader::new(StaticModuleLoader::new([])));
   let mut runtime = JsRuntime::new(RuntimeOptions {
-    module_loader: Some(loader),
+    module_loader: Some(loader.clone()),
     ..Default::default()
   });
 
@@ -580,68 +488,21 @@ async fn dyn_import_err() {
 
     // We should get an error here.
     let result = runtime.poll_event_loop(cx, false);
-    if let Poll::Ready(Ok(_)) = result {
-      unreachable!();
-    }
-    assert_eq!(count.load(Ordering::Relaxed), 4);
+    assert!(matches!(result, Poll::Ready(Err(_))));
+    assert_eq!(loader.counts(), ModuleLoadEventCounts::new(4, 1, 1));
     Poll::Ready(())
   })
   .await;
 }
 
-#[derive(Clone, Default)]
-struct DynImportOkLoader {
-  pub prepare_load_count: Arc<AtomicUsize>,
-  pub resolve_count: Arc<AtomicUsize>,
-  pub load_count: Arc<AtomicUsize>,
-}
-
-impl ModuleLoader for DynImportOkLoader {
-  fn resolve(
-    &self,
-    specifier: &str,
-    referrer: &str,
-    _kind: ResolutionKind,
-  ) -> Result<ModuleSpecifier, Error> {
-    let c = self.resolve_count.fetch_add(1, Ordering::Relaxed);
-    assert!(c < 7);
-    assert_eq!(specifier, "./b.js");
-    assert_eq!(referrer, "file:///dyn_import3.js");
-    let s = resolve_import(specifier, referrer).unwrap();
-    Ok(s)
-  }
-
-  fn load(
-    &self,
-    specifier: &ModuleSpecifier,
-    _maybe_referrer: Option<&ModuleSpecifier>,
-    _is_dyn_import: bool,
-  ) -> Pin<Box<ModuleSourceFuture>> {
-    self.load_count.fetch_add(1, Ordering::Relaxed);
-    let info =
-      ModuleSource::for_test("export function b() { return 'b' }", specifier);
-    async move { Ok(info) }.boxed()
-  }
-
-  fn prepare_load(
-    &self,
-    _module_specifier: &ModuleSpecifier,
-    _maybe_referrer: Option<String>,
-    _is_dyn_import: bool,
-  ) -> Pin<Box<dyn Future<Output = Result<(), Error>>>> {
-    self.prepare_load_count.fetch_add(1, Ordering::Relaxed);
-    async { Ok(()) }.boxed_local()
-  }
-}
-
 #[tokio::test]
 async fn dyn_import_ok() {
-  let loader = Rc::new(DynImportOkLoader::default());
-  let prepare_load_count = loader.prepare_load_count.clone();
-  let resolve_count = loader.resolve_count.clone();
-  let load_count = loader.load_count.clone();
+  let loader = Rc::new(CountingModuleLoader::new(StaticModuleLoader::with(
+    Url::parse("file:///b.js").unwrap(),
+    ascii_str!("export function b() { return 'b' }"),
+  )));
   let mut runtime = JsRuntime::new(RuntimeOptions {
-    module_loader: Some(loader),
+    module_loader: Some(loader.clone()),
     ..Default::default()
   });
   poll_fn(move |cx| {
@@ -669,15 +530,12 @@ async fn dyn_import_ok() {
       runtime.poll_event_loop(cx, false),
       Poll::Ready(Ok(_))
     ));
-    assert_eq!(prepare_load_count.load(Ordering::Relaxed), 1);
-    assert_eq!(resolve_count.load(Ordering::Relaxed), 7);
-    assert_eq!(load_count.load(Ordering::Relaxed), 1);
+    assert_eq!(loader.counts(), ModuleLoadEventCounts::new(7, 1, 1));
     assert!(matches!(
       runtime.poll_event_loop(cx, false),
       Poll::Ready(Ok(_))
     ));
-    assert_eq!(resolve_count.load(Ordering::Relaxed), 7);
-    assert_eq!(load_count.load(Ordering::Relaxed), 1);
+    assert_eq!(loader.counts(), ModuleLoadEventCounts::new(7, 1, 1));
     Poll::Ready(())
   })
   .await;
@@ -686,10 +544,12 @@ async fn dyn_import_ok() {
 #[tokio::test]
 async fn dyn_import_borrow_mut_error() {
   // https://github.com/denoland/deno/issues/6054
-  let loader = Rc::new(DynImportOkLoader::default());
-  let prepare_load_count = loader.prepare_load_count.clone();
+  let loader = Rc::new(CountingModuleLoader::new(StaticModuleLoader::with(
+    Url::parse("file:///b.js").unwrap(),
+    ascii_str!("export function b() { return 'b' }"),
+  )));
   let mut runtime = JsRuntime::new(RuntimeOptions {
-    module_loader: Some(loader),
+    module_loader: Some(loader.clone()),
     ..Default::default()
   });
 
@@ -707,11 +567,16 @@ async fn dyn_import_borrow_mut_error() {
         "#,
       )
       .unwrap();
+    // TODO(mmastrac): These checks here are pretty broken and make a lot of assumptions
+    // about the ordering of resolution and events. If these break, we can remove them.
+
+    // Old comments that are likely wrong:
     // First poll runs `prepare_load` hook.
     let _ = runtime.poll_event_loop(cx, false);
-    assert_eq!(prepare_load_count.load(Ordering::Relaxed), 1);
+    assert_eq!(loader.counts(), ModuleLoadEventCounts::new(4, 1, 1));
     // Second poll triggers error
     let _ = runtime.poll_event_loop(cx, false);
+    assert_eq!(loader.counts(), ModuleLoadEventCounts::new(4, 1, 1));
     Poll::Ready(())
   })
   .await;
@@ -720,51 +585,24 @@ async fn dyn_import_borrow_mut_error() {
 // Regression test for https://github.com/denoland/deno/issues/3736.
 #[test]
 fn dyn_concurrent_circular_import() {
-  #[derive(Clone, Default)]
-  struct DynImportCircularLoader {
-    pub resolve_count: Arc<AtomicUsize>,
-    pub load_count: Arc<AtomicUsize>,
-  }
-
-  impl ModuleLoader for DynImportCircularLoader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      self.resolve_count.fetch_add(1, Ordering::Relaxed);
-      let s = resolve_import(specifier, referrer).unwrap();
-      Ok(s)
-    }
-
-    fn load(
-      &self,
-      specifier: &ModuleSpecifier,
-      _maybe_referrer: Option<&ModuleSpecifier>,
-      _is_dyn_import: bool,
-    ) -> Pin<Box<ModuleSourceFuture>> {
-      self.load_count.fetch_add(1, Ordering::Relaxed);
-      let filename = PathBuf::from(specifier.to_string())
-        .file_name()
-        .unwrap()
-        .to_string_lossy()
-        .to_string();
-      let code = match filename.as_str() {
-        "a.js" => "import './b.js';",
-        "b.js" => "import './c.js';\nimport './a.js';",
-        "c.js" => "import './d.js';",
-        "d.js" => "// pass",
-        _ => unreachable!(),
-      };
-      let info = ModuleSource::for_test(code, specifier);
-      async move { Ok(info) }.boxed()
-    }
-  }
+  let loader = Rc::new(CountingModuleLoader::new(StaticModuleLoader::new([
+    (
+      Url::parse("file:///a.js").unwrap(),
+      ascii_str!("import './b.js';"),
+    ),
+    (
+      Url::parse("file:///b.js").unwrap(),
+      ascii_str!("import './c.js';\nimport './a.js';"),
+    ),
+    (
+      Url::parse("file:///c.js").unwrap(),
+      ascii_str!("import './d.js';"),
+    ),
+    (Url::parse("file:///d.js").unwrap(), ascii_str!("// pass")),
+  ])));
 
-  let loader = Rc::new(DynImportCircularLoader::default());
   let mut runtime = JsRuntime::new(RuntimeOptions {
-    module_loader: Some(loader),
+    module_loader: Some(loader.clone()),
     ..Default::default()
   });
 
@@ -1090,46 +928,22 @@ if (import.meta.url != 'file:///main_with_code.js') throw Error();
 
 #[test]
 fn main_and_side_module() {
-  struct ModsLoader {}
-
   let main_specifier = resolve_url("file:///main_module.js").unwrap();
   let side_specifier = resolve_url("file:///side_module.js").unwrap();
 
-  impl ModuleLoader for ModsLoader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      let s = resolve_import(specifier, referrer).unwrap();
-      Ok(s)
-    }
-
-    fn load(
-      &self,
-      module_specifier: &ModuleSpecifier,
-      _maybe_referrer: Option<&ModuleSpecifier>,
-      _is_dyn_import: bool,
-    ) -> Pin<Box<ModuleSourceFuture>> {
-      let module_source = match module_specifier.as_str() {
-        "file:///main_module.js" => ModuleSource::for_test(
-          "if (!import.meta.main) throw Error();",
-          "file:///main_module.js",
-        ),
-        "file:///side_module.js" => ModuleSource::for_test(
-          "if (import.meta.main) throw Error();",
-          "file:///side_module.js",
-        ),
-        _ => unreachable!(),
-      };
-      async move { Ok(module_source) }.boxed()
-    }
-  }
+  let loader = StaticModuleLoader::new([
+    (
+      main_specifier.clone(),
+      ascii_str!("if (!import.meta.main) throw Error();"),
+    ),
+    (
+      side_specifier.clone(),
+      ascii_str!("if (import.meta.main) throw Error();"),
+    ),
+  ]);
 
-  let loader = Rc::new(ModsLoader {});
   let mut runtime = JsRuntime::new(RuntimeOptions {
-    module_loader: Some(loader),
+    module_loader: Some(Rc::new(loader)),
     ..Default::default()
   });
 
diff --git a/core/runtime/tests/jsrealm.rs b/core/runtime/tests/jsrealm.rs
index b517972..6ca645d 100644
--- a/core/runtime/tests/jsrealm.rs
+++ b/core/runtime/tests/jsrealm.rs
@@ -1,6 +1,7 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
 use crate as deno_core;
 use crate::modules::ModuleCode;
+use crate::modules::StaticModuleLoader;
 use crate::*;
 use anyhow::Error;
 use deno_ops::op;
@@ -15,6 +16,7 @@ use std::sync::atomic::AtomicUsize;
 use std::sync::atomic::Ordering;
 use std::task::Context;
 use std::task::Poll;
+use url::Url;
 
 #[tokio::test]
 async fn test_set_promise_reject_callback_realms() {
@@ -542,45 +544,20 @@ async fn test_realm_modules() {
 
 #[tokio::test]
 async fn test_cross_realm_imports() {
-  struct Loader;
-  impl ModuleLoader for Loader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      assert_eq!(specifier, "file:///test.js");
-      assert_eq!(referrer, "");
-      let s = crate::resolve_import(specifier, referrer).unwrap();
-      Ok(s)
-    }
-
-    fn load(
-      &self,
-      _module_specifier: &ModuleSpecifier,
-      _maybe_referrer: Option<&ModuleSpecifier>,
-      _is_dyn_import: bool,
-    ) -> Pin<Box<ModuleSourceFuture>> {
-      async {
-        Ok(ModuleSource::for_test(
-          "export default globalThis;",
-          "file:///test.js",
-        ))
-      }
-      .boxed_local()
-    }
-  }
+  let loader: Rc<dyn ModuleLoader> = Rc::new(StaticModuleLoader::with(
+    Url::parse("file:///test.js").unwrap(),
+    ascii_str!("export default globalThis;"),
+  ));
 
-  let loader = Rc::new(Loader);
+  let loader = Rc::new(loader);
   let mut runtime = JsRuntime::new(RuntimeOptions {
-    module_loader: Some(loader.clone()),
+    module_loader: Some(Rc::clone(&loader)),
     ..Default::default()
   });
   let main_realm = runtime.main_realm();
   let other_realm = runtime
     .create_realm(CreateRealmOptions {
-      module_loader: Some(loader),
+      module_loader: Some(Rc::clone(&loader)),
     })
     .unwrap();
 
@@ -650,35 +627,10 @@ async fn test_cross_realm_imports() {
 // different realms at the same time works.
 #[tokio::test]
 async fn test_realms_concurrent_module_evaluations() {
-  struct Loader;
-  impl ModuleLoader for Loader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      assert_eq!(specifier, "file:///test.js");
-      assert_eq!(referrer, ".");
-      let s = crate::resolve_import(specifier, referrer).unwrap();
-      Ok(s)
-    }
-
-    fn load(
-      &self,
-      _module_specifier: &ModuleSpecifier,
-      _maybe_referrer: Option<&ModuleSpecifier>,
-      _is_dyn_import: bool,
-    ) -> Pin<Box<ModuleSourceFuture>> {
-      async {
-        Ok(ModuleSource::for_test(
-          r#"await Deno.core.opAsync("op_wait");"#,
-          "file:///test.js",
-        ))
-      }
-      .boxed_local()
-    }
-  }
+  let loader: Rc<dyn ModuleLoader> = Rc::new(StaticModuleLoader::with(
+    Url::parse("file:///test.js").unwrap(),
+    ascii_str!(r#"await Deno.core.opAsync("op_wait");"#),
+  ));
 
   #[op]
   async fn op_wait(op_state: Rc<RefCell<OpState>>) {
@@ -693,7 +645,7 @@ async fn test_realms_concurrent_module_evaluations() {
 
   let mut runtime = JsRuntime::new(RuntimeOptions {
     extensions: vec![test_ext::init_ops()],
-    module_loader: Some(Rc::new(Loader)),
+    module_loader: Some(Rc::clone(&loader)),
     ..Default::default()
   });
   runtime
@@ -704,7 +656,7 @@ async fn test_realms_concurrent_module_evaluations() {
   let main_realm = runtime.main_realm();
   let other_realm = runtime
     .create_realm(CreateRealmOptions {
-      module_loader: Some(Rc::new(Loader)),
+      module_loader: Some(loader),
     })
     .unwrap();
 
@@ -757,35 +709,10 @@ async fn test_realms_concurrent_module_evaluations() {
 // realms at the same time works.
 #[tokio::test]
 async fn test_realm_concurrent_dynamic_imports() {
-  struct Loader;
-  impl ModuleLoader for Loader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      assert_eq!(specifier, "file:///test.js");
-      assert_eq!(referrer, "");
-      let s = crate::resolve_import(specifier, referrer).unwrap();
-      Ok(s)
-    }
-
-    fn load(
-      &self,
-      _module_specifier: &ModuleSpecifier,
-      _maybe_referrer: Option<&ModuleSpecifier>,
-      _is_dyn_import: bool,
-    ) -> Pin<Box<ModuleSourceFuture>> {
-      async {
-        Ok(ModuleSource::for_test(
-          r#"await Deno.core.opAsync("op_wait");"#,
-          "file:///test.js",
-        ))
-      }
-      .boxed_local()
-    }
-  }
+  let loader = Rc::new(StaticModuleLoader::with(
+    Url::parse("file:///test.js").unwrap(),
+    ascii_str!(r#"await Deno.core.opAsync("op_wait");"#),
+  ));
 
   #[op]
   async fn op_wait(op_state: Rc<RefCell<OpState>>) {
@@ -800,7 +727,7 @@ async fn test_realm_concurrent_dynamic_imports() {
 
   let mut runtime = JsRuntime::new(RuntimeOptions {
     extensions: vec![test_ext::init_ops()],
-    module_loader: Some(Rc::new(Loader)),
+    module_loader: Some(loader.clone()),
     ..Default::default()
   });
   runtime
@@ -811,7 +738,7 @@ async fn test_realm_concurrent_dynamic_imports() {
   let main_realm = runtime.main_realm();
   let other_realm = runtime
     .create_realm(CreateRealmOptions {
-      module_loader: Some(Rc::new(Loader)),
+      module_loader: Some(loader.clone()),
     })
     .unwrap();
 
diff --git a/core/runtime/tests/misc.rs b/core/runtime/tests/misc.rs
index cc003d3..62309a6 100644
--- a/core/runtime/tests/misc.rs
+++ b/core/runtime/tests/misc.rs
@@ -1,13 +1,9 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
 use crate as deno_core;
-use crate::error::generic_error;
+
 use crate::error::AnyError;
 use crate::error::JsError;
-use crate::module_specifier::ModuleSpecifier;
-use crate::modules::ModuleLoader;
-use crate::modules::ModuleSource;
-use crate::modules::ModuleSourceFuture;
-use crate::modules::ResolutionKind;
+use crate::modules::StaticModuleLoader;
 use crate::runtime::tests::setup;
 use crate::runtime::tests::Mode;
 use crate::*;
@@ -17,8 +13,6 @@ use cooked_waker::Wake;
 use cooked_waker::WakeRef;
 use deno_ops::op;
 use futures::future::poll_fn;
-use futures::FutureExt;
-use std::pin::Pin;
 use std::rc::Rc;
 use std::sync::atomic::AtomicBool;
 use std::sync::atomic::AtomicI8;
@@ -28,6 +22,7 @@ use std::sync::Arc;
 use std::task::Context;
 use std::task::Poll;
 use std::time::Duration;
+use url::Url;
 
 #[test]
 fn test_execute_script_return_value() {
@@ -379,36 +374,8 @@ fn inspector() {
 
 #[test]
 fn test_get_module_namespace() {
-  #[derive(Default)]
-  struct ModsLoader;
-
-  impl ModuleLoader for ModsLoader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      assert_eq!(specifier, "file:///main.js");
-      assert_eq!(referrer, ".");
-      let s = crate::resolve_import(specifier, referrer).unwrap();
-      Ok(s)
-    }
-
-    fn load(
-      &self,
-      _module_specifier: &ModuleSpecifier,
-      _maybe_referrer: Option<&ModuleSpecifier>,
-      _is_dyn_import: bool,
-    ) -> Pin<Box<ModuleSourceFuture>> {
-      async { Err(generic_error("Module loading is not supported")) }
-        .boxed_local()
-    }
-  }
-
-  let loader = std::rc::Rc::new(ModsLoader);
   let mut runtime = JsRuntime::new(RuntimeOptions {
-    module_loader: Some(loader),
+    module_loader: Some(Rc::new(NoopModuleLoader)),
     ..Default::default()
   });
 
@@ -744,41 +711,8 @@ fn test_has_tick_scheduled() {
 
 #[test]
 fn terminate_during_module_eval() {
-  #[derive(Default)]
-  struct ModsLoader;
-
-  impl ModuleLoader for ModsLoader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      assert_eq!(specifier, "file:///main.js");
-      assert_eq!(referrer, ".");
-      let s = crate::resolve_import(specifier, referrer).unwrap();
-      Ok(s)
-    }
-
-    fn load(
-      &self,
-      _module_specifier: &ModuleSpecifier,
-      _maybe_referrer: Option<&ModuleSpecifier>,
-      _is_dyn_import: bool,
-    ) -> Pin<Box<ModuleSourceFuture>> {
-      async move {
-        Ok(ModuleSource::for_test(
-          "console.log('hello world');",
-          "file:///main.js",
-        ))
-      }
-      .boxed_local()
-    }
-  }
-
-  let loader = std::rc::Rc::new(ModsLoader);
   let mut runtime = JsRuntime::new(RuntimeOptions {
-    module_loader: Some(loader),
+    module_loader: Some(Rc::new(NoopModuleLoader)),
     ..Default::default()
   });
 
@@ -886,43 +820,21 @@ async fn test_set_promise_reject_callback_top_level_await() {
 
   deno_core::extension!(test_ext, ops = [op_promise_reject]);
 
-  #[derive(Default)]
-  struct ModsLoader;
-
-  impl ModuleLoader for ModsLoader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      assert_eq!(specifier, "file:///main.js");
-      assert_eq!(referrer, ".");
-      let s = crate::resolve_import(specifier, referrer).unwrap();
-      Ok(s)
-    }
-
-    fn load(
-      &self,
-      _module_specifier: &ModuleSpecifier,
-      _maybe_referrer: Option<&ModuleSpecifier>,
-      _is_dyn_import: bool,
-    ) -> Pin<Box<ModuleSourceFuture>> {
-      let code = r#"
-      Deno.core.ops.op_set_promise_reject_callback((type, promise, reason) => {
-        Deno.core.ops.op_promise_reject();
-      });
-      throw new Error('top level throw');
-      "#;
-
-      async move { Ok(ModuleSource::for_test(code, "file:///main.js")) }
-        .boxed_local()
-    }
-  }
+  let loader = StaticModuleLoader::with(
+    Url::parse("file:///main.js").unwrap(),
+    ascii_str!(
+      r#"
+  Deno.core.ops.op_set_promise_reject_callback((type, promise, reason) => {
+    Deno.core.ops.op_promise_reject();
+  });
+  throw new Error('top level throw');
+  "#
+    ),
+  );
 
   let mut runtime = JsRuntime::new(RuntimeOptions {
     extensions: vec![test_ext::init_ops()],
-    module_loader: Some(Rc::new(ModsLoader)),
+    module_loader: Some(Rc::new(loader)),
     ..Default::default()
   });
 
@@ -961,40 +873,14 @@ fn test_array_by_copy() {
 // realm other than the main one.
 #[tokio::test]
 async fn test_stalled_tla() {
-  struct Loader;
-  impl ModuleLoader for Loader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      assert_eq!(specifier, "file:///test.js");
-      assert_eq!(referrer, ".");
-      let s = crate::resolve_import(specifier, referrer).unwrap();
-      Ok(s)
-    }
-
-    fn load(
-      &self,
-      _module_specifier: &ModuleSpecifier,
-      _maybe_referrer: Option<&ModuleSpecifier>,
-      _is_dyn_import: bool,
-    ) -> Pin<Box<ModuleSourceFuture>> {
-      async {
-        Ok(ModuleSource::for_test(
-          r#"await new Promise(() => {});"#,
-          "file:///test.js",
-        ))
-      }
-      .boxed_local()
-    }
-  }
-
+  let loader = StaticModuleLoader::with(
+    Url::parse("file:///test.js").unwrap(),
+    ascii_str!("await new Promise(() => {});"),
+  );
   let mut runtime = JsRuntime::new(Default::default());
   let realm = runtime
     .create_realm(CreateRealmOptions {
-      module_loader: Some(Rc::new(Loader)),
+      module_loader: Some(Rc::new(loader)),
     })
     .unwrap();
 
@@ -1023,93 +909,3 @@ 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 bb6745e..8c2155c 100644
--- a/core/runtime/tests/snapshot.rs
+++ b/core/runtime/tests/snapshot.rs
@@ -1,20 +1,16 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
 use crate::extensions::Op;
-use crate::module_specifier::ModuleSpecifier;
 use crate::modules::AssertedModuleType;
+use crate::modules::LoggingModuleLoader;
 use crate::modules::ModuleInfo;
 use crate::modules::ModuleLoadId;
-use crate::modules::ModuleLoader;
-use crate::modules::ModuleSourceFuture;
 use crate::modules::ModuleType;
-use crate::modules::ResolutionKind;
 use crate::modules::SymbolicModule;
 use crate::*;
 use anyhow::Error;
 use deno_ops::op;
-use futures::FutureExt;
-use std::pin::Pin;
 use std::rc::Rc;
+use url::Url;
 
 #[test]
 fn will_snapshot() {
@@ -135,31 +131,6 @@ fn test_from_boxed_snapshot() {
 
 #[test]
 fn es_snapshot() {
-  #[derive(Default)]
-  struct ModsLoader;
-
-  impl ModuleLoader for ModsLoader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      let s = crate::resolve_import(specifier, referrer).unwrap();
-      Ok(s)
-    }
-
-    fn load(
-      &self,
-      _module_specifier: &ModuleSpecifier,
-      _maybe_referrer: Option<&ModuleSpecifier>,
-      _is_dyn_import: bool,
-    ) -> Pin<Box<ModuleSourceFuture>> {
-      eprintln!("load() should not be called");
-      unreachable!()
-    }
-  }
-
   fn create_module(
     runtime: &mut JsRuntime,
     i: usize,
@@ -237,10 +208,8 @@ fn es_snapshot() {
     Ok(String::from("test"))
   }
 
-  let loader = Rc::new(ModsLoader);
   let mut runtime = JsRuntimeForSnapshot::new(
     RuntimeOptions {
-      module_loader: Some(loader.clone()),
       extensions: vec![Extension::builder("text_ext")
         .ops(vec![op_test::DECL])
         .build()],
@@ -278,7 +247,6 @@ fn es_snapshot() {
 
   let mut runtime2 = JsRuntimeForSnapshot::new(
     RuntimeOptions {
-      module_loader: Some(loader.clone()),
       startup_snapshot: Some(Snapshot::JustCreated(snapshot)),
       extensions: vec![Extension::builder("text_ext")
         .ops(vec![op_test::DECL])
@@ -298,7 +266,6 @@ fn es_snapshot() {
   let snapshot2 = runtime2.snapshot();
 
   let mut runtime3 = JsRuntime::new(RuntimeOptions {
-    module_loader: Some(loader),
     startup_snapshot: Some(Snapshot::JustCreated(snapshot2)),
     extensions: vec![Extension::builder("text_ext")
       .ops(vec![op_test::DECL])
@@ -363,16 +330,24 @@ fn es_snapshot_without_runtime_module_loader() {
     );
   }
 
-  // Make sure we can't import the module.
+  // We can still import a module that was registered manually
   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_ok());
+
+  // But not a new one
+  let dyn_import_promise = runtime
+    .execute_script_static("", "import('ext:module_snapshot/test2.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 """#
+    r#"Uncaught TypeError: Module loading is not supported; attempted to load: "ext:module_snapshot/test2.js" from "(no referrer)""#
   );
 }
 
@@ -408,39 +383,92 @@ fn preserve_snapshotted_modules() {
     runtime.snapshot()
   };
 
-  struct Loader;
+  let loader = Rc::new(LoggingModuleLoader::new(NoopModuleLoader));
 
-  impl ModuleLoader for Loader {
-    fn resolve(
-      &self,
-      specifier: &str,
-      referrer: &str,
-      _kind: ResolutionKind,
-    ) -> Result<ModuleSpecifier, Error> {
-      Ok(resolve_import(specifier, referrer)?)
-    }
+  let mut runtime = JsRuntime::new(RuntimeOptions {
+    module_loader: Some(loader.clone()),
+    startup_snapshot: Some(Snapshot::JustCreated(startup_data)),
+    preserve_snapshotted_modules: Some(&["test:preserved"]),
+    ..Default::default()
+  });
 
-    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()
-    }
+  // We can't import "test:not-preserved"
+  {
+    assert_eq!(loader.log(), vec![]);
+    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: Module loading is not supported; attempted to load: \"test:not-preserved\" from \"(no referrer)\""
+    );
+    // Ensure that we tried to load `test:not-preserved`
+    assert_eq!(
+      loader.log(),
+      vec![Url::parse("test:not-preserved").unwrap()]
+    );
   }
 
+  // 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"
+    );
+  }
+}
+
+/// 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();
+
+  let loader = Rc::new(LoggingModuleLoader::new(NoopModuleLoader));
+
   let mut runtime = JsRuntime::new(RuntimeOptions {
-    module_loader: Some(Rc::new(Loader)),
-    startup_snapshot: Some(Snapshot::JustCreated(startup_data)),
+    module_loader: Some(loader.clone()),
+    extensions: vec![extension],
     preserve_snapshotted_modules: Some(&["test:preserved"]),
     ..Default::default()
   });
 
   // We can't import "test:not-preserved"
   {
+    assert_eq!(loader.log(), vec![]);
     let dyn_import_promise = runtime
       .execute_script_static("", "import('test:not-preserved')")
       .unwrap();
@@ -449,7 +477,12 @@ fn preserve_snapshotted_modules() {
     assert!(dyn_import_result.is_err());
     assert_eq!(
       dyn_import_result.err().unwrap().to_string().as_str(),
-      "Uncaught TypeError: Couldn't load module"
+      "Uncaught TypeError: Module loading is not supported; attempted to load: \"test:not-preserved\" from \"(no referrer)\""
+    );
+    // Ensure that we tried to load `test:not-preserved`
+    assert_eq!(
+      loader.log(),
+      vec![Url::parse("test:not-preserved").unwrap()]
     );
   }