// Copyright 2018-2026 the Deno authors. MIT license.

use std::borrow::Cow;
use std::cell::Cell;
use std::cell::RefCell;
use std::collections::HashMap;
use std::future::Future;
use std::ops::DerefMut;
use std::pin::Pin;
use std::rc::Rc;
use std::task::Context;
use std::task::Poll;

use capacity_builder::StringBuilder;
use deno_core::FastString;
use deno_core::error::CoreError;
use deno_error::JsErrorBox;
use futures::StreamExt;
use futures::future::Either;
use futures::future::FutureExt;
use futures::stream::FuturesUnordered;
use futures::stream::StreamFuture;
use futures::task::AtomicWaker;
use indexmap::IndexMap;
use sourcemap::DecodedMap;
use tokio::sync::oneshot;
use v8::Function;
use v8::PromiseState;
use wasm_dep_analyzer::WasmDeps;

use super::CustomModuleEvaluationKind;
use super::ImportAttributesContext;
use super::IntoModuleCodeString;
use super::IntoModuleName;
use super::LazyEsmModuleLoader;
use super::ModuleConcreteError;
use super::RequestedModuleType;
use super::loaders::ModuleLoadOptions;
use super::module_map_data::ModuleMapData;
use super::module_map_data::ModuleMapSnapshotData;
use crate::FastStaticString;
use crate::JsRuntime;
use crate::ModuleCodeBytes;
use crate::ModuleLoadResponse;
use crate::ModuleResolveResponse;
use crate::ModuleSource;
use crate::ModuleSourceCode;
use crate::ModuleSpecifier;
use crate::ascii_str;
use crate::error::CoreErrorKind;
use crate::error::JsError;
use crate::error::exception_to_err;
use crate::error::exception_to_err_result;
use crate::modules::ImportAttributesKind;
use crate::modules::ModuleCodeString;
use crate::modules::ModuleError;
use crate::modules::ModuleId;
use crate::modules::ModuleImportPhase;
use crate::modules::ModuleLoadId;
use crate::modules::ModuleLoader;
use crate::modules::ModuleName;
use crate::modules::ModuleReference;
use crate::modules::ModuleRequest;
use crate::modules::ModuleType;
use crate::modules::ResolutionKind;
use crate::modules::get_requested_module_type_from_attributes;
use crate::modules::module_map_data::ModuleSourceKey;
use crate::modules::parse_import_attributes;
use crate::modules::recursive_load::RecursiveModuleLoad;
use crate::modules::recursive_load::RegisterOutcome;
use crate::runtime::JsRealm;
use crate::runtime::SnapshotLoadDataStore;
use crate::runtime::SnapshotStoreDataStore;
use crate::runtime::exception_state::ExceptionState;
use crate::source_map::SourceMapper;

const DATA_PREFIX: &str = "data:";

type PrepareLoadFuture =
  dyn Future<Output = (ModuleLoadId, Result<RecursiveModuleLoad, CoreError>)>;

type CodeCacheReadyFuture = dyn Future<Output = ()>;

struct ModEvaluate {
  module_map: Rc<ModuleMap>,
  sender: Option<oneshot::Sender<Result<(), Box<JsError>>>>,
  module: Option<v8::Global<v8::Module>>,
  notify: Vec<v8::Global<v8::Function>>,
}

impl ModEvaluate {
  fn notify(&mut self, scope: &mut v8::PinScope) {
    if !self.notify.is_empty() {
      let module = v8::Local::new(scope, self.module.take().unwrap());
      let ns = module.get_module_namespace();
      let recv = v8::undefined(scope).into();
      let args = &[ns];
      for notify in std::mem::take(&mut self.notify).into_iter() {
        let notify = v8::Local::new(scope, notify);
        notify.call(scope, recv, args);
      }
    }
    _ = self.sender.take().unwrap().send(Ok(()));
  }
}

type CodeCacheReadyCallback =
  Box<dyn FnOnce(&[u8]) -> Pin<Box<dyn Future<Output = ()>>>>;
pub(crate) struct CodeCacheInfo {
  data: Option<Cow<'static, [u8]>>,
  ready_callback: CodeCacheReadyCallback,
}

pub const BOM_CHAR: &[u8] = &[0xef, 0xbb, 0xbf];

/// Strips the byte order mark from the provided text if it exists.
fn strip_bom(source_code: &[u8]) -> &[u8] {
  if source_code.starts_with(BOM_CHAR) {
    &source_code[BOM_CHAR.len()..]
  } else {
    source_code
  }
}

/// A `FuturesUnordered` paired with a `Cell<bool>` flag that tracks whether
/// the collection has pending items. The flag avoids borrowing the `RefCell`
/// just to check `is_empty()`.
struct TrackedFutures<F> {
  futs: RefCell<FuturesUnordered<F>>,
  pending: Cell<bool>,
}

impl<F> Default for TrackedFutures<F> {
  fn default() -> Self {
    Self {
      futs: Default::default(),
      pending: Cell::new(false),
    }
  }
}

impl<F: Future + Unpin> TrackedFutures<F> {
  fn is_pending(&self) -> bool {
    self.pending.get()
  }

  fn push(&self, fut: F) {
    self.futs.borrow_mut().push(fut);
    self.pending.set(true);
  }

  /// Polls the inner `FuturesUnordered`. When the result is not
  /// `Ready(Some(_))` (i.e., no more ready items), the pending flag is
  /// synced from the collection's emptiness.
  fn poll_next_unpin(&self, cx: &mut Context) -> Poll<Option<F::Output>> {
    let poll = self.futs.borrow_mut().poll_next_unpin(cx);
    if !matches!(poll, Poll::Ready(Some(_))) {
      self.pending.set(!self.futs.borrow().is_empty());
    }
    poll
  }

  fn clear(&self) {
    self.futs.borrow_mut().clear();
    self.pending.set(false);
  }
}

/// A `Vec<T>` paired with a `Cell<bool>` flag that tracks whether the
/// collection has pending items.
struct TrackedVec<T> {
  vec: RefCell<Vec<T>>,
  pending: Cell<bool>,
}

impl<T> Default for TrackedVec<T> {
  fn default() -> Self {
    Self {
      vec: RefCell::new(Vec::new()),
      pending: Cell::new(false),
    }
  }
}

impl<T> TrackedVec<T> {
  fn is_pending(&self) -> bool {
    self.pending.get()
  }

  fn push(&self, item: T) {
    self.vec.borrow_mut().push(item);
    self.pending.set(true);
  }

  fn take(&self) -> Vec<T> {
    let v = std::mem::take(self.vec.borrow_mut().deref_mut());
    self.pending.set(false);
    v
  }

  fn set(&self, items: Vec<T>) {
    self.pending.set(!items.is_empty());
    *self.vec.borrow_mut() = items;
  }

  fn borrow(&self) -> std::cell::Ref<'_, Vec<T>> {
    self.vec.borrow()
  }

  fn clear(&self) {
    self.vec.borrow_mut().clear();
    self.pending.set(false);
  }
}

struct DynImportModEvaluate {
  load_id: ModuleLoadId,
  module_id: ModuleId,
  promise: v8::Global<v8::Promise>,
}

#[derive(Debug, Clone)]
struct DynImportState {
  resolver: v8::Global<v8::PromiseResolver>,
  cped: v8::Global<v8::Value>,
  phase: ModuleImportPhase,
}

/// A collection of JS modules.
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) source_mapper: Rc<RefCell<SourceMapper>>,
  exception_state: Rc<ExceptionState>,
  dynamic_import_map: RefCell<HashMap<ModuleLoadId, DynImportState>>,
  preparing_dynamic_imports: TrackedFutures<Pin<Box<PrepareLoadFuture>>>,
  pending_dynamic_imports: TrackedFutures<StreamFuture<RecursiveModuleLoad>>,
  pending_dyn_mod_evaluations: TrackedVec<DynImportModEvaluate>,
  pending_tla_waiters:
    RefCell<HashMap<ModuleId, Vec<v8::Global<v8::PromiseResolver>>>>,
  pending_mod_evaluation: Cell<bool>,
  /// Set to `true` while inside `module.evaluate()` in `mod_evaluate`.
  /// Used to suppress microtask checkpoints in `lazy_load_es_module_with_code`
  /// during module evaluation, preventing premature draining of TLA-related microtasks.
  evaluating_top_level: Cell<bool>,
  code_cache_ready_futs: TrackedFutures<Pin<Box<CodeCacheReadyFuture>>>,
  module_waker: AtomicWaker,
  data: RefCell<ModuleMapData>,
  will_snapshot: bool,

  /// A counter used to delay our dynamic import deadlock detection by one spin
  /// of the event loop.
  pub(crate) dyn_module_evaluate_idle_counter: Cell<u32>,

  /// Tracks module IDs currently being evaluated via `op_import_sync` to
  /// detect require() cycles that V8's module status alone cannot catch.
  pub(crate) import_sync_eval_stack: RefCell<Vec<ModuleId>>,
}

/// Outcome of compiling a module's source.
pub(crate) enum NewModuleResult {
  Ready(ModuleId),
}

impl NewModuleResult {
  fn into_ready(self) -> ModuleId {
    match self {
      NewModuleResult::Ready(id) => id,
    }
  }
}

impl ModuleMap {
  /// There is a circular Rc reference between the module map and the futures,
  /// so when destroying the module map we need to clear the pending futures.
  pub(crate) fn destroy(&self) {
    self.dynamic_import_map.borrow_mut().clear();
    self.preparing_dynamic_imports.clear();
    self.pending_dynamic_imports.clear();
    self.pending_dyn_mod_evaluations.clear();
    self.pending_tla_waiters.borrow_mut().clear();
    self.code_cache_ready_futs.clear();
    std::mem::take(&mut *self.data.borrow_mut());
  }

  pub(crate) fn next_load_id(&self) -> i32 {
    // TODO(mmastrac): move recursive module loading into here so we can avoid making this pub
    let mut data = self.data.borrow_mut();
    let id = data.next_load_id;
    data.next_load_id += 1;
    id + 1
  }

  #[cfg(debug_assertions)]
  pub(crate) fn check_all_modules_evaluated(
    &self,
    scope: &mut v8::PinScope,
  ) -> Result<(), CoreError> {
    let mut not_evaluated = vec![];
    let data = self.data.borrow();

    for (handle, i) in data.handles_inverted.iter() {
      let module = v8::Local::new(scope, handle);
      match module.get_status() {
        v8::ModuleStatus::Errored => {
          return Err(
            CoreErrorKind::Js(JsError::from_v8_exception(
              scope,
              module.get_exception(),
            ))
            .into_box(),
          );
        }
        v8::ModuleStatus::Evaluated => {}
        _ => {
          not_evaluated.push(data.info[*i].name.as_str().to_string());
        }
      }
    }

    if !not_evaluated.is_empty() {
      return Err(CoreErrorKind::NonEvaluatedModules(not_evaluated).into_box());
    }

    Ok(())
  }

  pub(crate) fn new(
    loader: Rc<dyn ModuleLoader>,
    source_mapper: Rc<RefCell<SourceMapper>>,
    exception_state: Rc<ExceptionState>,
    will_snapshot: bool,
  ) -> Self {
    Self {
      will_snapshot,
      loader: loader.into(),
      source_mapper,
      exception_state,
      dyn_module_evaluate_idle_counter: Default::default(),
      dynamic_import_map: Default::default(),
      preparing_dynamic_imports: Default::default(),
      pending_dynamic_imports: Default::default(),
      pending_dyn_mod_evaluations: Default::default(),
      pending_tla_waiters: Default::default(),
      pending_mod_evaluation: Default::default(),
      evaluating_top_level: Default::default(),
      code_cache_ready_futs: Default::default(),
      module_waker: Default::default(),
      data: Default::default(),
      import_sync_eval_stack: Default::default(),
    }
  }

  pub(crate) fn update_with_snapshotted_data(
    &self,
    scope: &mut v8::PinScope,
    data_store: &mut SnapshotLoadDataStore,
    data: ModuleMapSnapshotData,
  ) {
    self
      .data
      .borrow_mut()
      .update_with_snapshotted_data(scope, data_store, data);
  }

  /// Get module id, following all aliases in case of module specifier
  /// that had been redirected.
  pub(crate) fn get_id(
    &self,
    name: &str,
    requested_module_type: impl AsRef<RequestedModuleType>,
  ) -> Option<ModuleId> {
    self.data.borrow().get_id(name, requested_module_type)
  }

  /// Register an additional `(name, requested_module_type) -> module_id`
  /// mapping for an already-registered module. See
  /// `ModuleMapData::register_under_type`.
  pub(crate) fn register_under_type(
    &self,
    name: FastString,
    requested_module_type: &RequestedModuleType,
    module_id: ModuleId,
  ) {
    self.data.borrow_mut().register_under_type(
      name,
      requested_module_type,
      module_id,
    );
  }

  pub(crate) fn is_main_module(&self, global: &v8::Global<v8::Module>) -> bool {
    self.data.borrow().is_main_module(global)
  }

  pub(crate) fn is_main_module_id(&self, id: ModuleId) -> bool {
    self.data.borrow().main_module_id == Some(id)
  }

  pub(crate) fn get_name_by_module(
    &self,
    global: &v8::Global<v8::Module>,
  ) -> Option<String> {
    self.data.borrow().get_name_by_module(global)
  }

  pub(crate) fn get_name_by_id(&self, id: ModuleId) -> Option<String> {
    self.data.borrow().get_name_by_id(id)
  }

  pub(crate) fn get_type_by_module(
    &self,
    global: &v8::Global<v8::Module>,
  ) -> Option<ModuleType> {
    self.data.borrow().get_type_by_module(global)
  }

  pub(crate) fn get_handle(
    &self,
    id: ModuleId,
  ) -> Option<v8::Global<v8::Module>> {
    self.data.borrow().get_handle(id)
  }

  pub(crate) fn serialize_for_snapshotting(
    &self,
    data_store: &mut SnapshotStoreDataStore,
  ) -> ModuleMapSnapshotData {
    let data = std::mem::take(&mut *self.data.borrow_mut());
    data.serialize_for_snapshotting(data_store)
  }

  #[cfg(test)]
  pub fn is_alias(
    &self,
    name: &str,
    requested_module_type: impl AsRef<RequestedModuleType>,
  ) -> bool {
    self.data.borrow().is_alias(name, requested_module_type)
  }

  pub(crate) fn get_data(&self) -> &RefCell<ModuleMapData> {
    &self.data
  }

  #[cfg(test)]
  pub fn assert_module_map(&self, modules: &Vec<super::ModuleInfo>) {
    self.data.borrow().assert_module_map(modules);
  }

  #[cfg(all(test, not(miri)))]
  pub(crate) fn new_module(
    &self,
    scope: &mut v8::PinScope,
    main: bool,
    dynamic: bool,
    module_source: ModuleSource,
  ) -> Result<ModuleId, ModuleError> {
    Ok(
      self
        .new_module_with_pending(scope, main, dynamic, module_source)?
        .into_ready(),
    )
  }

