// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

pub mod deno_json;
mod flags;
mod flags_net;
mod import_map;
mod lockfile;
mod package_json;

use deno_ast::MediaType;
use deno_ast::SourceMapOption;
use deno_config::deno_json::NodeModulesDirMode;
use deno_config::workspace::CreateResolverOptions;
use deno_config::workspace::FolderConfigs;
use deno_config::workspace::PackageJsonDepResolution;
use deno_config::workspace::VendorEnablement;
use deno_config::workspace::Workspace;
use deno_config::workspace::WorkspaceDirectory;
use deno_config::workspace::WorkspaceDirectoryEmptyOptions;
use deno_config::workspace::WorkspaceDiscoverOptions;
use deno_config::workspace::WorkspaceDiscoverStart;
use deno_config::workspace::WorkspaceLintConfig;
use deno_config::workspace::WorkspaceResolver;
use deno_core::resolve_url_or_path;
use deno_graph::GraphKind;
use deno_npm::npm_rc::NpmRc;
use deno_npm::npm_rc::ResolvedNpmRc;
use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot;
use deno_npm::NpmSystemInfo;
use deno_npm_cache::NpmCacheSetting;
use deno_path_util::normalize_path;
use deno_semver::npm::NpmPackageReqReference;
use deno_telemetry::OtelConfig;
use import_map::resolve_import_map_value_from_specifier;

pub use deno_config::deno_json::BenchConfig;
pub use deno_config::deno_json::ConfigFile;
pub use deno_config::deno_json::FmtOptionsConfig;
pub use deno_config::deno_json::LintRulesConfig;
pub use deno_config::deno_json::ProseWrap;
pub use deno_config::deno_json::TsConfig;
pub use deno_config::deno_json::TsConfigForEmit;
pub use deno_config::deno_json::TsConfigType;
pub use deno_config::deno_json::TsTypeLib;
pub use deno_config::glob::FilePatterns;
pub use deno_json::check_warn_tsconfig;
pub use flags::*;
pub use lockfile::CliLockfile;
pub use lockfile::CliLockfileReadFromPathOptions;
pub use package_json::NpmInstallDepsProvider;
pub use package_json::PackageJsonDepValueParseWithLocationError;

use deno_ast::ModuleSpecifier;
use deno_core::anyhow::bail;
use deno_core::anyhow::Context;
use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_core::url::Url;
use deno_runtime::deno_permissions::PermissionsOptions;
use deno_runtime::deno_tls::deno_native_certs::load_native_certs;
use deno_runtime::deno_tls::rustls;
use deno_runtime::deno_tls::rustls::RootCertStore;
use deno_runtime::deno_tls::rustls_pemfile;
use deno_runtime::deno_tls::webpki_roots;
use deno_runtime::inspector_server::InspectorServer;
use deno_terminal::colors;
use dotenvy::from_filename;
use once_cell::sync::Lazy;
use serde::Deserialize;
use serde::Serialize;
use std::borrow::Cow;
use std::collections::HashMap;
use std::env;
use std::io::BufReader;
use std::io::Cursor;
use std::io::Read;
use std::io::Seek;
use std::net::SocketAddr;
use std::num::NonZeroUsize;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use thiserror::Error;

use crate::cache;
use crate::cache::DenoDirProvider;
use crate::file_fetcher::FileFetcher;
use crate::util::fs::canonicalize_path_maybe_not_exists;
use crate::version;

use deno_config::deno_json::FmtConfig;
use deno_config::deno_json::LintConfig;
use deno_config::deno_json::TestConfig;

pub fn npm_registry_url() -> &'static Url {
  static NPM_REGISTRY_DEFAULT_URL: Lazy<Url> = Lazy::new(|| {
    let env_var_name = "NPM_CONFIG_REGISTRY";
    if let Ok(registry_url) = std::env::var(env_var_name) {
      // ensure there is a trailing slash for the directory
      let registry_url = format!("{}/", registry_url.trim_end_matches('/'));
      match Url::parse(&registry_url) {
        Ok(url) => {
          return url;
        }
        Err(err) => {
          log::debug!(
            "Invalid {} environment variable: {:#}",
            env_var_name,
            err,
          );
        }
      }
    }

    Url::parse("https://registry.npmjs.org").unwrap()
  });

  &NPM_REGISTRY_DEFAULT_URL
}

pub static DENO_DISABLE_PEDANTIC_NODE_WARNINGS: Lazy<bool> = Lazy::new(|| {
  std::env::var("DENO_DISABLE_PEDANTIC_NODE_WARNINGS")
    .ok()
    .is_some()
});

pub fn jsr_url() -> &'static Url {
  static JSR_URL: Lazy<Url> = Lazy::new(|| {
    let env_var_name = "JSR_URL";
    if let Ok(registry_url) = std::env::var(env_var_name) {
      // ensure there is a trailing slash for the directory
      let registry_url = format!("{}/", registry_url.trim_end_matches('/'));
      match Url::parse(&registry_url) {
        Ok(url) => {
          return url;
        }
        Err(err) => {
          log::debug!(
            "Invalid {} environment variable: {:#}",
            env_var_name,
            err,
          );
        }
      }
    }

    Url::parse("https://jsr.io/").unwrap()
  });

  &JSR_URL
}

pub fn jsr_api_url() -> &'static Url {
  static JSR_API_URL: Lazy<Url> = Lazy::new(|| {
    let mut jsr_api_url = jsr_url().clone();
    jsr_api_url.set_path("api/");
    jsr_api_url
  });

  &JSR_API_URL
}

pub fn ts_config_to_transpile_and_emit_options(
  config: deno_config::deno_json::TsConfig,
) -> Result<(deno_ast::TranspileOptions, deno_ast::EmitOptions), AnyError> {
  let options: deno_config::deno_json::EmitConfigOptions =
    serde_json::from_value(config.0)
      .context("Failed to parse compilerOptions")?;
  let imports_not_used_as_values =
    match options.imports_not_used_as_values.as_str() {
      "preserve" => deno_ast::ImportsNotUsedAsValues::Preserve,
      "error" => deno_ast::ImportsNotUsedAsValues::Error,
      _ => deno_ast::ImportsNotUsedAsValues::Remove,
    };
  let (transform_jsx, jsx_automatic, jsx_development, precompile_jsx) =
    match options.jsx.as_str() {
      "react" => (true, false, false, false),
      "react-jsx" => (true, true, false, false),
      "react-jsxdev" => (true, true, true, false),
      "precompile" => (false, false, false, true),
      _ => (false, false, false, false),
    };
  let source_map = if options.inline_source_map {
    SourceMapOption::Inline
  } else if options.source_map {
    SourceMapOption::Separate
  } else {
    SourceMapOption::None
  };
  Ok((
    deno_ast::TranspileOptions {
      use_ts_decorators: options.experimental_decorators,
      use_decorators_proposal: !options.experimental_decorators,
      emit_metadata: options.emit_decorator_metadata,
      imports_not_used_as_values,
      jsx_automatic,
      jsx_development,
      jsx_factory: options.jsx_factory,
      jsx_fragment_factory: options.jsx_fragment_factory,
      jsx_import_source: options.jsx_import_source,
      precompile_jsx,
      precompile_jsx_skip_elements: options.jsx_precompile_skip_elements,
      precompile_jsx_dynamic_props: None,
      transform_jsx,
      var_decl_imports: false,
      // todo(dsherret): support verbatim_module_syntax here properly
      verbatim_module_syntax: false,
    },
    deno_ast::EmitOptions {
      inline_sources: options.inline_sources,
      remove_comments: false,
      source_map,
      source_map_base: None,
      source_map_file: None,
    },
  ))
}

/// Indicates how cached source files should be handled.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum CacheSetting {
  /// Only the cached files should be used.  Any files not in the cache will
  /// error.  This is the equivalent of `--cached-only` in the CLI.
  Only,
  /// No cached source files should be used, and all files should be reloaded.
  /// This is the equivalent of `--reload` in the CLI.
  ReloadAll,
  /// Only some cached resources should be used.  This is the equivalent of
  /// `--reload=jsr:@std/http/file-server` or
  /// `--reload=jsr:@std/http/file-server,jsr:@std/assert/assert-equals`.
  ReloadSome(Vec<String>),
  /// The usability of a cached value is determined by analyzing the cached
  /// headers and other metadata associated with a cached response, reloading
  /// any cached "non-fresh" cached responses.
  RespectHeaders,
  /// The cached source files should be used for local modules.  This is the
  /// default behavior of the CLI.
  Use,
}

