Repository: denoland/deno_core
Pull request #354 (2023-11-26 22:45:34)

feat: Add RuntimeOptions::import_meta_resolve_callback
This commits adds "RuntimeOptions::import_meta_resolve_callback" option
that allows to customize behavior of `import.meta.resolve` API.

Required for https://github.com/denoland/deno/issues/21298.


Patch:
diff --git a/core/modules/map.rs b/core/modules/map.rs
index 8769cdb..a64361e 100644
--- a/core/modules/map.rs
+++ b/core/modules/map.rs
@@ -40,8 +40,10 @@ use std::task::Poll;
 type PrepareLoadFuture =
   dyn Future<Output = (ModuleLoadId, Result<RecursiveModuleLoad, Error>)>;
 
+use super::default_import_meta_resolve_cb;
 use super::module_map_data::ModuleMapData;
 use super::AssertedModuleType;
+use super::ImportMetaResolveCallback;
 
 pub const BOM_CHAR: &[u8] = &[0xef, 0xbb, 0xbf];
 
@@ -66,6 +68,7 @@ pub(crate) struct ModuleMap {
   // Handling of futures for loading module sources
   // TODO(mmastrac): we should not be swapping this loader out
   pub(crate) loader: RefCell<Rc<dyn ModuleLoader>>,
+  pub(crate) import_meta_resolve_cb: ImportMetaResolveCallback,
 
   dynamic_import_map:
     RefCell<HashMap<ModuleLoadId, v8::Global<v8::PromiseResolver>>>,
@@ -117,9 +120,13 @@ impl ModuleMap {
     }
   }
 