  pub(crate) fn new_module_with_pending(
    &self,
    scope: &mut v8::PinScope,
    main: bool,
    dynamic: bool,
    module_source: ModuleSource,
  ) -> Result<NewModuleResult, ModuleError> {
    let ModuleSource {
      code,
      module_type,
      module_url_found,
      module_url_specified,
      code_cache,
    } = module_source;

    // Register the module in the module map unless it's already there. If the
    // specified URL and the "true" URL are different, register the alias.
    let module_url_found = if let Some(module_url_found) = module_url_found {
      let (module_url_found1, module_url_found2) =
        module_url_found.into_cheap_copy();
      self.data.borrow_mut().alias(
        module_url_specified,
        &module_type.clone().into(),
        module_url_found1,
      );
      module_url_found2
    } else {
      module_url_specified
    };

    // TODO(bartlomieju): I have a hunch that this is wrong - write a test
    // that tries to "confuse" the type system, by first requesting a module
    // with type `RequestedModuleType::Other("foo".into)``, and then the loader
    // actually returns `ModuleType::Other("bar".into())`. See if it leads to
    // unexpected result in how `ModuleMap` is structured and verify how
    // querying the module map works (`ModuleMap::get_by_id`, `ModuleMap::get_by_name`).
    let requested_module_type = RequestedModuleType::from(module_type.clone());
    let maybe_module_id = self.get_id(&module_url_found, requested_module_type);

    if let Some(module_id) = maybe_module_id {
      return Ok(NewModuleResult::Ready(module_id));
    }
    let module_id = match module_type {
      ModuleType::JavaScript => {
        let code = ModuleSource::get_string_source(code);

        let (code_cache_info, module_url_found) =
          if let Some(code_cache) = code_cache {
            let (module_url_found1, module_url_found2) =
              module_url_found.into_cheap_copy();
            let loader = self.loader.borrow().clone();
            (
              Some(CodeCacheInfo {
                data: code_cache.data,
                ready_callback: Box::new(move |cache| {
                  let specifier =
                    ModuleSpecifier::parse(module_url_found1.as_str()).unwrap();
                  loader.code_cache_ready(specifier, code_cache.hash, cache)
                }),
              }),
              module_url_found2,
            )
          } else {
            (None, module_url_found)
          };

        self
          .new_module_from_js_source_with_pending(
            scope,
            main,
            ModuleType::JavaScript,
            module_url_found,
            code,
            dynamic,
            code_cache_info,
          )?
          .into_ready()
      }
      ModuleType::Wasm => {
        self.new_wasm_module(scope, module_url_found, code, dynamic)?
      }
      ModuleType::Json => self.new_json_module(
        scope,
        module_url_found,
        ModuleSource::get_string_source(code),
      )?,
      ModuleType::Text => self.new_text_module(
        scope,
        module_url_found,
        ModuleSource::get_string_source(code),
      )?,
      ModuleType::Bytes => {
        let ModuleSourceCode::Bytes(code) = code else {
          return Err(ModuleError::Concrete(
            ModuleConcreteError::BytesNotBytes,
          ));
        };
        self.new_bytes_module(scope, module_url_found, code)?
      }
      ModuleType::Other(module_type) => {
        let state = JsRuntime::state_from(scope);
        let custom_module_evaluation_cb =
          state.custom_module_evaluation_cb.as_ref();

        let Some(custom_evaluation_cb) = custom_module_evaluation_cb else {
          return Err(ModuleError::Concrete(
            ModuleConcreteError::UnsupportedKind(module_type.to_string()),
          ));
        };

        // TODO(bartlomieju): creating a global just to create a local from it
        // seems superfluous. However, changing `CustomModuleEvaluationCb` to have
        // a lifetime will have a viral effect and required `JsRuntimeOptions`
        // to have a callback as well as `JsRuntime`.
        let module_evaluation_kind = custom_evaluation_cb(
          scope,
          module_type.clone(),
          &module_url_found,
          code,
        )
        .map_err(|e| ModuleError::Core(e.into()))?;

        match module_evaluation_kind {
          // Simple case, we just got a single value so we create a regular
          // synthetic module.
          CustomModuleEvaluationKind::Synthetic(value_global) => {
            let value = v8::Local::new(scope, value_global);
            let exports = vec![(ascii_str!("default"), value)];
            self.new_synthetic_module(
              scope,
              module_url_found,
              ModuleType::Other(module_type.clone()),
              exports,
            )
          }

          // Complex case - besides a synthetic module, we will create a new
          // module from JS code.
          CustomModuleEvaluationKind::ComputedAndSynthetic(
            computed_src,
            synthetic_value,
            synthetic_module_type,
          ) => {
            let (url1, url2) = module_url_found.into_cheap_copy();
            let value = v8::Local::new(scope, synthetic_value);
            let exports = vec![(ascii_str!("default"), value)];
            let _synthetic_mod_id = self.new_synthetic_module(
              scope,
              url1,
              synthetic_module_type,
              exports,
            );

            let (code_cache_info, url2) = if let Some(code_cache) = code_cache {
              let (url1, url2) = url2.into_cheap_copy();
              let loader = self.loader.borrow().clone();
              (
                Some(CodeCacheInfo {
                  data: code_cache.data,
                  ready_callback: Box::new(move |cache| {
                    let specifier =
                      ModuleSpecifier::parse(url1.as_str()).unwrap();
                    loader.code_cache_ready(specifier, code_cache.hash, cache)
                  }),
                }),
                url2,
              )
            } else {
              (None, url2)
            };

            self
              .new_module_from_js_source_with_pending(
                scope,
                main,
                ModuleType::Other(module_type.clone()),
                url2,
                computed_src,
                dynamic,
                code_cache_info,
              )?
              .into_ready()
          }
        }
      }
    };
    Ok(NewModuleResult::Ready(module_id))
  }

  /// Creates a synthetic module whose exports mirror the own string-keyed
  /// properties of `exports_obj`, plus a `default` export pointing at
  /// `exports_obj` itself. Matches the shape of Node's
  /// `BuiltinModule.getESMFacade` so a CJS-style polyfill module can be
  /// imported as ESM without a hand-written wrapper.
  ///
  /// Property values are read once at creation time — synthetic exports
  /// are static snapshots, not live references back to the object.
  pub fn new_synthetic_module_from_exports_object<'s, 'i>(
    &self,
    scope: &mut v8::PinScope<'s, 'i>,
    name: impl IntoModuleName,
    exports_obj: v8::Local<'s, v8::Object>,
  ) -> ModuleId {
    let name = name.into_module_name();
    let name_str = name.v8_string(scope).unwrap();

    // Enumerate own string-keyed properties of the exports object.
    let property_names = exports_obj
      .get_own_property_names(
        scope,
        v8::GetPropertyNamesArgsBuilder::new()
          .mode(v8::KeyCollectionMode::OwnOnly)
          .property_filter(v8::PropertyFilter::SKIP_SYMBOLS)
          .key_conversion(v8::KeyConversionMode::ConvertToString)
          .build(),
      )
      .unwrap();
    let len = property_names.length();

    let mut export_names: Vec<v8::Local<v8::String>> =
      Vec::with_capacity(len as usize + 1);
    let mut export_values: Vec<v8::Local<v8::Value>> =
      Vec::with_capacity(len as usize + 1);
    // If the IIFE returns `{ default: <ns>, ...named }`, treat the inner
    // `default` as the ESM default export. This mirrors the manual
    // `export default mod.default` pattern used by the old `*_esm.ts`
    // wrappers and Node's behavior for builtins whose `module.exports`
    // includes a `default` property. Otherwise fall back to the entire
    // exports object as the default (matches `module.exports = { ... }`
    // shape).
    let mut default_value: v8::Local<v8::Value> = exports_obj.into();
    for i in 0..len {
      let key_val = property_names.get_index(scope, i).unwrap();
      let key_str = key_val.to_string(scope).unwrap();
      let value = exports_obj.get(scope, key_val).unwrap();
      if key_str.to_rust_string_lossy(scope) == "default" {
        default_value = value;
        continue;
      }
      export_names.push(key_str);
      export_values.push(value);
    }
    let default_str = v8::String::new(scope, "default").unwrap();
    export_names.push(default_str);
    export_values.push(default_value);

    let module = v8::Module::create_synthetic_module(
      scope,
      name_str,
      &export_names,
      synthetic_module_evaluation_steps,
    );

    let handle = v8::Global::<v8::Module>::new(scope, module);
    let mut exports_global = Vec::with_capacity(export_names.len());
    for i in 0..export_names.len() {
      exports_global.push((
        v8::Global::new(scope, export_names[i]),
        v8::Global::new(scope, export_values[i]),
      ));
    }

    self
      .data
      .borrow_mut()
      .synthetic_module_exports_store
      .insert(handle.clone(), exports_global);

    let id = self.data.borrow_mut().create_module_info(
      name,
      ModuleType::JavaScript,
      handle,
      false,
      vec![],
    );

    // Synthetic modules have no imports so their instantation must never fail.
    self.instantiate_module(scope, id).unwrap();
    // Eagerly evaluate so the `synthetic_module_evaluation_steps` callback
    // fires now (which sets the exports from the staged store) instead of
    // at first read. Important during snapshot creation: V8 needs the
    // module in `Evaluated` state with its exports populated before the
    // snapshot is serialized; otherwise consumers that look up the
    // module's namespace at snapshot-finalize time hit
    // "GetModuleNamespace must be used on an instantiated module" or get
    // unbound exports. Evaluation is synchronous for synthetic modules.
    {
      let handle = self.get_handle(id).unwrap();
      let local = v8::Local::new(scope, handle);
      let _ = local.evaluate(scope);
    }

    id
  }