impl CacheSetting {
  pub fn as_npm_cache_setting(&self) -> NpmCacheSetting {
    match self {
      CacheSetting::Only => NpmCacheSetting::Only,
      CacheSetting::ReloadAll => NpmCacheSetting::ReloadAll,
      CacheSetting::ReloadSome(values) => {
        if values.iter().any(|v| v == "npm:") {
          NpmCacheSetting::ReloadAll
        } else {
          NpmCacheSetting::ReloadSome {
            npm_package_names: values
              .iter()
              .filter_map(|v| v.strip_prefix("npm:"))
              .map(|n| n.to_string())
              .collect(),
          }
        }
      }
      CacheSetting::RespectHeaders => unreachable!(), // not supported
      CacheSetting::Use => NpmCacheSetting::Use,
    }
  }
}

pub struct WorkspaceBenchOptions {
  pub filter: Option<String>,
  pub json: bool,
  pub no_run: bool,
}

impl WorkspaceBenchOptions {
  pub fn resolve(bench_flags: &BenchFlags) -> Self {
    Self {
      filter: bench_flags.filter.clone(),
      json: bench_flags.json,
      no_run: bench_flags.no_run,
    }
  }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BenchOptions {
  pub files: FilePatterns,
}

impl BenchOptions {
  pub fn resolve(bench_config: BenchConfig, _bench_flags: &BenchFlags) -> Self {
    // this is the same, but keeping the same pattern as everywhere else for the future
    Self {
      files: bench_config.files,
    }
  }
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct UnstableFmtOptions {
  pub component: bool,
  pub sql: bool,
}

#[derive(Clone, Debug)]
pub struct FmtOptions {
  pub options: FmtOptionsConfig,
  pub unstable: UnstableFmtOptions,
  pub files: FilePatterns,
}

impl Default for FmtOptions {
  fn default() -> Self {
    Self::new_with_base(PathBuf::from("/"))
  }
}

impl FmtOptions {
  pub fn new_with_base(base: PathBuf) -> Self {
    Self {
      options: FmtOptionsConfig::default(),
      unstable: Default::default(),
      files: FilePatterns::new_with_base(base),
    }
  }

  pub fn resolve(
    fmt_config: FmtConfig,
    unstable: UnstableFmtOptions,
    fmt_flags: &FmtFlags,
  ) -> Self {
    Self {
      options: resolve_fmt_options(fmt_flags, fmt_config.options),
      unstable: UnstableFmtOptions {
        component: unstable.component || fmt_flags.unstable_component,
        sql: unstable.sql || fmt_flags.unstable_sql,
      },
      files: fmt_config.files,
    }
  }
}

fn resolve_fmt_options(
  fmt_flags: &FmtFlags,
  mut options: FmtOptionsConfig,
) -> FmtOptionsConfig {
  if let Some(use_tabs) = fmt_flags.use_tabs {
    options.use_tabs = Some(use_tabs);
  }

  if let Some(line_width) = fmt_flags.line_width {
    options.line_width = Some(line_width.get());
  }

  if let Some(indent_width) = fmt_flags.indent_width {
    options.indent_width = Some(indent_width.get());
  }

  if let Some(single_quote) = fmt_flags.single_quote {
    options.single_quote = Some(single_quote);
  }

  if let Some(prose_wrap) = &fmt_flags.prose_wrap {
    options.prose_wrap = Some(match prose_wrap.as_str() {
      "always" => ProseWrap::Always,
      "never" => ProseWrap::Never,
      "preserve" => ProseWrap::Preserve,
      // validators in `flags.rs` makes other values unreachable
      _ => unreachable!(),
    });
  }

  if let Some(no_semis) = &fmt_flags.no_semicolons {
    options.semi_colons = Some(!no_semis);
  }

  options
}

#[derive(Clone, Debug)]
pub struct WorkspaceTestOptions {
  pub doc: bool,
  pub no_run: bool,
  pub fail_fast: Option<NonZeroUsize>,
  pub permit_no_files: bool,
  pub filter: Option<String>,
  pub shuffle: Option<u64>,
  pub concurrent_jobs: NonZeroUsize,
  pub trace_leaks: bool,
  pub reporter: TestReporterConfig,
  pub junit_path: Option<String>,
  pub hide_stacktraces: bool,
}

impl WorkspaceTestOptions {
  pub fn resolve(test_flags: &TestFlags) -> Self {
    Self {
      permit_no_files: test_flags.permit_no_files,
      concurrent_jobs: test_flags
        .concurrent_jobs
        .unwrap_or_else(|| NonZeroUsize::new(1).unwrap()),
      doc: test_flags.doc,
      fail_fast: test_flags.fail_fast,
      filter: test_flags.filter.clone(),
      no_run: test_flags.no_run,
      shuffle: test_flags.shuffle,
      trace_leaks: test_flags.trace_leaks,
      reporter: test_flags.reporter,
      junit_path: test_flags.junit_path.clone(),
      hide_stacktraces: test_flags.hide_stacktraces,
    }
  }
}

#[derive(Debug, Clone)]
pub struct TestOptions {
  pub files: FilePatterns,
}

impl TestOptions {
  pub fn resolve(test_config: TestConfig, _test_flags: &TestFlags) -> Self {
    // this is the same, but keeping the same pattern as everywhere else for the future
    Self {
      files: test_config.files,
    }
  }
}

#[derive(Clone, Copy, Default, Debug)]
pub enum LintReporterKind {
  #[default]
  Pretty,
  Json,
  Compact,
}

#[derive(Clone, Debug)]
pub struct WorkspaceLintOptions {
  pub reporter_kind: LintReporterKind,
}

impl WorkspaceLintOptions {
  pub fn resolve(
    lint_config: &WorkspaceLintConfig,
    lint_flags: &LintFlags,
  ) -> Result<Self, AnyError> {
    let mut maybe_reporter_kind = if lint_flags.json {
      Some(LintReporterKind::Json)
    } else if lint_flags.compact {
      Some(LintReporterKind::Compact)
    } else {
      None
    };

    if maybe_reporter_kind.is_none() {
      // Flag not set, so try to get lint reporter from the config file.
      maybe_reporter_kind = match lint_config.report.as_deref() {
        Some("json") => Some(LintReporterKind::Json),
        Some("compact") => Some(LintReporterKind::Compact),
        Some("pretty") => Some(LintReporterKind::Pretty),
        Some(_) => {
          bail!("Invalid lint report type in config file")
        }
        None => None,
      }
    }
    Ok(Self {
      reporter_kind: maybe_reporter_kind.unwrap_or_default(),
    })
  }
}

#[derive(Clone, Debug)]
pub struct LintOptions {
  pub rules: LintRulesConfig,
  pub files: FilePatterns,
  pub fix: bool,
}

impl Default for LintOptions {
  fn default() -> Self {
    Self::new_with_base(PathBuf::from("/"))
  }
}

impl LintOptions {
  pub fn new_with_base(base: PathBuf) -> Self {
    Self {
      rules: Default::default(),
      files: FilePatterns::new_with_base(base),
      fix: false,
    }
  }

