Repository: denoland/deno_core
Pull request #544 (2024-02-02 19:02:26)

 refactor(core): clean up include_js_files! macros 
This looks pretty big, but it's mostly a cleanup of `include_js_files!` that makes it easier to understand and easier to maintain moving forward.

`include_js_files!` supports four styles of includes:

 - `"file"` <- old
 - `"file" with_specifier "specifier"` <- old, triggers a deprecation warning with a hint
 - `specifier = "file"` <- new
 - `specifier = { source = "source" }` <- new, see below

The `specifier = "file"` syntax will be preferred to replace the current `"file" with_specifier "specifier"` syntax, but won't be removed yet. If we want to add any other possible options to this macro, it's just too hard to expand the old syntax (which is why I introduced this new syntax!).

We now support an inline, embeddable string script for all places where `include_js_files!` is used:

```
    deno_core::extension!(
      module_snapshot,
      esm_entry_point = "mod:test",
      esm = ["mod:test" =
        { source = "globalThis.TEST = 'foo'; export const TEST = 'bar';" },]
    );
```

Changes:
 - `ExtensionFileSource` cannot be constructed using the bare struct syntax anymore (a new field named `_unconstructable_use_new` helps suggest the right course of action)
 - `ExtensionFileSource` has new `const` constructors 
 - `include_js_files!` macro and related macros have been rewritten



Patch:
diff --git a/core/extensions.rs b/core/extensions.rs
index c0eec44..b07ef4d 100644
--- a/core/extensions.rs
+++ b/core/extensions.rs
@@ -5,16 +5,18 @@ use crate::OpState;
 use anyhow::Context as _;
 use anyhow::Error;
 use std::borrow::Cow;
+use std::marker::PhantomData;
 use std::sync::Arc;
 use v8::fast_api::FastFunction;
 use v8::MapFnTo;
 