  /// Creates a "synthetic module", that contains only a single, "default" export.
  ///
  /// The module gets instantiated and its ID is returned.
  pub fn new_synthetic_module<'s, 'i>(
    &self,
    scope: &mut v8::PinScope<'s, 'i>,
    name: impl IntoModuleName,
    module_type: ModuleType,
    exports: Vec<(FastStaticString, v8::Local<'s, v8::Value>)>,
  ) -> ModuleId {
    let name = name.into_module_name();
    let name_str = name.v8_string(scope).unwrap();

    let export_names = exports
      .iter()
      .map(|(name, _)| name.v8_string(scope).unwrap())
      .collect::<Vec<_>>();
    let module = v8::Module::create_synthetic_module(
      scope,
      name_str,
      &export_names,
      synthetic_module_evaluation_steps,
    );

    let handle = v8::Global::<v8::Module>::new(scope, module);
    let mut exports_global = Vec::with_capacity(exports.len());

    for i in 0..exports.len() {
      let export_name = export_names[i];
      let (_, export_value) = exports[i];
      exports_global.push((
        v8::Global::new(scope, export_name),
        v8::Global::new(scope, export_value),
      ));
    }

    self
      .data
      .borrow_mut()
      .synthetic_module_exports_store
      .insert(handle.clone(), exports_global);

    let id = self.data.borrow_mut().create_module_info(
      name,
      module_type,
      handle,
      false,
      vec![],
    );

    // Synthetic modules have no imports so their instantation must never fail.
    self.instantiate_module(scope, id).unwrap();

    id
  }

  // TODO(bartlomieju): remove this method or rename it to `new_js_module`.
  /// Create and compile an ES module.
  pub(crate) fn new_es_module(
    &self,
    scope: &mut v8::PinScope,
    main: bool,
    name: ModuleName,
    source: ModuleCodeString,
    is_dynamic_import: bool,
    code_cache_info: Option<CodeCacheInfo>,
  ) -> Result<ModuleId, ModuleError> {
    self.new_module_from_js_source(
      scope,
      main,
      ModuleType::JavaScript,
      name,
      source,
      is_dynamic_import,
      code_cache_info,
    )
  }

  /// Provided given JavaScript source code, compile and create a module of given
  /// type.
  ///
  /// Passed type doesn't have to be [`ModuleType::JavaScript`]! This method
  /// can be used to create "shim" modules, that execute some JS and act as a
  /// proxy to the actual underlying module (eg. you might create a "shim" for
  /// Wasm module).
  ///
  /// Imports in the executed code are parsed (along their import attributes)
  /// and attached to associated [`ModuleInfo`].
  ///
  /// Returns an ID of newly created module.
  ///
  /// Sync call sites can use this directly.
  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  pub(crate) fn new_module_from_js_source(
    &self,
    scope: &mut v8::PinScope,
    main: bool,
    module_type: ModuleType,
    name: ModuleName,
    source: ModuleCodeString,
    is_dynamic_import: bool,
    code_cache_info: Option<CodeCacheInfo>,
  ) -> Result<ModuleId, ModuleError> {
    Ok(
      self
        .new_module_from_js_source_with_pending(
          scope,
          main,
          module_type,
          name,
          source,
          is_dynamic_import,
          code_cache_info,
        )?
        .into_ready(),
    )
  }

  /// Same as [`new_module_from_js_source`] but returns [`NewModuleResult`].
  #[allow(clippy::too_many_arguments, reason = "TODO: cleanup")]
  pub(crate) fn new_module_from_js_source_with_pending(
    &self,
    scope: &mut v8::PinScope,
    main: bool,
    module_type: ModuleType,
    name: ModuleName,
    source: ModuleCodeString,
    is_dynamic_import: bool,
    mut code_cache_info: Option<CodeCacheInfo>,
  ) -> Result<NewModuleResult, ModuleError> {
    if main {
      let data = self.data.borrow();
      if let Some(main_module) = data.main_module_id {
        let main_name = self.data.borrow().get_name_by_id(main_module).unwrap();
        return Err(ModuleError::Concrete(
          ModuleConcreteError::MainModuleAlreadyExists {
            main_module: main_name.to_string(),
            new_module: name.to_string(),
          },
        ));
      }
    }

    let name_str = name.v8_string(scope).unwrap();
    let source_str = source.v8_string(scope).unwrap();
    let host_defined_options = self
      .loader
      .borrow()
      .get_host_defined_options(scope, name.as_str());
    let origin = script_origin(scope, name_str, true, host_defined_options);

    v8::tc_scope!(let tc_scope, scope);

    let (maybe_module, try_store_code_cache) = code_cache_info
      .as_ref()
      .and_then(|code_cache_info| {
        code_cache_info.data.as_ref().map(|cache| {
          let mut source = v8::script_compiler::Source::new_with_cached_data(
            source_str,
            Some(&origin),
            v8::CachedData::new(cache),
          );
          let maybe_module = v8::script_compiler::compile_module2(
            tc_scope,
            &mut source,
            v8::script_compiler::CompileOptions::ConsumeCodeCache,
            v8::script_compiler::NoCacheReason::NoReason,
          );
          // Check if the provided code cache is rejected by V8.
          let rejected = match source.get_cached_data() {
            Some(cached_data) => cached_data.rejected(),
            _ => true,
          };
          (maybe_module, rejected)
        })
      })
      .unwrap_or_else(|| {
        let mut source =
          v8::script_compiler::Source::new(source_str, Some(&origin));
        (
          v8::script_compiler::compile_module(tc_scope, &mut source),
          true,
        )
      });

    if tc_scope.has_caught() {
      assert!(maybe_module.is_none());
      let exception = tc_scope.exception().unwrap();
      let exception = v8::Global::new(tc_scope, exception);
      // TODO(bartlomieju): add a more concrete variant - like `ModuleError::CompileError`?
      return Err(ModuleError::Exception(exception));
    }

    let module = maybe_module.unwrap();

    // V8 does not support creating code caches while also snapshotting,
    // and it's not needed anyway, as the snapshot already contains it.
    if try_store_code_cache
      && !self.will_snapshot
      && let Some(code_cache_info) = code_cache_info.take()
    {
      let unbound_module_script = module.get_unbound_module_script(tc_scope);
      let code_cache =
        unbound_module_script.create_code_cache().ok_or_else(|| {
          ModuleError::Concrete(
            ModuleConcreteError::UnboundModuleScriptCodeCache,
          )
        })?;
      let fut =
        async move { (code_cache_info.ready_callback)(&code_cache).await }
          .boxed_local();
      self.code_cache_ready_futs.push(fut);
    }

    // Extract native source map URL from V8
    let unbound_module_script = module.get_unbound_module_script(tc_scope);
    let source_mapping_url_value =
      unbound_module_script.get_source_mapping_url(tc_scope);
    if !source_mapping_url_value.is_undefined()
      && !source_mapping_url_value.is_null()
    {
      let mut source_mapping_url_buf: [std::mem::MaybeUninit<u8>; 1024] =
        [std::mem::MaybeUninit::uninit(); 1024];
      let source_mapping_url: v8::Local<v8::String> =
        source_mapping_url_value.try_cast().unwrap();
      let source_mapping_url = source_mapping_url
        .to_rust_cow_lossy(tc_scope, &mut source_mapping_url_buf);

      let module_name = name
        .try_clone()
        .unwrap_or_else(|| ModuleName::from(name.as_str().to_string()));

      if source_mapping_url.starts_with(DATA_PREFIX) {
        if let Ok(DecodedMap::Regular(sm)) =
          sourcemap::decode_data_url(&source_mapping_url)
        {
          self
            .source_mapper
            .borrow_mut()
            .add_source_map(module_name, sm);
        }
      } else {
        // Resolve external source map URL relative to the module URL
        let resolved_url =
          if let Ok(module_url) = ModuleSpecifier::parse(name.as_str()) {
            module_url
              .join(&source_mapping_url)
              .unwrap_or(module_url)
              .to_string()
          } else {
            source_mapping_url.into_owned()
          };

        self
          .source_mapper
          .borrow_mut()
          .add_source_map_url(module_name, resolved_url);
      }
    }

    // TODO(bartlomieju): maybe move to a helper function?
    let module_requests = module.get_module_requests();
    let requests_len = module_requests.length();
    let mut requests = Vec::with_capacity(requests_len);
    for i in 0..module_requests.length() {
      let module_request = v8::Local::<v8::ModuleRequest>::try_from(
        module_requests.get(tc_scope, i).unwrap(),
      )
      .unwrap();
      let mut import_specifier_buf: [std::mem::MaybeUninit<u8>; 1024] =
        [std::mem::MaybeUninit::uninit(); 1024];
      let import_specifier = module_request
        .get_specifier()
        .to_rust_cow_lossy(tc_scope, &mut import_specifier_buf);

      let import_attributes = module_request.get_import_attributes();

      let attributes = parse_import_attributes(
        tc_scope,
        import_attributes,
        ImportAttributesKind::StaticImport,
      );

      // FIXME(bartomieju): there are no stack frames if exception
      // is thrown here
      {
        let state = JsRuntime::state_from(tc_scope);
        if let Some(validate_import_attributes_cb) =
          &state.validate_import_attributes_cb
        {
          let location = module
            .source_offset_to_location(module_request.get_source_offset());
          let context = ImportAttributesContext {
            referrer: name.as_str().to_string(),
            specifier: import_specifier.to_string(),
            // V8 reports 0-based line/column; report them 1-based.
            line_number: Some(location.get_line_number() as u32 + 1),
            column_number: Some(location.get_column_number() as u32 + 1),
          };
          (validate_import_attributes_cb)(tc_scope, &attributes, &context);
        }
      }

      if tc_scope.has_caught() {
        let exception = tc_scope.exception().unwrap();
        let exception = v8::Global::new(tc_scope, exception);
        return Err(ModuleError::Exception(exception));
      }

      let resolve_kind = if is_dynamic_import {
        ResolutionKind::DynamicImport
      } else {
        ResolutionKind::Import
      };
      let module_specifier = match self.resolve_with_scope(
        tc_scope,
        &import_specifier,
        name.as_ref(),
        resolve_kind,
        &attributes,
      ) {
        Ok(s) => s,
        Err(e) => {
          // Fall back to lazy ESM sources for bare internal specifiers (e.g.
          // `node:_http_common` from `node:_http_outgoing`) that the
          // user-facing loader doesn't know about. If the specifier matches
          // a registered lazy ESM entry, use it verbatim.
          if self.has_lazy_esm_source(&import_specifier)
            && let Ok(parsed) = ModuleSpecifier::parse(&import_specifier)
          {
            parsed
          } else {
            return Err(ModuleError::Core(e.into()));
          }
        }
      };
      let requested_module_type =
        get_requested_module_type_from_attributes(&attributes);
      let referrer_source_offset = if let ModuleType::Wasm = module_type {
        // Wasm sources will have been rendered to synthetic JS modules, so any
        // `ModuleRequest::referrer:source_offset`s we get from v8 are not
        // applicable to user code. Disregard it.
        None
      } else {
        Some(module_request.get_source_offset())
      };
      if crate::modules::import_graph::is_enabled() {
        crate::modules::import_graph::record_esm_import(
          name.as_ref(),
          module_specifier.as_str(),
        );
      }
      let request = ModuleRequest {
        reference: ModuleReference {
          specifier: module_specifier,
          requested_module_type,
        },
        specifier_key: Some(import_specifier.into_owned()),
        referrer_source_offset,
        phase: match module_request.get_phase() {
          v8::ModuleImportPhase::kEvaluation => ModuleImportPhase::Evaluation,
          v8::ModuleImportPhase::kSource => ModuleImportPhase::Source,
          v8::ModuleImportPhase::kDefer => ModuleImportPhase::Defer,
        },
      };
      requests.push(request);
    }

    let handle = v8::Global::<v8::Module>::new(tc_scope, module);
    let id = self.data.borrow_mut().create_module_info(
      name,
      module_type,
      handle,
      main,
      requests,
    );
    Ok(NewModuleResult::Ready(id))
  }

  pub(crate) fn new_wasm_module_source(
    &self,
    scope: &mut v8::PinScope,
    module_reference: &ModuleReference,
    mut loaded_source: ModuleSource,
  ) -> Result<ModuleSource, ModuleError> {
    if let Some(module_url_found) = loaded_source.cheap_copy_module_url_found()
    {
      self.data.borrow_mut().alias(
        loaded_source.cheap_copy_module_url_specified(),
        &loaded_source.module_type.clone().into(),
        module_url_found,
      );
    }
    let reference_key = ModuleSourceKey::from_reference(module_reference);
    if self.data.borrow().sources.contains_key(&reference_key) {
      return Ok(loaded_source);
    }
    let loaded_key = ModuleSourceKey::from_loaded_source(&mut loaded_source);
    if let Some(source) = self.data.borrow().sources.get(&loaded_key).cloned() {
      self.data.borrow_mut().sources.insert(reference_key, source);
      return Ok(loaded_source);
    }

    let ModuleSourceCode::Bytes(code) = &loaded_source.code else {
      return Err(ModuleError::Concrete(ModuleConcreteError::WasmNotBytes));
    };
    let Some(wasm_module) =
      v8::WasmModuleObject::compile(scope, code.as_bytes())
    else {
      return Err(
        ModuleConcreteError::WasmCompile(loaded_key.name.to_string()).into(),
      );
    };
    let wasm_module_object: v8::Local<v8::Object> = wasm_module.into();
    let source = v8::Global::new(scope, wasm_module_object);
    {
      let mut data = self.data.borrow_mut();
      data.sources.insert(reference_key, source.clone());
      data.sources.insert(loaded_key, source);
    }
    Ok(loaded_source)
  }

  pub(crate) fn new_wasm_module(
    &self,
    scope: &mut v8::PinScope,
    name: ModuleName,
    source: ModuleSourceCode,
    is_dynamic_import: bool,
  ) -> Result<ModuleId, ModuleError> {
    let bytes = source.as_bytes();
    let wasm_module_analysis = WasmDeps::parse(
      bytes,
      wasm_dep_analyzer::ParseOptions { skip_types: true },
    )
    .map_err(ModuleConcreteError::WasmParse)?;

    let js_wasm_module_source =
      render_js_wasm_module(name.as_str(), wasm_module_analysis);

    self.new_module_from_js_source(
      scope,
      false,
      ModuleType::Wasm,
      name,
      js_wasm_module_source.into(),
      is_dynamic_import,
      None,
    )
  }

  pub(crate) fn new_json_module(
    &self,
    scope: &mut v8::PinScope,
    name: impl IntoModuleName,
    code: impl IntoModuleCodeString,
  ) -> Result<ModuleId, ModuleError> {
    let name = name.into_module_name();
    let code = code.into_module_code();
    let source_str = v8::String::new_from_utf8(
      scope,
      strip_bom(code.as_bytes()),
      v8::NewStringType::Normal,
    )
    .unwrap();
    v8::tc_scope!(let tc_scope, scope);

    let parsed_json = match v8::json::parse(tc_scope, source_str) {
      Some(parsed_json) => parsed_json,
      None => {
        assert!(tc_scope.has_caught());
        let exception = tc_scope.exception().unwrap();
        let exception = v8::Global::new(tc_scope, exception);
        return Err(ModuleError::Exception(exception));
      }
    };
    let exports = vec![(ascii_str!("default"), parsed_json)];
    Ok(self.new_synthetic_module(tc_scope, name, ModuleType::Json, exports))
  }

  #[allow(
    clippy::unnecessary_wraps,
    reason = "consistent return type with other module constructors"
  )]
  pub(crate) fn new_text_module(
    &self,
    scope: &mut v8::PinScope,
    name: impl IntoModuleName,
    code: impl IntoModuleCodeString,
  ) -> Result<ModuleId, ModuleError> {
    let name = name.into_module_name();
    let code = code.into_module_code();
    // TODO(bartlomieju): would be much better if the string was ensured to not contain
    // BOM, then we could use a more efficient string type with `FastString::v8_string`.
    let source_str = v8::String::new_from_utf8(
      scope,
      strip_bom(code.as_bytes()),
      v8::NewStringType::Normal,
    )
    .unwrap();
    let source_str_local = v8::Local::new(scope, source_str);
    let source_value_local = v8::Local::<v8::Value>::from(source_str_local);
    let exports = vec![(ascii_str!("default"), source_value_local)];
    Ok(self.new_synthetic_module(scope, name, ModuleType::Text, exports))
  }

  #[allow(
    clippy::unnecessary_wraps,
    reason = "consistent return type with other module constructors"
  )]
  pub(crate) fn new_bytes_module(
    &self,
    scope: &mut v8::PinScope,
    name: impl IntoModuleName,
    code: ModuleCodeBytes,
  ) -> Result<ModuleId, ModuleError> {
    let name = name.into_module_name();
    let (buf_len, backing_store) = match code {
      ModuleCodeBytes::Static(bytes) => (
        bytes.len(),
        v8::ArrayBuffer::new_backing_store_from_vec(bytes.to_vec()),
      ),
      ModuleCodeBytes::Boxed(bytes) => (
        bytes.len(),
        v8::ArrayBuffer::new_backing_store_from_boxed_slice(bytes),
      ),
      ModuleCodeBytes::Arc(bytes) => (
        bytes.len(),
        v8::ArrayBuffer::new_backing_store_from_vec(bytes.to_vec()),
      ),
    };
    let backing_store_shared = backing_store.make_shared();
    let ab = v8::ArrayBuffer::with_backing_store(scope, &backing_store_shared);
    let uint8_array = v8::Uint8Array::new(scope, ab, 0, buf_len).unwrap();
    let value: v8::Local<v8::Value> = uint8_array.into();
    let exports = vec![(ascii_str!("default"), value)];
    Ok(self.new_synthetic_module(scope, name, ModuleType::Bytes, exports))
  }

  pub(crate) fn instantiate_module<'s, 'i>(
    &self,
    scope: &mut v8::PinScope<'s, 'i>,
    id: ModuleId,
  ) -> Result<(), v8::Global<v8::Value>> {
    v8::tc_scope!(let tc_scope, scope);

    let module = self
      .get_handle(id)
      .map(|handle| v8::Local::new(tc_scope, handle))
      .expect("ModuleInfo not found");

    if module.get_status() == v8::ModuleStatus::Errored {
      return Err(v8::Global::new(tc_scope, module.get_exception()));
    }

    // FIXME: instantiate_module is called more than it should be,
    // especially for dynamic imports. As a hack, bail out if the
    // module status is already being instantiated.
    if module.get_status() != v8::ModuleStatus::Uninstantiated {
      return Ok(());
    }

    tc_scope.set_slot(self as *const _);
    let instantiate_result = module.instantiate_module2(
      tc_scope,
      Self::module_resolve_callback,
      Self::module_source_callback,
    );
    tc_scope.remove_slot::<*const Self>();
    if instantiate_result.is_none() {
      let exception = tc_scope.exception().unwrap();
      return Err(v8::Global::new(tc_scope, exception));
    }

    Ok(())
  }

  /// Called by V8 during `JsRuntime::instantiate_module`. This is only used internally, so we use the Isolate's annex
  /// to propagate a &Self.
  fn module_resolve_callback<'s>(
    context: v8::Local<'s, v8::Context>,
    specifier: v8::Local<'s, v8::String>,
    import_attributes: v8::Local<'s, v8::FixedArray>,
    referrer: v8::Local<'s, v8::Module>,
  ) -> Option<v8::Local<'s, v8::Module>> {
    // SAFETY: `CallbackScope` can be safely constructed from `Local<Context>`
    v8::callback_scope!(unsafe scope, context);

    let module_map =
      // SAFETY: We retrieve the pointer from the slot, having just set it a few stack frames up
      unsafe { scope.get_slot::<*const Self>().unwrap().as_ref().unwrap() };

    let referrer_global = v8::Global::new(scope, referrer);

    let referrer_name = module_map
      .data
      .borrow()
      .get_name_by_module(&referrer_global)
      .expect("ModuleInfo not found");

    let mut specifier_buf: [std::mem::MaybeUninit<u8>; 1024] =
      [std::mem::MaybeUninit::uninit(); 1024];
    let specifier_str = specifier.to_rust_cow_lossy(scope, &mut specifier_buf);

    let attributes = parse_import_attributes(
      scope,
      import_attributes,
      ImportAttributesKind::StaticImport,
    );
    let requested_module_type =
      get_requested_module_type_from_attributes(&attributes);
    let pre_resolved_specifier = {
      let module_map_data = module_map.data.borrow();
      let referrer_info = module_map_data
        .get_info_by_module(&referrer_global)
        .expect("ModuleInfo not found");
      referrer_info
        .requests
        .iter()
        .find(|r| {
          r.specifier_key
            .as_ref()
            .is_some_and(|s| s == &specifier_str)
            && r.reference.requested_module_type == requested_module_type
        })
        .map(|r| r.reference.specifier.clone())
    };
    let maybe_module = module_map.resolve_callback(
      scope,
      &specifier_str,
      &referrer_name,
      attributes,
      pre_resolved_specifier,
    );
    if let Some(module) = maybe_module {
      return Some(module);
    }

    crate::error::throw_js_error_class(
      scope,
      &JsErrorBox::type_error(format!(
        r#"Cannot resolve module "{specifier_str}" from "{referrer_name}""#
      )),
    );
    None
  }

  fn module_source_callback<'s>(
    context: v8::Local<'s, v8::Context>,
    specifier: v8::Local<'s, v8::String>,
    import_attributes: v8::Local<'s, v8::FixedArray>,
    referrer: v8::Local<'s, v8::Module>,
  ) -> Option<v8::Local<'s, v8::Object>> {
    // SAFETY: `CallbackScope` can be safely constructed from `Local<Context>`
    v8::callback_scope!(unsafe scope, context);

    let module_map =
      // SAFETY: We retrieve the pointer from the slot, having just set it a few stack frames up
      unsafe { scope.get_slot::<*const Self>().unwrap().as_ref().unwrap() };

    let mut specifier_buf: [std::mem::MaybeUninit<u8>; 1024] =
      [std::mem::MaybeUninit::uninit(); 1024];
    let specifier_str = specifier.to_rust_cow_lossy(scope, &mut specifier_buf);
    let referrer_global = v8::Global::new(scope, referrer);
    let attributes = parse_import_attributes(
      scope,
      import_attributes,
      ImportAttributesKind::StaticImport,
    );
    let requested_module_type =
      get_requested_module_type_from_attributes(&attributes);
    let module_reference = {
      let module_map_data = module_map.data.borrow();
      let referrer_info = module_map_data
        .get_info_by_module(&referrer_global)
        .expect("ModuleInfo not found");
      let module_request = referrer_info
        .requests
        .iter()
        .find(|r| {
          r.specifier_key
            .as_ref()
            .is_some_and(|s| s == &specifier_str) && r.reference.requested_module_type == requested_module_type
        })
        .expect("ModuleInfo::requests did not contain a matching specifier_key when getting source");
      module_request.reference.clone()
    };
    let key = ModuleSourceKey::from_reference(&module_reference);
    if let Some(entry) = module_map.data.borrow().sources.get(&key) {
      Some(v8::Local::new(scope, entry))
    } else {
      let message = v8::String::new(
        scope,
        &format!(r#"Module source can not be imported for "{specifier_str}""#),
      )
      .unwrap();
      let exception = v8::Exception::reference_error(scope, message);
      scope.throw_exception(exception);
      None
    }
  }

  /// Resolve provided module. This function calls out to `loader.resolve`,
  /// but applies some additional checks that disallow resolving/importing
  /// certain modules (eg. `ext:` or `node:` modules).
  ///
  pub fn resolve(
    &self,
    specifier: &str,
    referrer: &str,
    kind: ResolutionKind,
  ) -> ModuleResolveResponse {
    if specifier.starts_with("ext:")
      && !referrer.starts_with("ext:")
      && !referrer.starts_with("node:")
      && !referrer.starts_with("checkin:")
      && referrer != "."
      && kind != ResolutionKind::MainModule
    {
      let referrer = if referrer.is_empty() {
        "(no referrer)"
      } else {
        referrer
      };
      let msg = format!(
        "Importing ext: modules is only allowed from ext: and node: modules. Tried to import {} from {}",
        specifier, referrer
      );
      return Err(JsErrorBox::type_error(msg));
    }

    self.loader.borrow().resolve(specifier, referrer, kind)
  }

  pub fn resolve_with_scope(
    &self,
    scope: &mut v8::PinScope,
    specifier: &str,
    referrer: &str,
    kind: ResolutionKind,
    import_attributes: &HashMap<String, String>,
  ) -> ModuleResolveResponse {
    if specifier.starts_with("ext:")
      && !referrer.starts_with("ext:")
      && !referrer.starts_with("node:")
      && !referrer.starts_with("checkin:")
      && referrer != "."
      && kind != ResolutionKind::MainModule
    {
      let referrer = if referrer.is_empty() {
        "(no referrer)"
      } else {
        referrer
      };
      let msg = format!(
        "Importing ext: modules is only allowed from ext: and node: modules. Tried to import {} from {}",
        specifier, referrer
      );
      return Err(JsErrorBox::type_error(msg));
    }

    self.loader.borrow().resolve_with_scope(
      scope,
      specifier,
      referrer,
      kind,
      import_attributes,
    )
  }

  /// Called by `module_resolve_callback` during module instantiation.
  fn resolve_callback<'s, 'i>(
    &self,
    scope: &mut v8::PinScope<'s, 'i>,
    specifier: &str,
    referrer: &str,
    import_attributes: HashMap<String, String>,
    pre_resolved_specifier: Option<ModuleSpecifier>,
  ) -> Option<v8::Local<'s, v8::Module>> {
    // Synthetic ESM dispatch first, by raw specifier. The active loader
    // may not know about the spec (e.g. `LazyEsmModuleLoader` only
    // resolves `lazy_loaded_esm` entries), so checking before
    // `resolve_sync` ensures the synthetic dispatch wins over a loader
    // "cannot resolve" error. `node:foo` specifiers are their own
    // canonical form, so no further resolution is needed.
    if self.has_synthetic_esm_module(specifier) {
      if let Some(id) = self.get_id(specifier, &RequestedModuleType::None)
        && let Some(handle) = self.get_handle(id)
      {
        return Some(v8::Local::new(scope, handle));
      }
      if let Some(module) = self.try_resolve_synthetic_esm(scope, specifier) {
        return Some(module);
      }
    }

    let module_type =
      get_requested_module_type_from_attributes(&import_attributes);
    let resolved_specifier = match pre_resolved_specifier {
      Some(specifier) => specifier,
      None => match self.resolve_with_scope(
        scope,
        specifier,
        referrer,
        ResolutionKind::Import,
        &import_attributes,
      ) {
        Ok(s) => s,
        Err(e) => {
          // Fall back to lazy ESM sources for bare internal specifiers like
          // `node:_http_common` that the runtime's user-facing loader
          // doesn't know how to resolve (only public `node:` modules go
          // through the normal path). The lazy_esm registry has them by
          // exact specifier, so if the specifier is a registered lazy ESM
          // entry, use it verbatim instead of erroring.
          if self.has_lazy_esm_source(specifier)
            && let Ok(parsed) = ModuleSpecifier::parse(specifier)
          {
            parsed
          } else {
            crate::error::throw_js_error_class(scope, &e);
            return None;
          }
        }
      },
    };

    if let Some(id) = self.get_id(resolved_specifier.as_str(), module_type)
      && let Some(handle) = self.get_handle(id)
    {
      return Some(v8::Local::new(scope, handle));
    }

    // Synthetic ESM dispatch (post-resolve): in case the loader returned
    // a redirected/normalized form, also check here. Most callers hit
    // the pre-resolve branch above.
    if let Some(module) =
      self.try_resolve_synthetic_esm(scope, resolved_specifier.as_str())
    {
      return Some(module);
    }

    // Fallback: check lazy-loaded ESM sources (modules embedded in the
    // binary but not included in the snapshot).
    let maybe_source = self.take_lazy_esm_source(resolved_specifier.as_str());
    if let Some(source_code) = maybe_source {
      match self.new_es_module(
        scope,
        false,
        resolved_specifier.into(),
        source_code,
        false,
        None,
      ) {
        Ok(mod_id) => {
          if let Some(handle) = self.get_handle(mod_id) {
            return Some(v8::Local::new(scope, handle));
          }
        }
        Err(e) => {
          let err = e.into_error(scope, false, true);
          crate::error::throw_js_error_class(scope, &err);
          return None;
        }
      }
    }

    None
  }

  pub(crate) fn get_requested_modules(
    &self,
    id: ModuleId,
  ) -> Option<Vec<ModuleRequest>> {
    // TODO(mmastrac): Remove cloning. We were originally cloning this at the call sites but that's no excuse.
    self.data.borrow().info.get(id).map(|i| i.requests.clone())
  }

  // Initiate loading of a module graph imported using `import()`.
  #[allow(clippy::too_many_arguments, reason = "internal code")]
  pub(crate) fn load_dynamic_import(
    self: Rc<Self>,
    scope: &mut v8::PinScope,
    specifier: String,
    referrer: String,
    requested_module_type: RequestedModuleType,
    phase: ModuleImportPhase,
    resolver_handle: v8::Global<v8::PromiseResolver>,
    cped_handle: v8::Global<v8::Value>,
  ) -> bool {
    let resolve_response = self.resolve_with_scope(
      scope,
      &specifier,
      &referrer,
      ResolutionKind::DynamicImport,
      &HashMap::new(),
    );

    // Fast path: if the module is already loaded, resolve the import
    // immediately without async work.
    if phase == ModuleImportPhase::Evaluation
      && let ref resolve_result = resolve_response
      && let Ok(module_specifier) = resolve_result
      && let Some(id) = self
        .data
        .borrow()
        .get_id(module_specifier.as_str(), &requested_module_type)
    {
      let module = self
        .data
        .borrow()
        .get_handle(id)
        .map(|handle| v8::Local::new(scope, handle))
        .expect("Dyn import module info not found");

      if module.get_status() == v8::ModuleStatus::Evaluated {
        // Check if this module has a pending TLA (top-level await) evaluation.
        let has_pending_tla = self
          .pending_dyn_mod_evaluations
          .borrow()
          .iter()
          .any(|pending| pending.module_id == id);

        // Queue this resolver to be resolved when the TLA completes.
        if has_pending_tla {
          self
            .pending_tla_waiters
            .borrow_mut()
            .entry(id)
            .or_default()
            .push(resolver_handle);
          return false;
        }

        // No pending TLA, safe to resolve immediately
        let resolver = resolver_handle.open(scope);
        let module_namespace = module.get_module_namespace();
        resolver.resolve(scope, module_namespace).unwrap();

        return false;
      }
    }

    // Fast path for lazy-loaded ESM: load synchronously and resolve
    // immediately, avoiding the async RecursiveModuleLoad path entirely.
    if phase == ModuleImportPhase::Evaluation
      && let ref resolve_result = resolve_response
      && let Ok(module_specifier) = resolve_result
      && self.has_lazy_esm_source(module_specifier.as_str())
    {
      match self.lazy_load_esm_module(scope, module_specifier.as_str()) {
        Ok(module_ns) => {
          let resolver = resolver_handle.open(scope);
          let module_ns_local = v8::Local::new(scope, module_ns);
          resolver.resolve(scope, module_ns_local).unwrap();
          return false;
        }
        Err(e) => {
          let exception = e.to_v8_error(scope);
          let exception_local = v8::Local::new(scope, exception);
          let resolver = resolver_handle.open(scope);
          resolver.reject(scope, exception_local).unwrap();
          return false;
        }
      }
    }

    // Fast path for `synthetic_esm`-registered modules: build the
    // synthetic module synchronously and resolve immediately, same
    // pattern as the lazy ESM fast path above.
    if phase == ModuleImportPhase::Evaluation
      && let ref resolve_result = resolve_response
      && let Ok(module_specifier) = resolve_result
      && self.has_synthetic_esm_module(module_specifier.as_str())
      && !self
        .loader
        .borrow()
        .should_load_synthetic_esm(module_specifier.as_str())
    {
      match self
        .lazy_load_synthetic_esm_module(scope, module_specifier.as_str())
      {
        Ok(module_ns) => {
          let resolver = resolver_handle.open(scope);
          let module_ns_local = v8::Local::new(scope, module_ns);
          resolver.resolve(scope, module_ns_local).unwrap();
          return false;
        }
        Err(e) => {
          let exception = e.to_v8_error(scope);
          let exception_local = v8::Local::new(scope, exception);
          let resolver = resolver_handle.open(scope);
          resolver.reject(scope, exception_local).unwrap();
          return false;
        }
      }
    }

    let load = RecursiveModuleLoad::new_dynamic_import(
      specifier,
      referrer,
      requested_module_type,
      phase,
      self.clone(),
      resolve_response,
    );

    self.dynamic_import_map.borrow_mut().insert(
      load.id(),
      DynImportState {
        resolver: resolver_handle,
        cped: cped_handle,
        phase,
      },
    );

    let load_id = load.id();
    let fut = async move {
      let mut load = load;
      (load_id, load.prepare().await.map(|()| load))
    }
    .boxed_local();

    self.preparing_dynamic_imports.push(fut);

    true
  }

  pub(crate) fn has_pending_dynamic_imports(&self) -> bool {
    self.preparing_dynamic_imports.is_pending()
      || self.pending_dynamic_imports.is_pending()
  }

  pub(crate) fn has_pending_module_evaluation(&self) -> bool {
    self.pending_mod_evaluation.get()
  }
  pub(crate) fn has_pending_dyn_module_evaluation(&self) -> bool {
    self.pending_dyn_mod_evaluations.is_pending()
  }

  /// See [`JsRuntime::mod_evaluate`].
  pub fn mod_evaluate<'s, 'i>(
    self: &Rc<Self>,
    scope: &mut v8::PinScope<'s, 'i>,
    id: ModuleId,
  ) -> impl Future<Output = Result<(), CoreError>> + use<> {
    v8::tc_scope!(tc_scope, scope);

    let module = self
      .get_handle(id)
      .map(|handle| v8::Local::new(tc_scope, handle))
      .expect("ModuleInfo not found");
    let mut status = module.get_status();

    // If the module is already evaluated, return early as there's nothing to do
    if status == v8::ModuleStatus::Evaluated {
      return Either::Left(futures::future::ready(Ok(())));
    }

    assert_eq!(
      status,
      v8::ModuleStatus::Instantiated,
      "Module not instantiated: {} ({})",
      self.get_name_by_id(id).unwrap(),
      id,
    );

    let (sender, receiver) = oneshot::channel::<Result<_, Box<JsError>>>();
    let receiver = receiver.map(|res| {
      res
        .map(|r| r.map_err(|r| CoreErrorKind::Js(r).into_box()))
        .unwrap_or_else(|_| Err(CoreErrorKind::ExecutionTerminated.into_box()))
    });

    self.evaluating_top_level.set(true);
    let Some(value) = module.evaluate(tc_scope) else {
      self.evaluating_top_level.set(false);
      if tc_scope.has_terminated() || tc_scope.is_execution_terminating() {
        let undefined = v8::undefined(tc_scope).into();
        _ = sender
          .send(exception_to_err_result(tc_scope, undefined, true, false));
      } else {
        debug_assert_eq!(module.get_status(), v8::ModuleStatus::Errored);
      }
      return Either::Right(receiver);
    };
    self.evaluating_top_level.set(false);

    self.pending_mod_evaluation.set(true);

    // Update status after evaluating.
    status = module.get_status();

    if self.exception_state.has_dispatched_exception() {
      // This will be overridden in `exception_to_err_result()`.
      let exception = v8::undefined(tc_scope).into();
      sender
        .send(exception_to_err_result(tc_scope, exception, true, false))
        .expect("Failed to send module evaluation error.");
    } else {
      debug_assert!(
        status == v8::ModuleStatus::Evaluated
          || status == v8::ModuleStatus::Errored
      );
      let promise = v8::Local::<v8::Promise>::try_from(value)
        .expect("Expected to get promise as module evaluation result");

      // If this is a main module, claim the main module notification functions
      let (notify, module) = if self.is_main_module_id(id) {
        let module = Some(v8::Global::new(tc_scope, module));
        (
          std::mem::take(&mut self.data.borrow_mut().main_module_callbacks),
          module,
        )
      } else {
        (vec![], None)
      };

      // Create a ModEvaluate instance and stash it in an external
      let evaluation = v8::External::new(
        tc_scope,
        Box::into_raw(Box::new(ModEvaluate {
          module_map: self.clone(),
          sender: Some(sender),
          notify,
          module,
        })) as _,
      );

      fn get_sender(arg: v8::Local<v8::Value>) -> ModEvaluate {
        let sender = v8::Local::<v8::External>::try_from(arg).unwrap();
        *unsafe { Box::from_raw(sender.value() as _) }
      }

      let on_fulfilled = Function::builder(
        |scope: &mut v8::PinScope<'_, '_>,
         args: v8::FunctionCallbackArguments<'_>,
         _rv: v8::ReturnValue| {
          let mut sender = get_sender(args.data());
          sender.module_map.pending_mod_evaluation.set(false);
          sender.module_map.module_waker.wake();
          sender.notify(scope);
        },
      )
      .data(evaluation.into())
      .build(tc_scope);

      let on_rejected = Function::builder(
        |scope: &mut v8::PinScope<'_, '_>,
         args: v8::FunctionCallbackArguments<'_>,
         _rv: v8::ReturnValue| {
          let mut sender = get_sender(args.data());
          sender.module_map.pending_mod_evaluation.set(false);
          sender.module_map.module_waker.wake();
          _ = sender.sender.take().unwrap().send(Ok(()));
          scope.throw_exception(args.get(0));
        },
      )
      .data(evaluation.into())
      .build(tc_scope);

      // V8 GC roots all promises, so we don't need to worry about it after this
      // then2 will return None if the runtime is shutting down
      if on_fulfilled.is_none()
        || on_rejected.is_none()
        || promise
          .then2(tc_scope, on_fulfilled.unwrap(), on_rejected.unwrap())
          .is_none()
      {
        // There are two reasons we could be here:
        // 1. The runtime is shutting down, and JS ops are disabled with termination exceptions.
        // 2. User code has tampered with the runtime globals in some way that prevents us from
        //    attaching `on_fulfilled`/`on_rejected` to `promise`.
        // In these cases we still need to report something back, so synthesize the result from the
        // promise.

        // Unset pending mod evaluation as the handlers will never run. See debug_assert below.
        self.pending_mod_evaluation.set(false);

        let mut sender = get_sender(evaluation.into());
        match promise.state() {
          PromiseState::Fulfilled => {
            if let Some(exception) = tc_scope.exception() {
              _ = sender.sender.take().unwrap().send(exception_to_err_result(
                tc_scope, exception, true, false,
              ));
            } else {
              // Module loaded OK
              sender.notify(tc_scope);
            }
          }
          PromiseState::Rejected => {
            // Module was rejected
            let err = promise.result(tc_scope);
            let err = JsError::from_v8_exception(tc_scope, err);
            _ = sender.sender.take().unwrap().send(Err(err));
          }
          PromiseState::Pending => {
            // User code shouldn't be able to both cause the runtime to fail and leave the promise as
            // pending because the only way to adopt a pending promise is to use `await` and
            // `await` won't work if you've broken the runtime in such a way that `promise::then`
            // didn't work.
            debug_assert!(tc_scope.is_execution_terminating());
            // Module pending, just drop the sender at this point -- we can't do anything with a shut-down runtime.
            drop(sender);
          }
        }
      }

      // Under Explicit microtask policy, run the module-evaluation
      // checkpoint here. This matches Node's ESM ordering: Promise and
      // queueMicrotask jobs queued during top-level module evaluation run
      // before process.nextTick callbacks queued in the same evaluation.
      //
      // For async module graphs (with TLA), this checkpoint is also critical
      // for draining V8-internal TLA resume microtasks. If skipped, the
      // evaluation promise may never resolve because V8's internal async
      // module evaluation state machine relies on these microtasks being
      // processed.
      tc_scope.perform_microtask_checkpoint();
    }

    Either::Right(receiver)
  }

  /// Helper function that allows to evaluate a module and ensure it's fully
  /// evaluated without the need to poll the event loop.
  ///
  /// This is useful for evaluating internal modules that can't use Top-Level Await.
  pub(crate) fn mod_evaluate_sync(
    self: &Rc<Self>,
    scope: &mut v8::PinScope,
    id: ModuleId,
  ) -> Result<(), CoreError> {
    v8::tc_scope!(let tc_scope, scope);

    let module = self
      .get_handle(id)
      .map(|handle| v8::Local::new(tc_scope, handle))
      .expect("ModuleInfo not found");
    let status = module.get_status();

    // If the module is already evaluated, return early as there's nothing to do
    if status == v8::ModuleStatus::Evaluated {
      return Ok(());
    }

    assert_eq!(
      status,
      v8::ModuleStatus::Instantiated,
      "Module not instantiated: {} ({})",
      self.get_name_by_id(id).unwrap(),
      id,
    );

    if module.is_graph_async() {
      return Err(CoreErrorKind::TLA.into_box());
    }

    let Some(value) = module.evaluate(tc_scope) else {
      let exception = tc_scope.exception().unwrap();
      return Err(
        CoreErrorKind::Js(JsError::from_v8_exception(tc_scope, exception))
          .into_box(),
      );
    };

    // Under Explicit microtask policy, V8 won't drain microtasks after
    // module.evaluate(). We must do it ourselves so that the module
    // evaluation promise resolves for synchronous modules.
    tc_scope.perform_microtask_checkpoint();

    if let Some(exception) = tc_scope.exception() {
      return Err(
        CoreErrorKind::Js(JsError::from_v8_exception(tc_scope, exception))
          .into_box(),
      );
    }

    let status = module.get_status();
    debug_assert!(
      status == v8::ModuleStatus::Evaluated
        || status == v8::ModuleStatus::Errored
    );
    let promise = v8::Local::<v8::Promise>::try_from(value)
      .expect("Expected to get promise as module evaluation result");

    promise.mark_as_handled();

    match promise.state() {
      PromiseState::Fulfilled => Ok(()),
      PromiseState::Rejected => {
        let err = promise.result(tc_scope);

        let exception_state = JsRealm::exception_state_from_scope(tc_scope);
        // TODO: remove after crrev.com/c/7595271
        exception_state.track_promise_rejection(
          tc_scope,
          promise,
          v8::PromiseRejectEvent::PromiseHandlerAddedAfterReject,
          None,
        );

        Err(
          CoreErrorKind::Js(JsError::from_v8_exception(tc_scope, err))
            .into_box(),
        )
      }
      PromiseState::Pending => {
        unreachable!()
      }
    }
  }

  fn dynamic_import_module_evaluate(
    &self,
    scope: &mut v8::PinScope,
    id: ModuleId,
    load_id: ModuleLoadId,
    state: DynImportState,
  ) -> Result<(), CoreError> {
    let module_handle = self.get_handle(id).expect("ModuleInfo not found");

    let status = {
      let module = module_handle.open(scope);
      module.get_status()
    };

    match status {
      v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated => {}
      _ => return Ok(()),
    }

    // IMPORTANT: Top-level-await is enabled, which means that return value
    // of module evaluation is a promise.
    //
    // This promise is internal, and not the same one that gets returned to
    // the user. We add handlers to wake the event loop when the promise resolves
    // (or rejects). The catch handler also serves to prevent an exception if the internal promise
    // rejects. That will instead happen for the other if not handled by the user.
    //
    // For more details see:
    // https://github.com/denoland/deno/issues/4908
    // https://v8.dev/features/top-level-await#module-execution-order
    v8::tc_scope!(let tc_scope, scope);

    let cped = v8::Local::new(tc_scope, state.cped);
    tc_scope.set_continuation_preserved_embedder_data(cped);

    let module = v8::Local::new(tc_scope, &module_handle);
    // Set `evaluating_top_level` so that any nested `lazy_load_esm_module`
    // calls (triggered e.g. by CJS `require()` chains under
    // `npm:` packages) skip their post-evaluate `perform_microtask_checkpoint`.
    // Draining microtasks while V8 is inside `module.evaluate()` on an
    // async module graph leaves the evaluation promise permanently
    // Pending — V8 advances AsyncModuleExecutionFulfilled for the resumed
    // TLA dep but cannot then run `ExecuteModule` on the still-evaluating
    // parent.
    self.evaluating_top_level.set(true);
    let maybe_value = module.evaluate(tc_scope);
    self.evaluating_top_level.set(false);

    // Update status after evaluating.
    let status = module.get_status();

    if let Some(value) = maybe_value {
      debug_assert!(
        status == v8::ModuleStatus::Evaluated
          || status == v8::ModuleStatus::Errored
      );

      fn wake_module(
        scope: &mut v8::PinScope<'_, '_>,
        _args: v8::FunctionCallbackArguments<'_>,
        _rv: v8::ReturnValue,
      ) {
        let module_map = JsRealm::module_map_from(scope);
        module_map.module_waker.wake();
      }

      let promise = v8::Local::<v8::Promise>::try_from(value)
        .expect("Expected to get promise as module evaluation result");

      let wake_module_cb = Function::builder(wake_module).build(tc_scope);

      if let Some(wake_module_cb) = wake_module_cb {
        promise.then2(tc_scope, wake_module_cb, wake_module_cb);
      } else {
        // If the runtime is shutting down, we can't attach the handlers.
        // It doesn't really matter though, because they're just for waking the
        // event loop.
      }
      let dyn_import_mod_evaluate = DynImportModEvaluate {
        load_id,
        module_id: id,
        promise: v8::Global::new(tc_scope, promise),
      };

      self
        .pending_dyn_mod_evaluations
        .push(dyn_import_mod_evaluate);
    } else if tc_scope.has_terminated() || tc_scope.is_execution_terminating() {
      return Err(CoreErrorKind::EvaluateDynamicImportedModule.into_box());
    } else {
      assert_eq!(status, v8::ModuleStatus::Errored);
    }

    Ok(())
  }

  // Returns true if some dynamic import was resolved.
  fn evaluate_dyn_imports(&self, scope: &mut v8::PinScope) -> bool {
    if !self.pending_dyn_mod_evaluations.is_pending() {
      return false;
    }

    let pending = self.pending_dyn_mod_evaluations.take();
    let mut resolved_any = false;
    let mut still_pending = vec![];
    for eval in pending {
      let promise = eval.promise.open(scope);
      match promise.state() {
        v8::PromiseState::Pending => {
          still_pending.push(eval);
        }
        v8::PromiseState::Fulfilled => {
          resolved_any = true;
          self.dynamic_import_resolve(scope, eval.load_id, eval.module_id);
          self.resolve_tla_waiters(scope, eval.module_id);
        }
        v8::PromiseState::Rejected => {
          resolved_any = true;
          let exception = v8::Global::new(scope, promise.result(scope));
          self.dynamic_import_reject(scope, eval.load_id, exception.clone());
          self.reject_tla_waiters(scope, eval.module_id, exception);
        }
      }
    }
    self.pending_dyn_mod_evaluations.set(still_pending);
    resolved_any
  }

  /// Resolve all waiters that are waiting for a module's TLA to complete.
  fn resolve_tla_waiters(&self, scope: &mut v8::PinScope, module_id: ModuleId) {
    let waiters = self.pending_tla_waiters.borrow_mut().remove(&module_id);
    if let Some(waiters) = waiters
      && let Some(module) = self
        .data
        .borrow()
        .get_handle(module_id)
        .map(|handle| v8::Local::new(scope, handle))
    {
      let module_namespace = module.get_module_namespace();

      for resolver_handle in waiters {
        let resolver = resolver_handle.open(scope);
        resolver.resolve(scope, module_namespace).unwrap();
      }
      if !JsRealm::state_from_scope(scope).has_tick_scheduled() {
        scope.perform_microtask_checkpoint();
      }
    }
  }

  /// Reject all waiters that are waiting for a module's TLA to complete.
  fn reject_tla_waiters(
    &self,
    scope: &mut v8::PinScope,
    module_id: ModuleId,
    exception: v8::Global<v8::Value>,
  ) {
    let waiters = self.pending_tla_waiters.borrow_mut().remove(&module_id);
    if let Some(waiters) = waiters {
      let exception = v8::Local::new(scope, exception);
      for resolver_handle in waiters {
        let resolver = resolver_handle.open(scope);
        resolver.reject(scope, exception).unwrap();
      }
      if !JsRealm::state_from_scope(scope).has_tick_scheduled() {
        scope.perform_microtask_checkpoint();
      }
    }
  }

  pub(crate) fn dynamic_import_reject(
    &self,
    scope: &mut v8::PinScope,
    id: ModuleLoadId,
    exception: v8::Global<v8::Value>,
  ) {
    let resolver_handle = self
      .dynamic_import_map
      .borrow_mut()
      .remove(&id)
      .expect("Invalid dynamic import id")
      .resolver;
    let resolver = resolver_handle.open(scope);

    let exception = v8::Local::new(scope, exception);
    resolver.reject(scope, exception).unwrap();
    if !JsRealm::state_from_scope(scope).has_tick_scheduled() {
      scope.perform_microtask_checkpoint();
    }
  }

  pub(crate) fn dynamic_import_resolve(
    &self,
    scope: &mut v8::PinScope,
    id: ModuleLoadId,
    mod_id: ModuleId,
  ) {
    let resolver_handle = self
      .dynamic_import_map
      .borrow_mut()
      .remove(&id)
      .expect("Invalid dynamic import id")
      .resolver;
    let resolver = resolver_handle.open(scope);

    let module = self
      .data
      .borrow()
      .get_handle(mod_id)
      .map(|handle| v8::Local::new(scope, handle))
      .expect("Dyn import module info not found");
    // Resolution success
    assert_eq!(module.get_status(), v8::ModuleStatus::Evaluated);

    // IMPORTANT: No borrows to `ModuleMap` can be held at this point because
    // resolving the promise might initiate another `import()` which will
    // in turn call `bindings::host_import_module_dynamically_callback` which
    // will reach into `ModuleMap` from within the isolate.
    let module_namespace = module.get_module_namespace();
    resolver.resolve(scope, module_namespace).unwrap();
    self.dyn_module_evaluate_idle_counter.set(0);
    if !JsRealm::state_from_scope(scope).has_tick_scheduled() {
      scope.perform_microtask_checkpoint();
    }
  }

  /// Drain all ready module loading work: preparing dynamic imports,
  /// loading dynamic imports, evaluating them, and flushing code cache
  /// futures. Loops until no more progress can be made.
  ///
  /// The waker from `cx` is registered so the event loop is woken when
  /// any module future makes progress.
  pub(crate) fn poll_progress(
    &self,
    cx: &mut Context,
    scope: &mut v8::PinScope,
  ) -> Result<(), CoreError> {
    let mut has_evaluated = true;

    // TODO(mmastrac): We register this waker unconditionally because we occasionally need to re-run
    // the event loop. Eventually we will want this method to correctly wake the waker on any forward
    // progress.
    self.module_waker.register(cx.waker());

    // Run in a loop so that dynamic imports that only depend on another
    // dynamic import can be resolved in this event loop iteration.
    //
    // For example, a dynamically imported module like the following can be
    // immediately resolved after `dependency.ts` is fully evaluated, but it
    // wouldn't if not for this loop.
    //
    //    await delay(1000);
    //    await import("./dependency.ts");
    //    console.log("test")
    //
    // These dynamic import dependencies can be cross-realm:
    //
    //    await delay(1000);
    //    await new ShadowRealm().importValue("./dependency.js", "default");
    //
    while has_evaluated {
      has_evaluated = false;
      loop {
        self.drain_prepare_dyn_imports(cx, scope);
        self.drain_dyn_imports(cx, scope)?;
        self.drain_code_cache_ready(cx);

        if self.evaluate_dyn_imports(scope) {
          has_evaluated = true;
        } else {
          break;
        }
      }
    }

    Ok(())
  }

  /// Drain all ready preparing-dynamic-import futures, moving successful
  /// loads into `pending_dynamic_imports` and rejecting failures.
  fn drain_prepare_dyn_imports(
    &self,
    cx: &mut Context,
    scope: &mut v8::PinScope,
  ) {
    if !self.preparing_dynamic_imports.is_pending() {
      return;
    }

    while let Poll::Ready(Some((dyn_import_id, prepare_result))) =
      self.preparing_dynamic_imports.poll_next_unpin(cx)
    {
      match prepare_result {
        Ok(load) => {
          self
            .pending_dynamic_imports
            .push(StreamExt::into_future(load));
        }
        Err(err) => {
          let exception = err.to_v8_error(scope);
          self.dynamic_import_reject(scope, dyn_import_id, exception);
        }
      }
    }
  }

  /// Drain all ready pending-dynamic-import streams, registering loaded
  /// modules and instantiating/evaluating completed imports.
  fn drain_dyn_imports(
    &self,
    cx: &mut Context,
    scope: &mut v8::PinScope,
  ) -> Result<(), CoreError> {
    if !self.pending_dynamic_imports.is_pending() {
      return Ok(());
    }

    while let Poll::Ready(Some((maybe_result, mut load))) =
      self.pending_dynamic_imports.poll_next_unpin(cx)
    {
      let dyn_import_id = load.id();

      match maybe_result {
        Some(Ok((request, info))) => {
          // A module (not necessarily the one dynamically imported) has been
          // fetched. Create and register it, and if successful, poll for the
          // next recursive-load event related to this dynamic import.
          match load.register_and_recurse(scope, &request, info) {
            Ok(RegisterOutcome::Done) => {
              // Keep importing until it's fully drained
              self
                .pending_dynamic_imports
                .push(StreamExt::into_future(load));
            }
            Err(err) => {
              let exception = match err {
                ModuleError::Exception(e) => e,
                ModuleError::Core(e) => e.to_v8_error(scope),
                ModuleError::Concrete(e) => {
                  CoreErrorKind::Module(e).to_v8_error(scope)
                }
              };
              self.dynamic_import_reject(scope, dyn_import_id, exception);
            }
          }
        }
        Some(Err(err)) => {
          // A non-javascript error occurred; this could be due to an invalid
          // module specifier, or a problem with the source map, or a failure
          // to fetch the module source code.
          let exception = err.to_v8_error(scope);
          self.dynamic_import_reject(scope, dyn_import_id, exception);
        }
        None => {
          // Stream finished — the full module graph has been loaded.
          let state = self
            .dynamic_import_map
            .borrow()
            .get(&dyn_import_id)
            .unwrap()
            .clone();
          match state.phase {
            ModuleImportPhase::Evaluation => {
              let module_id =
                load.root_module_id().expect("Root module should be loaded");
              let result = self.instantiate_module(scope, module_id);
              if let Err(exception) = result {
                self.dynamic_import_reject(scope, dyn_import_id, exception);
              }
              self.dynamic_import_module_evaluate(
                scope,
                module_id,
                dyn_import_id,
                state,
              )?;
            }
            ModuleImportPhase::Defer => {
              // For defer phase imports, the module is instantiated but NOT
              // eagerly evaluated. We call evaluate_for_import_defer which
              // gathers and evaluates async transitive dependencies, then
              // resolve with a deferred namespace that triggers evaluation
              // on first property access.
              let module_id =
                load.root_module_id().expect("Root module should be loaded");
              let result = self.instantiate_module(scope, module_id);
              if let Err(exception) = result {
                self.dynamic_import_reject(scope, dyn_import_id, exception);
                continue;
              }
              let module_handle =
                self.get_handle(module_id).expect("ModuleInfo not found");

              v8::tc_scope!(let tc_scope, scope);

              let cped = v8::Local::new(tc_scope, state.cped.clone());
              tc_scope.set_continuation_preserved_embedder_data(cped);

              let module = v8::Local::new(tc_scope, &module_handle);

              // Gather async transitive dependencies. Returns a promise
              // that resolves when all async deps are ready.
              let maybe_promise = module.evaluate_for_import_defer(tc_scope);

              let Some(promise_val) = maybe_promise else {
                let exception = tc_scope.exception().unwrap();
                let exception = v8::Global::new(tc_scope, exception);
                self.dynamic_import_reject(tc_scope, dyn_import_id, exception);
                continue;
              };

              // Get the deferred namespace — this triggers evaluation on
              // first property access.
              let module_namespace = module
                .get_module_namespace_with_phase(v8::ModuleImportPhase::kDefer);

              let promise = v8::Local::<v8::Promise>::try_from(promise_val)
                .expect("evaluate_for_import_defer should return a promise");

              match promise.state() {
                v8::PromiseState::Fulfilled => {
                  // All async deps are ready, resolve immediately.
                  let resolver_handle = self
                    .dynamic_import_map
                    .borrow_mut()
                    .remove(&dyn_import_id)
                    .expect("Invalid dynamic import id")
                    .resolver;
                  let resolver = resolver_handle.open(tc_scope);
                  resolver.resolve(tc_scope, module_namespace).unwrap();
                  tc_scope.perform_microtask_checkpoint();
                }
                v8::PromiseState::Rejected => {
                  let err = promise.result(tc_scope);
                  let err = v8::Global::new(tc_scope, err);
                  self.dynamic_import_reject(tc_scope, dyn_import_id, err);
                }
                v8::PromiseState::Pending => {
                  // Async deps still loading. Store for later resolution.
                  // The module_waker will wake us when the promise settles.
                  fn wake_module(
                    scope: &mut v8::PinScope<'_, '_>,
                    _args: v8::FunctionCallbackArguments<'_>,
                    _rv: v8::ReturnValue,
                  ) {
                    let module_map = JsRealm::module_map_from(scope);
                    module_map.module_waker.wake();
                  }

                  let wake_module_cb =
                    v8::Function::builder(wake_module).build(tc_scope);
                  if let Some(wake_module_cb) = wake_module_cb {
                    promise.then2(tc_scope, wake_module_cb, wake_module_cb);
                  }

                  let dyn_import_mod_evaluate = DynImportModEvaluate {
                    load_id: dyn_import_id,
                    module_id,
                    promise: v8::Global::new(tc_scope, promise),
                  };
                  self
                    .pending_dyn_mod_evaluations
                    .push(dyn_import_mod_evaluate);
                }
              }
            }
            ModuleImportPhase::Source => {
              let module_reference = load.root_module_reference().expect(
                "Root module reference had to have been resolved to get here.",
              );
              let key = ModuleSourceKey::from_reference(module_reference);
              let source = {
                let data = self.data.borrow();
                let source = data.sources.get(&key).expect("Source had to have been inserted successfully, or recursion would error.");
                v8::Local::new(scope, source).into()
              };
              let resolver = state.resolver.open(scope);
              resolver.resolve(scope, source).unwrap();
            }
          }
        }
      }
    }

    Ok(())
  }

  /// Drain all ready code-cache futures.
  fn drain_code_cache_ready(&self, cx: &mut Context) {
    if !self.code_cache_ready_futs.is_pending() {
      return;
    }

    while let Poll::Ready(Some(_)) =
      self.code_cache_ready_futs.poll_next_unpin(cx)
    {}
  }

  pub(crate) fn get_module<'s, 'i>(
    &self,
    scope: &v8::PinScope<'s, 'i>,
    module_id: ModuleId,
  ) -> Option<v8::Local<'s, v8::Module>> {
    self
      .data
      .borrow()
      .get_handle(module_id)
      .map(|g| v8::Local::new(scope, g))
  }

  /// Returns the namespace object of a module.
  ///
  /// This is only available after module evaluation has completed.
  /// This function panics if module has not been instantiated.
  pub fn get_module_namespace(
    &self,
    scope: &mut v8::PinScope,
    module_id: ModuleId,
  ) -> Result<v8::Global<v8::Object>, CoreError> {
    let module_handle = self
      .data
      .borrow()
      .get_handle(module_id)
      .expect("ModuleInfo not found");

    let module = module_handle.open(scope);

    if module.get_status() == v8::ModuleStatus::Errored {
      let exception = module.get_exception();
      return exception_to_err_result(scope, exception, false, false)
        .map_err(|e| CoreErrorKind::Js(e).into_box());
    }

    assert!(matches!(
      module.get_status(),
      v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated
    ));

    let module_namespace: v8::Local<v8::Object> =
      v8::Local::try_from(module.get_module_namespace())?;

    Ok(v8::Global::new(scope, module_namespace))
  }

  fn get_stalled_top_level_await_message_for_module(
    &self,
    scope: &mut v8::PinScope,
    module_id: ModuleId,
  ) -> Vec<v8::Global<v8::Message>> {
    let data = self.data.borrow();
    let module_handle = data.handles.get(module_id).unwrap();

    let module = v8::Local::new(scope, module_handle);
    // v8::Module::GetStalledTopLevelAwaitMessage() must not be called on
    // a synthetic module.
    if module.is_synthetic_module() {
      return vec![];
    }

    let stalled = module.get_stalled_top_level_await_message(scope);
    let mut messages = vec![];
    for (_, message) in stalled {
      messages.push(v8::Global::new(scope, message));
    }
    messages
  }

  pub(crate) fn find_stalled_top_level_await(
    &self,
    scope: &mut v8::PinScope,
  ) -> Vec<v8::Global<v8::Message>> {
    // First check if that's root module
    let root_module_id = self
      .data
      .borrow()
      .info
      .iter()
      .filter(|m| m.main)
      .map(|m| m.id)
      .next();

    if let Some(root_module_id) = root_module_id {
      let messages = self
        .get_stalled_top_level_await_message_for_module(scope, root_module_id);
      if !messages.is_empty() {
        return messages;
      }
    }

    // It wasn't a top module, so iterate over all modules and try to find
    // any with stalled top level await
    for module_id in 0..self.data.borrow().handles.len() {
      let messages =
        self.get_stalled_top_level_await_message_for_module(scope, module_id);
      if !messages.is_empty() {
        return messages;
      }
    }

    vec![]
  }

  /// Load and evaluate an ES module provided the specifier and source code.
  ///
  /// The module should not have Top-Level Await (that is, it should be
  /// possible to evaluate it synchronously).
  ///
  /// It is caller's responsibility to ensure that not duplicate specifiers are
  /// passed to this method.
  pub(crate) fn lazy_load_es_module_with_code(
    &self,
    scope: &mut v8::PinScope,
    module_specifier: &str,
    source_code: ModuleCodeString,
    code_cache_info: Option<CodeCacheInfo>,
  ) -> Result<v8::Global<v8::Value>, CoreError> {
    let specifier = ModuleSpecifier::parse(module_specifier)?;
    let mod_id = self
      .new_es_module(
        scope,
        false,
        specifier.into(),
        source_code,
        false,
        code_cache_info,
      )
      .map_err(|e| e.into_error(scope, false, true))?;

    self.instantiate_module(scope, mod_id).map_err(|e| {
      let exception = v8::Local::new(scope, e);
      exception_to_err(scope, exception, false, true)
    })?;

    let module_handle = self.get_handle(mod_id).unwrap();
    let module_local = v8::Local::<v8::Module>::new(scope, module_handle);

    let status = module_local.get_status();
    assert_eq!(status, v8::ModuleStatus::Instantiated);

    let value = module_local.evaluate(scope).unwrap();
    // Under Explicit microtask policy, drain microtasks so the module
    // evaluation promise resolves for synchronous modules.
    //
    // However, skip the checkpoint when we are inside a top-level
    // `module.evaluate()` call (i.e. `evaluating_top_level` is set).
    // Draining microtasks at this point can prematurely resolve
    // TLA-related microtasks (e.g. `await` resume jobs from eagerly-
    // resolved async ops), which prevents the module evaluation promise
    // from settling correctly later.
    if !self.evaluating_top_level.get() {
      scope.perform_microtask_checkpoint();
    }
    let promise = v8::Local::<v8::Promise>::try_from(value).unwrap();
    let result = promise.result(scope);
    if !result.is_undefined() {
      return Err(
        CoreErrorKind::Js(exception_to_err(scope, result, false, true))
          .into_box(),
      );
    }

    let status = module_local.get_status();
    assert_eq!(status, v8::ModuleStatus::Evaluated);

    let mod_ns = module_local.get_module_namespace();

    Ok(v8::Global::new(scope, mod_ns))
  }

  /// Check if a lazy-loaded ESM module is known to exist for the given
  /// specifier. This checks the metadata set which survives snapshotting,
  /// not just the source code map.
  pub(crate) fn has_lazy_esm_source(&self, specifier: &str) -> bool {
    self
      .data
      .borrow()
      .known_lazy_esm
      .borrow()
      .contains(specifier)
  }

  /// Get a lazy-loaded ESM source by specifier. Returns a cheap clone of the
  /// source code if found, keeping it in the lazy sources map so concurrent
  /// loads of the same lazy specifier from independent `RecursiveModuleLoad`
  /// instances each get their own copy. The duplicate `ModuleSource`s are
  /// deduplicated by `new_module_with_pending`'s `get_id` check at register
  /// time.
  pub(crate) fn take_lazy_esm_source(
    &self,
    specifier: &str,
  ) -> Option<ModuleCodeString> {
    let data = self.data.borrow();
    let mut sources = data.lazy_esm_sources.borrow_mut();
    let entry = sources.get_mut(specifier)?;
    // `into_cheap_copy` always returns two cheap handles to the same backing
    // storage (Arc or Static), so we can keep one in the map and return the
    // other. The Owned variant gets promoted to Arc in the process.
    let placeholder = ModuleCodeString::from_static("");
    let owned = std::mem::replace(entry, placeholder);
    let (keep, give) = owned.into_cheap_copy();
    *entry = keep;
    data
      .consumed_lazy_specifiers
      .borrow_mut()
      .insert(specifier.to_string());
    Some(give)
  }

  pub(crate) fn add_lazy_loaded_esm_source(
    &self,
    specifier: ModuleName,
    code: ModuleCodeString,
  ) {
    let data = self.data.borrow_mut();
    data
      .known_lazy_esm
      .borrow_mut()
      .insert(specifier.as_str().to_string());
    assert!(
      data
        .lazy_esm_sources
        .borrow_mut()
        .insert(specifier, code)
        .is_none()
    );
  }

  /// Lazy load and evaluate an ES module. Only modules that have been added
  /// during build time can be executed (the ones stored in
  /// `ModuleMapData::lazy_esm_sources`), not _any, random_ module.
  /// Sync load + evaluate path for a `synthetic_esm` module. Used by the
  /// dynamic-import fast path. Returns the module namespace.
  pub(crate) fn lazy_load_synthetic_esm_module(
    &self,
    scope: &mut v8::PinScope,
    module_specifier: &str,
  ) -> Result<v8::Global<v8::Value>, CoreError> {
    // Use existing module if already constructed.
    {
      let data = self.data.borrow();
      if let Some(id) = data.get_id(module_specifier, RequestedModuleType::None)
      {
        let handle = data.get_handle(id).unwrap();
        let handle_local = v8::Local::new(scope, handle);
        if handle_local.get_status() == v8::ModuleStatus::Instantiated {
          let value = handle_local.evaluate(scope).unwrap();
          if !self.evaluating_top_level.get() {
            scope.perform_microtask_checkpoint();
          }
          let promise = v8::Local::<v8::Promise>::try_from(value).unwrap();
          let result = promise.result(scope);
          if !result.is_undefined() {
            return Err(
              CoreErrorKind::Js(exception_to_err(scope, result, false, true))
                .into_box(),
            );
          }
        }
        return Ok(v8::Global::new(scope, handle_local.get_module_namespace()));
      }
    }

    let module_id = self.build_synthetic_esm_module(scope, module_specifier)?;
    let handle = self.get_handle(module_id).unwrap();
    let handle_local = v8::Local::new(scope, handle);
    let value = handle_local.evaluate(scope).unwrap();
    if !self.evaluating_top_level.get() {
      scope.perform_microtask_checkpoint();
    }
    let promise = v8::Local::<v8::Promise>::try_from(value).unwrap();
    let result = promise.result(scope);
    if !result.is_undefined() {
      return Err(
        CoreErrorKind::Js(exception_to_err(scope, result, false, true))
          .into_box(),
      );
    }
    Ok(v8::Global::new(scope, handle_local.get_module_namespace()))
  }

  pub(crate) fn lazy_load_esm_module(
    &self,
    scope: &mut v8::PinScope,
    module_specifier: &str,
  ) -> Result<v8::Global<v8::Value>, CoreError> {
    let lazy_esm_sources = self.data.borrow().lazy_esm_sources.clone();
    let loader = LazyEsmModuleLoader::new(lazy_esm_sources);

    // Check if this module has already been loaded. We release the
    // `self.data` borrow before doing anything that could re-enter the
    // module map (notably `module.evaluate(scope)`, which can recursively
    // compile dependent modules and would otherwise panic with a
    // `RefCell already borrowed` at `new_module_from_js_source`).
    let cached_id_and_handle = {
      let module_map_data = self.data.borrow();
      module_map_data
        .get_id(module_specifier, RequestedModuleType::None)
        .and_then(|id| module_map_data.get_handle(id).map(|h| (id, h)))
    };
    if let Some((cached_id, handle)) = cached_id_and_handle {
      crate::modules::import_graph::record_lazy_esm_cached(
        scope,
        module_specifier,
      );
      let handle_local = v8::Local::new(scope, handle);
      // The module may be present in the map but not yet instantiated --
      // e.g. when this lazy load fires from inside a sibling module's
      // resolve-callback (a `synthetic_esm` backing script synchronously
      // calling `op_lazy_load_esm` for a peer that the surrounding
      // `RecursiveModuleLoad` has registered but not yet instantiated).
      // `get_module_namespace` requires Instantiated+, so drive the module
      // forward synchronously here.
      if handle_local.get_status() == v8::ModuleStatus::Uninstantiated {
        self.instantiate_module(scope, cached_id).map_err(|e| {
          let exception = v8::Local::new(scope, e);
          exception_to_err(scope, exception, false, true)
        })?;
      }
      // Returning the namespace before evaluation leaves `export const`
      // bindings in the temporal dead zone, so trigger evaluation here.
      if handle_local.get_status() == v8::ModuleStatus::Instantiated {
        let value = handle_local.evaluate(scope).unwrap();
        if !self.evaluating_top_level.get() {
          scope.perform_microtask_checkpoint();
        }
        let promise = v8::Local::<v8::Promise>::try_from(value).unwrap();
        let result = promise.result(scope);
        if !result.is_undefined() {
          return Err(
            CoreErrorKind::Js(exception_to_err(scope, result, false, true))
              .into_box(),
          );
        }
      }
      let module = v8::Global::new(scope, handle_local.get_module_namespace());
      return Ok(module);
    }

    // Cache miss: real load incoming. This is the call that pays the
    // parse/compile/evaluate cost at runtime.
    crate::modules::import_graph::record_lazy_esm(scope, module_specifier);

    let specifier = ModuleSpecifier::parse(module_specifier)?;

    let load_response = loader.load(
      &specifier,
      None,
      ModuleLoadOptions {
        is_dynamic_import: false,
        is_synchronous: false,
        requested_module_type: RequestedModuleType::None,
      },
    );

    let source = match load_response {
      ModuleLoadResponse::Sync(result) => result,
      ModuleLoadResponse::Async(fut) => futures::executor::block_on(fut),
    }?;

    // `LazyEsmModuleLoader` only knows the in-binary static sources and has no
    // DENO_DIR access, so `source.code_cache` is always `None` here. Read the
    // persisted V8 code cache from the REAL loader (Cli/EmbeddedModuleLoader)
    // instead — the same seam the residual `lazy_loaded_js` path uses. Without
    // this, residual ESM (node:process, node:module, the stream/net/tty
    // closure) re-pays parse+compile in every isolate on every cold start.
    let source_code = ModuleSource::get_string_source(source.code);
    // Hash key for the on-disk cache. `v8_string` borrows `&source_code`, so it
    // stays usable for the compile below.
    let v8_source = source_code.v8_string(scope).unwrap();
    // Build a `CodeCacheInfo` whenever the loader returns `Some` — even when
    // `data` is `None` (cold run). The `Some`-with-`data: None` case is what
    // arms the write side so the first run stores; warm runs consume.
    let code_cache_info = self
      .loader
      .borrow()
      .get_code_cache(&specifier, &v8_source)
      .map(|info| {
        let loader = self.loader.borrow().clone();
        CodeCacheInfo {
          data: info.data,
          // `specifier` is unused after this, so move it straight in.
          ready_callback: Box::new(move |cache| {
            loader.code_cache_ready(specifier, info.hash, cache)
          }),
        }
      });

    self.lazy_load_es_module_with_code(
      scope,
      module_specifier,
      source_code,
      code_cache_info,
    )
  }

  pub(crate) fn add_lazy_loaded_script_source(
    &self,
    specifier: ModuleName,
    code: ModuleCodeString,
  ) {
    let data = self.data.borrow_mut();
    assert!(
      data
        .lazy_script_sources
        .borrow_mut()
        .insert(specifier, code)
        .is_none(),
      "Duplicate lazy script source"
    );
  }

  /// Check if a specifier was registered as a `synthetic_esm` module.
  pub(crate) fn has_synthetic_esm_module(&self, specifier: &str) -> bool {
    let data = self.data.borrow();
    let modules = data.synthetic_esm_modules.borrow();
    let key: ModuleName = String::from(specifier).into();
    modules.contains_key(&key)
  }

  /// Register a `(module_specifier -> backing_script_specifier)` mapping
  /// for the `synthetic_esm` dispatch. Called at extension init from each
  /// extension's `synthetic_esm_modules` list.
  pub(crate) fn add_synthetic_esm_module(
    &self,
    module_specifier: ModuleName,
    backing_specifier: ModuleName,
  ) {
    let data = self.data.borrow();
    assert!(
      data
        .synthetic_esm_modules
        .borrow_mut()
        .insert(module_specifier, backing_specifier)
        .is_none(),
      "Duplicate synthetic_esm module mapping"
    );
  }

  /// Build a synthetic module for a `synthetic_esm`-registered specifier
  /// by evaluating its backing script (cache hit on second+ call) and
  /// deriving exports from the returned IIFE object. Returns the new
  /// `ModuleId`, or an error if the specifier is not registered or the
  /// backing script did not return an object.
  pub(crate) fn build_synthetic_esm_module(
    &self,
    scope: &mut v8::PinScope,
    specifier: &str,
  ) -> Result<ModuleId, CoreError> {
    let backing_specifier = {
      let data = self.data.borrow();
      let modules = data.synthetic_esm_modules.borrow();
      let key: ModuleName = String::from(specifier).into();
      modules.get(&key).map(|v| v.as_str().to_string())
    }
    .ok_or_else(|| {
      CoreError::from(JsErrorBox::generic(format!(
        "Specifier {specifier} is not a synthetic_esm module"
      )))
    })?;

    let exports_global = self.load_ext_script(scope, &backing_specifier)?;
    let exports_local = v8::Local::new(scope, exports_global);
    let exports_obj = v8::Local::<v8::Object>::try_from(exports_local)
      .map_err(|_| {
        CoreError::from(JsErrorBox::type_error(format!(
          "synthetic_esm backing script {backing_specifier} did not return an object"
        )))
      })?;

    Ok(self.new_synthetic_module_from_exports_object(
      scope,
      String::from(specifier),
      exports_obj,
    ))
  }

  /// Convenience wrapper around `build_synthetic_esm_module` for the V8
  /// `module_resolve_callback` path: returns the module's V8 handle if
  /// `specifier` is registered as a `synthetic_esm` target, or `None` if
  /// not. Throws into `scope` on evaluation failure.
  fn try_resolve_synthetic_esm<'s, 'i>(
    &self,
    scope: &mut v8::PinScope<'s, 'i>,
    specifier: &str,
  ) -> Option<v8::Local<'s, v8::Module>> {
    if !self.has_synthetic_esm_module(specifier) {
      return None;
    }
    match self.build_synthetic_esm_module(scope, specifier) {
      Ok(module_id) => {
        let handle = self.get_handle(module_id)?;
        Some(v8::Local::new(scope, handle))
      }
      Err(e) => {
        crate::error::throw_js_error_class(scope, &e);
        None
      }
    }
  }

  /// Load and evaluate a script on demand. The evaluated result is
  /// cached in `loaded_script_results` so later callers (the JS
  /// `Deno.core.loadExtScript()` op, the `synthetic_esm` dispatch, etc.)
  /// share a single evaluation — the source is consumed on first eval,
  /// and re-evaluating polyfill IIFEs would clobber registered hooks and
  /// duplicate class identities. Circular dependencies are detected and
  /// cause an error.
  pub(crate) fn load_ext_script(
    &self,
    scope: &mut v8::PinScope,
    specifier: &str,
  ) -> Result<v8::Global<v8::Value>, CoreError> {
    crate::modules::import_graph::record_lazy_script(scope, specifier);
    let specifier_str = String::from(specifier);
    let data = self.data.borrow();

    // Cache hit: return the previously evaluated result.
    {
      let specifier_key: ModuleName = specifier_str.clone().into();
      if let Some(cached) =
        data.loaded_script_results.borrow().get(&specifier_key)
      {
        return Ok(cached.clone());
      }
    }

    // Circular dependency detection.
    {
      let specifier_key: ModuleName = specifier_str.clone().into();
      let loading = data.lazy_script_loading.borrow();
      if loading.contains(&specifier_key) {
        return Err(
          JsErrorBox::generic(format!(
            "Circular dependency detected when loading script \"{specifier}\""
          ))
          .into(),
        );
      }
    }

    // Look up source.
    let source = {
      let specifier_key: ModuleName = specifier_str.clone().into();
      let mut sources = data.lazy_script_sources.borrow_mut();
      sources.remove(&specifier_key)
    };
    let source = match source {
      Some(s) => s,
      None => {
        return Err(
          JsErrorBox::generic(format!(
            "Script \"{specifier}\" cannot be lazy-loaded as it was not included in the binary."
          ))
          .into(),
        );
      }
    };

    data
      .consumed_lazy_specifiers
      .borrow_mut()
      .insert(specifier_str.clone());

    // Mark as loading for circular dep detection.
    data
      .lazy_script_loading
      .borrow_mut()
      .insert(specifier_str.clone().into());

    // We need to drop `data` before executing the script, since the script
    // may call back into `load_ext_script` for its own dependencies.
    drop(data);

    // Each script's IIFE preamble destructures the snapshot-time
    // `__bootstrap` view (a frozen clone of `core.ops` captured before
    // `removeImportedOps()` runs). To avoid leaving `__bootstrap` on
    // `globalThis` (where user code or `Object.keys` would see it), we
    // compile the source as the body of a function with one named
    // parameter `__bootstrap` and invoke it with the captured value. The
    // script's `(function () { ... })()` is the function body's single
    // expression, so we wrap it with `return ( ... );` to surface the
    // IIFE's result as the function's return value.
    let name = v8::String::new(scope, specifier).unwrap();
    let origin = v8::ScriptOrigin::new(
      scope,
      name.into(),
      0,
      0,
      false,
      -1,
      None,
      false,
      false,
      false,
      None,
    );

    v8::tc_scope!(let tc_scope, scope);

    // The compile_function body is `"use strict"; return (<IIFE>);`. For
    // residual lazy scripts this wrapping is performed at build time
    // (`cli/snapshot/build.rs`), so `source` is a `&'static` external string we
    // can hand to V8 without an owned heap copy (avoiding a per-script source
    // string in the V8 heap). Sources that arrive unwrapped (e.g. consumed
    // during snapshot creation, before that path is build-time wrapped) are
    // wrapped here at runtime via `wrap_lazy_ext_script`.
    let v8_source = if AsRef::<str>::as_ref(&source)
      .starts_with("\"use strict\"; return (")
    {
      // Build-time wrapped residual: hand V8 the `&'static` external string
      // directly so the source stays off the V8 heap (file-backed/clean).
      source.v8_string(tc_scope).unwrap()
    } else {
      // Unwrapped (e.g. consumed during snapshot creation): wrap at runtime.
      let wrapped_source = wrap_lazy_ext_script(AsRef::<str>::as_ref(&source));
      v8::String::new(tc_scope, &wrapped_source).unwrap()
    };
    let bootstrap_param = v8::String::new(tc_scope, "__bootstrap").unwrap();
    // Persist a V8 code cache for this residual `lazy_loaded_js` module through
    // the same on-disk (DENO_DIR) cache user scripts use, keyed by specifier +
    // source hash. Residuals ship source-only in the binary, so without this a
    // node-heavy program re-pays parse+compile of the whole node-polyfill
    // closure at runtime on every cold start. The first run compiles and
    // stores; warm runs consume and skip parse+compile. Producing and consuming
    // binary are identical, so the cache is always accepted.
    let cache_specifier = crate::ModuleSpecifier::parse(&specifier_str).ok();
    let code_cache_info = cache_specifier
      .as_ref()
      .and_then(|spec| self.loader.borrow().get_code_cache(spec, &v8_source));
    let (mut compile_source, compile_options) =
      match code_cache_info.as_ref().and_then(|i| i.data.as_ref()) {
        Some(data) => (
          v8::script_compiler::Source::new_with_cached_data(
            v8_source,
            Some(&origin),
            v8::CachedData::new(data),
          ),
          v8::script_compiler::CompileOptions::ConsumeCodeCache,
        ),
        None => (
          v8::script_compiler::Source::new(v8_source, Some(&origin)),
          v8::script_compiler::CompileOptions::NoCompileOptions,
        ),
      };
    let function = match v8::script_compiler::compile_function(
      tc_scope,
      &mut compile_source,
      &[bootstrap_param],
      &[],
      compile_options,
      v8::script_compiler::NoCacheReason::NoReason,
    ) {
      Some(f) => f,
      None => {
        let exception = tc_scope.exception().unwrap();
        let err = JsError::from_v8_exception(tc_scope, exception);
        self
          .data
          .borrow()
          .lazy_script_loading
          .borrow_mut()
          .remove(&ModuleName::from(specifier_str.clone()));
        return Err(CoreErrorKind::Js(err).into_box());
      }
    };
    // Store the freshly-compiled cache on the first run (cold), or if V8
    // rejected the existing cache (e.g. the source changed).
    let rejected = compile_source
      .get_cached_data()
      .map(|d| d.rejected())
      .unwrap_or(true);
    let had_data = code_cache_info
      .as_ref()
      .map(|i| i.data.is_some())
      .unwrap_or(false);
    if (!had_data || rejected)
      && let (Some(spec), Some(info)) =
        (cache_specifier.as_ref(), code_cache_info.as_ref())
      && let Some(cache) = function.create_code_cache()
    {
      let fut = self.loader.borrow().code_cache_ready(
        spec.clone(),
        info.hash,
        &cache[..],
      );
      self.code_cache_ready_futs.push(fut);
    }

    let captured = self.data.borrow().captured_bootstrap.borrow().clone();
    let bootstrap_arg = match &captured {
      Some(global) => v8::Local::new(tc_scope, global),
      None => v8::undefined(tc_scope).into(),
    };
    let undefined: v8::Local<v8::Value> = v8::undefined(tc_scope).into();
    let result = match function.call(tc_scope, undefined, &[bootstrap_arg]) {
      Some(value) => v8::Global::new(tc_scope, value),
      None => {
        assert!(tc_scope.has_caught());
        let exception = tc_scope.exception().unwrap();
        let err = JsError::from_v8_exception(tc_scope, exception);
        self
          .data
          .borrow()
          .lazy_script_loading
          .borrow_mut()
          .remove(&ModuleName::from(specifier_str.clone()));
        return Err(CoreErrorKind::Js(err).into_box());
      }
    };

    // Remove from loading set and cache the result.
    {
      let data = self.data.borrow();
      data
        .lazy_script_loading
        .borrow_mut()
        .remove(&ModuleName::from(specifier_str.clone()));
      data
        .loaded_script_results
        .borrow_mut()
        .insert(ModuleName::from(specifier_str), result.clone());
    }

    Ok(result)
  }

  /// Stash the snapshot-time `__bootstrap` view registered from JS via
  /// `op_set_captured_bootstrap`. `load_ext_script` injects this value as
  /// the `__bootstrap` parameter when invoking each lazy script's compiled
  /// function — keeping the value off `globalThis` so user code never sees
  /// it.
  pub(crate) fn set_captured_bootstrap(&self, value: v8::Global<v8::Value>) {
    *self.data.borrow().captured_bootstrap.borrow_mut() = Some(value);
  }

  /// The snapshot-time `__bootstrap` view stashed by `set_captured_bootstrap`,
  /// if any. Used by the deferred fast-call upgrade to also update the cloned
  /// `core.ops` that residual ext modules read through.
  pub(crate) fn captured_bootstrap(&self) -> Option<v8::Global<v8::Value>> {
    self.data.borrow().captured_bootstrap.borrow().clone()
  }
}