  pub fn resolve(lint_config: LintConfig, lint_flags: &LintFlags) -> Self {
    Self {
      files: lint_config.files,
      rules: resolve_lint_rules_options(
        lint_config.options.rules,
        lint_flags.maybe_rules_tags.clone(),
        lint_flags.maybe_rules_include.clone(),
        lint_flags.maybe_rules_exclude.clone(),
      ),
      fix: lint_flags.fix,
    }
  }
}

fn resolve_lint_rules_options(
  config_rules: LintRulesConfig,
  mut maybe_rules_tags: Option<Vec<String>>,
  mut maybe_rules_include: Option<Vec<String>>,
  mut maybe_rules_exclude: Option<Vec<String>>,
) -> LintRulesConfig {
  // Try to get configured rules. CLI flags take precedence
  // over config file, i.e. if there's `rules.include` in config file
  // and `--rules-include` CLI flag, only the flag value is taken into account.
  if maybe_rules_include.is_none() {
    maybe_rules_include = config_rules.include;
  }
  if maybe_rules_exclude.is_none() {
    maybe_rules_exclude = config_rules.exclude;
  }
  if maybe_rules_tags.is_none() {
    maybe_rules_tags = config_rules.tags;
  }

  LintRulesConfig {
    exclude: maybe_rules_exclude,
    include: maybe_rules_include,
    tags: maybe_rules_tags,
  }
}

pub fn discover_npmrc_from_workspace(
  workspace: &Workspace,
) -> Result<(Arc<ResolvedNpmRc>, Option<PathBuf>), AnyError> {
  let root_folder = workspace.root_folder_configs();
  discover_npmrc(
    root_folder.pkg_json.as_ref().map(|p| p.path.clone()),
    root_folder.deno_json.as_ref().and_then(|cf| {
      if cf.specifier.scheme() == "file" {
        Some(cf.specifier.to_file_path().unwrap())
      } else {
        None
      }
    }),
  )
}

/// Discover `.npmrc` file - currently we only support it next to `package.json`
/// or next to `deno.json`.
///
/// In the future we will need to support it in user directory or global directory
/// as per https://docs.npmjs.com/cli/v10/configuring-npm/npmrc#files.
fn discover_npmrc(
  maybe_package_json_path: Option<PathBuf>,
  maybe_deno_json_path: Option<PathBuf>,
) -> Result<(Arc<ResolvedNpmRc>, Option<PathBuf>), AnyError> {
  const NPMRC_NAME: &str = ".npmrc";

  fn get_env_var(var_name: &str) -> Option<String> {
    std::env::var(var_name).ok()
  }

  #[derive(Debug, Error)]
  #[error("Error loading .npmrc at {}.", path.display())]
  struct NpmRcLoadError {
    path: PathBuf,
    #[source]
    source: std::io::Error,
  }

  fn try_to_read_npmrc(
    dir: &Path,
  ) -> Result<Option<(String, PathBuf)>, NpmRcLoadError> {
    let path = dir.join(NPMRC_NAME);
    let maybe_source = match std::fs::read_to_string(&path) {
      Ok(source) => Some(source),
      Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
      Err(err) => return Err(NpmRcLoadError { path, source: err }),
    };

    Ok(maybe_source.map(|source| (source, path)))
  }

  fn try_to_parse_npmrc(
    source: String,
    path: &Path,
  ) -> Result<Arc<ResolvedNpmRc>, AnyError> {
    let npmrc = NpmRc::parse(&source, &get_env_var).with_context(|| {
      format!("Failed to parse .npmrc at {}", path.display())
    })?;
    let resolved = npmrc
      .as_resolved(npm_registry_url())
      .context("Failed to resolve .npmrc options")?;
    log::debug!(".npmrc found at: '{}'", path.display());
    Ok(Arc::new(resolved))
  }

  // 1. Try `.npmrc` next to `package.json`
  if let Some(package_json_path) = maybe_package_json_path {
    if let Some(package_json_dir) = package_json_path.parent() {
      if let Some((source, path)) = try_to_read_npmrc(package_json_dir)? {
        return try_to_parse_npmrc(source, &path).map(|r| (r, Some(path)));
      }
    }
  }

  // 2. Try `.npmrc` next to `deno.json(c)`
  if let Some(deno_json_path) = maybe_deno_json_path {
    if let Some(deno_json_dir) = deno_json_path.parent() {
      if let Some((source, path)) = try_to_read_npmrc(deno_json_dir)? {
        return try_to_parse_npmrc(source, &path).map(|r| (r, Some(path)));
      }
    }
  }

  // TODO(bartlomieju): update to read both files - one in the project root and one and
  // home dir and then merge them.
  // 3. Try `.npmrc` in the user's home directory
  if let Some(home_dir) = cache::home_dir() {
    match try_to_read_npmrc(&home_dir) {
      Ok(Some((source, path))) => {
        return try_to_parse_npmrc(source, &path).map(|r| (r, Some(path)));
      }
      Ok(None) => {}
      Err(err) if err.source.kind() == std::io::ErrorKind::PermissionDenied => {
        log::debug!(
          "Skipping .npmrc in home directory due to permission denied error. {:#}",
          err
        );
      }
      Err(err) => {
        return Err(err.into());
      }
    }
  }

  log::debug!("No .npmrc file found");
  Ok((create_default_npmrc(), None))
}

pub fn create_default_npmrc() -> Arc<ResolvedNpmRc> {
  Arc::new(ResolvedNpmRc {
    default_config: deno_npm::npm_rc::RegistryConfigWithUrl {
      registry_url: npm_registry_url().clone(),
      config: Default::default(),
    },
    scopes: Default::default(),
    registry_configs: Default::default(),
  })
}

#[derive(Error, Debug, Clone)]
pub enum RootCertStoreLoadError {
  #[error(
    "Unknown certificate store \"{0}\" specified (allowed: \"system,mozilla\")"
  )]
  UnknownStore(String),
  #[error("Unable to add pem file to certificate store: {0}")]
  FailedAddPemFile(String),
  #[error("Failed opening CA file: {0}")]
  CaFileOpenError(String),
}

/// Create and populate a root cert store based on the passed options and
/// environment.
pub fn get_root_cert_store(
  maybe_root_path: Option<PathBuf>,
  maybe_ca_stores: Option<Vec<String>>,
  maybe_ca_data: Option<CaData>,
) -> Result<RootCertStore, RootCertStoreLoadError> {
  let mut root_cert_store = RootCertStore::empty();
  let ca_stores: Vec<String> = maybe_ca_stores
    .or_else(|| {
      let env_ca_store = env::var("DENO_TLS_CA_STORE").ok()?;
      Some(
        env_ca_store
          .split(',')
          .map(|s| s.trim().to_string())
          .filter(|s| !s.is_empty())
          .collect(),
      )
    })
    .unwrap_or_else(|| vec!["mozilla".to_string()]);

  for store in ca_stores.iter() {
    match store.as_str() {
      "mozilla" => {
        root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.to_vec());
      }
      "system" => {
        let roots = load_native_certs().expect("could not load platform certs");
        for root in roots {
          if let Err(err) = root_cert_store
            .add(rustls::pki_types::CertificateDer::from(root.0.clone()))
          {
            log::error!(
              "{}",
              colors::yellow(&format!(
                "Unable to add system certificate to certificate store: {:?}",
                err
              ))
            );
            let hex_encoded_root = faster_hex::hex_string(&root.0);
            log::error!("{}", colors::gray(&hex_encoded_root));
          }
        }
      }
      _ => {
        return Err(RootCertStoreLoadError::UnknownStore(store.clone()));
      }
    }
  }

  let ca_data =
    maybe_ca_data.or_else(|| env::var("DENO_CERT").ok().map(CaData::File));
  if let Some(ca_data) = ca_data {
    let result = match ca_data {
      CaData::File(ca_file) => {
        let ca_file = if let Some(root) = &maybe_root_path {
          root.join(&ca_file)
        } else {
          PathBuf::from(ca_file)
        };
        let certfile = std::fs::File::open(ca_file).map_err(|err| {
          RootCertStoreLoadError::CaFileOpenError(err.to_string())
        })?;
        let mut reader = BufReader::new(certfile);
        rustls_pemfile::certs(&mut reader).collect::<Result<Vec<_>, _>>()
      }
      CaData::Bytes(data) => {
        let mut reader = BufReader::new(Cursor::new(data));
        rustls_pemfile::certs(&mut reader).collect::<Result<Vec<_>, _>>()
      }
    };

    match result {
      Ok(certs) => {
        root_cert_store.add_parsable_certificates(certs);
      }
      Err(e) => {
        return Err(RootCertStoreLoadError::FailedAddPemFile(e.to_string()));
      }
    }
  }

  Ok(root_cert_store)
}

/// State provided to the process via an environment variable.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NpmProcessState {
  pub kind: NpmProcessStateKind,
  pub local_node_modules_path: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum NpmProcessStateKind {
  Snapshot(deno_npm::resolution::SerializedNpmResolutionSnapshot),
  Byonm,
}

static NPM_PROCESS_STATE: Lazy<Option<NpmProcessState>> = Lazy::new(|| {
  use deno_runtime::ops::process::NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME;
  let fd = std::env::var(NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME).ok()?;
  std::env::remove_var(NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME);
  let fd = fd.parse::<usize>().ok()?;
  let mut file = {
    use deno_runtime::deno_io::FromRawIoHandle;
    unsafe { std::fs::File::from_raw_io_handle(fd as _) }
  };
  let mut buf = Vec::new();
  // seek to beginning. after the file is written the position will be inherited by this subprocess,
  // and also this file might have been read before
  file.seek(std::io::SeekFrom::Start(0)).unwrap();
  file
    .read_to_end(&mut buf)
    .inspect_err(|e| {
      log::error!("failed to read npm process state from fd {fd}: {e}");
    })
    .ok()?;
  let state: NpmProcessState = serde_json::from_slice(&buf)
    .inspect_err(|e| {
      log::error!(
        "failed to deserialize npm process state: {e} {}",
        String::from_utf8_lossy(&buf)
      )
    })
    .ok()?;
  Some(state)
});

