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

use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::HashSet;
use std::future::Future;
use std::path::Path;
use std::path::PathBuf;
use std::pin::Pin;
use std::rc::Rc;
use std::str;
use std::sync::Arc;
use std::sync::atomic::AtomicU16;
use std::sync::atomic::Ordering;
use std::time::SystemTime;

use deno_ast::MediaType;
use deno_ast::ModuleKind;
use deno_cache_dir::file_fetcher::FetchLocalOptions;
use deno_cache_dir::file_fetcher::MemoryFiles as _;
use deno_core::FastString;
use deno_core::ModuleLoadOptions;
use deno_core::ModuleLoadReferrer;
use deno_core::ModuleLoader;
use deno_core::ModuleResolutionError;
use deno_core::ModuleSource;
use deno_core::ModuleSourceCode;
use deno_core::ModuleSpecifier;
use deno_core::ModuleType;
use deno_core::RequestedModuleType;
use deno_core::SourceCodeCacheInfo;
use deno_core::anyhow::Context as _;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_core::error::ModuleLoaderError;
use deno_core::futures::StreamExt;
use deno_core::futures::future::FutureExt;
use deno_core::futures::io::BufReader;
use deno_core::futures::stream::FuturesOrdered;
use deno_core::parking_lot::Mutex;
use deno_core::resolve_url;
use deno_core::serde_json;
use deno_error::JsErrorBox;
use deno_error::JsErrorClass;
use deno_graph::GraphKind;
use deno_graph::ModuleGraph;
use deno_graph::WalkOptions;
use deno_lib::loader::as_deno_resolver_requested_module_type;
use deno_lib::loader::loaded_module_source_to_module_source_code;
use deno_lib::loader::module_type_from_media_and_requested_type;
use deno_lib::npm::NpmRegistryReadPermissionChecker;
use deno_lib::util::hash::FastInsecureHasher;
use deno_lib::worker::CreateModuleLoaderResult;
use deno_lib::worker::ModuleLoaderFactory;
use deno_npm_installer::resolution::HasJsExecutionStartedFlagRc;
use deno_path_util::PathToUrlError;
use deno_path_util::resolve_url_or_path;
use deno_resolver::cache::ParsedSourceCache;
use deno_resolver::file_fetcher::FetchOptions;
use deno_resolver::file_fetcher::FetchPermissionsOptionRef;
use deno_resolver::graph::ResolveWithGraphErrorKind;
use deno_resolver::graph::ResolveWithGraphOptions;
use deno_resolver::graph::format_range_with_colors;
use deno_resolver::loader::LoadCodeSourceError;
use deno_resolver::loader::LoadPreparedModuleError;
use deno_resolver::loader::LoadedModule;
use deno_resolver::loader::LoadedModuleOrAsset;
use deno_resolver::loader::MemoryFiles;
use deno_resolver::loader::StrippingTypesNodeModulesError;
use deno_resolver::npm::DenoInNpmPackageChecker;
use deno_runtime::code_cache;
use deno_runtime::deno_node::NodeRequireLoader;
use deno_runtime::deno_node::create_host_defined_options;
use deno_runtime::deno_node::ops::require::UnableToGetCwdError;
use deno_runtime::deno_permissions::CheckSpecifierKind;
use deno_runtime::deno_permissions::PermissionsContainer;
use deno_runtime::deno_web::Blob;
use deno_runtime::tokio_util::create_basic_runtime;
use deno_semver::npm::NpmPackageReqReference;
use deno_semver::package::PackageReq;
use eszip::EszipV2;
use node_resolver::InNpmPackageChecker;
use node_resolver::NodeResolutionKind;
use node_resolver::ResolutionMode;
use node_resolver::errors::PackageJsonLoadError;
use sys_traits::FsCanonicalize;
use sys_traits::FsMetadata;
use sys_traits::FsMetadataValue;
use sys_traits::FsRead;
use tokio_util::compat::TokioAsyncReadCompatExt;

use crate::args::CliLockfile;
use crate::args::CliOptions;
use crate::args::DenoSubcommand;
use crate::args::TsTypeLib;
use crate::args::jsr_url;
use crate::cache::CodeCache;
use crate::file_fetcher::CliFileFetcher;
use crate::graph_container::MainModuleGraphContainer;
use crate::graph_container::ModuleGraphContainer;
use crate::graph_container::ModuleGraphUpdatePermit;
use crate::graph_util::BuildGraphRequest;
use crate::graph_util::BuildGraphWithNpmOptions;
use crate::graph_util::ModuleGraphBuilder;
use crate::npm::CliNpmResolver;
use crate::resolver::CliCjsTracker;
use crate::resolver::CliResolver;
use crate::sys::CliSys;
use crate::type_checker::CheckError;
use crate::type_checker::CheckOptions;
use crate::type_checker::TypeChecker;
use crate::util::file_watcher::WatcherCommunicator;
use crate::util::progress_bar::ProgressBar;
use crate::util::text_encoding::code_without_source_map;
use crate::util::text_encoding::source_map_from_code;

pub type CliEmitter =
  deno_resolver::emit::Emitter<DenoInNpmPackageChecker, CliSys>;