/// Skip past a lazy-loaded script's leading prologue (line/block comments and
/// directive prologue like `"use strict";`) and return a slice that starts at
/// the IIFE's opening `(function`. Returns `None` if no such opener is found
/// — callers fall back to using the original source unchanged.
fn strip_script_prologue(source: &str) -> Option<&str> {
  let mut rest = source;
  loop {
    let trimmed = rest.trim_start();
    if let Some(after) = trimmed.strip_prefix("//") {
      // Line comment — skip to end of line.
      let nl = after.find('\n').map(|i| i + 1).unwrap_or(after.len());
      rest = &after[nl..];
      continue;
    }
    if let Some(after) = trimmed.strip_prefix("/*") {
      // Block comment — skip to closing `*/`.
      let end = after.find("*/").map(|i| i + 2).unwrap_or(after.len());
      rest = &after[end..];
      continue;
    }
    // Directive prologue: `"use strict";` or `'use strict';` (or any other
    // string-literal directive). A directive is a string literal followed
    // by a `;`/newline at the statement boundary.
    if trimmed.starts_with('"') || trimmed.starts_with('\'') {
      let quote = trimmed.as_bytes()[0];
      let after_quote = &trimmed[1..];
      if let Some(close) = after_quote.find(quote as char) {
        let after_str = after_quote[close + 1..].trim_start();
        if let Some(after_semi) = after_str.strip_prefix(';') {
          rest = after_semi;
          continue;
        }
      }
    }
    if trimmed.starts_with("(function") {
      return Some(trimmed);
    }
    return None;
  }
}