/// Overrides for the options below that when set will
/// use these values over the values derived from the
/// CLI flags or config file.
#[derive(Default, Clone)]
struct CliOptionOverrides {
  import_map_specifier: Option<Option<ModuleSpecifier>>,
}

/// Holds the resolved options of many sources used by subcommands
/// and provides some helper function for creating common objects.
pub struct CliOptions {
  // the source of the options is a detail the rest of the
  // application need not concern itself with, so keep these private
  flags: Arc<Flags>,
  initial_cwd: PathBuf,
  main_module_cell: std::sync::OnceLock<Result<ModuleSpecifier, AnyError>>,
  maybe_node_modules_folder: Option<PathBuf>,
  npmrc: Arc<ResolvedNpmRc>,
  maybe_lockfile: Option<Arc<CliLockfile>>,
  overrides: CliOptionOverrides,
  pub start_dir: Arc<WorkspaceDirectory>,
  pub deno_dir_provider: Arc<DenoDirProvider>,
}

impl CliOptions {
  pub fn new(
    flags: Arc<Flags>,
    initial_cwd: PathBuf,
    maybe_lockfile: Option<Arc<CliLockfile>>,
    npmrc: Arc<ResolvedNpmRc>,
    start_dir: Arc<WorkspaceDirectory>,
    force_global_cache: bool,
  ) -> Result<Self, AnyError> {
    if let Some(insecure_allowlist) =
      flags.unsafely_ignore_certificate_errors.as_ref()
    {
      let domains = if insecure_allowlist.is_empty() {
        "for all hostnames".to_string()
      } else {
        format!("for: {}", insecure_allowlist.join(", "))
      };
      let msg =
        format!("DANGER: TLS certificate validation is disabled {}", domains);
      {
        log::error!("{}", colors::yellow(msg));
      }
    }

    let maybe_lockfile = maybe_lockfile.filter(|_| !force_global_cache);
    let deno_dir_provider =
      Arc::new(DenoDirProvider::new(flags.internal.cache_path.clone()));
    let maybe_node_modules_folder = resolve_node_modules_folder(
      &initial_cwd,
      &flags,
      &start_dir.workspace,
      &deno_dir_provider,
    )
    .with_context(|| "Resolving node_modules folder.")?;

    load_env_variables_from_env_file(flags.env_file.as_ref());

    Ok(Self {
      flags,
      initial_cwd,
      maybe_lockfile,
      npmrc,
      maybe_node_modules_folder,
      overrides: Default::default(),
      main_module_cell: std::sync::OnceLock::new(),
      start_dir,
      deno_dir_provider,
    })
  }

  pub fn from_flags(flags: Arc<Flags>) -> Result<Self, AnyError> {
    let initial_cwd =
      std::env::current_dir().with_context(|| "Failed getting cwd.")?;
    let maybe_vendor_override = flags.vendor.map(|v| match v {
      true => VendorEnablement::Enable { cwd: &initial_cwd },
      false => VendorEnablement::Disable,
    });
    let resolve_workspace_discover_options = || {
      let additional_config_file_names: &'static [&'static str] =
        if matches!(flags.subcommand, DenoSubcommand::Publish(..)) {
          &["jsr.json", "jsr.jsonc"]
        } else {
          &[]
        };
      let config_parse_options =
        deno_config::deno_json::ConfigParseOptions::default();
      let discover_pkg_json = flags.config_flag != ConfigFlag::Disabled
        && !flags.no_npm
        && !has_flag_env_var("DENO_NO_PACKAGE_JSON");
      if !discover_pkg_json {
        log::debug!("package.json auto-discovery is disabled");
      }
      WorkspaceDiscoverOptions {
        fs: Default::default(), // use real fs
        deno_json_cache: None,
        pkg_json_cache: Some(&node_resolver::PackageJsonThreadLocalCache),
        workspace_cache: None,
        config_parse_options,
        additional_config_file_names,
        discover_pkg_json,
        maybe_vendor_override,
      }
    };
    let resolve_empty_options = || WorkspaceDirectoryEmptyOptions {
      root_dir: Arc::new(
        ModuleSpecifier::from_directory_path(&initial_cwd).unwrap(),
      ),
      use_vendor_dir: maybe_vendor_override
        .unwrap_or(VendorEnablement::Disable),
    };

    let start_dir = match &flags.config_flag {
      ConfigFlag::Discover => {
        if let Some(start_paths) = flags.config_path_args(&initial_cwd) {
          WorkspaceDirectory::discover(
            WorkspaceDiscoverStart::Paths(&start_paths),
            &resolve_workspace_discover_options(),
          )?
        } else {
          WorkspaceDirectory::empty(resolve_empty_options())
        }
      }
      ConfigFlag::Path(path) => {
        let config_path = normalize_path(initial_cwd.join(path));
        WorkspaceDirectory::discover(
          WorkspaceDiscoverStart::ConfigFile(&config_path),
          &resolve_workspace_discover_options(),
        )?
      }
      ConfigFlag::Disabled => {
        WorkspaceDirectory::empty(resolve_empty_options())
      }
    };

    for diagnostic in start_dir.workspace.diagnostics() {
      log::warn!("{} {}", colors::yellow("Warning"), diagnostic);
    }

    let (npmrc, _) = discover_npmrc_from_workspace(&start_dir.workspace)?;

    let maybe_lock_file = CliLockfile::discover(&flags, &start_dir.workspace)?;

    log::debug!("Finished config loading.");