-  pub(crate) fn new(loader: Rc<dyn ModuleLoader>) -> Self {
+  pub(crate) fn new(
+    loader: Rc<dyn ModuleLoader>,
+    import_meta_resolve_cb: ImportMetaResolveCallback,
+  ) -> Self {
     Self {
       loader: loader.into(),
+      import_meta_resolve_cb,
       dyn_module_evaluate_idle_counter: Default::default(),
       dynamic_import_map: Default::default(),
       preparing_dynamic_imports: Default::default(),
@@ -134,10 +141,11 @@ impl ModuleMap {
 
   pub(crate) fn new_from_snapshotted_data(
     loader: Rc<dyn ModuleLoader>,
+    import_meta_resolve_cb: ImportMetaResolveCallback,
     scope: &mut v8::HandleScope,
     data: SnapshottedData,
   ) -> Self {
-    let new = Self::new(loader);
+    let new = Self::new(loader, import_meta_resolve_cb);
     new
       .data
       .borrow_mut()
@@ -1068,7 +1076,10 @@ impl ModuleMap {
 
 impl Default for ModuleMap {
   fn default() -> Self {
-    Self::new(Rc::new(NoopModuleLoader))
+    Self::new(
+      Rc::new(NoopModuleLoader),
+      Box::new(default_import_meta_resolve_cb),
+    )
   }
 }
 
diff --git a/core/modules/mod.rs b/core/modules/mod.rs
index 7c97639..82a0da2 100644
--- a/core/modules/mod.rs
+++ b/core/modules/mod.rs
@@ -1,6 +1,7 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
 use crate::fast_string::FastString;
 use crate::module_specifier::ModuleSpecifier;
+use anyhow::bail;
 use anyhow::Error;
 use serde::Deserialize;
 use serde::Serialize;
@@ -36,6 +37,23 @@ pub(crate) type ModuleLoadId = i32;
 pub type ModuleCode = FastString;
 pub type ModuleName = FastString;
 
+/// Callback to customize value of `import.meta.resolve("./foo.ts")`.
+pub type ImportMetaResolveCallback = Box<
+  dyn Fn(&dyn ModuleLoader, String, String) -> Result<ModuleSpecifier, Error>,
+>;
+
+pub(crate) fn default_import_meta_resolve_cb(
+  loader: &dyn ModuleLoader,
+  specifier: String,
+  referrer: String,
+) -> Result<ModuleSpecifier, Error> {
+  if specifier.starts_with("npm:") {
+    bail!("\"npm:\" specifiers are currently not supported in import.meta.resolve()");
+  }
+
+  loader.resolve(&specifier, &referrer, ResolutionKind::DynamicImport)
+}
+
 /// Callback to validate import attributes. If the validation fails and exception
 /// should be thrown using `scope.throw_exception()`.
 pub type ValidateImportAttributesCb =
diff --git a/core/runtime/bindings.rs b/core/runtime/bindings.rs
index be97892..25d3bf9 100644
--- a/core/runtime/bindings.rs
+++ b/core/runtime/bindings.rs
@@ -13,7 +13,6 @@ use crate::modules::get_asserted_module_type_from_assertions;
 use crate::modules::parse_import_assertions;
 use crate::modules::ImportAssertionsKind;
 use crate::modules::ModuleMap;
-use crate::modules::ResolutionKind;
 use crate::ops::OpCtx;
 use crate::runtime::InitMode;
 use crate::runtime::JsRealm;
@@ -389,16 +388,13 @@ fn import_meta_resolve(
   let loader = module_map_rc.loader.clone();
   let specifier_str = specifier.to_rust_string_lossy(scope);
 
-  if specifier_str.starts_with("npm:") {
-    throw_type_error(scope, "\"npm:\" specifiers are currently not supported in import.meta.resolve()");
-    return;
-  }
+  let import_meta_resolve_result = {
+    let loader = loader.borrow();
+    let loader = loader.as_ref();
+    (module_map_rc.import_meta_resolve_cb)(loader, specifier_str, referrer)
+  };
 
-  match loader.borrow().resolve(
-    &specifier_str,
-    &referrer,
-    ResolutionKind::DynamicImport,
-  ) {
+  match import_meta_resolve_result {
     Ok(resolved) => {
       let resolved_val = serde_v8::to_v8(scope, resolved.as_str()).unwrap();
       rv.set(resolved_val);
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index b6f5b24..7440f58 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -14,8 +14,10 @@ use crate::extensions::OpDecl;
 use crate::include_js_files;
 use crate::inspector::JsRuntimeInspector;
 use crate::module_specifier::ModuleSpecifier;
+use crate::modules::default_import_meta_resolve_cb;
 use crate::modules::AssertedModuleType;
 use crate::modules::ExtModuleLoader;
+use crate::modules::ImportMetaResolveCallback;
 use crate::modules::ModuleCode;
 use crate::modules::ModuleId;
 use crate::modules::ModuleLoader;
@@ -444,9 +446,15 @@ pub struct RuntimeOptions {
   pub feature_checker: Option<Arc<FeatureChecker>>,
 
   /// A callback that can be used to validate import attributes received at
-  /// the import site. If not callback is provided, a default one is used. The
+  /// the import site. If no callback is provided, a default one is used. The
   /// default callback only allows `"type"` attribute, with a value of `"json"`.
   pub validate_import_attributes_cb: Option<ValidateImportAttributesCb>,
+
+  /// A callback that can be used to customize behavior of
+  /// `import.meta.resolve()` API. If no callback is provided, a default one
+  /// is used. The default callback returns value of
+  /// `RuntimeOptions::module_loader::resolve()` call.
+  pub import_meta_resolve_callback: Option<ImportMetaResolveCallback>,
 }
 
 impl RuntimeOptions {
@@ -743,15 +751,19 @@ impl JsRuntime {
     let loader = options
       .module_loader
       .unwrap_or_else(|| Rc::new(NoopModuleLoader));
+    let import_meta_resolve_cb = options
+      .import_meta_resolve_callback
+      .unwrap_or_else(|| Box::new(default_import_meta_resolve_cb));
 
     let module_map = if let Some(snapshotted_data) = snapshotted_data {
       Rc::new(ModuleMap::new_from_snapshotted_data(
         loader,
+        import_meta_resolve_cb,
         scope,
         snapshotted_data,
       ))
     } else {
-      Rc::new(ModuleMap::new(loader))
+      Rc::new(ModuleMap::new(loader, import_meta_resolve_cb))
     };
 
     context.set_slot(scope, module_map.clone());


Test patch:
diff --git a/core/modules/tests.rs b/core/modules/tests.rs
index f1a0f36..ec9d9c2 100644
--- a/core/modules/tests.rs
+++ b/core/modules/tests.rs
@@ -20,6 +20,7 @@ use crate::ModuleType;
 use crate::ResolutionKind;
 use crate::RuntimeOptions;
 use crate::Snapshot;
+use anyhow::bail;
 use anyhow::Error;
 use deno_ops::op2;
 use futures::future::poll_fn;
@@ -1327,3 +1328,47 @@ async fn no_duplicate_loads() {
   let _ = runtime.mod_evaluate(a_id);
   runtime.run_event_loop(false).await.unwrap();
 }
+
+#[test]
+fn import_meta_resolve_cb() {
+  fn import_meta_resolve_cb(
+    _loader: &dyn ModuleLoader,
+    specifier: String,
+    _referrer: String,
+  ) -> Result<ModuleSpecifier, Error> {
+    if specifier == "foo" {
+      return Ok(ModuleSpecifier::parse("foo:bar").unwrap());
+    }
+
+    if specifier == "./mod.js" {
+      return Ok(ModuleSpecifier::parse("file:///mod.js").unwrap());
+    }
+
+    bail!("unexpected")
+  }
+
+  let mut runtime = JsRuntime::new(RuntimeOptions {
+    import_meta_resolve_callback: Some(Box::new(import_meta_resolve_cb)),
+    ..Default::default()
+  });
+
+  let spec = ModuleSpecifier::parse("file:///test.js").unwrap();
+  let source = r#"
+  if (import.meta.resolve("foo") !== "foo") throw new Error();
+  if (import.meta.resolve("./mod.js") !== "file:///mod.js") throw new Error();
+  let caught = false;
+  try {
+    import.meta.resolve("boom!");
+  } catch (e) {
+    if (!(e instanceof TypeError)) throw new Error();
+  }
+  if (!caught) throw new Error();
+  "#
+  .to_string();
+  let a_id_fut = runtime.load_main_module(&spec, Some(source.into()));
+  let a_id = futures::executor::block_on(a_id_fut).unwrap();
+
+  #[allow(clippy::let_underscore_future)]
+  let _ = runtime.mod_evaluate(a_id);
+  futures::executor::block_on(runtime.run_event_loop(false)).unwrap();
+}