/// Wrap a lazy ext-script (`loadExtScript`) source into the `compile_function`
/// body we evaluate at load time: `"use strict"; return (<IIFE>);`.
///
/// The IIFE's completion value is the script's exports object (what the
/// original `Script::run` produced). We strip the leading prologue (comments +
/// any `"use strict";` directive — a bare directive is a statement and can't
/// sit inside `return ( ... )`) and the trailing `;`/whitespace so the IIFE is
/// the single operand of `return ( ... )`, and re-emit `"use strict";` at the
/// head so the body still runs in strict mode.
///
/// Performed at build time for residual lazy scripts so the stored source is a
/// `&'static` string that compiles without an owned heap copy; also used as the
/// runtime fallback for sources that arrive unwrapped.
pub fn wrap_lazy_ext_script(source: &str) -> String {
  let expr_start = strip_script_prologue(source).unwrap_or(source);
  let trimmed =
    expr_start.trim_end_matches(|c: char| c.is_whitespace() || c == ';');
  format!("\"use strict\"; return ({trimmed});")
}

// Clippy thinks the return value doesn't need to be an Option, it's unaware
// of the mapping that MapFnFrom<F> does for ResolveModuleCallback.
#[allow(
  clippy::unnecessary_wraps,
  reason = "required by MapFnFrom<F> for ResolveModuleCallback"
)]
pub(crate) fn synthetic_module_evaluation_steps<'s>(
  context: v8::Local<'s, v8::Context>,
  module: v8::Local<'s, v8::Module>,
) -> Option<v8::Local<'s, v8::Value>> {
  // SAFETY: `CallbackScope` can be safely constructed from `Local<Context>`
  v8::callback_scope!(unsafe scope, context);
  v8::tc_scope!(tc_scope, scope);

  let module_map = JsRealm::module_map_from(tc_scope);

  let handle = v8::Global::<v8::Module>::new(tc_scope, module);
  let exports = module_map
    .data
    .borrow_mut()
    .synthetic_module_exports_store
    .remove(&handle)
    .unwrap();

  for (export_name, export_value) in exports {
    let name = v8::Local::new(tc_scope, export_name);
    let value = v8::Local::new(tc_scope, export_value);

    // This should never fail
    assert!(
      module
        .set_synthetic_module_export(tc_scope, name, value)
        .unwrap()
    );
    assert!(!tc_scope.has_caught());
  }

  // Since Top-Level Await is active we need to return a promise.
  // This promise is resolved immediately.
  let resolver = v8::PromiseResolver::new(tc_scope).unwrap();
  let undefined = v8::undefined(tc_scope);
  resolver.resolve(tc_scope, undefined.into());
  Some(resolver.get_promise(tc_scope).into())
}