    Self::new(
      flags,
      initial_cwd,
      maybe_lock_file.map(Arc::new),
      npmrc,
      Arc::new(start_dir),
      false,
    )
  }

  /// This method is purposefully verbose to disourage its use. Do not use it
  /// except in the factory structs. Instead, prefer specific methods on `CliOptions`
  /// that can take all sources of information into account (ex. config files or env vars).
  pub fn into_self_and_flags(
    self: Arc<CliOptions>,
  ) -> (Arc<CliOptions>, Arc<Flags>) {
    let flags = self.flags.clone();
    (self, flags)
  }

  #[inline(always)]
  pub fn initial_cwd(&self) -> &Path {
    &self.initial_cwd
  }

  #[inline(always)]
  pub fn workspace(&self) -> &Arc<Workspace> {
    &self.start_dir.workspace
  }

  pub fn graph_kind(&self) -> GraphKind {
    match self.sub_command() {
      DenoSubcommand::Cache(_) => GraphKind::All,
      DenoSubcommand::Check(_) => GraphKind::TypesOnly,
      DenoSubcommand::Install(InstallFlags {
        kind: InstallKind::Local(_),
      }) => GraphKind::All,
      _ => self.type_check_mode().as_graph_kind(),
    }
  }

  pub fn ts_type_lib_window(&self) -> TsTypeLib {
    TsTypeLib::DenoWindow
  }

  pub fn ts_type_lib_worker(&self) -> TsTypeLib {
    TsTypeLib::DenoWorker
  }

  pub fn cache_setting(&self) -> CacheSetting {
    if self.flags.cached_only {
      CacheSetting::Only
    } else if !self.flags.cache_blocklist.is_empty() {
      CacheSetting::ReloadSome(self.flags.cache_blocklist.clone())
    } else if self.flags.reload {
      CacheSetting::ReloadAll
    } else {
      CacheSetting::Use
    }
  }

  pub fn npm_system_info(&self) -> NpmSystemInfo {
    match self.sub_command() {
      DenoSubcommand::Compile(CompileFlags {
        target: Some(target),
        ..
      }) => {
        // the values of NpmSystemInfo align with the possible values for the
        // `arch` and `platform` fields of Node.js' `process` global:
        // https://nodejs.org/api/process.html
        match target.as_str() {
          "aarch64-apple-darwin" => NpmSystemInfo {
            os: "darwin".to_string(),
            cpu: "arm64".to_string(),
          },
          "aarch64-unknown-linux-gnu" => NpmSystemInfo {
            os: "linux".to_string(),
            cpu: "arm64".to_string(),
          },
          "x86_64-apple-darwin" => NpmSystemInfo {
            os: "darwin".to_string(),
            cpu: "x64".to_string(),
          },
          "x86_64-unknown-linux-gnu" => NpmSystemInfo {
            os: "linux".to_string(),
            cpu: "x64".to_string(),
          },
          "x86_64-pc-windows-msvc" => NpmSystemInfo {
            os: "win32".to_string(),
            cpu: "x64".to_string(),
          },
          value => {
            log::warn!(
              concat!(
                "Not implemented npm system info for target '{}'. Using current ",
                "system default. This may impact architecture specific dependencies."
              ),
              value,
            );
            NpmSystemInfo::default()
          }
        }
      }
      _ => NpmSystemInfo::default(),
    }
  }

  /// Resolve the specifier for a specified import map.
  ///
  /// This will NOT include the config file if it
  /// happens to be an import map.
  pub fn resolve_specified_import_map_specifier(
    &self,
  ) -> Result<Option<ModuleSpecifier>, AnyError> {
    match self.overrides.import_map_specifier.clone() {
      Some(maybe_url) => Ok(maybe_url),
      None => resolve_import_map_specifier(
        self.flags.import_map_path.as_deref(),
        self.workspace().root_deno_json().map(|c| c.as_ref()),
        &self.initial_cwd,
      ),
    }
  }

  pub async fn create_workspace_resolver(
    &self,
    file_fetcher: &FileFetcher,
    pkg_json_dep_resolution: PackageJsonDepResolution,
  ) -> Result<WorkspaceResolver, AnyError> {
    let overrode_no_import_map = self
      .overrides
      .import_map_specifier
      .as_ref()
      .map(|s| s.is_none())
      == Some(true);
    let cli_arg_specified_import_map = if overrode_no_import_map {
      // use a fake empty import map
      Some(deno_config::workspace::SpecifiedImportMap {
        base_url: self.workspace().root_dir().join("import_map.json").unwrap(),
        value: serde_json::Value::Object(Default::default()),
      })
    } else {
      let maybe_import_map_specifier =
        self.resolve_specified_import_map_specifier()?;
      match maybe_import_map_specifier {
        Some(specifier) => {
          let value =
            resolve_import_map_value_from_specifier(&specifier, file_fetcher)
              .await
              .with_context(|| {
                format!("Unable to load '{}' import map", specifier)
              })?;
          Some(deno_config::workspace::SpecifiedImportMap {
            base_url: specifier,
            value,
          })
        }
        None => None,
      }
    };
    Ok(self.workspace().create_resolver(
      CreateResolverOptions {
        pkg_json_dep_resolution,
        specified_import_map: cli_arg_specified_import_map,
      },
      |path| Ok(std::fs::read_to_string(path)?),
    )?)
  }

  pub fn node_ipc_fd(&self) -> Option<i64> {
    let maybe_node_channel_fd = std::env::var("NODE_CHANNEL_FD").ok();
    if let Some(node_channel_fd) = maybe_node_channel_fd {
      // Remove so that child processes don't inherit this environment variable.
      std::env::remove_var("NODE_CHANNEL_FD");
      node_channel_fd.parse::<i64>().ok()
    } else {
      None
    }
  }

  pub fn serve_port(&self) -> Option<u16> {
    if let DenoSubcommand::Serve(flags) = self.sub_command() {
      Some(flags.port)
    } else {
      None
    }
  }

  pub fn serve_host(&self) -> Option<String> {
    if let DenoSubcommand::Serve(flags) = self.sub_command() {
      Some(flags.host.clone())
    } else {
      None
    }
  }

  pub fn otel_config(&self) -> Option<OtelConfig> {
    self.flags.otel_config()
  }

  pub fn env_file_name(&self) -> Option<&Vec<String>> {
    self.flags.env_file.as_ref()
  }

  pub fn resolve_main_module(&self) -> Result<&ModuleSpecifier, AnyError> {
    self
      .main_module_cell
      .get_or_init(|| {
        Ok(match &self.flags.subcommand {
          DenoSubcommand::Compile(compile_flags) => {
            resolve_url_or_path(&compile_flags.source_file, self.initial_cwd())?
          }
          DenoSubcommand::Eval(_) => {
            resolve_url_or_path("./$deno$eval.mts", self.initial_cwd())?
          }
          DenoSubcommand::Repl(_) => {
            resolve_url_or_path("./$deno$repl.mts", self.initial_cwd())?
          }
          DenoSubcommand::Run(run_flags) => {
            if run_flags.is_stdin() {
              resolve_url_or_path("./$deno$stdin.mts", self.initial_cwd())?
            } else {
              let url =
                resolve_url_or_path(&run_flags.script, self.initial_cwd())?;
              if self.is_node_main()
                && url.scheme() == "file"
                && MediaType::from_specifier(&url) == MediaType::Unknown
              {
                try_resolve_node_binary_main_entrypoint(
                  &run_flags.script,
                  self.initial_cwd(),
                )?
                .unwrap_or(url)
              } else {
                url
              }
            }
          }
          DenoSubcommand::Serve(run_flags) => {
            resolve_url_or_path(&run_flags.script, self.initial_cwd())?
          }
          _ => {
            bail!("No main module.")
          }
        })
      })
      .as_ref()
      .map_err(|err| deno_core::anyhow::anyhow!("{}", err))
  }

  pub fn resolve_file_header_overrides(
    &self,
  ) -> HashMap<ModuleSpecifier, HashMap<String, String>> {
    let maybe_main_specifier = self.resolve_main_module().ok();
    // TODO(Cre3per): This mapping moved to deno_ast with https://github.com/denoland/deno_ast/issues/133 and should be available in deno_ast >= 0.25.0 via `MediaType::from_path(...).as_media_type()`
    let maybe_content_type =
      self.flags.ext.as_ref().and_then(|el| match el.as_str() {
        "ts" => Some("text/typescript"),
        "tsx" => Some("text/tsx"),
        "js" => Some("text/javascript"),
        "jsx" => Some("text/jsx"),
        _ => None,
      });

    if let (Some(main_specifier), Some(content_type)) =
      (maybe_main_specifier, maybe_content_type)
    {
      HashMap::from([(
        main_specifier.clone(),
        HashMap::from([("content-type".to_string(), content_type.to_string())]),
      )])
    } else {
      HashMap::default()
    }
  }

  pub fn resolve_npm_resolution_snapshot(
    &self,
  ) -> Result<Option<ValidSerializedNpmResolutionSnapshot>, AnyError> {
    if let Some(NpmProcessStateKind::Snapshot(snapshot)) =
      NPM_PROCESS_STATE.as_ref().map(|s| &s.kind)
    {
      // TODO(bartlomieju): remove this clone
      Ok(Some(snapshot.clone().into_valid()?))
    } else {
      Ok(None)
    }
  }

  // If the main module should be treated as being in an npm package.
  // This is triggered via a secret environment variable which is used
  // for functionality like child_process.fork. Users should NOT depend
  // on this functionality.
  pub fn is_node_main(&self) -> bool {
    NPM_PROCESS_STATE.is_some()
  }

  pub fn has_node_modules_dir(&self) -> bool {
    self.maybe_node_modules_folder.is_some()
  }

  pub fn node_modules_dir_path(&self) -> Option<&PathBuf> {
    self.maybe_node_modules_folder.as_ref()
  }

  pub fn node_modules_dir(
    &self,
  ) -> Result<Option<NodeModulesDirMode>, AnyError> {
    if let Some(flag) = self.flags.node_modules_dir {
      return Ok(Some(flag));
    }
    self.workspace().node_modules_dir().map_err(Into::into)
  }

  pub fn vendor_dir_path(&self) -> Option<&PathBuf> {
    self.workspace().vendor_dir_path()
  }

  pub fn resolve_ts_config_for_emit(
    &self,
    config_type: TsConfigType,
  ) -> Result<TsConfigForEmit, AnyError> {
    self.workspace().resolve_ts_config_for_emit(config_type)
  }

  pub fn resolve_inspector_server(
    &self,
  ) -> Result<Option<InspectorServer>, AnyError> {
    let maybe_inspect_host = self
      .flags
      .inspect
      .or(self.flags.inspect_brk)
      .or(self.flags.inspect_wait);

    let Some(host) = maybe_inspect_host else {
      return Ok(None);
    };

    Ok(Some(InspectorServer::new(
      host,
      version::DENO_VERSION_INFO.user_agent,
    )?))
  }

  pub fn maybe_lockfile(&self) -> Option<&Arc<CliLockfile>> {
    self.maybe_lockfile.as_ref()
  }

  pub fn to_compiler_option_types(
    &self,
  ) -> Result<Vec<deno_graph::ReferrerImports>, AnyError> {
    self
      .workspace()
      .to_compiler_option_types()
      .map(|maybe_imports| {
        maybe_imports
          .into_iter()
          .map(|(referrer, imports)| deno_graph::ReferrerImports {
            referrer,
            imports,
          })
          .collect()
      })
  }

  pub fn npmrc(&self) -> &Arc<ResolvedNpmRc> {
    &self.npmrc
  }

  pub fn resolve_fmt_options_for_members(
    &self,
    fmt_flags: &FmtFlags,
  ) -> Result<Vec<(WorkspaceDirectory, FmtOptions)>, AnyError> {
    let cli_arg_patterns =
      fmt_flags.files.as_file_patterns(self.initial_cwd())?;
    let member_configs = self
      .workspace()
      .resolve_fmt_config_for_members(&cli_arg_patterns)?;
    let unstable = self.resolve_config_unstable_fmt_options();
    let mut result = Vec::with_capacity(member_configs.len());
    for (ctx, config) in member_configs {
      let options = FmtOptions::resolve(config, unstable.clone(), fmt_flags);
      result.push((ctx, options));
    }
    Ok(result)
  }

  pub fn resolve_config_unstable_fmt_options(&self) -> UnstableFmtOptions {
    let workspace = self.workspace();
    UnstableFmtOptions {
      component: workspace.has_unstable("fmt-component"),
      sql: workspace.has_unstable("fmt-sql"),
    }
  }

  pub fn resolve_workspace_lint_options(
    &self,
    lint_flags: &LintFlags,
  ) -> Result<WorkspaceLintOptions, AnyError> {
    let lint_config = self.workspace().to_lint_config()?;
    WorkspaceLintOptions::resolve(&lint_config, lint_flags)
  }

  pub fn resolve_lint_options_for_members(
    &self,
    lint_flags: &LintFlags,
  ) -> Result<Vec<(WorkspaceDirectory, LintOptions)>, AnyError> {
    let cli_arg_patterns =
      lint_flags.files.as_file_patterns(self.initial_cwd())?;
    let member_configs = self
      .workspace()
      .resolve_lint_config_for_members(&cli_arg_patterns)?;
    let mut result = Vec::with_capacity(member_configs.len());
    for (ctx, config) in member_configs {
      let options = LintOptions::resolve(config, lint_flags);
      result.push((ctx, options));
    }
    Ok(result)
  }

  pub fn resolve_deno_lint_config(
    &self,
  ) -> Result<deno_lint::linter::LintConfig, AnyError> {
    let ts_config_result =
      self.resolve_ts_config_for_emit(TsConfigType::Emit)?;

    let (transpile_options, _) =
      crate::args::ts_config_to_transpile_and_emit_options(
        ts_config_result.ts_config,
      )?;

    Ok(deno_lint::linter::LintConfig {
      default_jsx_factory: (!transpile_options.jsx_automatic)
        .then(|| transpile_options.jsx_factory.clone()),
      default_jsx_fragment_factory: (!transpile_options.jsx_automatic)
        .then(|| transpile_options.jsx_fragment_factory.clone()),
    })
  }

  pub fn resolve_workspace_test_options(
    &self,
    test_flags: &TestFlags,
  ) -> WorkspaceTestOptions {
    WorkspaceTestOptions::resolve(test_flags)
  }

  pub fn resolve_test_options_for_members(
    &self,
    test_flags: &TestFlags,
  ) -> Result<Vec<(WorkspaceDirectory, TestOptions)>, AnyError> {
    let cli_arg_patterns =
      test_flags.files.as_file_patterns(self.initial_cwd())?;
    let workspace_dir_configs = self
      .workspace()
      .resolve_test_config_for_members(&cli_arg_patterns)?;
    let mut result = Vec::with_capacity(workspace_dir_configs.len());
    for (member_dir, config) in workspace_dir_configs {
      let options = TestOptions::resolve(config, test_flags);
      result.push((member_dir, options));
    }
    Ok(result)
  }

  pub fn resolve_workspace_bench_options(
    &self,
    bench_flags: &BenchFlags,
  ) -> WorkspaceBenchOptions {
    WorkspaceBenchOptions::resolve(bench_flags)
  }

  pub fn resolve_bench_options_for_members(
    &self,
    bench_flags: &BenchFlags,
  ) -> Result<Vec<(WorkspaceDirectory, BenchOptions)>, AnyError> {
    let cli_arg_patterns =
      bench_flags.files.as_file_patterns(self.initial_cwd())?;
    let workspace_dir_configs = self
      .workspace()
      .resolve_bench_config_for_members(&cli_arg_patterns)?;
    let mut result = Vec::with_capacity(workspace_dir_configs.len());
    for (member_dir, config) in workspace_dir_configs {
      let options = BenchOptions::resolve(config, bench_flags);
      result.push((member_dir, options));
    }
    Ok(result)
  }

  /// Vector of user script CLI arguments.
  pub fn argv(&self) -> &Vec<String> {
    &self.flags.argv
  }

  pub fn ca_data(&self) -> &Option<CaData> {
    &self.flags.ca_data
  }

  pub fn ca_stores(&self) -> &Option<Vec<String>> {
    &self.flags.ca_stores
  }

  pub fn check_js(&self) -> bool {
    self.workspace().check_js()
  }

  pub fn coverage_dir(&self) -> Option<String> {
    match &self.flags.subcommand {
      DenoSubcommand::Test(test) => test
        .coverage_dir
        .as_ref()
        .map(ToOwned::to_owned)
        .or_else(|| env::var("DENO_UNSTABLE_COVERAGE_DIR").ok()),
      _ => None,
    }
  }

  pub fn enable_op_summary_metrics(&self) -> bool {
    self.flags.enable_op_summary_metrics
      || matches!(
        self.flags.subcommand,
        DenoSubcommand::Test(_)
          | DenoSubcommand::Repl(_)
          | DenoSubcommand::Jupyter(_)
      )
  }

  pub fn enable_testing_features(&self) -> bool {
    self.flags.enable_testing_features
  }

  pub fn ext_flag(&self) -> &Option<String> {
    &self.flags.ext
  }

  pub fn has_hmr(&self) -> bool {
    if let DenoSubcommand::Run(RunFlags {
      watch: Some(WatchFlagsWithPaths { hmr, .. }),
      ..
    }) = &self.flags.subcommand
    {
      *hmr
    } else if let DenoSubcommand::Serve(ServeFlags {
      watch: Some(WatchFlagsWithPaths { hmr, .. }),
      ..
    }) = &self.flags.subcommand
    {
      *hmr
    } else {
      false
    }
  }

  /// If the --inspect or --inspect-brk flags are used.
  pub fn is_inspecting(&self) -> bool {
    self.flags.inspect.is_some()
      || self.flags.inspect_brk.is_some()
      || self.flags.inspect_wait.is_some()
  }

  pub fn inspect_brk(&self) -> Option<SocketAddr> {
    self.flags.inspect_brk
  }

  pub fn inspect_wait(&self) -> Option<SocketAddr> {
    self.flags.inspect_wait
  }

  pub fn log_level(&self) -> Option<log::Level> {
    self.flags.log_level
  }

  pub fn is_quiet(&self) -> bool {
    self
      .log_level()
      .map(|l| l == log::Level::Error)
      .unwrap_or(false)
  }

  pub fn location_flag(&self) -> &Option<Url> {
    &self.flags.location
  }

  pub fn no_remote(&self) -> bool {
    self.flags.no_remote
  }

  pub fn no_npm(&self) -> bool {
    self.flags.no_npm
  }

  pub fn permission_flags(&self) -> &PermissionFlags {
    &self.flags.permissions
  }

  pub fn permissions_options(&self) -> PermissionsOptions {
    fn files_to_urls(files: &[String]) -> Vec<Cow<'_, Url>> {
      files
        .iter()
        .filter_map(|f| Url::parse(f).ok().map(Cow::Owned))
        .collect()
    }

    // get a list of urls to imply for --allow-import
    let cli_arg_urls = self
      .resolve_main_module()
      .ok()
      .map(|url| vec![Cow::Borrowed(url)])
      .or_else(|| match &self.flags.subcommand {
        DenoSubcommand::Cache(cache_flags) => {
          Some(files_to_urls(&cache_flags.files))
        }
        DenoSubcommand::Check(check_flags) => {
          Some(files_to_urls(&check_flags.files))
        }
        DenoSubcommand::Install(InstallFlags {
          kind: InstallKind::Global(flags),
        }) => Url::parse(&flags.module_url)
          .ok()
          .map(|url| vec![Cow::Owned(url)]),
        DenoSubcommand::Doc(DocFlags {
          source_files: DocSourceFileFlag::Paths(paths),
          ..
        }) => Some(files_to_urls(paths)),
        _ => None,
      })
      .unwrap_or_default();
    self.flags.permissions.to_options(&cli_arg_urls)
  }

  pub fn reload_flag(&self) -> bool {
    self.flags.reload
  }

  pub fn seed(&self) -> Option<u64> {
    self.flags.seed
  }

  pub fn sub_command(&self) -> &DenoSubcommand {
    &self.flags.subcommand
  }

  pub fn strace_ops(&self) -> &Option<Vec<String>> {
    &self.flags.strace_ops
  }

  pub fn take_binary_npm_command_name(&self) -> Option<String> {
    match self.sub_command() {
      DenoSubcommand::Run(flags) => {
        const NPM_CMD_NAME_ENV_VAR_NAME: &str = "DENO_INTERNAL_NPM_CMD_NAME";
        match std::env::var(NPM_CMD_NAME_ENV_VAR_NAME) {
          Ok(var) => {
            // remove the env var so that child sub processes won't pick this up
            std::env::remove_var(NPM_CMD_NAME_ENV_VAR_NAME);
            Some(var)
          }
          Err(_) => NpmPackageReqReference::from_str(&flags.script)
            .ok()
            .map(|req_ref| npm_pkg_req_ref_to_binary_command(&req_ref)),
        }
      }
      _ => None,
    }
  }

  pub fn type_check_mode(&self) -> TypeCheckMode {
    self.flags.type_check_mode
  }

  pub fn unsafely_ignore_certificate_errors(&self) -> &Option<Vec<String>> {
    &self.flags.unsafely_ignore_certificate_errors
  }

  pub fn unstable_bare_node_builtins(&self) -> bool {
    self.flags.unstable_config.bare_node_builtins
      || self.workspace().has_unstable("bare-node-builtins")
  }

  pub fn unstable_detect_cjs(&self) -> bool {
    self.flags.unstable_config.detect_cjs
      || self.workspace().has_unstable("detect-cjs")
  }

  pub fn detect_cjs(&self) -> bool {
    // only enabled when there's a package.json in order to not have a
    // perf penalty for non-npm Deno projects of searching for the closest
    // package.json beside each module
    self.workspace().package_jsons().next().is_some() || self.is_node_main()
  }

  fn byonm_enabled(&self) -> bool {
    // check if enabled via unstable
    self.node_modules_dir().ok().flatten() == Some(NodeModulesDirMode::Manual)
      || NPM_PROCESS_STATE
        .as_ref()
        .map(|s| matches!(s.kind, NpmProcessStateKind::Byonm))
        .unwrap_or(false)
  }

  pub fn use_byonm(&self) -> bool {
    if matches!(
      self.sub_command(),
      DenoSubcommand::Install(_)
        | DenoSubcommand::Add(_)
        | DenoSubcommand::Remove(_)
        | DenoSubcommand::Init(_)
        | DenoSubcommand::Outdated(_)
    ) {
      // For `deno install/add/remove/init` we want to force the managed resolver so it can set up `node_modules/` directory.
      return false;
    }
    if self.node_modules_dir().ok().flatten().is_none()
      && self.maybe_node_modules_folder.is_some()
      && self
        .workspace()
        .config_folders()
        .values()
        .any(|f| f.pkg_json.is_some())
    {
      return true;
    }

    self.byonm_enabled()
  }

  pub fn unstable_sloppy_imports(&self) -> bool {
    self.flags.unstable_config.sloppy_imports
      || self.workspace().has_unstable("sloppy-imports")
  }

  pub fn unstable_features(&self) -> Vec<String> {
    let mut from_config_file = self.workspace().unstable_features().to_vec();

    self
      .flags
      .unstable_config
      .features
      .iter()
      .for_each(|feature| {
        if !from_config_file.contains(feature) {
          from_config_file.push(feature.to_string());
        }
      });

    if !from_config_file.is_empty() {
      let all_valid_unstable_flags: Vec<&str> = crate::UNSTABLE_GRANULAR_FLAGS
        .iter()
        .map(|granular_flag| granular_flag.name)
        .chain([
          "sloppy-imports",
          "byonm",
          "bare-node-builtins",
          "detect-cjs",
          "fmt-component",
          "fmt-sql",
        ])
        .collect();

      // check and warn if the unstable flag of config file isn't supported, by
      // iterating through the vector holding the unstable flags
      for unstable_value_from_config_file in &from_config_file {
        if !all_valid_unstable_flags
          .contains(&unstable_value_from_config_file.as_str())
        {
          log::warn!(
            "{} '{}' isn't a valid unstable feature",
            colors::yellow("Warning"),
            unstable_value_from_config_file
          );
        }
      }
    }

    from_config_file
  }

  pub fn v8_flags(&self) -> &Vec<String> {
    &self.flags.v8_flags
  }

  pub fn code_cache_enabled(&self) -> bool {
    self.flags.code_cache_enabled
  }

  pub fn watch_paths(&self) -> Vec<PathBuf> {
    let mut full_paths = Vec::new();
    if let DenoSubcommand::Run(RunFlags {
      watch: Some(WatchFlagsWithPaths { paths, .. }),
      ..
    })
    | DenoSubcommand::Serve(ServeFlags {
      watch: Some(WatchFlagsWithPaths { paths, .. }),
      ..
    }) = &self.flags.subcommand
    {
      full_paths.extend(paths.iter().map(|path| self.initial_cwd.join(path)));
    }

    if let Ok(Some(import_map_path)) = self
      .resolve_specified_import_map_specifier()
      .map(|ms| ms.and_then(|ref s| s.to_file_path().ok()))
    {
      full_paths.push(import_map_path);
    }

    for (_, folder) in self.workspace().config_folders() {
      if let Some(deno_json) = &folder.deno_json {
        if deno_json.specifier.scheme() == "file" {
          if let Ok(path) = deno_json.specifier.to_file_path() {
            full_paths.push(path);
          }
        }
      }
      if let Some(pkg_json) = &folder.pkg_json {
        full_paths.push(pkg_json.path.clone());
      }
    }
    full_paths
  }

  pub fn lifecycle_scripts_config(&self) -> LifecycleScriptsConfig {
    LifecycleScriptsConfig {
      allowed: self.flags.allow_scripts.clone(),
      initial_cwd: self.initial_cwd.clone(),
      root_dir: self.workspace().root_dir_path(),
      explicit_install: matches!(
        self.sub_command(),
        DenoSubcommand::Install(_)
          | DenoSubcommand::Cache(_)
          | DenoSubcommand::Add(_)
      ),
    }
  }
}