pub type CliDenoResolverModuleLoader =
  deno_resolver::loader::ModuleLoader<CliSys>;

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum PrepareModuleLoadError {
  #[class(inherit)]
  #[error(transparent)]
  BuildGraphWithNpmResolution(
    #[from] crate::graph_util::BuildGraphWithNpmResolutionError,
  ),
  #[class(inherit)]
  #[error(transparent)]
  Check(#[from] CheckError),
  #[class(inherit)]
  #[error(transparent)]
  LockfileWrite(#[from] deno_resolver::lockfile::LockfileWriteError),
  #[class(inherit)]
  #[error(transparent)]
  Other(#[from] JsErrorBox),
}

pub struct ModuleLoadPreparer {
  options: Arc<CliOptions>,
  lockfile: Option<Arc<CliLockfile>>,
  module_graph_builder: Arc<ModuleGraphBuilder>,
  progress_bar: ProgressBar,
  type_checker: Arc<TypeChecker>,
}

pub struct PrepareModuleLoadOptions<'a> {
  pub is_dynamic: bool,
  pub lib: TsTypeLib,
  pub permissions: PermissionsContainer,
  pub ext_overwrite: Option<&'a String>,
  pub allow_unknown_media_types: bool,
  /// Whether to skip validating the graph roots. This is useful
  /// for when you want to defer doing this until later (ex. get the
  /// graph back, reload some specifiers in it, then do graph validation).
  pub skip_graph_roots_validation: bool,
  pub file_content_overrides: HashMap<ModuleSpecifier, Arc<[u8]>>,
  /// Per-specifier response header overrides (e.g. `content-type`) applied to
  /// the graph loader. Used to preserve a blob's media type when its content
  /// is supplied out-of-band via `file_content_overrides`.
  pub file_header_overrides: HashMap<ModuleSpecifier, HashMap<String, String>>,
}

impl ModuleLoadPreparer {
  #[allow(clippy::too_many_arguments, reason = "construction")]
  pub fn new(
    options: Arc<CliOptions>,
    lockfile: Option<Arc<CliLockfile>>,
    module_graph_builder: Arc<ModuleGraphBuilder>,
    progress_bar: ProgressBar,
    type_checker: Arc<TypeChecker>,
  ) -> Self {
    Self {
      options,
      lockfile,
      module_graph_builder,
      progress_bar,
      type_checker,
    }
  }

  /// This method must be called for a module or a static importer of that
  /// module before attempting to `load()` it from a `JsRuntime`. It will
  /// populate the graph data in memory with the necessary source code, write
  /// emits where necessary or report any module graph / type checking errors.
  pub async fn prepare_module_load(
    &self,
    graph: &mut ModuleGraph,
    roots: &[ModuleSpecifier],
    options: PrepareModuleLoadOptions<'_>,
  ) -> Result<(), PrepareModuleLoadError> {
    log::debug!("Preparing module load.");
    let PrepareModuleLoadOptions {
      is_dynamic,
      lib,
      permissions,
      ext_overwrite,
      allow_unknown_media_types,
      skip_graph_roots_validation,
      file_content_overrides,
      file_header_overrides,
    } = options;
    let _pb_clear_guard = self.progress_bar.deferred_keep_initialize_alive();

    let mut loader = self
      .module_graph_builder
      .create_graph_loader_with_permissions(permissions);
    if !file_content_overrides.is_empty() {
      loader.set_file_content_overrides(file_content_overrides);
    }
    for (specifier, headers) in file_header_overrides {
      loader.insert_file_header_override(specifier, headers);
    }
    if let Some(ext) = ext_overwrite {
      let maybe_content_type = match ext.as_str() {
        "ts" => Some("text/typescript"),
        "tsx" => Some("text/tsx"),
        "js" => Some("text/javascript"),
        "jsx" => Some("text/jsx"),
        _ => None,
      };
      if let Some(content_type) = maybe_content_type {
        for root in roots {
          loader.insert_file_header_override(
            root.clone(),
            std::collections::HashMap::from([(
              "content-type".to_string(),
              content_type.to_string(),
            )]),
          );
        }
      }
    }
    log::debug!("Building module graph.");
    let has_type_checked = !graph.roots.is_empty();

    self
      .module_graph_builder
      .build_graph_roots_with_npm_resolution(
        graph,
        roots.to_vec(),
        BuildGraphWithNpmOptions {
          is_dynamic,
          loader: Some(&loader),
          npm_caching: self.options.default_npm_caching_strategy(),
        },
      )
      .await?;

    if !skip_graph_roots_validation {
      self.graph_roots_valid(graph, roots, allow_unknown_media_types, false)?;
    }

    drop(_pb_clear_guard);

    // type check if necessary
    if self.options.type_check_mode().is_true() && !has_type_checked {
      self.type_checker.check(
        // todo(perf): since this is only done the first time the graph is
        // created, we could avoid the clone of the graph here by providing
        // the actual graph on the first run and then getting the Arc<ModuleGraph>
        // back from the return value.
        graph.clone(),
        CheckOptions {
          build_fast_check_graph: true,
          lib,
          reload: self.options.reload_flag(),
          type_check_mode: self.options.type_check_mode(),
        },
      )?;
    }

    // write the lockfile if there is one and do so after type checking
    // as type checking might discover `@types/node`
    if let Some(lockfile) = &self.lockfile {
      lockfile.write_if_changed()?;
    }

    log::debug!("Prepared module load.");

    Ok(())
  }

  pub async fn reload_specifiers(
    &self,
    graph: &mut ModuleGraph,
    specifiers: Vec<ModuleSpecifier>,
    is_dynamic: bool,
    permissions: PermissionsContainer,
  ) -> Result<(), PrepareModuleLoadError> {
    log::debug!(
      "Reloading modified files: {}",
      specifiers
        .iter()
        .map(|s| s.as_str())
        .collect::<Vec<_>>()
        .join(", ")
    );
    let _pb_clear_guard = self.progress_bar.deferred_keep_initialize_alive();

    let loader = self
      .module_graph_builder
      .create_graph_loader_with_permissions(permissions);
    self
      .module_graph_builder
      .build_graph_with_npm_resolution(
        graph,
        BuildGraphRequest::Reload(specifiers),
        BuildGraphWithNpmOptions {
          is_dynamic,
          loader: Some(&loader),
          npm_caching: self.options.default_npm_caching_strategy(),
        },
      )
      .await?;

    if let Some(lockfile) = &self.lockfile {
      lockfile.write_if_changed()?;
    }

    Ok(())
  }

  pub fn graph_roots_valid(
    &self,
    graph: &ModuleGraph,
    roots: &[ModuleSpecifier],
    allow_unknown_media_types: bool,
    allow_unknown_jsr_exports: bool,
  ) -> Result<(), JsErrorBox> {
    self.module_graph_builder.graph_roots_valid(
      graph,
      roots,
      allow_unknown_media_types,
      allow_unknown_jsr_exports,
    )
  }
}

struct SharedCliModuleLoaderState {
  graph_kind: GraphKind,
  lib_window: TsTypeLib,
  lib_worker: TsTypeLib,
  initial_cwd: PathBuf,
  is_inspecting: bool,
  is_repl: bool,
  cjs_tracker: Arc<CliCjsTracker>,
  code_cache: Option<Arc<CodeCache>>,
  emitter: Arc<CliEmitter>,
  file_fetcher: Arc<CliFileFetcher>,
  has_js_execution_started_flag: HasJsExecutionStartedFlagRc,
  in_npm_pkg_checker: DenoInNpmPackageChecker,
  main_module_graph_container: Arc<MainModuleGraphContainer>,
  memory_files: Arc<MemoryFiles>,
  module_load_preparer: Arc<ModuleLoadPreparer>,
  npm_registry_permission_checker:
    Arc<NpmRegistryReadPermissionChecker<CliSys>>,
  npm_resolver: CliNpmResolver,
  parsed_source_cache: Arc<ParsedSourceCache>,
  module_loader: Arc<CliDenoResolverModuleLoader>,
  resolver: Arc<CliResolver>,
  sys: CliSys,
  in_flight_loads_tracker: InFlightModuleLoadsTracker,
  maybe_eszip_loader: Option<Arc<EszipModuleLoader>>,
  watcher_communicator: Option<Arc<WatcherCommunicator>>,
}

struct InFlightModuleLoadsTracker {
  loads_number: Arc<AtomicU16>,
  cleanup_task_timeout: u64,
  cleanup_task_handle: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
}

impl InFlightModuleLoadsTracker {
  pub fn increase(&self) {
    self.loads_number.fetch_add(1, Ordering::Relaxed);
    if let Some(task) = self.cleanup_task_handle.lock().take() {
      task.abort();
    }
  }

  pub fn decrease(&self, parsed_source_cache: &Arc<ParsedSourceCache>) {
    let prev = self.loads_number.fetch_sub(1, Ordering::Relaxed);
    if prev == 1 {
      let parsed_source_cache = parsed_source_cache.clone();
      let timeout = self.cleanup_task_timeout;
      let task_handle = tokio::spawn(async move {
        // We use a timeout here, which is defined to 10s,
        // so that in situations when dynamic imports are loaded after the startup,
        // we don't need to recompute and analyze multiple modules.
        tokio::time::sleep(std::time::Duration::from_millis(timeout)).await;
        parsed_source_cache.free_all();
      });
      let maybe_prev_task =
        self.cleanup_task_handle.lock().replace(task_handle);
      if let Some(prev_task) = maybe_prev_task {
        prev_task.abort();
      }
    }
  }
}

pub struct CliModuleLoaderFactory {
  shared: Arc<SharedCliModuleLoaderState>,
}

impl CliModuleLoaderFactory {
  #[allow(clippy::too_many_arguments, reason = "construction")]
  pub fn new(
    options: &CliOptions,
    cjs_tracker: Arc<CliCjsTracker>,
    code_cache: Option<Arc<CodeCache>>,
    emitter: Arc<CliEmitter>,
    file_fetcher: Arc<CliFileFetcher>,
    has_js_execution_started_flag: HasJsExecutionStartedFlagRc,
    in_npm_pkg_checker: DenoInNpmPackageChecker,
    main_module_graph_container: Arc<MainModuleGraphContainer>,
    memory_files: Arc<MemoryFiles>,
    module_load_preparer: Arc<ModuleLoadPreparer>,
    npm_registry_permission_checker: Arc<
      NpmRegistryReadPermissionChecker<CliSys>,
    >,
    npm_resolver: CliNpmResolver,
    parsed_source_cache: Arc<ParsedSourceCache>,
    module_loader: Arc<CliDenoResolverModuleLoader>,
    resolver: Arc<CliResolver>,
    sys: CliSys,
    maybe_eszip_loader: Option<Arc<EszipModuleLoader>>,
    watcher_communicator: Option<Arc<WatcherCommunicator>>,
  ) -> Self {
    Self {
      shared: Arc::new(SharedCliModuleLoaderState {
        graph_kind: options.graph_kind(),
        lib_window: options.ts_type_lib_window(),
        lib_worker: options.ts_type_lib_worker(),
        initial_cwd: options.initial_cwd().to_path_buf(),
        is_inspecting: options.is_inspecting(),
        is_repl: matches!(
          options.sub_command(),
          DenoSubcommand::Repl(_) | DenoSubcommand::Jupyter(_)
        ),
        cjs_tracker,
        code_cache,
        emitter,
        file_fetcher,
        has_js_execution_started_flag,
        in_npm_pkg_checker,
        main_module_graph_container,
        memory_files,
        module_load_preparer,
        npm_registry_permission_checker,
        npm_resolver,
        parsed_source_cache,
        module_loader,
        resolver,
        sys,
        in_flight_loads_tracker: InFlightModuleLoadsTracker {
          loads_number: Arc::new(AtomicU16::new(0)),
          cleanup_task_timeout: 10_000,
          cleanup_task_handle: Arc::new(Mutex::new(None)),
        },
        maybe_eszip_loader,
        watcher_communicator,
      }),
    }
  }

  #[allow(clippy::too_many_arguments, reason = "construction")]
  fn create_with_lib<TGraphContainer: ModuleGraphContainer>(
    &self,
    graph_container: TGraphContainer,
    lib: TsTypeLib,
    is_worker: bool,
    parent_permissions: PermissionsContainer,
    permissions: PermissionsContainer,
    maybe_main_module_blob: Option<(ModuleSpecifier, Arc<Blob>)>,
  ) -> CreateModuleLoaderResult {
    let hook_registry =
      deno_runtime::deno_node::ops::module_hooks::LoaderHookRegistry::default();
    let module_loader =
      Rc::new(CliModuleLoader(Rc::new(CliModuleLoaderInner {
        lib,
        is_worker,
        parent_permissions,
        permissions,
        graph_container: graph_container.clone(),
        shared: self.shared.clone(),
        loaded_files: Default::default(),
        hook_registry: hook_registry.clone(),
        maybe_main_module_blob,
      })));
    {
      let inner = module_loader.0.clone();
      hook_registry.set_default_resolve(Rc::new(
        move |specifier: &str, referrer: &str| {
          inner
            .inner_resolve(
              specifier,
              referrer,
              deno_core::ResolutionKind::Import,
              false,
            )
            .map(|s| s.to_string())
            .map_err(|e| JsErrorBox::generic(e.to_string()))
        },
      ));
    }
    let node_require_loader = Rc::new(CliNodeRequireLoader {
      cjs_tracker: self.shared.cjs_tracker.clone(),
      emitter: self.shared.emitter.clone(),
      npm_resolver: self.shared.npm_resolver.clone(),
      sys: self.shared.sys.clone(),
      graph_container,
      in_npm_pkg_checker: self.shared.in_npm_pkg_checker.clone(),
      memory_files: self.shared.memory_files.clone(),
      npm_registry_permission_checker: self
        .shared
        .npm_registry_permission_checker
        .clone(),
    });
    CreateModuleLoaderResult {
      module_loader,
      node_require_loader,
      hook_registry: Some(hook_registry),
    }
  }
}

impl ModuleLoaderFactory for CliModuleLoaderFactory {
  fn create_for_main(
    &self,
    root_permissions: PermissionsContainer,
  ) -> CreateModuleLoaderResult {
    self.create_with_lib(
      (*self.shared.main_module_graph_container).clone(),
      self.shared.lib_window,
      /* is worker */ false,
      root_permissions.clone(),
      root_permissions,
      None,
    )
  }

  fn create_for_worker(
    &self,
    parent_permissions: PermissionsContainer,
    permissions: PermissionsContainer,
    maybe_main_module_blob: Option<(ModuleSpecifier, Arc<Blob>)>,
  ) -> CreateModuleLoaderResult {
    self.create_with_lib(
      // create a fresh module graph for the worker
      WorkerModuleGraphContainer::new(Arc::new(ModuleGraph::new(
        self.shared.graph_kind,
      ))),
      self.shared.lib_worker,
      /* is worker */ true,
      parent_permissions,
      permissions,
      maybe_main_module_blob,
    )
  }
}

struct ModuleCodeStringSource {
  pub code: ModuleSourceCode,
  pub found_url: ModuleSpecifier,
  pub module_type: ModuleType,
}

struct CliModuleLoaderInner<TGraphContainer: ModuleGraphContainer> {
  lib: TsTypeLib,
  is_worker: bool,
  /// The initial set of permissions used to resolve the static imports in the
  /// worker. These are "allow all" for main worker, and parent thread
  /// permissions for Web Worker.
  parent_permissions: PermissionsContainer,
  permissions: PermissionsContainer,
  shared: Arc<SharedCliModuleLoaderState>,
  graph_container: TGraphContainer,
  loaded_files: RefCell<HashSet<ModuleSpecifier>>,
  hook_registry: deno_runtime::deno_node::ops::module_hooks::LoaderHookRegistry,
  /// For blob/object-URL module workers, the captured root blob and its
  /// specifier. Captured synchronously at worker construction so that a
  /// subsequent `URL.revokeObjectURL` cannot race the worker's module load.
  /// Its content and media type are injected into the graph during
  /// `prepare_load`.
  maybe_main_module_blob: Option<(ModuleSpecifier, Arc<Blob>)>,
}

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum ResolveReferrerError {
  #[class(inherit)]
  #[error(transparent)]
  UnableToGetCwd(#[from] UnableToGetCwdError),
  #[class(inherit)]
  #[error(transparent)]
  PathToUrl(#[from] PathToUrlError),
  #[class(inherit)]
  #[error(transparent)]
  ModuleResolution(#[from] ModuleResolutionError),
}

#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum CliModuleLoaderError {
  #[class(inherit)]
  #[error(transparent)]
  Fetch(#[from] deno_resolver::file_fetcher::FetchError),
  #[class(inherit)]
  #[error(transparent)]
  LoadCodeSource(#[from] LoadCodeSourceError),
  #[class(inherit)]
  #[error(transparent)]
  LoadPreparedModule(#[from] LoadPreparedModuleError),
  #[class(inherit)]
  #[error(transparent)]
  PathToUrl(#[from] PathToUrlError),
  #[class(inherit)]
  #[error(transparent)]
  ResolveNpmReqRef(#[from] deno_resolver::npm::ResolveNpmReqRefError),
  #[class(inherit)]
  #[error(transparent)]
  ResolveReferrer(#[from] ResolveReferrerError),
}

/// Applies `deno_resolver::patch_react_cves` to JavaScript module source,
/// handling both string and (valid UTF-8) byte representations without
/// allocating when no patch is needed.
fn patch_react_cves_source(
  specifier: &ModuleSpecifier,
  code: ModuleSourceCode,
) -> ModuleSourceCode {
  // Borrow the source as `&str` and compute the patched output (if any) in an
  // inner scope so the borrow of `code` ends before we move it below.
  let patched: Option<String> = {
    let src = match &code {
      ModuleSourceCode::String(s) => s.as_str(),
      ModuleSourceCode::Bytes(b) => match std::str::from_utf8(b.as_bytes()) {
        Ok(s) => s,
        // Non UTF-8 source can't contain the ASCII patterns we look for.
        Err(_) => return code,
      },
    };
    match deno_resolver::patch_react_cves(
      specifier.as_str(),
      Cow::Borrowed(src),
    ) {
      Cow::Borrowed(_) => None,
      Cow::Owned(s) => Some(s),
    }
  };
  match patched {
    Some(s) => ModuleSourceCode::String(s.into()),
    None => code,
  }
}

impl<TGraphContainer: ModuleGraphContainer>
  CliModuleLoaderInner<TGraphContainer>
{
  /// Best-effort prepare a specifier through the module graph when ESM
  /// hooks are active. Used both for the initial load preparation and for
  /// redirects introduced by a load hook calling `nextLoad(newUrl)`.
  /// Failures are intentionally swallowed: a user hook may resolve to a
  /// virtual URL the graph builder cannot fetch, in which case the load
  /// is serviced entirely by hooks.
  async fn prepare_hooked_specifier(
    &self,
    specifier: &ModuleSpecifier,
    is_dynamic_import: bool,
  ) {
    let permissions = if is_dynamic_import {
      &self.permissions
    } else {
      &self.parent_permissions
    };
    let is_dynamic = is_dynamic_import || self.is_worker;
    let mut update_permit = self.graph_container.acquire_update_permit().await;
    let graph = update_permit.graph_mut();
    let _ = self
      .shared
      .module_load_preparer
      .prepare_module_load(
        graph,
        std::slice::from_ref(specifier),
        PrepareModuleLoadOptions {
          is_dynamic,
          lib: self.lib,
          permissions: permissions.clone(),
          ext_overwrite: None,
          allow_unknown_media_types: false,
          skip_graph_roots_validation: true,
          file_content_overrides: HashMap::new(),
          file_header_overrides: HashMap::new(),
        },
      )
      .await;
    graph.prune_types();
    update_permit.commit();
  }

  async fn load_inner(
    &self,
    specifier: &ModuleSpecifier,
    maybe_referrer: Option<&ModuleSpecifier>,
    requested_module_type: &RequestedModuleType,
  ) -> Result<ModuleSource, ModuleLoaderError> {
    if specifier.path().ends_with(".node") {
      return Err(JsErrorBox::type_error(format!(
        "Cannot import native Node.js addon \"{specifier}\" via ESM. \
         Native `.node` addons can only be loaded through `require()`. \
         Use `createRequire()` from `node:module`, e.g.:\n  \
         import {{ createRequire }} from \"node:module\";\n  \
         const require = createRequire(import.meta.url);\n  \
         const addon = require(\"./addon.node\");"
      )));
    }

    let code_source = self
      .load_code_source(specifier, maybe_referrer, requested_module_type)
      .await
      .map_err(JsErrorBox::from_err)?;

    self.code_source_to_module_source(specifier, code_source)
  }

  fn try_load_inner_sync(
    &self,
    specifier: &ModuleSpecifier,
    requested_module_type: &RequestedModuleType,
  ) -> Option<Result<ModuleSource, ModuleLoaderError>> {
    if specifier.path().ends_with(".node") {
      return None;
    }

    let code_source = match self
      .try_load_code_source_sync(specifier, requested_module_type)
      .map_err(JsErrorBox::from_err)
    {
      Ok(Some(code_source)) => code_source,
      Ok(None) => return None,
      Err(err) => return Some(Err(err)),
    };

    Some(self.code_source_to_module_source(specifier, code_source))
  }

  fn code_source_to_module_source(
    &self,
    specifier: &ModuleSpecifier,
    code_source: ModuleCodeStringSource,
  ) -> Result<ModuleSource, ModuleLoaderError> {
    let code = if self.shared.is_inspecting
      || code_source.module_type == ModuleType::Wasm
    {
      // we need the code with the source map in order for
      // it to work with --inspect or --inspect-brk
      code_source.code
    } else {
      // v8 is slower when source maps are present, so we strip them
      code_without_source_map(code_source.code)
    };

    // Apply load-time security mitigations for known React Server Components
    // CVEs to JavaScript source. Opt in via `DENO_PATCH_REACT_CVE`. See
    // `deno_resolver::patch_react_cves`.
    let code = if code_source.module_type == ModuleType::JavaScript
      && deno_resolver::is_react_cve_patch_enabled(&self.shared.sys)
    {
      patch_react_cves_source(specifier, code)
    } else {
      code
    };

    let code_cache = if code_source.module_type == ModuleType::JavaScript {
      self.shared.code_cache.as_ref().map(|cache| {
        let code_hash = FastInsecureHasher::new_deno_versioned()
          .write_hashable(&code)
          .finish();
        let data = cache
          .get_sync(specifier, code_cache::CodeCacheType::EsModule, code_hash)
          .map(Cow::from)
          .inspect(|_| {
            // This log line is also used by tests.
            log::debug!(
              "V8 code cache hit for ES module: {specifier}, [{code_hash:?}]"
            );
          });
        SourceCodeCacheInfo {
          hash: code_hash,
          data,
        }
      })
    } else {
      None
    };

    Ok(ModuleSource::new_with_redirect(
      code_source.module_type,
      code,
      specifier,
      &code_source.found_url,
      code_cache,
    ))
  }

  fn try_load_code_source_sync(
    &self,
    specifier: &ModuleSpecifier,
    requested_module_type: &RequestedModuleType,
  ) -> Result<Option<ModuleCodeStringSource>, CliModuleLoaderError> {
    if NpmPackageReqReference::from_specifier(specifier).is_ok()
      || self.shared.in_npm_pkg_checker.in_npm_package(specifier)
      || self.hook_registry.load_active.get()
    {
      return Ok(None);
    }

    let graph = self.graph_container.graph();
    let deno_resolver_requested_module_type =
      as_deno_resolver_requested_module_type(requested_module_type);
    let Some(prepared_module) =
      self.shared.module_loader.try_load_prepared_module_sync(
        &graph,
        specifier,
        &deno_resolver_requested_module_type,
      )?
    else {
      return Ok(None);
    };

    Ok(Some(self.loaded_module_to_module_code_string_source(
      prepared_module,
      requested_module_type,
    )))
  }

  async fn load_code_source(
    &self,
    specifier: &ModuleSpecifier,
    maybe_referrer: Option<&ModuleSpecifier>,
    requested_module_type: &RequestedModuleType,
  ) -> Result<ModuleCodeStringSource, CliModuleLoaderError> {
    // this loader maintains npm specifiers in dynamic imports when resolving
    // so that they can be properly preloaded, but now we might receive them
    // here, so we need to actually resolve them to a file: specifier here
    let specifier = if let Ok(reference) =
      NpmPackageReqReference::from_specifier(specifier)
    {
      let referrer = match maybe_referrer {
        // if we're here, it means it was importing from a dynamic import
        // and so there will be a referrer
        Some(r) => Cow::Borrowed(r),
        // but the repl may also end up here and it won't have
        // a referrer so create a referrer for it here
        None => Cow::Owned(self.resolve_referrer("")?),
      };
      Cow::Owned(
        self
          .shared
          .resolver
          .resolve_non_workspace_npm_req_ref_to_file(
            &reference,
            &referrer,
            ResolutionMode::Import,
            NodeResolutionKind::Execution,
          )?
          .into_url()?,
      )
    } else {
      Cow::Borrowed(specifier)
    };

    let graph = self.graph_container.graph();
    let deno_resolver_requested_module_type =
      as_deno_resolver_requested_module_type(requested_module_type);
    match self
      .shared
      .module_loader
      .load(
        &graph,
        &specifier,
        maybe_referrer,
        &deno_resolver_requested_module_type,
      )
      .await?
    {
      LoadedModuleOrAsset::Module(prepared_module) => {
        Ok(self.loaded_module_to_module_code_string_source(
          prepared_module,
          requested_module_type,
        ))
      }
      LoadedModuleOrAsset::ExternalAsset {
        specifier,
        statically_analyzable,
      } => {
        Ok(
          self
            .load_asset(
              &specifier,
              if statically_analyzable {
                CheckSpecifierKind::Static
              } else {
                // force permissions
                CheckSpecifierKind::Dynamic
              },
              requested_module_type,
            )
            .await?,
        )
      }
    }
  }

  fn loaded_module_to_module_code_string_source(
    &self,
    loaded_module: LoadedModule,
    requested_module_type: &RequestedModuleType,
  ) -> ModuleCodeStringSource {
    ModuleCodeStringSource {
      code: loaded_module_source_to_module_source_code(loaded_module.source),
      found_url: loaded_module.specifier.into_owned(),
      module_type: module_type_from_media_and_requested_type(
        loaded_module.media_type,
        requested_module_type,
      ),
    }
  }

  async fn load_asset(
    &self,
    specifier: &ModuleSpecifier,
    check_specifier_kind: CheckSpecifierKind,
    requested_module_type: &RequestedModuleType,
  ) -> Result<ModuleCodeStringSource, deno_resolver::file_fetcher::FetchError>
  {
    let file = self
      .shared
      .file_fetcher
      .fetch_with_options(
        specifier,
        // Always check against this worker's own permissions, never the
        // broader `parent_permissions`. Dynamic `import()` of a text/bytes
        // asset (`with { type: "text" | "bytes" }`) returns the file's raw
        // contents to the caller and therefore requires read access; routing
        // it through `parent_permissions` would let a worker created with
        // restricted permissions read files its parent can but it cannot.
        // For statically analyzable imports `check_specifier` skips the read
        // check entirely, so the chosen container does not matter there.
        FetchPermissionsOptionRef::Restricted(
          &self.permissions,
          check_specifier_kind,
        ),
        FetchOptions {
          local: FetchLocalOptions {
            include_mtime: false,
          },
          maybe_auth: None,
          maybe_accept: None,
          maybe_cache_setting: Some(
            &deno_cache_dir::file_fetcher::CacheSetting::Use,
          ),
        },
      )
      .await?;

    let module_type = match requested_module_type {
      RequestedModuleType::Text => ModuleType::Text,
      RequestedModuleType::Bytes => ModuleType::Bytes,
      RequestedModuleType::Other(kind) => ModuleType::Other(kind.clone()),
      RequestedModuleType::None => {
        match file.resolve_media_type_and_charset().0 {
          MediaType::Wasm => ModuleType::Wasm,
          _ => ModuleType::JavaScript,
        }
      }
      t => unreachable!("{t}"),
    };

    Ok(ModuleCodeStringSource {
      code: ModuleSourceCode::Bytes(file.source.into()),
      found_url: file.url,
      module_type,
    })
  }

  async fn maybe_reload_dynamic(
    &self,
    graph: &ModuleGraph,
    specifier: &ModuleSpecifier,
    permissions: &PermissionsContainer,
  ) -> Result<bool, PrepareModuleLoadError> {
    let specifiers_to_reload =
      self.check_specifiers_to_reload_for_dynamic_import(graph, specifier);

    if specifiers_to_reload.is_empty() {
      return Ok(false);
    }

    let mut graph_permit = self.graph_container.acquire_update_permit().await;
    let graph = graph_permit.graph_mut();
    self
      .shared
      .module_load_preparer
      .reload_specifiers(
        graph,
        specifiers_to_reload,
        /* is dynamic */ true,
        permissions.clone(),
      )
      .await?;
    graph_permit.commit();
    Ok(true)
  }

  fn check_specifiers_to_reload_for_dynamic_import(
    &self,
    graph: &ModuleGraph,
    specifier: &ModuleSpecifier,
  ) -> Vec<ModuleSpecifier> {
    let mut specifiers_to_reload = Vec::new();
    let mut module_iter = graph.walk(
      std::iter::once(specifier),
      WalkOptions {
        check_js: deno_graph::CheckJsOption::False,
        follow_dynamic: false,
        kind: GraphKind::CodeOnly,
        prefer_fast_check_graph: false,
      },
    );
    while let Some((specifier, module_entry)) = module_iter.next() {
      if specifier.scheme() != "file"
        || self.loaded_files.borrow().contains(specifier)
      {
        module_iter.skip_previous_dependencies(); // no need to analyze this module's dependencies
        continue;
      }
      let should_reload = match module_entry {
        deno_graph::ModuleEntryRef::Module(module) => {
          self.has_module_changed_on_file_system(specifier, module.mtime())
        }
        deno_graph::ModuleEntryRef::Err(err) => {
          if matches!(
            err.as_kind(),
            deno_graph::ModuleErrorKind::Missing { .. }
          ) {
            self.mtime_of_specifier(specifier).is_some() // it exists now
          } else {
            self.has_module_changed_on_file_system(specifier, err.mtime())
          }
        }
        deno_graph::ModuleEntryRef::Redirect(_) => false,
      };
      if should_reload {
        specifiers_to_reload.push(specifier.clone());
      }
    }

    self.loaded_files.borrow_mut().insert(specifier.clone());

    specifiers_to_reload
  }

  fn has_module_changed_on_file_system(
    &self,
    specifier: &ModuleSpecifier,
    mtime: Option<SystemTime>,
  ) -> bool {
    let Some(loaded_mtime) = mtime else {
      return false;
    };
    self
      .mtime_of_specifier(specifier)
      .map(|mtime| mtime > loaded_mtime)
      .unwrap_or(false)
  }

  fn mtime_of_specifier(
    &self,
    specifier: &ModuleSpecifier,
  ) -> Option<SystemTime> {
    deno_path_util::url_to_file_path(specifier)
      .ok()
      .and_then(|path| self.shared.sys.fs_symlink_metadata(&path).ok())
      .and_then(|metadata| metadata.modified().ok())
  }

  #[allow(clippy::result_large_err, reason = "TODO: investigate")]
  fn resolve_referrer(
    &self,
    referrer: &str,
  ) -> Result<ModuleSpecifier, ResolveReferrerError> {
    let referrer = if referrer.is_empty() && self.shared.is_repl {
      // FIXME(bartlomieju): this is a hacky way to provide compatibility with REPL
      // and `Deno.core.evalContext` API. Ideally we should always have a referrer filled
      "./$deno$repl.mts"
    } else {
      referrer
    };

    Ok(if deno_path_util::specifier_has_uri_scheme(referrer) {
      deno_core::resolve_url(referrer)?
    } else if referrer == "." {
      // main module, use the initial cwd
      deno_path_util::resolve_path(referrer, &self.shared.initial_cwd)?
    } else {
      // this cwd check is slow, so try to avoid it
      #[allow(
        clippy::disallowed_methods,
        reason = "ok, actually needs the current cwd"
      )]
      let cwd = std::env::current_dir().map_err(UnableToGetCwdError)?;
      deno_path_util::resolve_path(referrer, &cwd)?
    })
  }

  fn inner_resolve(
    &self,
    raw_specifier: &str,
    raw_referrer: &str,
    kind: deno_core::ResolutionKind,
    is_import_meta: bool,
  ) -> Result<ModuleSpecifier, ModuleLoaderError> {
    fn ensure_not_jsr_non_jsr_remote_import(
      specifier: &ModuleSpecifier,
      referrer: &ModuleSpecifier,
    ) -> Result<(), JsErrorBox> {
      if referrer.as_str().starts_with(jsr_url().as_str())
        && !specifier.as_str().starts_with(jsr_url().as_str())
        && matches!(specifier.scheme(), "http" | "https")
      {
        return Err(JsErrorBox::generic(format!(
          "Importing {} blocked. JSR packages cannot import non-JSR remote modules for security reasons.",
          specifier
        )));
      }
      Ok(())
    }

    // An `npm:` package's bin entry chosen as the main module is
    // resolved to a concrete `file:` URL inside the global npm cache before it
    // reaches here. deno_core then re-resolves that already-resolved specifier
    // as the main module, running it through the import map. An
    // `"imports": { "/": "./" }` entry (recommended in the docs for
    // project-root absolute imports) normalizes to a `file:///` =>
    // `file:///<project>/` mapping whose prefix match rewrites any file URL
    // outside the project root (such as the npm cache) to live underneath the
    // project directory, breaking the load. Such a package-internal main
    // module is not a user source file and must not be remapped, so bypass the
    // import map. A user-provided path entry (e.g. `deno run ./thing.ts`) is
    // intentionally left to the import map.
    if matches!(kind, deno_core::ResolutionKind::MainModule)
      && let Ok(url) = ModuleSpecifier::parse(raw_specifier)
      && url.scheme() == "file"
      && self.shared.in_npm_pkg_checker.in_npm_package(&url)
    {
      let canonicalized_url = deno_path_util::url_to_file_path(&url)
        .ok()
        .and_then(|path| self.shared.sys.fs_canonicalize(&path).ok())
        .and_then(|path| deno_path_util::url_from_file_path(&path).ok())
        .unwrap_or(url);
      return Ok(canonicalized_url);
    }

    let referrer = self
      .resolve_referrer(raw_referrer)
      .map_err(JsErrorBox::from_err)?;
    let graph = self.graph_container.graph();
    let result = self.shared.resolver.resolve_with_graph(
      graph.as_ref(),
      raw_specifier,
      &referrer,
      deno_graph::Position::zeroed(),
      ResolveWithGraphOptions {
        mode: ResolutionMode::Import,
        kind: NodeResolutionKind::Execution,
        // leave npm specifiers as-is for dynamic imports so that
        // the loader can properly install them if necessary
        maintain_npm_specifiers: matches!(
          kind,
          deno_core::ResolutionKind::DynamicImport
        ) && !is_import_meta,
      },
    );
    let specifier = match result {
      Ok(specifier) => specifier,
      Err(err) => {
        if let Some(specifier) = err
          .maybe_specifier()
          .filter(|_| is_import_meta)
          .and_then(|s| s.into_owned().into_url().ok())
        {
          specifier
        } else {
          match err.into_kind() {
            ResolveWithGraphErrorKind::Resolution(err) => {
              // todo(dsherret): why do we have a newline here? Document it.
              return Err(JsErrorBox::type_error(format!(
                "{}\n",
                err.to_string_with_range()
              )));
            }
            err => return Err(JsErrorBox::from_err(err)),
          }
        }
      }
    };

    // only verify this for an import and not import.meta.resolve
    if !is_import_meta {
      ensure_not_jsr_non_jsr_remote_import(&specifier, &referrer)?;
    }

    Ok(specifier)
  }
}

#[derive(Clone)]
// todo(dsherret): this double Rc boxing is not ideal
pub struct CliModuleLoader<TGraphContainer: ModuleGraphContainer>(
  Rc<CliModuleLoaderInner<TGraphContainer>>,
);

impl<TGraphContainer: ModuleGraphContainer> ModuleLoader
  for CliModuleLoader<TGraphContainer>
{
  fn resolve(
    &self,
    specifier: &str,
    referrer: &str,
    kind: deno_core::ResolutionKind,
  ) -> deno_core::ModuleResolveResponse {
    self.0.inner_resolve(specifier, referrer, kind, false)
  }

  fn resolve_with_scope(
    &self,
    scope: &mut deno_core::v8::PinScope,
    specifier: &str,
    referrer: &str,
    kind: deno_core::ResolutionKind,
    import_attributes: &std::collections::HashMap<String, String>,
  ) -> deno_core::ModuleResolveResponse {
    // CJS modules generated by Deno import `node:module` internally. Node does
    // not expose that implementation detail to resolve hooks, so only bypass
    // hooks for CJS referrers. User ESM imports of `node:module` still go
    // through hooks.
    if specifier == "node:module"
      && let Ok(referrer) = ModuleSpecifier::parse(referrer)
      && self.0.shared.cjs_tracker.get_referrer_kind(&referrer)
        == ResolutionMode::Require
    {
      return self
        .0
        .inner_resolve(specifier, referrer.as_str(), kind, false);
    }
    if specifier == "node:module"
      && let Ok(referrer) = ModuleSpecifier::parse(referrer)
    {
      let media_type = MediaType::from_specifier(&referrer);
      if self
        .0
        .shared
        .cjs_tracker
        .is_maybe_cjs(&referrer, media_type)
        .unwrap_or(false)
      {
        return self
          .0
          .inner_resolve(specifier, referrer.as_str(), kind, false);
      }
    }
    let resolved = if let Some(url) = self.0.hook_registry.resolve(
      scope,
      specifier,
      referrer,
      import_attributes,
    )? {
      ModuleSpecifier::parse(&url).map_err(JsErrorBox::from_err)
    } else {
      self.0.inner_resolve(specifier, referrer, kind, false)
    };
    // Stash the full import attributes against the resolved URL so the load
    // hook (which V8 only hands the `type` attribute) can recover them.
    if let Ok(resolved) = &resolved {
      self
        .0
        .hook_registry
        .record_resolved_attributes(resolved.as_str(), import_attributes);
    }
    resolved
  }

  fn pump_event_loop_during_load(&self) -> bool {
    // Load hooks respond through the async bridge, so the event loop must be
    // pumped during the recursive load or the load deadlocks.
    self.0.hook_registry.load_active.get()
  }

  fn import_meta_resolve(
    &self,
    specifier: &str,
    referrer: &str,
  ) -> Result<ModuleSpecifier, ModuleLoaderError> {
    self.0.inner_resolve(
      specifier,
      referrer,
      deno_core::ResolutionKind::DynamicImport,
      true,
    )
  }

  fn get_host_defined_options<'s>(
    &self,
    scope: &mut deno_core::v8::PinScope<'s, '_>,
    name: &str,
  ) -> Option<deno_core::v8::Local<'s, deno_core::v8::Data>> {
    let name = deno_core::ModuleSpecifier::parse(name).ok()?;
    if self.0.shared.in_npm_pkg_checker.in_npm_package(&name) {
      Some(create_host_defined_options(scope))
    } else {
      None
    }
  }

  fn load(
    &self,
    specifier: &ModuleSpecifier,
    maybe_referrer: Option<&ModuleLoadReferrer>,
    options: ModuleLoadOptions,
  ) -> deno_core::ModuleLoadResponse {
    let inner = self.0.clone();

    if let Some(eszip_loader) = &inner.shared.maybe_eszip_loader {
      return eszip_loader.load(specifier);
    }

    self.0.loaded_files.borrow_mut().insert(specifier.clone());

    let specifier = specifier.clone();
    let maybe_referrer = maybe_referrer.cloned();

    // When load hooks are active, delegate to JS hooks first.
    // The hook function itself is synchronous, but this load path has no V8
    // scope. The async bridge lets the JS event loop run the hook; blocking
    // here waiting for JS would deadlock the same runtime.
    // Skip the hook bridge for CJS modules — they go through the
    // default loader which invokes the sync load hook chain
    // (executeLoadHookChain) in Module._load. Going through both
    // the ESM bridge AND the CJS path would call hooks twice.
    if self.0.hook_registry.load_active.get()
      && !options.is_synchronous
      && !{
        let media_type = deno_media_type::MediaType::from_specifier(&specifier);
        self
          .0
          .shared
          .cjs_tracker
          .is_maybe_cjs(&specifier, media_type)
          .unwrap_or(false)
      }
    {
      let import_attributes = hook_load_import_attributes(
        &self.0.hook_registry,
        specifier.as_str(),
        &options.requested_module_type,
      );
      let receiver = self
        .0
        .hook_registry
        .push_load(specifier.to_string(), import_attributes);
      return deno_core::ModuleLoadResponse::Async(
        async move {
          let hook_result = match receiver.await {
            Ok(r) => r,
            Err(_) => {
              return Err(JsErrorBox::generic("module load hook cancelled"));
            }
          };
          match hook_result {
            Ok((Some(source), format, _effective_url)) => {
              let module_type = pick_hook_module_type(
                format.as_deref(),
                &options.requested_module_type,
              );
              Ok(deno_core::ModuleSource::new(
                module_type,
                deno_core::ModuleSourceCode::String(source.into()),
                &specifier,
                None,
              ))
            }
            Ok((None, Some(format), _effective_url))
              if format == "builtin" && specifier.scheme() == "node" =>
            {
              Ok(deno_core::ModuleSource::new(
                deno_core::ModuleType::JavaScript,
                deno_core::ModuleSourceCode::String("".to_string().into()),
                &specifier,
                None,
              ))
            }
            Ok((None, _, effective_url)) => {
              // Fallthrough: hooks didn't intercept. If the user's load hook
              // chain delegated via `nextLoad(newUrl)`, fetch source from
              // that URL but keep the module's identity at the original
              // specifier (matches Node's hook semantics).
              let fetch_specifier = effective_url
                .as_deref()
                .and_then(|u| deno_core::ModuleSpecifier::parse(u).ok())
                .filter(|u| u != &specifier);
              if let Some(redirect) = fetch_specifier.as_ref() {
                // The redirect target wasn't seen by the initial graph
                // preparation pass; prepare it now so load_inner can find
                // it in the graph.
                inner
                  .prepare_hooked_specifier(redirect, options.is_dynamic_import)
                  .await;
              }
              let load_url = fetch_specifier.as_ref().unwrap_or(&specifier);
              let source = inner
                .load_inner(
                  load_url,
                  maybe_referrer.as_ref().map(|r| &r.specifier),
                  &options.requested_module_type,
                )
                .await?;
              if fetch_specifier.is_some() {
                Ok(deno_core::ModuleSource::new(
                  source.module_type,
                  source.code,
                  &specifier,
                  source.code_cache,
                ))
              } else {
                Ok(source)
              }
            }
            Err(err) => Err(JsErrorBox::generic(err)),
          }
        }
        .boxed_local(),
      );
    }

    if options.is_synchronous
      && let Some(result) =
        inner.try_load_inner_sync(&specifier, &options.requested_module_type)
    {
      return deno_core::ModuleLoadResponse::Sync(result);
    }

    deno_core::ModuleLoadResponse::Async(
      async move {
        // A `registerHooks` resolve hook may have rewritten this specifier
        // to a URL the initial graph preparation never saw (e.g. appended a
        // fragment for cache busting), so prepare it on demand.
        if inner.hook_registry.resolve_active()
          && !matches!(
            options.requested_module_type,
            RequestedModuleType::Text | RequestedModuleType::Bytes
          )
          && !inner.shared.in_npm_pkg_checker.in_npm_package(&specifier)
          && inner.graph_container.graph().get(&specifier).is_none()
        {
          inner
            .prepare_hooked_specifier(&specifier, options.is_dynamic_import)
            .await;
        }
        inner
          .load_inner(
            &specifier,
            maybe_referrer.as_ref().map(|r| &r.specifier),
            &options.requested_module_type,
          )
          .await
          .map_err(|err| {
            let Some(referrer) = maybe_referrer else {
              return err;
            };
            let position = deno_graph::Position {
              line: referrer.line_number as usize - 1,
              character: referrer.column_number as usize - 1,
            };
            JsErrorBox::new(
              err.get_class(),
              format!(
                "{err}\n    at {}",
                format_range_with_colors(&deno_graph::Range {
                  specifier: referrer.specifier,
                  range: deno_graph::PositionRange {
                    start: position,
                    end: position
                  },
                  resolution_mode: None
                })
              ),
            )
          })
      }
      .boxed_local(),
    )
  }

  fn should_load_synthetic_esm(&self, specifier: &str) -> bool {
    self.0.hook_registry.load_active.get() && specifier.starts_with("node:")
  }

  fn prepare_load(
    &self,
    specifier: &ModuleSpecifier,
    _maybe_referrer: Option<String>,
    maybe_code: Option<String>,
    options: ModuleLoadOptions,
  ) -> Pin<Box<dyn Future<Output = Result<(), ModuleLoaderError>>>> {
    // always call this first unconditionally because it will be
    // decremented unconditionally in "finish_load"
    self.0.shared.in_flight_loads_tracker.increase();

    if matches!(
      options.requested_module_type,
      RequestedModuleType::Text
        | RequestedModuleType::Bytes
        | RequestedModuleType::Other(_)
    ) {
      // Text/Bytes imports skip graph preparation, so the file watcher's
      // graph reporter never sees them. For dynamic imports, register the
      // file directly with the watcher so editing it triggers a reload.
      // (Static text/bytes imports are picked up by the initial graph
      // build via deno_graph's asset/text edge analysis.)
      if options.is_dynamic_import
        && let Some(watcher_communicator) =
          self.0.shared.watcher_communicator.as_ref()
        && specifier.scheme() == "file"
        && let Ok(file_path) = specifier.to_file_path()
      {
        let _ = watcher_communicator.watch_paths(vec![file_path]);
      }
      return Box::pin(deno_core::futures::future::ready(Ok(())));
    }

    // When ESM hooks are active, attempt graph preparation best-effort.
    // A user hook may have resolved this specifier to a virtual URL the
    // graph builder cannot fetch; in that case the load itself will be
    // serviced entirely by hooks, so failure here is fine and must not
    // abort the load.
    if self.0.hook_registry.load_active.get() {
      self.0.shared.has_js_execution_started_flag.raise();
      let specifier = specifier.clone();
      let inner = self.0.clone();
      let is_dynamic_import = options.is_dynamic_import;
      return Box::pin(async move {
        inner
          .prepare_hooked_specifier(&specifier, is_dynamic_import)
          .await;
        Ok(())
      });
    }

    if self.0.shared.in_npm_pkg_checker.in_npm_package(specifier) {
      self.0.shared.has_js_execution_started_flag.raise();
      return Box::pin(deno_core::futures::future::ready(Ok(())));
    }

    if self.0.shared.maybe_eszip_loader.is_some() {
      self.0.shared.has_js_execution_started_flag.raise();
      return Box::pin(deno_core::futures::future::ready(Ok(())));
    }

    let specifier = specifier.clone();
    let inner = self.0.clone();
    let is_synchronous = options.is_synchronous;

    let future = async move {
      let graph_container = &inner.graph_container;
      let module_load_preparer = &inner.shared.module_load_preparer;
      let permissions = if options.is_dynamic_import {
        &inner.permissions
      } else {
        &inner.parent_permissions
      };

      if options.is_dynamic_import {
        // This doesn't acquire a graph update permit because that will
        // clone the graph which is a bit slow.
        let mut graph = graph_container.graph();
        // When the specifier is already in the graph then it means it
        // was previously loaded, so we can skip that and only check if
        // this part of the graph is valid.
        if !graph.roots.is_empty() && graph.get(&specifier).is_some() {
          let did_reload = inner
            .maybe_reload_dynamic(&graph, &specifier, permissions)
            .await
            .map_err(JsErrorBox::from_err)?;
          if did_reload {
            graph = inner.graph_container.graph();
          }

          log::debug!("Skipping prepare module load.");
          // roots are already validated so we can skip those
          if did_reload || !graph.roots.contains(&specifier) {
            module_load_preparer.graph_roots_valid(
              &graph,
              &[specifier],
              false,
              false,
            )?;
          }
          return Ok(());
        }
      }

      let is_dynamic = options.is_dynamic_import || inner.is_worker; // consider workers as dynamic for permissions
      let lib = inner.lib;
      let mut update_permit = graph_container.acquire_update_permit().await;
      // For a blob/object-URL worker root, inject the captured blob's content
      // and media type so the graph builds from the synchronously-captured
      // bytes even if the object URL has since been revoked. Otherwise fall
      // back to any caller-provided source code.
      let captured_blob =
        inner
          .maybe_main_module_blob
          .as_ref()
          .filter(|(blob_specifier, _)| {
            maybe_code.is_none() && *blob_specifier == specifier
          });
      let (file_overrides, header_overrides) =
        if let Some((blob_specifier, blob)) = captured_blob {
          let bytes = blob.read_all().await;
          (
            HashMap::from([(blob_specifier.clone(), Arc::from(bytes))]),
            HashMap::from([(
              blob_specifier.clone(),
              HashMap::from([(
                "content-type".to_string(),
                blob.media_type.clone(),
              )]),
            )]),
          )
        } else {
          let file_overrides = maybe_code
            .map(|code| {
              HashMap::from([(specifier.clone(), Arc::from(code.into_bytes()))])
            })
            .unwrap_or_default();
          (file_overrides, HashMap::new())
        };
      let specifiers = &[specifier];
      {
        let graph = update_permit.graph_mut();
        module_load_preparer
          .prepare_module_load(
            graph,
            specifiers,
            PrepareModuleLoadOptions {
              is_dynamic,
              lib,
              permissions: permissions.clone(),
              ext_overwrite: None,
              allow_unknown_media_types: false,
              skip_graph_roots_validation: is_dynamic,
              file_content_overrides: file_overrides,
              file_header_overrides: header_overrides,
            },
          )
          .await
          .map_err(JsErrorBox::from_err)?;
        graph.prune_types();
        update_permit.commit();
        inner.shared.has_js_execution_started_flag.raise();
      }

      if is_dynamic {
        inner
          .maybe_reload_dynamic(
            &graph_container.graph(),
            &specifiers[0],
            permissions,
          )
          .await
          .map_err(JsErrorBox::from_err)?;
        // always validate the graph roots because we skipped doing it above
        module_load_preparer.graph_roots_valid(
          &graph_container.graph(),
          specifiers,
          false,
          false,
        )?;
      }

      Ok(())
    };

    // when is_synchronous is true, the caller is only polling the future
    // without a tokio runtime, so we need to run it in a new runtime
    if is_synchronous {
      let result = run_future_in_new_runtime(future);
      Box::pin(std::future::ready(result))
    } else {
      Box::pin(future)
    }
  }

  fn finish_load(&self) {
    self
      .0
      .shared
      .in_flight_loads_tracker
      .decrease(&self.0.shared.parsed_source_cache);
  }

  fn get_code_cache(
    &self,
    specifier: &ModuleSpecifier,
    source: &deno_core::v8::String,
  ) -> Option<SourceCodeCacheInfo> {
    let cache = self.0.shared.code_cache.as_ref()?;
    let source_hash = {
      use std::hash::Hash;
      use std::hash::Hasher;
      let mut hasher = twox_hash::XxHash64::default();
      source.hash(&mut hasher);
      hasher.finish()
    };
    let data = cache
      .get_sync(specifier, code_cache::CodeCacheType::EsModule, source_hash)
      .inspect(|_| {
        // This log line is also used by tests.
        log::debug!(
          "V8 code cache hit for ES module: {specifier}, [{source_hash}]"
        );
      })
      .map(Cow::from);
    Some(SourceCodeCacheInfo {
      hash: source_hash,
      data,
    })
  }

  fn code_cache_ready(
    &self,
    specifier: ModuleSpecifier,
    source_hash: u64,
    code_cache: &[u8],
  ) -> Pin<Box<dyn Future<Output = ()>>> {
    if let Some(cache) = self.0.shared.code_cache.as_ref() {
      // This log line is also used by tests.
      log::debug!(
        "Updating V8 code cache for ES module: {specifier}, [{source_hash:?}]"
      );
      cache.set_sync(
        &specifier,
        code_cache::CodeCacheType::EsModule,
        source_hash,
        code_cache,
      );
    }
    std::future::ready(()).boxed_local()
  }

  fn get_source_map(&self, file_name: &str) -> Option<Cow<'_, [u8]>> {
    let specifier = resolve_url(file_name).ok()?;
    match specifier.scheme() {
      // we should only be looking for emits for schemes that denote external
      // modules, which the disk_cache supports
      "wasm" | "file" | "http" | "https" | "data" | "blob" => (),
      _ => return None,
    }

    // Load the prepared module and extract inline source map
    let graph = self.0.graph_container.graph();
    let source = self
      .0
      .shared
      .module_loader
      .load_prepared_module_for_source_map_sync(&graph, &specifier)
      .ok()??;
    source_map_from_code(source.source.as_bytes()).map(Cow::Owned)
  }

  fn load_external_source_map(
    &self,
    source_map_url: &str,
  ) -> Option<Cow<'_, [u8]>> {
    let specifier = resolve_url(source_map_url).ok()?;

    if let Ok(Some(file)) = self
      .0
      .shared
      .file_fetcher
      .get_cached_source_or_local(&specifier)
    {
      return Some(Cow::Owned(file.source.to_vec()));
    }

    None
  }

  // todo(dsherret): this method is actually only to determine whether
  // to show the filename in the stack traces so we should rename it
  // to something more clear that reflects that (since we skip checking
  // this for non-npm packages)
  fn source_map_source_exists(&self, source_url: &str) -> Option<bool> {
    let specifier = resolve_url(source_url).ok()?;

    // some npm packages rely on the file existing or not to end up in
    // the stack trace, so for backwards compat reasons only check this
    // for npm packages because we don't want the perf hit otherwise
    if self.0.shared.in_npm_pkg_checker.in_npm_package(&specifier)
      && let Ok(path) = deno_path_util::url_to_file_path(&specifier)
    {
      return Some(path.is_file());
    }

    Some(true)
  }

  fn get_source_mapped_source_line(
    &self,
    file_name: &str,
    line_number: usize,
  ) -> Option<String> {
    let specifier = resolve_url(file_name).ok()?;
    let graph = self.0.graph_container.graph();

    let code = match graph.get(&specifier) {
      Some(deno_graph::Module::Js(module)) => &module.source.text,
      Some(deno_graph::Module::Json(module)) => &module.source.text,
      Some(
        deno_graph::Module::Wasm(_)
        | deno_graph::Module::Npm(_)
        | deno_graph::Module::Node(_)
        | deno_graph::Module::External(_),
      ) => {
        return None;
      }
      None => {
        // Not in graph, try to read from file system (for source-mapped original files)
        if let Ok(Some(file)) = self
          .0
          .shared
          .file_fetcher
          .get_cached_source_or_local(&specifier)
        {
          return extract_source_line(
            &String::from_utf8_lossy(&file.source),
            line_number,
          );
        } else {
          return None;
        }
      }
    };

    extract_source_line(code, line_number)
  }
}

/// Build the `importAttributes` object handed to a `module.registerHooks()`
/// load hook. V8 only surfaces the `type` attribute at load time, so recover
/// the full `with { ... }` clause recorded during resolution and make sure
/// `type` is present for the common JSON/text/bytes case.
fn hook_load_import_attributes(
  registry: &deno_runtime::deno_node::ops::module_hooks::LoaderHookRegistry,
  specifier: &str,
  requested_module_type: &deno_core::RequestedModuleType,
) -> std::collections::HashMap<String, String> {
  let mut attrs = registry.take_resolved_attributes(specifier);
  if let Some(ty) = requested_module_type.as_str() {
    attrs
      .entry("type".to_string())
      .or_insert_with(|| ty.to_string());
  }
  attrs
}

/// Pick the `ModuleType` for source returned by a `module.registerHooks()`
/// load hook. Honors a hook-supplied `format` first (Node's hook contract),
/// then falls back to the importer's `with { type: "..." }` attribute, so
/// e.g. `import x from "./x.json" with { type: "json" }` still parses as
/// JSON when the hook passes the source straight through.
fn pick_hook_module_type(
  format: Option<&str>,
  requested: &deno_core::RequestedModuleType,
) -> deno_core::ModuleType {
  if let Some(format) = format {
    match format {
      "json" => return deno_core::ModuleType::Json,
      "text" => return deno_core::ModuleType::Text,
      "bytes" => return deno_core::ModuleType::Bytes,
      "wasm" => return deno_core::ModuleType::Wasm,
      _ => {}
    }
  }
  match requested {
    deno_core::RequestedModuleType::Json => deno_core::ModuleType::Json,
    deno_core::RequestedModuleType::Text => deno_core::ModuleType::Text,
    deno_core::RequestedModuleType::Bytes => deno_core::ModuleType::Bytes,
    _ => deno_core::ModuleType::JavaScript,
  }
}

/// Extracts a specific line from source code text.
fn extract_source_line(text: &str, line_number: usize) -> Option<String> {
  // Do NOT use .lines(): it skips the terminating empty line.
  // (due to internally using_terminator() instead of .split())
  match text.split('\n').nth(line_number) {
    Some(line) => Some(line.to_string()),
    None => Some(format!(
      "{} Couldn't format source line: Line {} is out of bounds (source may have changed at runtime)",
      crate::colors::yellow("Warning"),
      line_number + 1,
    )),
  }
}

/// Holds the `ModuleGraph` in workers.
#[derive(Clone)]
struct WorkerModuleGraphContainer {
  // Allow only one request to update the graph data at a time,
  // but allow other requests to read from it at any time even
  // while another request is updating the data.
  update_queue: Rc<deno_core::unsync::TaskQueue>,
  inner: Rc<RefCell<Arc<ModuleGraph>>>,
}

impl WorkerModuleGraphContainer {
  pub fn new(module_graph: Arc<ModuleGraph>) -> Self {
    Self {
      update_queue: Default::default(),
      inner: Rc::new(RefCell::new(module_graph)),
    }
  }
}

impl ModuleGraphContainer for WorkerModuleGraphContainer {
  async fn acquire_update_permit(&self) -> impl ModuleGraphUpdatePermit {
    let permit = self.update_queue.acquire().await;
    WorkerModuleGraphUpdatePermit {
      permit,
      inner: self.inner.clone(),
      graph: (**self.inner.borrow()).clone(),
    }
  }

  fn graph(&self) -> Arc<ModuleGraph> {
    self.inner.borrow().clone()
  }
}

struct WorkerModuleGraphUpdatePermit {
  permit: deno_core::unsync::TaskQueuePermit,
  inner: Rc<RefCell<Arc<ModuleGraph>>>,
  graph: ModuleGraph,
}

impl ModuleGraphUpdatePermit for WorkerModuleGraphUpdatePermit {
  fn graph_mut(&mut self) -> &mut ModuleGraph {
    &mut self.graph
  }

  fn commit(self) {
    *self.inner.borrow_mut() = Arc::new(self.graph);
    drop(self.permit); // explicit drop for clarity
  }
}

#[derive(Debug)]
struct CliNodeRequireLoader<TGraphContainer: ModuleGraphContainer> {
  cjs_tracker: Arc<CliCjsTracker>,
  emitter: Arc<CliEmitter>,
  memory_files: Arc<MemoryFiles>,
  npm_resolver: CliNpmResolver,
  sys: CliSys,
  graph_container: TGraphContainer,
  in_npm_pkg_checker: DenoInNpmPackageChecker,
  npm_registry_permission_checker:
    Arc<NpmRegistryReadPermissionChecker<CliSys>>,
}

impl<TGraphContainer: ModuleGraphContainer> NodeRequireLoader
  for CliNodeRequireLoader<TGraphContainer>
{
  fn ensure_read_permission<'a>(
    &self,
    permissions: &mut PermissionsContainer,
    path: Cow<'a, Path>,
  ) -> Result<Cow<'a, Path>, JsErrorBox> {
    if let Ok(url) = deno_path_util::url_from_file_path(&path) {
      // allow reading if it's in the module graph
      if self.graph_container.graph().get(&url).is_some() {
        return Ok(path);
      }
    }
    self
      .npm_registry_permission_checker
      .ensure_read_permission(permissions, path)
      .map_err(JsErrorBox::from_err)
  }

  fn load_text_file_lossy(
    &self,
    path: &Path,
  ) -> Result<FastString, JsErrorBox> {
    // todo(dsherret): use the preloaded module from the graph if available?
    let media_type = MediaType::from_path(path);
    let text_result = self.sys.fs_read_to_string_lossy(path);
    let text = match text_result {
      Ok(text) => text,
      Err(err) => {
        // only bother on error for performance reasons
        if let Some(file) = deno_path_util::url_from_file_path(path)
          .ok()
          .and_then(|s| self.memory_files.get(&s))
        {
          Cow::Owned(String::from_utf8_lossy(&file.source).into_owned())
        } else {
          return Err(JsErrorBox::from_err(err));
        }
      }
    };
    if media_type.is_emittable() {
      let specifier = deno_path_util::url_from_file_path(path)
        .map_err(JsErrorBox::from_err)?;
      if self.in_npm_pkg_checker.in_npm_package(&specifier) {
        return Err(JsErrorBox::from_err(StrippingTypesNodeModulesError {
          specifier,
        }));
      }
      let text = self
        .emitter
        .maybe_emit_source_sync(
          &specifier,
          media_type,
          // this is probably not super accurate due to require esm, but probably ok.
          // If we find this causes a lot of churn in the emit cache then we should
          // investigate how we can make this better
          ModuleKind::Cjs,
          &text.into(),
        )
        .map_err(JsErrorBox::from_err)?;
      Ok(text.into())
    } else {
      Ok(match text {
        Cow::Borrowed(s) => FastString::from_static(s),
        Cow::Owned(s) => s.into(),
      })
    }
  }

  fn is_maybe_cjs(
    &self,
    specifier: &ModuleSpecifier,
  ) -> Result<bool, PackageJsonLoadError> {
    let media_type = MediaType::from_specifier(specifier);
    self.cjs_tracker.is_maybe_cjs(specifier, media_type)
  }

  fn is_maybe_cjs_from_require(
    &self,
    specifier: &ModuleSpecifier,
  ) -> Result<bool, PackageJsonLoadError> {
    let media_type = MediaType::from_specifier(specifier);
    self
      .cjs_tracker
      .is_maybe_cjs_from_require(specifier, media_type)
  }

  fn resolve_require_node_module_paths(&self, from: &Path) -> Vec<String> {
    let is_global_resolver_and_from_in_global_cache = self
      .npm_resolver
      .as_managed()
      .filter(|r| r.root_node_modules_path().is_none())
      .map(|r| r.global_cache_root_path())
      .filter(|global_cache_path| from.starts_with(global_cache_path))
      .is_some();
    if is_global_resolver_and_from_in_global_cache {
      Vec::new()
    } else {
      deno_runtime::deno_node::default_resolve_require_node_module_paths(from)
    }
  }

  fn resolve_package_folder_from_name(
    &self,
    package_name: &str,
  ) -> Option<PathBuf> {
    // Only meaningful in global-cache mode: when a local node_modules
    // directory exists, byonm-style walking handles the lookup.
    let managed = self
      .npm_resolver
      .as_managed()
      .filter(|r| r.root_node_modules_path().is_none())?;
    let req = PackageReq::from_str(package_name).ok()?;
    managed.resolve_pkg_folder_from_deno_module_req(&req).ok()
  }
}

#[derive(Debug, Default)]
pub struct EszipModuleLoader {
  files: HashMap<ModuleSpecifier, Arc<[u8]>>,
}

impl EszipModuleLoader {
  pub async fn create(script: &str, cwd: &Path) -> Result<Self, AnyError> {
    // entrypoint#path1,path2,...
    let (_entrypoint, files) = script
      .split_once("#")
      .with_context(|| "eszip: invalid script string")?;

    // TODO: handle paths that contain ','
    let files = files.split(",").collect::<Vec<_>>();
    let mut loaded_eszips = FuturesOrdered::new();
    for path in files {
      let file = tokio::fs::File::open(path).await?;
      let eszip = BufReader::new(file.compat());
      let path = path.to_string();

      loaded_eszips.push_back(async move {
        let (eszip, loader) = EszipV2::parse(eszip)
          .await
          .with_context(|| format!("Error parsing eszip header at {}", path))?;
        loader
          .await
          .with_context(|| format!("Error loading eszip at {}", path))?;
        Ok(eszip)
      });
    }
    // At this point all eszips are fully loaded
    let loaded_eszips: Vec<Result<EszipV2, AnyError>> =
      loaded_eszips.collect::<Vec<_>>().await;

    let mut loader = Self::default();

    for loaded_eszip_result in loaded_eszips {
      let loaded_eszip = loaded_eszip_result?;
      let specifiers = loaded_eszip.specifiers();
      loader.files.reserve(specifiers.len());

      for specifier in specifiers {
        let module = loaded_eszip.get_module(&specifier).unwrap();
        let source = module.take_source().await.unwrap();
        let resolved_specifier = resolve_url_or_path(&specifier, cwd)?;
        let prev = loader.files.insert(resolved_specifier, source);
        assert!(prev.is_none());
      }
    }

    Ok(loader)
  }

  pub fn load_import_map_value(
    &self,
    specifier: &ModuleSpecifier,
  ) -> Result<serde_json::Value, AnyError> {
    match self.files.get(specifier) {
      Some(bytes) => Ok(serde_json::from_slice(bytes.as_ref())?),
      None => bail!("Import map not found in eszip: {}", specifier),
    }
  }

  fn load(&self, specifier: &ModuleSpecifier) -> deno_core::ModuleLoadResponse {
    match self.files.get(specifier) {
      Some(source) => {
        let module_source = ModuleSource::new(
          ModuleType::JavaScript,
          ModuleSourceCode::Bytes(deno_core::ModuleCodeBytes::Arc(
            source.clone(),
          )),
          specifier,
          None,
        );
        deno_core::ModuleLoadResponse::Sync(Ok(module_source))
      }
      None => deno_core::ModuleLoadResponse::Sync(Err(JsErrorBox::generic(
        "Module not found",
      ))),
    }
  }
}

/// Runs the future to completion in a new tokio runtime.
///
/// This is used for synchronous module loading (require ESM) where deno_core is blocking
/// the main thread and polling the future. In this case, tasks in the current runtime
/// aren't being processed, so we can get around this by creating a new runtime.
fn run_future_in_new_runtime<F, R>(future: F) -> R
where
  F: Future<Output = R> + 'static,
  R: Send + 'static,
{
  // SAFETY: few points here...
  // 1. The future is moved entirely to the new thread before any execution
  // 2. The future executes exclusively on that single thread
  // 3. The original thread waits (joins) until completion
  // 4. No concurrent access to the future's captured state occurs
  // 5. HOWEVER: Any thread local state won't work properly... which could
  //    possibly cause issues.
  let masked = unsafe { deno_core::unsync::MaskFutureAsSend::new(future) };

  std::thread::scope(|s| {
    s.spawn(move || {
      let rt = create_basic_runtime();
      rt.block_on(masked).into_inner()
    })
    .join()
    .unwrap()
  })
}

#[cfg(test)]
mod tests {
  use deno_graph::ast::ParsedSourceStore;

  use super::*;

  #[tokio::test]
  async fn test_inflight_module_loads_tracker() {
    let tracker = InFlightModuleLoadsTracker {
      loads_number: Default::default(),
      cleanup_task_timeout: 10,
      cleanup_task_handle: Default::default(),
    };

    let specifier = ModuleSpecifier::parse("file:///a.js").unwrap();
    let source = "const a = 'hello';";
    let parsed_source_cache = Arc::new(ParsedSourceCache::default());
    let parsed_source = parsed_source_cache
      .remove_or_parse_module(&specifier, MediaType::JavaScript, source.into())
      .unwrap();
    parsed_source_cache.set_parsed_source(specifier, parsed_source);

    assert_eq!(parsed_source_cache.len(), 1);
    assert!(tracker.cleanup_task_handle.lock().is_none());
    tracker.increase();
    tracker.increase();
    assert!(tracker.cleanup_task_handle.lock().is_none());
    tracker.decrease(&parsed_source_cache);
    assert!(tracker.cleanup_task_handle.lock().is_none());
    tracker.decrease(&parsed_source_cache);
    assert!(tracker.cleanup_task_handle.lock().is_some());
    assert_eq!(parsed_source_cache.len(), 1);
    tracker.increase();
    assert!(tracker.cleanup_task_handle.lock().is_none());
    assert_eq!(parsed_source_cache.len(), 1);
    tracker.decrease(&parsed_source_cache);
    // Rather long timeout, but to make sure CI is not flaking on it.
    tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    assert_eq!(parsed_source_cache.len(), 0);
  }
}