pub fn script_origin<'s, 'i>(
  s: &mut v8::PinScope<'s, 'i>,
  resource_name: v8::Local<'s, v8::String>,
  is_module: bool,
  host_defined_options: Option<v8::Local<'s, v8::Data>>,
) -> v8::ScriptOrigin<'s> {
  v8::ScriptOrigin::new(
    s,
    resource_name.into(),
    0,
    0,
    false,
    0,
    None,
    false,
    false,
    is_module,
    host_defined_options,
  )
}

/// Helper injected into the synthetic module for `.wasm` files that have global
/// exports. Per the Wasm ESM integration, a global export is unwrapped to its
/// underlying JS value (e.g. an `i32` global exports the number directly)
/// instead of being exposed as a `WebAssembly.Global` object, matching Node.js.
/// Reading `.value` throws for `v128` globals, so we fall back to the
/// `WebAssembly.Global` object in that case. The value is read once at
/// instantiation (a snapshot), so a later mutation of a mutable global is not
/// reflected in the export, which also matches Node. Wasm modules importing
/// the global are not affected by the snapshot: they link against the
/// original `WebAssembly.Global` via `import.meta.wasmInstances`.
const WASM_GLOBAL_UNWRAP_HELPER: &str = "const unwrapWasmGlobal = (g) => { try { return g.value; } catch { return g; } };\n";