/// Resolves the path to use for a local node_modules folder.
fn resolve_node_modules_folder(
  cwd: &Path,
  flags: &Flags,
  workspace: &Workspace,
  deno_dir_provider: &Arc<DenoDirProvider>,
) -> Result<Option<PathBuf>, AnyError> {
  fn resolve_from_root(root_folder: &FolderConfigs, cwd: &Path) -> PathBuf {
    root_folder
      .deno_json
      .as_ref()
      .map(|c| Cow::Owned(c.dir_path()))
      .or_else(|| {
        root_folder
          .pkg_json
          .as_ref()
          .map(|c| Cow::Borrowed(c.dir_path()))
      })
      .unwrap_or(Cow::Borrowed(cwd))
      .join("node_modules")
  }

  let root_folder = workspace.root_folder_configs();
  let use_node_modules_dir = if let Some(mode) = flags.node_modules_dir {
    Some(mode.uses_node_modules_dir())
  } else {
    workspace
      .node_modules_dir()?
      .map(|m| m.uses_node_modules_dir())
      .or(flags.vendor)
      .or_else(|| root_folder.deno_json.as_ref().and_then(|c| c.json.vendor))
  };
  let path = if use_node_modules_dir == Some(false) {
    return Ok(None);
  } else if let Some(state) = &*NPM_PROCESS_STATE {
    return Ok(state.local_node_modules_path.as_ref().map(PathBuf::from));
  } else if root_folder.pkg_json.is_some() {
    let node_modules_dir = resolve_from_root(root_folder, cwd);
    if let Ok(deno_dir) = deno_dir_provider.get_or_create() {
      // `deno_dir.root` can be symlink in macOS
      if let Ok(root) = canonicalize_path_maybe_not_exists(&deno_dir.root) {
        if node_modules_dir.starts_with(root) {
          // if the package.json is in deno_dir, then do not use node_modules
          // next to it as local node_modules dir
          return Ok(None);
        }
      }
    }
    node_modules_dir
  } else if use_node_modules_dir.is_none() {
    return Ok(None);
  } else {
    resolve_from_root(root_folder, cwd)
  };
  Ok(Some(canonicalize_path_maybe_not_exists(&path)?))
}