-#[derive(Clone, Debug)]
+#[derive(Clone)]
 pub enum ExtensionFileSourceCode {
   /// Source code is included in the binary produced. Either by being defined
   /// inline, or included using `include_str!()`. If you are snapshotting, this
   /// will result in two copies of the source code being included - one in the
   /// snapshot, the other the static string in the `Extension`.
+  #[deprecated = "Use ExtensionFileSource::new"]
   IncludedInBinary(&'static str),
 
   // Source code is loaded from a file on disk. It's meant to be used if the
@@ -22,24 +24,90 @@ pub enum ExtensionFileSourceCode {
   // during the build time and they will only be present in the V8 snapshot.
   LoadedFromFsDuringSnapshot(&'static str), // <- Path
 
+  // Source code was loaded from memory. It's meant to be used if the
+  // embedder is creating snapshots. Files will be loaded from memory
+  // during the build time and they will only be present in the V8 snapshot.
+  LoadedFromMemoryDuringSnapshot(&'static str),
+
   /// Source code may be computed at runtime.
   Computed(Arc<str>),
 }
 
+#[allow(deprecated)]
+impl std::fmt::Debug for ExtensionFileSourceCode {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    match *self {
+      Self::IncludedInBinary(..) => write!(f, "IncludedInBinary(..)"),
+      Self::LoadedFromFsDuringSnapshot(path) => {
+        write!(f, "LoadedFromFsDuringSnapshot({path})")
+      }
+      Self::LoadedFromMemoryDuringSnapshot(..) => {
+        write!(f, "LoadedFromMemoryDuringSnapshot(..)")
+      }
+      Self::Computed(..) => write!(f, "Computed(..)"),
+    }
+  }
+}
+
 #[derive(Clone, Debug)]
 pub struct ExtensionFileSource {
   pub specifier: &'static str,
   pub code: ExtensionFileSourceCode,
+  _unconstructable_use_new: PhantomData<()>,
 }
 
 impl ExtensionFileSource {
+  pub const fn new(specifier: &'static str, code: &'static str) -> Self {
+    #[allow(deprecated)]
+    Self {
+      specifier,
+      code: ExtensionFileSourceCode::IncludedInBinary(code),
+      _unconstructable_use_new: PhantomData,
+    }
+  }
+
+  pub const fn new_computed(specifier: &'static str, code: Arc<str>) -> Self {
+    #[allow(deprecated)]
+    Self {
+      specifier,
+      code: ExtensionFileSourceCode::Computed(code),
+      _unconstructable_use_new: PhantomData,
+    }
+  }
+
+  pub const fn loaded_during_snapshot(
+    specifier: &'static str,
+    path: &'static str,
+  ) -> Self {
+    #[allow(deprecated)]
+    Self {
+      specifier,
+      code: ExtensionFileSourceCode::LoadedFromFsDuringSnapshot(path),
+      _unconstructable_use_new: PhantomData,
+    }
+  }
+
+  pub const fn loaded_from_memory_during_snapshot(
+    specifier: &'static str,
+    code: &'static str,
+  ) -> Self {
+    #[allow(deprecated)]
+    Self {
+      specifier,
+      code: ExtensionFileSourceCode::LoadedFromMemoryDuringSnapshot(code),
+      _unconstructable_use_new: PhantomData,
+    }
+  }
+
   fn find_non_ascii(s: &str) -> String {
     s.chars().filter(|c| !c.is_ascii()).collect::<String>()
   }
 
+  #[allow(deprecated)]
   pub fn load(&self) -> Result<ModuleCodeString, Error> {
     match &self.code {
-      ExtensionFileSourceCode::IncludedInBinary(code) => {
+      ExtensionFileSourceCode::LoadedFromMemoryDuringSnapshot(code)
+      | ExtensionFileSourceCode::IncludedInBinary(code) => {
         debug_assert!(
           code.is_ascii(),
           "Extension code must be 7-bit ASCII: {} (found {})",
@@ -286,9 +354,9 @@ macro_rules! extension {
     $(, ops_fn = $ops_symbol:ident $( < $ops_param:ident > )? )?
     $(, ops = [ $( $(#[$m:meta])* $( $op:ident )::+ $( < $( $op_param:ident ),* > )?  ),+ $(,)? ] )?
     $(, esm_entry_point = $esm_entry_point:expr )?
-    $(, esm = [ $( dir $dir_esm:expr , )? $( $esm:literal $( with_specifier $esm_specifier:expr )? ),* $(,)? ] )?
-    $(, lazy_loaded_esm = [ $( dir $dir_lazy_loaded_esm:expr , )? $( $lazy_loaded_esm:literal $( with_specifier $lazy_loaded_esm_specifier:expr )? ),* $(,)? ] )?
-    $(, js = [ $( dir $dir_js:expr , )? $( $js:literal ),* $(,)? ] )?
+    $(, esm = [ $($esm:tt)* ] )?
+    $(, lazy_loaded_esm = [ $($lazy_loaded_esm:tt)* ] )?
+    $(, js = [ $($js:tt)* ] )?
     $(, options = { $( $options_id:ident : $options_type:ty ),* $(,)? } )?
     $(, middleware = $middleware_fn:expr )?
     $(, state = $state_fn:expr )?
@@ -329,16 +397,16 @@ macro_rules! extension {
           // Use intermediary `const`s here to disable user expressions which
           // can't be evaluated at compile-time.
           js_files: {
-            const V: ::std::borrow::Cow<'static, [$crate::ExtensionFileSource]> = ::std::borrow::Cow::Borrowed(&$crate::or!($($crate::include_js_files!( $name $( dir $dir_js , )? $( $js , )* ))?, []));
-            V
+            const JS: &'static [$crate::ExtensionFileSource] = &$crate::include_js_files!( $name $($($js)*)? );
+            ::std::borrow::Cow::Borrowed(JS)
           },
           esm_files: {
-            const V: ::std::borrow::Cow<'static, [$crate::ExtensionFileSource]> = ::std::borrow::Cow::Borrowed(&$crate::or!($($crate::include_js_files!( $name $( dir $dir_esm , )? $( $esm $( with_specifier $esm_specifier )? , )* ))?, []));
-            V
+            const JS: &'static [$crate::ExtensionFileSource] = &$crate::include_js_files!( $name $($($esm)*)? );
+            ::std::borrow::Cow::Borrowed(JS)
           },
           lazy_loaded_esm_files: {
-            const V: ::std::borrow::Cow<'static, [$crate::ExtensionFileSource]> = ::std::borrow::Cow::Borrowed(&$crate::or!($($crate::include_lazy_loaded_js_files!( $name $( dir $dir_lazy_loaded_esm , )? $( $lazy_loaded_esm $( with_specifier $lazy_loaded_esm_specifier )? , )* ))?, []));
-            V
+            const JS: &'static [$crate::ExtensionFileSource] = &$crate::include_lazy_loaded_js_files!( $name $($($lazy_loaded_esm)*)? );
+            ::std::borrow::Cow::Borrowed(JS)
           },
           esm_entry_point: {
             const V: ::std::option::Option<&'static ::std::primitive::str> = $crate::or!($(::std::option::Option::Some($esm_entry_point))?, ::std::option::Option::None);
@@ -534,15 +602,15 @@ impl Extension {
   /// returns JS source code to be loaded into the isolate (either at snapshotting,
   /// or at startup).  as a vector of a tuple of the file name, and the source code.
   pub fn get_js_sources(&self) -> &[ExtensionFileSource] {
-    self.js_files.as_ref()
+    &self.js_files
   }
 
   pub fn get_esm_sources(&self) -> &[ExtensionFileSource] {
-    self.esm_files.as_ref()
+    &self.esm_files
   }
 
   pub fn get_lazy_loaded_esm_sources(&self) -> &[ExtensionFileSource] {
-    self.lazy_loaded_esm_files.as_ref()
+    &self.lazy_loaded_esm_files
   }
 
   pub fn get_esm_entry_point(&self) -> Option<&'static str> {
@@ -603,105 +671,227 @@ impl Extension {
 }
 
 /// Helps embed JS files in an extension. Returns a vector of
-/// `ExtensionFileSource`, that represent the filename and source code. All
-/// specified files are rewritten into "ext:<extension_name>/<file_name>".
-///
-/// An optional "dir" option can be specified to prefix all files with a
-/// directory name.
+/// [`ExtensionFileSource`], that represents the filename and source code.
 ///
-/// ```ignore
-/// /// Example (for "my_extension"):
-/// include_js_files!(
+/// ```
+/// # use deno_core::include_js_files_doctest as include_js_files;
+/// // Example (for "my_extension"):
+/// let files = include_js_files!(
+///   my_extension
 ///   "01_hello.js",
 ///   "02_goodbye.js",
-/// )
+/// );
+///
 /// // Produces following specifiers:
 /// // - "ext:my_extension/01_hello.js"
 /// // - "ext:my_extension/02_goodbye.js"
+/// ```
 ///
-/// /// Example with "dir" option (for "my_extension"):
+/// An optional "dir" option can be specified to prefix all files with a
+/// directory name.
+///
+/// ```
+/// # use deno_core::include_js_files_doctest as include_js_files;
+/// // Example with "dir" option (for "my_extension"):
 /// include_js_files!(
+///   my_extension
 ///   dir "js",
 ///   "01_hello.js",
 ///   "02_goodbye.js",
-/// )
+/// );
 /// // Produces following specifiers:
-/// - "ext:my_extension/js/01_hello.js"
-/// - "ext:my_extension/js/02_goodbye.js"
+/// // - "ext:my_extension/js/01_hello.js"
+/// // - "ext:my_extension/js/02_goodbye.js"
+/// ```
+///
+/// You may also override the specifiers for each file like so:
+///
+/// ```
+/// # use deno_core::include_js_files_doctest as include_js_files;
+/// // Example with "dir" option (for "my_extension"):
+/// include_js_files!(
+///   my_extension
+///   "module:hello" = "01_hello.js",
+///   "module:goodbye" = "02_goodbye.js",
+/// );
+/// // Produces following specifiers:
+/// // - "module:hello"
+/// // - "module:goodbye"
 /// ```
-#[cfg(not(feature = "include_js_files_for_snapshotting"))]
 #[macro_export]
 macro_rules! include_js_files {
-  ($name:ident dir $dir:expr, $($file:literal $(with_specifier $esm_specifier:expr)?,)+) => {
-    [
-      $($crate::ExtensionFileSource {
-        specifier: $crate::or!($($esm_specifier)?, concat!("ext:", stringify!($name), "/", $file)),
-        code: $crate::ExtensionFileSourceCode::IncludedInBinary(
-          include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $dir, "/", $file))
-        ),
-      },)+
-    ]
+  // Valid inputs:
+  //  - "file"
+  //  - "file" with_specifier "specifier"
+  //  - "specifier" = "file"
+  //  - "specifier" = { source = "source" }
+  ($name:ident $( dir $dir:literal, )? $(
+    $s1:literal
+    $(with_specifier $s2:literal)?
+    $(= $config:tt)?
+  ),* $(,)?) => {
+    $crate::__extension_include_js_files_detect!(name=$name, dir=$crate::__extension_root_dir!($($dir)?), $([
+      // These entries will be parsed in __extension_include_js_files_inner
+      $s1 $(with_specifier $s2)? $(= $config)?
+    ]),*)
   };
+}
 
-  ($name:ident $($file:literal $(with_specifier $esm_specifier:expr)?,)+) => {
-    [
-      $($crate::ExtensionFileSource {
-        specifier: $crate::or!($($esm_specifier)?, concat!("ext:", stringify!($name), "/", $file)),
-        code: $crate::ExtensionFileSourceCode::IncludedInBinary(
-          include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $file))
-        ),
-      },)+
-    ]
+/// Helps embed JS files in an extension. Returns a vector of
+/// `ExtensionFileSource`, that represent the filename and source code. All
+/// specified files are rewritten into "ext:<extension_name>/<file_name>".
+///
+/// An optional "dir" option can be specified to prefix all files with a
+/// directory name.
+///
+/// See [`include_js_files!`] for details on available options.
+#[macro_export]
+macro_rules! include_lazy_loaded_js_files {
+  ($name:ident $( dir $dir:literal, )? $(
+    $s1:literal
+    $(with_specifier $s2:literal)?
+    $(= $config:tt)?
+  ),* $(,)?) => {
+    $crate::__extension_include_js_files_inner!(mode=included, name=$name, dir=$crate::__extension_root_dir!($($dir)?), $([
+      // These entries will be parsed in __extension_include_js_files_inner
+      $s1 $(with_specifier $s2)? $(= $config)?
+    ]),*)
   };
 }
 
+/// Used for doctests only. Won't try to load anything from disk.
+#[doc(hidden)]
+#[macro_export]
+macro_rules! include_js_files_doctest {
+  ($name:ident $( dir $dir:literal, )? $(
+    $s1:literal
+    $(with_specifier $s2:literal)?
+    $(= $config:tt)?
+  ),* $(,)?) => {
+    $crate::__extension_include_js_files_inner!(mode=loaded, name=$name, dir=$crate::__extension_root_dir!($($dir)?), $([
+      $s1 $(with_specifier $s2)? $(= $config)?
+    ]),*)
+  };
+}
+
+/// When `#[cfg(not(feature = "include_js_files_for_snapshotting"))]` matches, ie: the `include_js_files_for_snapshotting`
+/// feature is not set, we want all JS files to be included.
+///
+/// Maps `(...)` to `(mode=included, ...)`
+#[cfg(not(feature = "include_js_files_for_snapshotting"))]
+#[doc(hidden)]
+#[macro_export]
+macro_rules! __extension_include_js_files_detect {
+  ($($rest:tt)*) => { $crate::__extension_include_js_files_inner!(mode=included, $($rest)*) };
+}
+
+/// When `#[cfg(feature = "include_js_files_for_snapshotting")]` matches, ie: the `include_js_files_for_snapshotting`
+/// feature is set, we want the pathnames for the JS files to be included and not the file contents.
+///
+/// Maps `(...)` to `(mode=loaded, ...)`
 #[cfg(feature = "include_js_files_for_snapshotting")]
+#[doc(hidden)]
 #[macro_export]
-macro_rules! include_js_files {
-  ($name:ident dir $dir:expr, $($file:literal $(with_specifier $esm_specifier:expr)?,)+) => {
+macro_rules! __extension_include_js_files_detect {
+  ($($rest:tt)*) => { $crate::__extension_include_js_files_inner!(mode=loaded, $($rest)*) };
+}
+
+/// This is the core of the [`include_js_files!`] and [`include_lazy_loaded_js_files`] macros. The first
+/// rule is the entry point that receives a list of unparsed file entries. Each entry is extracted and
+/// then parsed with the `@parse_item` rules.
+#[doc(hidden)]
+#[macro_export]
+macro_rules! __extension_include_js_files_inner {
+  // Entry point: (mode=, name=, dir=, [... files])
+  (mode=$mode:ident, name=$name:ident, dir=$dir:expr, $([
+    $s1:literal
+    $(with_specifier $s2:literal)?
+    $(= $config:tt)?
+  ]),*) => {
     [
-      $($crate::ExtensionFileSource {
-        specifier: $crate::or!($($esm_specifier)?, concat!("ext:", stringify!($name), "/", $file)),
-        code: $crate::ExtensionFileSourceCode::LoadedFromFsDuringSnapshot(
-          concat!(env!("CARGO_MANIFEST_DIR"), "/", $dir, "/", $file)
-        ),
-      },)+
+      $(
+        $crate::__extension_include_js_files_inner!(
+          @parse_item
+          mode=$mode,
+          name=$name,
+          dir=$dir,
+          $s1 $(with_specifier $s2)? $(= $config)?
+        )
+      ),*
     ]
   };
 
-  ($name:ident $($file:literal $(with_specifier $esm_specifier:expr)?,)+) => {
-    [
-      $($crate::ExtensionFileSource {
-        specifier: $crate::or!($($esm_specifier)?, concat!("ext:", stringify!($name), "/", $file)),
-        code: $crate::ExtensionFileSourceCode::LoadedFromFsDuringSnapshot(
-          concat!(env!("CARGO_MANIFEST_DIR"), "/", $file)
-        ),
-      },)+
-    ]
+  // @parse_item macros will parse a single file entry, and then call @item macros with the destructured data
+
+  // "file" -> Include a file, use the generated specifier
+  (@parse_item mode=$mode:ident, name=$name:ident, dir=$dir:expr, $file:literal) => {
+    $crate::__extension_include_js_files_inner!(@item mode=$mode, dir=$dir, specifier=concat!("ext:", stringify!($name), "/", $file), file=$file)
+  };
+  // "file" with_specifier "specifier" -> Include a file, use the provided specifier
+  (@parse_item mode=$mode:ident, name=$name:ident, dir=$dir:expr, $file:literal with_specifier $specifier:literal) => {
+    {
+      #[deprecated="When including JS files 'file with_specifier specifier' is deprecated: use 'specifier = file' instead"]
+      struct WithSpecifierIsDeprecated {}
+      _ = WithSpecifierIsDeprecated {};
+      $crate::__extension_include_js_files_inner!(@item mode=$mode, dir=$dir, specifier=$specifier, file=$file)
+    }
+  };
+  // "specifier" = "file" -> Include a file, use the provided specifier
+  (@parse_item mode=$mode:ident, name=$name:ident, dir=$dir:expr, $specifier:literal = $file:literal) => {
+    $crate::__extension_include_js_files_inner!(@item mode=$mode, dir=$dir, specifier=$specifier, file=$file)
+  };
+  // "specifier" = { source = "source" } -> Include a file, use the provided specifier
+  (@parse_item mode=$mode:ident, name=$name:ident, dir=$dir:expr, $specifier:literal = { source = $source:literal }) => {
+    $crate::__extension_include_js_files_inner!(@item mode=$mode, specifier=$specifier, source=$source)
+  };
+
+  // @item macros generate the final output
+
+  // loaded, source
+  (@item mode=loaded, specifier=$specifier:expr, source=$source:expr) => {
+    $crate::ExtensionFileSource::loaded_from_memory_during_snapshot($specifier, $source)
+  };
+  // loaded, file
+  (@item mode=loaded, dir=$dir:expr, specifier=$specifier:expr, file=$file:literal) => {
+    $crate::ExtensionFileSource::loaded_during_snapshot($specifier, concat!($dir, "/", $file))
+  };
+  // included, source
+  (@item mode=included, specifier=$specifier:expr, source=$source:expr) => {
+    $crate::ExtensionFileSource::new($specifier, $source)
+  };
+  // included, file
+  (@item mode=included, dir=$dir:expr, specifier=$specifier:expr, file=$file:literal) => {
+    $crate::ExtensionFileSource::new($specifier, include_str!(concat!($dir, "/", $file)))
   };
 }
 
+/// Given an optional `$dir`, generates a crate-relative root directory.
+#[doc(hidden)]
 #[macro_export]
-macro_rules! include_lazy_loaded_js_files {
-  ($name:ident dir $dir:expr, $($file:literal $(with_specifier $esm_specifier:expr)?,)+) => {
-    [
-      $($crate::ExtensionFileSource {
-        specifier: $crate::or!($($esm_specifier)?, concat!("ext:", stringify!($name), "/", $file)),
-        code: $crate::ExtensionFileSourceCode::IncludedInBinary(
-          include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $dir, "/", $file))
-        ),
-      },)+
-    ]
+macro_rules! __extension_root_dir {
+  () => {
+    env!("CARGO_MANIFEST_DIR")
   };
-
-  ($name:ident $($file:literal $(with_specifier $esm_specifier:expr)?,)+) => {
-    [
-      $($crate::ExtensionFileSource {
-        specifier: $crate::or!($($esm_specifier)?, concat!("ext:", stringify!($name), "/", $file)),
-        code: $crate::ExtensionFileSourceCode::IncludedInBinary(
-          include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $file))
-        ),
-      },)+
-    ]
+  ($dir:expr) => {
+    concat!(env!("CARGO_MANIFEST_DIR"), "/", $dir)
   };
 }
+
+#[cfg(test)]
+mod tests {
+  #[test]
+  fn test_include_js() {
+    let files = include_js_files!(prefix "00_infra.js", "01_core.js",);
+    assert_eq!("ext:prefix/00_infra.js", files[0].specifier);
+    assert_eq!("ext:prefix/01_core.js", files[1].specifier);
+
+    let files = include_js_files!(prefix dir ".", "00_infra.js", "01_core.js",);
+    assert_eq!("ext:prefix/00_infra.js", files[0].specifier);
+    assert_eq!("ext:prefix/01_core.js", files[1].specifier);
+
+    let files = include_js_files!(prefix
+      "a" = { source = "b" }
+    );
+    assert_eq!("a", files[0].specifier);
+  }
+}
diff --git a/core/modules/loaders.rs b/core/modules/loaders.rs
index 6686fa3..64a23fe 100644
--- a/core/modules/loaders.rs
+++ b/core/modules/loaders.rs
@@ -13,12 +13,12 @@ use crate::Extension;
 use crate::ModuleSourceCode;
 
 use anyhow::anyhow;
+use anyhow::bail;
 use anyhow::Context;
 use anyhow::Error;
 use futures::future::FutureExt;
 use std::cell::RefCell;
 use std::collections::HashMap;
-use std::collections::HashSet;
 use std::future::Future;
 use std::pin::Pin;
 use std::rc::Rc;
@@ -122,23 +122,41 @@ pub type ExtModuleLoaderCb =
   Box<dyn Fn(&ExtensionFileSource) -> Result<ModuleCodeString, Error>>;
 
 pub(crate) struct ExtModuleLoader {
-  sources: RefCell<HashMap<String, ExtensionFileSource>>,
-  used_specifiers: RefCell<HashSet<String>>,
+  sources: RefCell<HashMap<&'static str, ModuleCodeString>>,
 }
 
 impl ExtModuleLoader {
-  pub fn new(extensions: &[Extension]) -> Self {
-    let mut sources = HashMap::new();
-    sources.extend(
-      extensions
-        .iter()
-        .flat_map(|e| e.get_esm_sources())
-        .map(|s| (s.specifier.to_string(), s.clone())),
-    );
-    ExtModuleLoader {
+  pub fn new(extensions: &'_ [Extension]) -> Result<Self, Error> {
+    // Guesstimate a length
+    let mut sources = HashMap::with_capacity(extensions.len() * 3);
+    for extension in extensions {
+      for esm in extension.get_esm_sources() {
+        let source = esm.load()?;
+        sources.insert(esm.specifier, source);
+      }
+    }
+    Ok(ExtModuleLoader {
       sources: RefCell::new(sources),
-      used_specifiers: Default::default(),
+    })
+  }
+
+  pub fn finalize(self) -> Result<(), Error> {
+    let sources = self.sources.take();
+    let unused_modules: Vec<_> = sources.iter().collect();
+
+    if !unused_modules.is_empty() {
+      let mut msg =
+        "Following modules were passed to ExtModuleLoader but never used:\n"
+          .to_string();
+      for m in unused_modules {
+        msg.push_str("  - ");
+        msg.push_str(m.0);
+        msg.push('\n');
+      }
+      bail!(msg);
     }
+
+    Ok(())
   }
 }
 
@@ -159,23 +177,16 @@ impl ModuleLoader for ExtModuleLoader {
     _is_dyn_import: bool,
     _requested_module_type: RequestedModuleType,
   ) -> ModuleLoadResponse {
-    let sources = self.sources.borrow();
-    let source = match sources.get(specifier.as_str()) {
+    let mut sources = self.sources.borrow_mut();
+    let source = match sources.remove(specifier.as_str()) {
       Some(source) => source,
       None => return ModuleLoadResponse::Sync(Err(anyhow!("Specifier \"{}\" was not passed as an extension module and was not included in the snapshot.", specifier))),
     };
-    self
-      .used_specifiers
-      .borrow_mut()
-      .insert(specifier.to_string());
-    let result = source.load().map(|code| {
-      ModuleSource::new(
-        ModuleType::JavaScript,
-        ModuleSourceCode::String(code),
-        specifier,
-      )
-    });
-    ModuleLoadResponse::Sync(result)
+    ModuleLoadResponse::Sync(Ok(ModuleSource::new(
+      ModuleType::JavaScript,
+      ModuleSourceCode::String(source),
+      specifier,
+    )))
   }
 
   fn prepare_load(
@@ -245,29 +256,6 @@ impl ModuleLoader for LazyEsmModuleLoader {
   }
 }
 
-impl Drop for ExtModuleLoader {
-  fn drop(&mut self) {
-    let sources = self.sources.get_mut();
-    let used_specifiers = self.used_specifiers.get_mut();
-    let unused_modules: Vec<_> = sources
-      .iter()
-      .filter(|(k, _)| !used_specifiers.contains(k.as_str()))
-      .collect();
-
-    if !unused_modules.is_empty() {
-      let mut msg =
-        "Following modules were passed to ExtModuleLoader but never used:\n"
-          .to_string();
-      for m in unused_modules {
-        msg.push_str("  - ");
-        msg.push_str(m.0);
-        msg.push('\n');
-      }
-      panic!("{}", msg);
-    }
-  }
-}
-
 /// Basic file system module loader.
 ///
 /// Note that this loader will **block** event loop
diff --git a/core/runtime/bindings.rs b/core/runtime/bindings.rs
index 91836ce..d92661a 100644
--- a/core/runtime/bindings.rs
+++ b/core/runtime/bindings.rs
@@ -244,7 +244,7 @@ pub(crate) fn initialize_deno_core_namespace<'s>(
 pub(crate) fn initialize_primordials_and_infra(
   scope: &mut v8::HandleScope,
 ) -> Result<(), AnyError> {
-  for file_source in CONTEXT_SETUP_SOURCES {
+  for file_source in &CONTEXT_SETUP_SOURCES {
     let code = file_source.load().unwrap();
     let source_str = code.v8_string(scope).unwrap();
     let name = v8_static_strings::new_from_static_str(
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index fdee5e0..6dbf31a 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -271,7 +271,7 @@ const VIRTUAL_OPS_MODULE_NAME: &str = "ext:core/ops";
 
 /// These files are executed just after a new context is created. They provided
 /// the necessary infrastructure to bind ops.
-pub(crate) const CONTEXT_SETUP_SOURCES: [ExtensionFileSource; 2] = include_js_files!(
+pub(crate) static CONTEXT_SETUP_SOURCES: [ExtensionFileSource; 2] = include_js_files!(
   core
   "00_primordials.js",
   "00_infra.js",
@@ -279,14 +279,14 @@ pub(crate) const CONTEXT_SETUP_SOURCES: [ExtensionFileSource; 2] = include_js_fi
 
 /// These files are executed when we start setting up extensions. They rely
 /// on ops being already fully set up.
-pub(crate) const BUILTIN_SOURCES: [ExtensionFileSource; 2] = include_js_files!(
+pub(crate) static BUILTIN_SOURCES: [ExtensionFileSource; 2] = include_js_files!(
   core
   "01_core.js",
   "02_error.js",
 );
 /// Executed after `BUILTIN_SOURCES` are executed. Provides a thin ES module
 /// that exports `core`, `internals` and `primordials` objects.
-pub(crate) const BUILTIN_ES_MODULES: [ExtensionFileSource; 1] =
+pub(crate) static BUILTIN_ES_MODULES: [ExtensionFileSource; 1] =
   include_js_files!(core "mod.js",);
 
 /// We have `ext:core/ops` and `ext:core/mod.js` that are always provided.
@@ -1076,8 +1076,8 @@ impl JsRuntime {
     // TODO(bartlomieju): maybe this should be a method on the `ModuleMap`,
     // instead of explicitly changing the `.loader` field?
     let loader = module_map.loader.borrow().clone();
-    let ext_loader = Rc::new(ExtModuleLoader::new(&extensions));
-    *module_map.loader.borrow_mut() = ext_loader;
+    let ext_loader = Rc::new(ExtModuleLoader::new(&extensions)?);
+    *module_map.loader.borrow_mut() = ext_loader.clone();
 
     futures::executor::block_on(self.init_extension_js_inner(
       realm,
@@ -1094,6 +1094,10 @@ impl JsRuntime {
 
     let module_map = realm.0.module_map();
     *module_map.loader.borrow_mut() = loader;
+    Rc::try_unwrap(ext_loader)
+      .map_err(drop)
+      .unwrap()
+      .finalize()?;
 
     Ok(())
   }
@@ -2203,6 +2207,7 @@ fn mark_as_loaded_from_fs_during_snapshot(
   files_loaded: &mut Vec<&'static str>,
   source: &ExtensionFileSourceCode,
 ) {
+  #[allow(deprecated)]
   if let ExtensionFileSourceCode::LoadedFromFsDuringSnapshot(path) = source {
     files_loaded.push(path);
   }


Test patch:
diff --git a/core/runtime/tests/jsrealm.rs b/core/runtime/tests/jsrealm.rs
index 3df62a7..f44fbe3 100644
--- a/core/runtime/tests/jsrealm.rs
+++ b/core/runtime/tests/jsrealm.rs
@@ -2,15 +2,11 @@
 use crate::error;
 use crate::modules::StaticModuleLoader;
 use crate::op2;
-use crate::Extension;
-use crate::ExtensionFileSource;
-use crate::ExtensionFileSourceCode;
 use crate::JsRuntime;
 use crate::JsRuntimeForSnapshot;
 use crate::RuntimeOptions;
 use crate::Snapshot;
 use futures::future::poll_fn;
-use std::borrow::Cow;
 use std::rc::Rc;
 use std::task::Poll;
 
@@ -132,20 +128,15 @@ async fn js_realm_ref_unref_ops() {
 #[test]
 fn es_snapshot() {
   let startup_data = {
-    let extension = Extension {
-      name: "module_snapshot",
-      esm_files: Cow::Borrowed(&[ExtensionFileSource {
-        specifier: "mod:test",
-        code: ExtensionFileSourceCode::IncludedInBinary(
-          "globalThis.TEST = 'foo'; export const TEST = 'bar';",
-        ),
-      }]),
-      esm_entry_point: Some("mod:test"),
-      ..Default::default()
-    };
+    deno_core::extension!(
+      module_snapshot,
+      esm_entry_point = "mod:test",
+      esm = ["mod:test" =
+        { source = "globalThis.TEST = 'foo'; export const TEST = 'bar';" },]
+    );
 
     let runtime = JsRuntimeForSnapshot::new(RuntimeOptions {
-      extensions: vec![extension],
+      extensions: vec![module_snapshot::init_ops_and_esm()],
       module_loader: Some(Rc::new(StaticModuleLoader::new([]))),
       ..Default::default()
     });
diff --git a/core/runtime/tests/misc.rs b/core/runtime/tests/misc.rs
index 36e453d..feb49b8 100644
--- a/core/runtime/tests/misc.rs
+++ b/core/runtime/tests/misc.rs
@@ -11,7 +11,6 @@ use cooked_waker::Wake;
 use cooked_waker::WakeRef;
 use futures::future::poll_fn;
 use rstest::rstest;
-use std::borrow::Cow;
 use std::rc::Rc;
 use std::sync::atomic::AtomicBool;
 use std::sync::atomic::AtomicI8;
@@ -1014,7 +1013,7 @@ async fn test_dynamic_import_module_error_stack() {
 
 Caused by:
     Top-level await promise never resolved
-        at mod:tla:3:13"#
+        at mod:tla:3:11"#
 )]
 async fn tla_in_esm_extensions_panics() {
   #[op2(async)]
@@ -1022,33 +1021,26 @@ async fn tla_in_esm_extensions_panics() {
     tokio::time::sleep(Duration::from_millis(ms as u64)).await
   }
 
-  let extension = Extension {
-    name: "test_ext",
-    ops: Cow::Borrowed(&[op_wait::DECL]),
-    esm_files: Cow::Borrowed(&[
-      ExtensionFileSource {
-        specifier: "mod:tla",
-        code: ExtensionFileSourceCode::IncludedInBinary(
-          r#"
-            const { op_wait } = Deno.core.ensureFastOps();
-            await op_wait(0);
-            export const TEST = "foo";
-        "#,
-        ),
-      },
-      ExtensionFileSource {
-        specifier: "mod:test",
-        code: ExtensionFileSourceCode::IncludedInBinary("import 'mod:tla';"),
-      },
-    ]),
-    esm_entry_point: Some("mod:test"),
-    ..Default::default()
-  };
+  deno_core::extension!(
+    test_ext,
+    ops = [op_wait],
+    esm_entry_point = "mod:test",
+    esm = [
+      "mod:test" = { source = "import 'mod:tla';" },
+      "mod:tla" = {
+        source = r#"
+          const { op_wait } = Deno.core.ensureFastOps();
+          await op_wait(0);
+          export const TEST = "foo";
+      "#
+      }
+    ],
+  );
 
   // Panics
   let _runtime = JsRuntime::new(RuntimeOptions {
     module_loader: Some(Rc::new(StaticModuleLoader::new([]))),
-    extensions: vec![extension],
+    extensions: vec![test_ext::init_ops_and_esm()],
     ..Default::default()
   });
 }
@@ -1058,8 +1050,8 @@ async fn tla_in_esm_extensions_panics() {
 #[tokio::test]
 #[should_panic(
   expected = r#"Failed to initialize a JsRuntime: Error: This fails
-    at a (mod:error:2:34)
-    at mod:error:3:13"#
+    at a (mod:error:2:30)
+    at mod:error:3:9"#
 )]
 async fn esm_extensions_throws() {
   #[op2(async)]
@@ -1067,32 +1059,25 @@ async fn esm_extensions_throws() {
     tokio::time::sleep(Duration::from_millis(ms as u64)).await
   }
 
-  let extension = Extension {
-    name: "test_ext",
-    ops: Cow::Borrowed(&[op_wait::DECL]),
-    esm_files: Cow::Borrowed(&[
-      ExtensionFileSource {
-        specifier: "mod:error",
-        code: ExtensionFileSourceCode::IncludedInBinary(
-          r#"
-            function a() { throw new Error("This fails") };
-            a();
-          "#,
-        ),
-      },
-      ExtensionFileSource {
-        specifier: "mod:test",
-        code: ExtensionFileSourceCode::IncludedInBinary("import 'mod:error';"),
-      },
-    ]),
-    esm_entry_point: Some("mod:test"),
-    ..Default::default()
-  };
+  deno_core::extension!(
+    test_ext,
+    ops = [op_wait],
+    esm_entry_point = "mod:test",
+    esm = [
+      "mod:test" = { source = "import 'mod:error';" },
+      "mod:error" = {
+        source = r#"
+        function a() { throw new Error("This fails") };
+        a();
+      "#
+      }
+    ],
+  );
 
   // Panics
   let _runtime = JsRuntime::new(RuntimeOptions {
     module_loader: Some(Rc::new(StaticModuleLoader::new([]))),
-    extensions: vec![extension],
+    extensions: vec![test_ext::init_ops_and_esm()],
     ..Default::default()
   });
 }
diff --git a/core/runtime/tests/snapshot.rs b/core/runtime/tests/snapshot.rs
index 32676eb..4ed0e62 100644
--- a/core/runtime/tests/snapshot.rs
+++ b/core/runtime/tests/snapshot.rs
@@ -256,20 +256,15 @@ fn es_snapshot() {
 #[test]
 pub(crate) fn es_snapshot_without_runtime_module_loader() {
   let startup_data = {
-    let extension = Extension {
-      name: "module_snapshot",
-      esm_files: Cow::Borrowed(&[ExtensionFileSource {
-        specifier: "ext:module_snapshot/test.js",
-        code: ExtensionFileSourceCode::IncludedInBinary(
-          "globalThis.TEST = 'foo'; export const TEST = 'bar';",
-        ),
-      }]),
-      esm_entry_point: Some("ext:module_snapshot/test.js"),
-      ..Default::default()
-    };
+    deno_core::extension!(
+      module_snapshot,
+      esm_entry_point = "ext:module_snapshot/test.js",
+      esm = ["ext:module_snapshot/test.js" =
+        { source = "globalThis.TEST = 'foo'; export const TEST = 'bar';" },]
+    );
 
     let runtime = JsRuntimeForSnapshot::new(RuntimeOptions {
-      extensions: vec![extension],
+      extensions: vec![module_snapshot::init_ops_and_esm()],
       ..Default::default()
     });
 
diff --git a/testing/checkin/runner/mod.rs b/testing/checkin/runner/mod.rs
index 68b12e0..03fc2d1 100644
--- a/testing/checkin/runner/mod.rs
+++ b/testing/checkin/runner/mod.rs
@@ -69,12 +69,12 @@ deno_core::extension!(
     dir "checkin/runtime",
     "__bootstrap.js",
     "__init.js",
-    "async.ts" with_specifier "checkin:async",
-    "console.ts" with_specifier "checkin:console",
-    "error.ts" with_specifier "checkin:error",
-    "testing.ts" with_specifier "checkin:testing",
-    "timers.ts" with_specifier "checkin:timers",
-    "worker.ts" with_specifier "checkin:worker",
+    "checkin:async" = "async.ts",
+    "checkin:console" = "console.ts",
+    "checkin:error" = "error.ts",
+    "checkin:testing" = "testing.ts",
+    "checkin:timers" = "timers.ts",
+    "checkin:worker" = "worker.ts",
   ],
   state = |state| {
     state.put(TestFunctions::default());