/// Whether a Wasm export is a global. The export kind is read directly from the
/// export section, so this is reliable even though we parse the module with
/// `skip_types: true` and never resolve the global's value type.
fn is_wasm_global_export(export_type: &wasm_dep_analyzer::ExportType) -> bool {
  matches!(export_type, wasm_dep_analyzer::ExportType::Global(_))
}

/// Whether a Wasm import is a global. Like [`is_wasm_global_export`], the import
/// kind is read directly from the import section, independent of the global's
/// value type.
fn is_wasm_global_import(import_type: &wasm_dep_analyzer::ImportType) -> bool {
  matches!(import_type, wasm_dep_analyzer::ImportType::Global(_))
}

fn render_js_wasm_module(specifier: &str, wasm_deps: WasmDeps) -> String {
  struct NamedImport {
    escaped_name: String,
    is_global: bool,
  }

  struct ImportInfo {
    key_escaped: String,
    named_imports: Vec<NamedImport>,
    has_global_import: bool,
  }

  fn aggregate_wasm_module_imports<'a>(
    imports: &'a [wasm_dep_analyzer::Import],
  ) -> IndexMap<&'a str, ImportInfo> {
    let mut imports_map = IndexMap::with_capacity(imports.len());

    for import in imports {
      let entry =
        imports_map
          .entry(import.module)
          .or_insert_with(|| ImportInfo {
            key_escaped: import.module.escape_default().to_string(),
            named_imports: Vec::new(),
            has_global_import: false,
          });
      let is_global = is_wasm_global_import(&import.import_type);
      entry.has_global_import |= is_global;
      entry.named_imports.push(NamedImport {
        escaped_name: import.name.escape_default().to_string(),
        is_global,
      });
    }

    imports_map
  }

  let aggregated_imports = aggregate_wasm_module_imports(&wasm_deps.imports);
  let exports = wasm_deps
    .exports
    .iter()
    .map(|e| {
      let escaped_name = if e.name == "default" {
        Cow::Borrowed(e.name)
      } else {
        Cow::Owned(e.name.escape_default().to_string())
      };
      (escaped_name, is_wasm_global_export(&e.export_type))
    })
    .collect::<Vec<_>>();
  let has_global_export = exports.iter().any(|(_, is_global)| *is_global);

  StringBuilder::build(|builder| {
    builder.append("import source wasmMod from \"");
    builder.append(specifier);
    builder.append("\";\n");

    // A module with global exports registers its instance exports under its
    // own namespace in `import.meta.wasmInstances`, so that an importing Wasm
    // module can link against the original `WebAssembly.Global` objects.
    if has_global_export {
      builder.append("import * as selfNs from \"");
      builder.append(specifier);
      builder.append("\";\n");
    }

    if !aggregated_imports.is_empty() {
      for (i, (_, import_info)) in aggregated_imports.iter().enumerate() {
        if import_info.has_global_import {
          builder.append("import * as import_ns_");
          builder.append(i);
          builder.append(" from \"");
          builder.append(&import_info.key_escaped);
          builder.append("\";\n");
        }
        builder.append("import { ");
        for (name_index, named_import) in import_info.named_imports.iter().enumerate() {
          if name_index > 0 {
            builder.append(", ");
          }
          builder.append('"');
          builder.append(&named_import.escaped_name);
          builder.append("\" as import_");
          builder.append(i);
          builder.append('_');
          builder.append(name_index);
        }
        builder.append(" } from \"");
        builder.append(&import_info.key_escaped);
        builder.append("\";\n");
      }

      // For global-typed imports, prefer the original `WebAssembly.Global`
      // from the dependency's instance when the dependency is itself a Wasm
      // module, so that mutable globals stay direct references between Wasm
      // modules. The JS binding only carries the unwrapped snapshot value.
      //
      // Limitation: for a circular Wasm<->Wasm mutable-global import the
      // dependency may not be evaluated yet when this `.get()` runs, so it
      // returns `undefined` and we fall back to the snapshot number, which
      // fails instantiation with a `LinkError`. Node.js has the same gap.
      for (i, (_, import_info)) in aggregated_imports.iter().enumerate() {
        if import_info.has_global_import {
          builder.append("const wasmExports_");
          builder.append(i);
          builder.append(" = import.meta.wasmInstances.get(import_ns_");
          builder.append(i);
          builder.append(");\n");
        }
      }

      builder.append("const importsObject = {\n");

      for (i, (_, import_info)) in aggregated_imports.iter().enumerate() {
        builder.append("  \"");
        builder.append(&import_info.key_escaped);
        builder.append("\": {\n");

        for (name_index, named_import) in import_info.named_imports.iter().enumerate() {
          builder.append("    \"");
          builder.append(&named_import.escaped_name);
          builder.append("\": ");
          if named_import.is_global {
            builder.append("wasmExports_");
            builder.append(i);
            builder.append(" === undefined ? import_");
            builder.append(i);
            builder.append('_');
            builder.append(name_index);
            builder.append(" : wasmExports_");
            builder.append(i);
            builder.append("[\"");
            builder.append(&named_import.escaped_name);
            builder.append("\"]");
          } else {
            builder.append("import_");
            builder.append(i);
            builder.append('_');
            builder.append(name_index);
          }
          builder.append(",\n");
        }

        builder.append("  },\n");
      }

      builder.append("};\n");

      builder.append("const modInstance = new import.meta.WasmInstance(wasmMod, importsObject);\n");
    } else {
      builder.append(
        "const modInstance = new import.meta.WasmInstance(wasmMod);\n"
      );
    }

    if has_global_export {
      // The generated source assumes `import.meta.wasmInstances` is present
      // whenever a module uses globals. The map shares `import.meta.WasmInstance`'s
      // lifecycle: both are absent only when WebAssembly is unavailable or during
      // snapshotting, where the `new import.meta.WasmInstance(...)` call above
      // would already have thrown. So if we reach here, the map exists.
      builder.append(
        "import.meta.wasmInstances.set(selfNs, modInstance.exports);\n",
      );
      builder.append(WASM_GLOBAL_UNWRAP_HELPER);
    }

    for (idx, (escaped_name, is_global)) in exports.iter().enumerate() {
      if escaped_name == "default" {
        builder.append("export default ");
        if *is_global {
          builder.append("unwrapWasmGlobal(modInstance.exports.");
          builder.append(escaped_name);
          builder.append(")");
        } else {
          builder.append("modInstance.exports.");
          builder.append(escaped_name);
        }
        builder.append(";\n");
      } else {
        builder.append("const export");
        builder.append(idx);
        builder.append(" = ");
        if *is_global {
          builder.append("unwrapWasmGlobal(modInstance.exports[\"");
          builder.append(escaped_name);
          builder.append("\"])");
        } else {
          builder.append("modInstance.exports[\"");
          builder.append(escaped_name);
          builder.append("\"]");
        }
        builder.append(";\nexport { export");
        builder.append(idx);
        builder.append(" as \"");
        builder.append(escaped_name);
        builder.append("\" };\n");
      }
    }
  }).unwrap()
}