fn try_resolve_node_binary_main_entrypoint(
  specifier: &str,
  initial_cwd: &Path,
) -> Result<Option<Url>, AnyError> {
  // node allows running files at paths without a `.js` extension
  // or at directories with an index.js file
  let path = deno_core::normalize_path(initial_cwd.join(specifier));
  if path.is_dir() {
    let index_file = path.join("index.js");
    Ok(if index_file.is_file() {
      Some(deno_path_util::url_from_file_path(&index_file)?)
    } else {
      None
    })
  } else {
    let path = path.with_extension(
      path
        .extension()
        .and_then(|s| s.to_str())
        .map(|s| format!("{}.js", s))
        .unwrap_or("js".to_string()),
    );
    if path.is_file() {
      Ok(Some(deno_path_util::url_from_file_path(&path)?))
    } else {
      Ok(None)
    }
  }
}

fn resolve_import_map_specifier(
  maybe_import_map_path: Option<&str>,
  maybe_config_file: Option<&ConfigFile>,
  current_dir: &Path,
) -> Result<Option<ModuleSpecifier>, AnyError> {
  if let Some(import_map_path) = maybe_import_map_path {
    if let Some(config_file) = &maybe_config_file {
      if config_file.json.import_map.is_some() {
        log::warn!("{} the configuration file \"{}\" contains an entry for \"importMap\" that is being ignored.", colors::yellow("Warning"), config_file.specifier);
      }
    }
    let specifier =
      deno_core::resolve_url_or_path(import_map_path, current_dir)
        .with_context(|| {
          format!("Bad URL (\"{import_map_path}\") for import map.")
        })?;
    Ok(Some(specifier))
  } else {
    Ok(None)
  }
}

pub struct StorageKeyResolver(Option<Option<String>>);

impl StorageKeyResolver {
  pub fn from_options(options: &CliOptions) -> Self {
    Self(if let Some(location) = &options.flags.location {
      // if a location is set, then the ascii serialization of the location is
      // used, unless the origin is opaque, and then no storage origin is set, as
      // we can't expect the origin to be reproducible
      let storage_origin = location.origin();
      if storage_origin.is_tuple() {
        Some(Some(storage_origin.ascii_serialization()))
      } else {
        Some(None)
      }
    } else {
      // otherwise we will use the path to the config file or None to
      // fall back to using the main module's path
      options
        .start_dir
        .maybe_deno_json()
        .map(|config_file| Some(config_file.specifier.to_string()))
    })
  }

  /// Creates a storage key resolver that will always resolve to being empty.
  pub fn empty() -> Self {
    Self(Some(None))
  }

  /// Resolves the storage key to use based on the current flags, config, or main module.
  pub fn resolve_storage_key(
    &self,
    main_module: &ModuleSpecifier,
  ) -> Option<String> {
    // use the stored value or fall back to using the path of the main module.
    if let Some(maybe_value) = &self.0 {
      maybe_value.clone()
    } else {
      Some(main_module.to_string())
    }
  }
}

/// Resolves the no_prompt value based on the cli flags and environment.
pub fn resolve_no_prompt(flags: &PermissionFlags) -> bool {
  flags.no_prompt || has_flag_env_var("DENO_NO_PROMPT")
}

pub fn has_trace_permissions_enabled() -> bool {
  has_flag_env_var("DENO_TRACE_PERMISSIONS")
}

pub fn has_flag_env_var(name: &str) -> bool {
  let value = env::var(name);
  matches!(value.as_ref().map(|s| s.as_str()), Ok("1"))
}

pub fn npm_pkg_req_ref_to_binary_command(
  req_ref: &NpmPackageReqReference,
) -> String {
  let binary_name = req_ref.sub_path().unwrap_or(req_ref.req().name.as_str());
  binary_name.to_string()
}

pub fn config_to_deno_graph_workspace_member(
  config: &ConfigFile,
) -> Result<deno_graph::WorkspaceMember, AnyError> {
  let name = match &config.json.name {
    Some(name) => name.clone(),
    None => bail!("Missing 'name' field in config file."),
  };
  let version = match &config.json.version {
    Some(name) => Some(deno_semver::Version::parse_standard(name)?),
    None => None,
  };
  Ok(deno_graph::WorkspaceMember {
    base: config.specifier.join("./").unwrap(),
    name,
    version,
    exports: config.to_exports_config()?.into_map(),
  })
}

fn load_env_variables_from_env_file(filename: Option<&Vec<String>>) {
  let Some(env_file_names) = filename else {
    return;
  };

  for env_file_name in env_file_names.iter().rev() {
    match from_filename(env_file_name) {
      Ok(_) => (),
      Err(error) => {
        match error {
          dotenvy::Error::LineParse(line, index)=> log::info!("{} Parsing failed within the specified environment file: {} at index: {} of the value: {}",colors::yellow("Warning"), env_file_name, index, line),
          dotenvy::Error::Io(_)=> log::info!("{} The `--env-file` flag was used, but the environment file specified '{}' was not found.",colors::yellow("Warning"),env_file_name),
          dotenvy::Error::EnvVar(_)=> log::info!("{} One or more of the environment variables isn't present or not unicode within the specified environment file: {}",colors::yellow("Warning"),env_file_name),
          _ => log::info!("{} Unknown failure occurred with the specified environment file: {}", colors::yellow("Warning"), env_file_name),
        }
      }
    }
  }
}

#[cfg(test)]
mod test {
  use pretty_assertions::assert_eq;

  use super::*;

  #[test]
  fn resolve_import_map_flags_take_precedence() {
    let config_text = r#"{
      "importMap": "import_map.json"
    }"#;
    let cwd = &std::env::current_dir().unwrap();
    let config_specifier =
      ModuleSpecifier::parse("file:///deno/deno.jsonc").unwrap();
    let config_file = ConfigFile::new(
      config_text,
      config_specifier,
      &deno_config::deno_json::ConfigParseOptions::default(),
    )
    .unwrap();
    let actual = resolve_import_map_specifier(
      Some("import-map.json"),
      Some(&config_file),
      cwd,
    );
    let import_map_path = cwd.join("import-map.json");
    let expected_specifier =
      ModuleSpecifier::from_file_path(import_map_path).unwrap();
    assert!(actual.is_ok());
    let actual = actual.unwrap();
    assert_eq!(actual, Some(expected_specifier));
  }

  #[test]
  fn resolve_import_map_none() {
    let config_text = r#"{}"#;
    let config_specifier =
      ModuleSpecifier::parse("file:///deno/deno.jsonc").unwrap();
    let config_file = ConfigFile::new(
      config_text,
      config_specifier,
      &deno_config::deno_json::ConfigParseOptions::default(),
    )
    .unwrap();
    let actual = resolve_import_map_specifier(
      None,
      Some(&config_file),
      &PathBuf::from("/"),
    );
    assert!(actual.is_ok());
    let actual = actual.unwrap();
    assert_eq!(actual, None);
  }

  #[test]
  fn resolve_import_map_no_config() {
    let actual = resolve_import_map_specifier(None, None, &PathBuf::from("/"));
    assert!(actual.is_ok());
    let actual = actual.unwrap();
    assert_eq!(actual, None);
  }

  #[test]
  fn storage_key_resolver_test() {
    let resolver = StorageKeyResolver(None);
    let specifier = ModuleSpecifier::parse("file:///a.ts").unwrap();
    assert_eq!(
      resolver.resolve_storage_key(&specifier),
      Some(specifier.to_string())
    );
    let resolver = StorageKeyResolver(Some(None));
    assert_eq!(resolver.resolve_storage_key(&specifier), None);
    let resolver = StorageKeyResolver(Some(Some("value".to_string())));
    assert_eq!(
      resolver.resolve_storage_key(&specifier),
      Some("value".to_string())
    );

    // test empty
    let resolver = StorageKeyResolver::empty();
    assert_eq!(resolver.resolve_storage_key(&specifier), None);
  }

  #[test]
  fn jsr_urls() {
    let reg_url = jsr_url();
    assert!(reg_url.as_str().ends_with('/'));
    let reg_api_url = jsr_api_url();
    assert!(reg_api_url.as_str().ends_with('/'));
  }
}