#[test]
fn test_render_js_wasm_module() {
  let deps = WasmDeps {
    imports: vec![],
    exports: vec![],
  };
  let rendered = render_js_wasm_module("./foo.wasm", deps);
  pretty_assertions::assert_eq!(
    rendered,
    r#"import source wasmMod from "./foo.wasm";
const modInstance = new import.meta.WasmInstance(wasmMod);
"#,
  );

  let deps = WasmDeps {
    imports: vec![
      wasm_dep_analyzer::Import {
        name: "foo",
        module: "./import.js",
        import_type: wasm_dep_analyzer::ImportType::Tag(
          wasm_dep_analyzer::TagType {
            kind: 1,
            type_index: 1,
          },
        ),
      },
      wasm_dep_analyzer::Import {
        name: "bar",
        module: "./import.js",
        import_type: wasm_dep_analyzer::ImportType::Function(1),
      },
      wasm_dep_analyzer::Import {
        name: "fizz",
        module: "./import.js",
        import_type: wasm_dep_analyzer::ImportType::Function(2),
      },
      wasm_dep_analyzer::Import {
        name: "buzz",
        module: "./buzz.js",
        import_type: wasm_dep_analyzer::ImportType::Function(3),
      },
    ],
    exports: vec![
      wasm_dep_analyzer::Export {
        name: "export1",
        index: 0,
        export_type: wasm_dep_analyzer::ExportType::Function(Ok(
          wasm_dep_analyzer::FunctionSignature {
            params: vec![],
            returns: vec![],
          },
        )),
      },
      wasm_dep_analyzer::Export {
        name: "export2",
        index: 1,
        export_type: wasm_dep_analyzer::ExportType::Table,
      },
      wasm_dep_analyzer::Export {
        name: "export3",
        index: 2,
        export_type: wasm_dep_analyzer::ExportType::Memory,
      },
      wasm_dep_analyzer::Export {
        name: "export4",
        index: 3,
        export_type: wasm_dep_analyzer::ExportType::Global(Ok(
          wasm_dep_analyzer::GlobalType {
            value_type: wasm_dep_analyzer::ValueType::F32,
            mutability: false,
          },
        )),
      },
      wasm_dep_analyzer::Export {
        name: "export5",
        index: 4,
        export_type: wasm_dep_analyzer::ExportType::Tag,
      },
      wasm_dep_analyzer::Export {
        name: "export6",
        index: 5,
        export_type: wasm_dep_analyzer::ExportType::Unknown,
      },
      wasm_dep_analyzer::Export {
        name: "default",
        index: 6,
        export_type: wasm_dep_analyzer::ExportType::Function(Ok(
          wasm_dep_analyzer::FunctionSignature {
            params: vec![],
            returns: vec![],
          },
        )),
      },
    ],
  };
  let rendered = render_js_wasm_module("./foo.wasm", deps);
  pretty_assertions::assert_eq!(
    rendered,
    r#"import source wasmMod from "./foo.wasm";
import * as selfNs from "./foo.wasm";
import { "foo" as import_0_0, "bar" as import_0_1, "fizz" as import_0_2 } from "./import.js";
import { "buzz" as import_1_0 } from "./buzz.js";
const importsObject = {
  "./import.js": {
    "foo": import_0_0,
    "bar": import_0_1,
    "fizz": import_0_2,
  },
  "./buzz.js": {
    "buzz": import_1_0,
  },
};
const modInstance = new import.meta.WasmInstance(wasmMod, importsObject);
import.meta.wasmInstances.set(selfNs, modInstance.exports);
const unwrapWasmGlobal = (g) => { try { return g.value; } catch { return g; } };
const export0 = modInstance.exports["export1"];
export { export0 as "export1" };
const export1 = modInstance.exports["export2"];
export { export1 as "export2" };
const export2 = modInstance.exports["export3"];
export { export2 as "export3" };
const export3 = unwrapWasmGlobal(modInstance.exports["export4"]);
export { export3 as "export4" };
const export4 = modInstance.exports["export5"];
export { export4 as "export5" };
const export5 = modInstance.exports["export6"];
export { export5 as "export6" };
export default modInstance.exports.default;
"#,
  );

  let deps = WasmDeps {
    imports: vec![wasm_dep_analyzer::Import {
      name: "\n",
      module: "\n",
      import_type: wasm_dep_analyzer::ImportType::Function(1),
    }],
    exports: vec![wasm_dep_analyzer::Export {
      name: "\n",
      index: 0,
      export_type: wasm_dep_analyzer::ExportType::Function(Ok(
        wasm_dep_analyzer::FunctionSignature {
          params: vec![],
          returns: vec![],
        },
      )),
    }],
  };
  let rendered = render_js_wasm_module("./bar.wasm", deps);
  pretty_assertions::assert_eq!(
    rendered,
    r#"import source wasmMod from "./bar.wasm";
import { "\n" as import_0_0 } from "\n";
const importsObject = {
  "\n": {
    "\n": import_0_0,
  },
};
const modInstance = new import.meta.WasmInstance(wasmMod, importsObject);
const export0 = modInstance.exports["\n"];
export { export0 as "\n" };
"#,
  );
}

#[test]
fn test_render_js_wasm_module_global_unwrap() {
  fn global(
    value_type: wasm_dep_analyzer::ValueType,
    mutability: bool,
  ) -> wasm_dep_analyzer::ExportType {
    wasm_dep_analyzer::ExportType::Global(Ok(wasm_dep_analyzer::GlobalType {
      value_type,
      mutability,
    }))
  }

  let deps = WasmDeps {
    imports: vec![],
    exports: vec![
      // immutable numeric global -> unwrapped to its value at runtime
      wasm_dep_analyzer::Export {
        name: "answer",
        index: 0,
        export_type: global(wasm_dep_analyzer::ValueType::I32, false),
      },
      // mutable numeric global -> still unwrapped (snapshot at instantiation)
      wasm_dep_analyzer::Export {
        name: "counter",
        index: 1,
        export_type: global(wasm_dep_analyzer::ValueType::I64, true),
      },
      // unresolved value type (e.g. v128 / reference type) is still a global
      // by kind, so it is wrapped; the helper falls back to the
      // WebAssembly.Global object if reading `.value` throws (v128).
      wasm_dep_analyzer::Export {
        name: "vec",
        index: 2,
        export_type: global(wasm_dep_analyzer::ValueType::Unknown, false),
      },
      // global whose value type failed to parse -> still wrapped by kind
      wasm_dep_analyzer::Export {
        name: "broken",
        index: 3,
        export_type: wasm_dep_analyzer::ExportType::Global(Err(
          wasm_dep_analyzer::ParseError::UnresolvedExportType,
        )),
      },
      // non-global export is left untouched
      wasm_dep_analyzer::Export {
        name: "fn_export",
        index: 4,
        export_type: wasm_dep_analyzer::ExportType::Function(Ok(
          wasm_dep_analyzer::FunctionSignature {
            params: vec![],
            returns: vec![],
          },
        )),
      },
      // default export that is a global -> unwrapped
      wasm_dep_analyzer::Export {
        name: "default",
        index: 5,
        export_type: global(wasm_dep_analyzer::ValueType::F64, false),
      },
    ],
  };
  let rendered = render_js_wasm_module("./globals.wasm", deps);
  pretty_assertions::assert_eq!(
    rendered,
    r#"import source wasmMod from "./globals.wasm";
import * as selfNs from "./globals.wasm";
const modInstance = new import.meta.WasmInstance(wasmMod);
import.meta.wasmInstances.set(selfNs, modInstance.exports);
const unwrapWasmGlobal = (g) => { try { return g.value; } catch { return g; } };
const export0 = unwrapWasmGlobal(modInstance.exports["answer"]);
export { export0 as "answer" };
const export1 = unwrapWasmGlobal(modInstance.exports["counter"]);
export { export1 as "counter" };
const export2 = unwrapWasmGlobal(modInstance.exports["vec"]);
export { export2 as "vec" };
const export3 = unwrapWasmGlobal(modInstance.exports["broken"]);
export { export3 as "broken" };
const export4 = modInstance.exports["fn_export"];
export { export4 as "fn_export" };
export default unwrapWasmGlobal(modInstance.exports.default);
"#,
  );
}

#[test]
fn test_render_js_wasm_module_global_import() {
  let deps = WasmDeps {
    imports: vec![
      // global-typed import -> linked against the original
      // `WebAssembly.Global` when the dependency is itself a Wasm module
      // (found in `import.meta.wasmInstances`), falling back to the JS
      // binding otherwise
      wasm_dep_analyzer::Import {
        name: "counter",
        module: "./dep.wasm",
        import_type: wasm_dep_analyzer::ImportType::Global(
          wasm_dep_analyzer::GlobalType {
            value_type: wasm_dep_analyzer::ValueType::I32,
            mutability: true,
          },
        ),
      },
      // non-global import from the same module is untouched
      wasm_dep_analyzer::Import {
        name: "bump",
        module: "./dep.wasm",
        import_type: wasm_dep_analyzer::ImportType::Function(0),
      },
      // module with no global imports gets no namespace import or lookup
      wasm_dep_analyzer::Import {
        name: "log",
        module: "./util.js",
        import_type: wasm_dep_analyzer::ImportType::Function(1),
      },
    ],
    exports: vec![wasm_dep_analyzer::Export {
      name: "read",
      index: 0,
      export_type: wasm_dep_analyzer::ExportType::Function(Ok(
        wasm_dep_analyzer::FunctionSignature {
          params: vec![],
          returns: vec![wasm_dep_analyzer::ValueType::I32],
        },
      )),
    }],
  };
  let rendered = render_js_wasm_module("./main.wasm", deps);
  pretty_assertions::assert_eq!(
    rendered,
    r#"import source wasmMod from "./main.wasm";
import * as import_ns_0 from "./dep.wasm";
import { "counter" as import_0_0, "bump" as import_0_1 } from "./dep.wasm";
import { "log" as import_1_0 } from "./util.js";
const wasmExports_0 = import.meta.wasmInstances.get(import_ns_0);
const importsObject = {
  "./dep.wasm": {
    "counter": wasmExports_0 === undefined ? import_0_0 : wasmExports_0["counter"],
    "bump": import_0_1,
  },
  "./util.js": {
    "log": import_1_0,
  },
};
const modInstance = new import.meta.WasmInstance(wasmMod, importsObject);
const export0 = modInstance.exports["read"];
export { export0 as "read" };
"#,
  );
}
