diff options
Diffstat (limited to 'cli/args')
-rw-r--r-- | cli/args/config_file.rs | 315 | ||||
-rw-r--r-- | cli/args/flags.rs | 10 | ||||
-rw-r--r-- | cli/args/mod.rs | 438 |
3 files changed, 578 insertions, 185 deletions
diff --git a/cli/args/config_file.rs b/cli/args/config_file.rs index bed155d32..570aeba0d 100644 --- a/cli/args/config_file.rs +++ b/cli/args/config_file.rs @@ -11,7 +11,6 @@ use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::custom_error; use deno_core::error::AnyError; -use deno_core::normalize_path; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; use deno_core::serde::Serializer; @@ -161,66 +160,6 @@ pub const IGNORED_RUNTIME_COMPILER_OPTIONS: &[&str] = &[ "watch", ]; -/// Filenames that Deno will recognize when discovering config. -const CONFIG_FILE_NAMES: [&str; 2] = ["deno.json", "deno.jsonc"]; - -pub fn discover(flags: &Flags) -> Result<Option<ConfigFile>, AnyError> { - match &flags.config_flag { - ConfigFlag::Disabled => Ok(None), - ConfigFlag::Path(config_path) => Ok(Some(ConfigFile::read(config_path)?)), - ConfigFlag::Discover => { - if let Some(config_path_args) = flags.config_path_args() { - let mut checked = HashSet::new(); - for f in config_path_args { - if let Some(cf) = discover_from(&f, &mut checked)? { - return Ok(Some(cf)); - } - } - // From CWD walk up to root looking for deno.json or deno.jsonc - let cwd = std::env::current_dir()?; - discover_from(&cwd, &mut checked) - } else { - Ok(None) - } - } - } -} - -pub fn discover_from( - start: &Path, - checked: &mut HashSet<PathBuf>, -) -> Result<Option<ConfigFile>, AnyError> { - for ancestor in start.ancestors() { - if checked.insert(ancestor.to_path_buf()) { - for config_filename in CONFIG_FILE_NAMES { - let f = ancestor.join(config_filename); - match ConfigFile::read(f) { - Ok(cf) => { - return Ok(Some(cf)); - } - Err(e) => { - if let Some(ioerr) = e.downcast_ref::<std::io::Error>() { - use std::io::ErrorKind::*; - match ioerr.kind() { - InvalidInput | PermissionDenied | NotFound => { - // ok keep going - } - _ => { - return Err(e); // Unknown error. Stop. - } - } - } else { - return Err(e); // Parse error or something else. Stop. - } - } - } - } - } - } - // No config file found. - Ok(None) -} - /// A function that works like JavaScript's `Object.assign()`. pub fn json_merge(a: &mut Value, b: &Value) { match (a, b) { @@ -235,56 +174,6 @@ pub fn json_merge(a: &mut Value, b: &Value) { } } -/// Based on an optional command line import map path and an optional -/// configuration file, return a resolved module specifier to an import map. -pub fn resolve_import_map_specifier( - maybe_import_map_path: Option<&str>, - maybe_config_file: Option<&ConfigFile>, -) -> 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.to_import_map_path().is_some() { - log::warn!("{} the configuration file \"{}\" contains an entry for \"importMap\" that is being ignored.", crate::colors::yellow("Warning"), config_file.specifier); - } - } - let specifier = deno_core::resolve_url_or_path(import_map_path) - .context(format!("Bad URL (\"{}\") for import map.", import_map_path))?; - return Ok(Some(specifier)); - } else if let Some(config_file) = &maybe_config_file { - // when the import map is specifier in a config file, it needs to be - // resolved relative to the config file, versus the CWD like with the flag - // and with config files, we support both local and remote config files, - // so we have treat them differently. - if let Some(import_map_path) = config_file.to_import_map_path() { - let specifier = - // with local config files, it might be common to specify an import - // map like `"importMap": "import-map.json"`, which is resolvable if - // the file is resolved like a file path, so we will coerce the config - // file into a file path if possible and join the import map path to - // the file path. - if let Ok(config_file_path) = config_file.specifier.to_file_path() { - let import_map_file_path = normalize_path(config_file_path - .parent() - .ok_or_else(|| { - anyhow!("Bad config file specifier: {}", config_file.specifier) - })? - .join(&import_map_path)); - ModuleSpecifier::from_file_path(import_map_file_path).unwrap() - // otherwise if the config file is remote, we have no choice but to - // use "import resolution" with the config file as the base. - } else { - deno_core::resolve_import(&import_map_path, config_file.specifier.as_str()) - .context(format!( - "Bad URL (\"{}\") for import map.", - import_map_path - ))? - }; - return Ok(Some(specifier)); - } - } - Ok(None) -} - fn parse_compiler_options( compiler_options: &HashMap<String, Value>, maybe_specifier: Option<ModuleSpecifier>, @@ -547,6 +436,66 @@ pub struct ConfigFile { } impl ConfigFile { + pub fn discover(flags: &Flags) -> Result<Option<ConfigFile>, AnyError> { + match &flags.config_flag { + ConfigFlag::Disabled => Ok(None), + ConfigFlag::Path(config_path) => Ok(Some(ConfigFile::read(config_path)?)), + ConfigFlag::Discover => { + if let Some(config_path_args) = flags.config_path_args() { + let mut checked = HashSet::new(); + for f in config_path_args { + if let Some(cf) = Self::discover_from(&f, &mut checked)? { + return Ok(Some(cf)); + } + } + // From CWD walk up to root looking for deno.json or deno.jsonc + let cwd = std::env::current_dir()?; + Self::discover_from(&cwd, &mut checked) + } else { + Ok(None) + } + } + } + } + + pub fn discover_from( + start: &Path, + checked: &mut HashSet<PathBuf>, + ) -> Result<Option<ConfigFile>, AnyError> { + /// Filenames that Deno will recognize when discovering config. + const CONFIG_FILE_NAMES: [&str; 2] = ["deno.json", "deno.jsonc"]; + + for ancestor in start.ancestors() { + if checked.insert(ancestor.to_path_buf()) { + for config_filename in CONFIG_FILE_NAMES { + let f = ancestor.join(config_filename); + match ConfigFile::read(f) { + Ok(cf) => { + return Ok(Some(cf)); + } + Err(e) => { + if let Some(ioerr) = e.downcast_ref::<std::io::Error>() { + use std::io::ErrorKind::*; + match ioerr.kind() { + InvalidInput | PermissionDenied | NotFound => { + // ok keep going + } + _ => { + return Err(e); // Unknown error. Stop. + } + } + } else { + return Err(e); // Parse error or something else. Stop. + } + } + } + } + } + } + // No config file found. + Ok(None) + } + pub fn read(path_ref: impl AsRef<Path>) -> Result<Self, AnyError> { let path = Path::new(path_ref.as_ref()); let config_file = if path.is_absolute() { @@ -744,12 +693,36 @@ impl ConfigFile { Ok(None) } } + + pub fn resolve_tasks_config( + &self, + ) -> Result<BTreeMap<String, String>, AnyError> { + let maybe_tasks_config = self.to_tasks_config()?; + if let Some(tasks_config) = maybe_tasks_config { + for key in tasks_config.keys() { + if key.is_empty() { + bail!("Configuration file task names cannot be empty"); + } else if !key + .chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | ':')) + { + bail!("Configuration file task names must only contain alpha-numeric characters, colons (:), underscores (_), or dashes (-). Task: {}", key); + } else if !key.chars().next().unwrap().is_ascii_alphabetic() { + bail!("Configuration file task names must start with an alphabetic character. Task: {}", key); + } + } + Ok(tasks_config) + } else { + bail!("No tasks found in configuration file") + } + } } #[cfg(test)] mod tests { use super::*; use deno_core::serde_json::json; + use pretty_assertions::assert_eq; #[test] fn read_config_file_relative() { @@ -996,7 +969,9 @@ mod tests { let testdata = test_util::testdata_path(); let c_md = testdata.join("fmt/with_config/subdir/c.md"); let mut checked = HashSet::new(); - let config_file = discover_from(&c_md, &mut checked).unwrap().unwrap(); + let config_file = ConfigFile::discover_from(&c_md, &mut checked) + .unwrap() + .unwrap(); assert!(checked.contains(c_md.parent().unwrap())); assert!(!checked.contains(&testdata)); let fmt_config = config_file.to_fmt_config().unwrap().unwrap(); @@ -1012,7 +987,9 @@ mod tests { } // If we call discover_from again starting at testdata, we ought to get None. - assert!(discover_from(&testdata, &mut checked).unwrap().is_none()); + assert!(ConfigFile::discover_from(&testdata, &mut checked) + .unwrap() + .is_none()); } #[test] @@ -1020,83 +997,71 @@ mod tests { let testdata = test_util::testdata_path(); let d = testdata.join("malformed_config/"); let mut checked = HashSet::new(); - let err = discover_from(&d, &mut checked).unwrap_err(); + let err = ConfigFile::discover_from(&d, &mut checked).unwrap_err(); assert!(err.to_string().contains("Unable to parse config file")); } - #[cfg(not(windows))] #[test] - fn resolve_import_map_config_file() { - let config_text = r#"{ - "importMap": "import_map.json" - }"#; - let config_specifier = - ModuleSpecifier::parse("file:///deno/deno.jsonc").unwrap(); - let config_file = ConfigFile::new(config_text, &config_specifier).unwrap(); - let actual = resolve_import_map_specifier(None, Some(&config_file)); - assert!(actual.is_ok()); - let actual = actual.unwrap(); - assert_eq!( - actual, - Some(ModuleSpecifier::parse("file:///deno/import_map.json").unwrap()) - ); + fn tasks_no_tasks() { + run_task_error_test(r#"{}"#, "No tasks found in configuration file"); } #[test] - fn resolve_import_map_config_file_remote() { - let config_text = r#"{ - "importMap": "./import_map.json" - }"#; - let config_specifier = - ModuleSpecifier::parse("https://example.com/deno.jsonc").unwrap(); - let config_file = ConfigFile::new(config_text, &config_specifier).unwrap(); - let actual = resolve_import_map_specifier(None, Some(&config_file)); - assert!(actual.is_ok()); - let actual = actual.unwrap(); - assert_eq!( - actual, - Some( - ModuleSpecifier::parse("https://example.com/import_map.json").unwrap() - ) + fn task_name_invalid_chars() { + run_task_error_test( + r#"{ + "tasks": { + "build": "deno test", + "some%test": "deno bundle mod.ts" + } + }"#, + concat!( + "Configuration file task names must only contain alpha-numeric ", + "characters, colons (:), underscores (_), or dashes (-). Task: some%test", + ), ); } #[test] - fn resolve_import_map_flags_take_precedence() { - let config_text = r#"{ - "importMap": "import_map.json" - }"#; - let config_specifier = - ModuleSpecifier::parse("file:///deno/deno.jsonc").unwrap(); - let config_file = ConfigFile::new(config_text, &config_specifier).unwrap(); - let actual = - resolve_import_map_specifier(Some("import-map.json"), Some(&config_file)); - let import_map_path = - std::env::current_dir().unwrap().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)); + fn task_name_non_alpha_starting_char() { + run_task_error_test( + r#"{ + "tasks": { + "build": "deno test", + "1test": "deno bundle mod.ts" + } + }"#, + concat!( + "Configuration file task names must start with an ", + "alphabetic character. Task: 1test", + ), + ); } #[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).unwrap(); - let actual = resolve_import_map_specifier(None, Some(&config_file)); - assert!(actual.is_ok()); - let actual = actual.unwrap(); - assert_eq!(actual, None); + fn task_name_empty() { + run_task_error_test( + r#"{ + "tasks": { + "build": "deno test", + "": "deno bundle mod.ts" + } + }"#, + "Configuration file task names cannot be empty", + ); } - #[test] - fn resolve_import_map_no_config() { - let actual = resolve_import_map_specifier(None, None); - assert!(actual.is_ok()); - let actual = actual.unwrap(); - assert_eq!(actual, None); + fn run_task_error_test(config_text: &str, expected_error: &str) { + let config_dir = ModuleSpecifier::parse("file:///deno/").unwrap(); + let config_specifier = config_dir.join("tsconfig.json").unwrap(); + let config_file = ConfigFile::new(config_text, &config_specifier).unwrap(); + assert_eq!( + config_file + .resolve_tasks_config() + .err() + .unwrap() + .to_string(), + expected_error, + ); } } diff --git a/cli/args/flags.rs b/cli/args/flags.rs index f61ead385..8b8cf8d86 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -234,7 +234,7 @@ impl Default for DenoSubcommand { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum TypeCheckMode { /// Type-check all modules. All, @@ -305,7 +305,6 @@ pub struct Flags { pub compat: bool, pub no_prompt: bool, pub reload: bool, - pub repl: bool, pub seed: Option<u64>, pub unstable: bool, pub unsafely_ignore_certificate_errors: Option<Vec<String>>, @@ -571,7 +570,6 @@ pub fn flags_from_vec(args: Vec<String>) -> clap::Result<Flags> { } fn handle_repl_flags(flags: &mut Flags, repl_flags: ReplFlags) { - flags.repl = true; flags.subcommand = DenoSubcommand::Repl(repl_flags); flags.allow_net = Some(vec![]); flags.allow_env = Some(vec![]); @@ -4022,7 +4020,6 @@ mod tests { assert_eq!( r.unwrap(), Flags { - repl: true, subcommand: DenoSubcommand::Repl(ReplFlags { eval_files: None, eval: None @@ -4047,7 +4044,6 @@ mod tests { assert_eq!( r.unwrap(), Flags { - repl: true, subcommand: DenoSubcommand::Repl(ReplFlags { eval_files: None, eval: None @@ -4085,7 +4081,6 @@ mod tests { assert_eq!( r.unwrap(), Flags { - repl: true, subcommand: DenoSubcommand::Repl(ReplFlags { eval_files: None, eval: Some("console.log('hello');".to_string()), @@ -4110,7 +4105,6 @@ mod tests { assert_eq!( r.unwrap(), Flags { - repl: true, subcommand: DenoSubcommand::Repl(ReplFlags { eval_files: Some(vec![ "./a.js".to_string(), @@ -4770,7 +4764,6 @@ mod tests { assert_eq!( r.unwrap(), Flags { - repl: true, subcommand: DenoSubcommand::Repl(ReplFlags { eval_files: None, eval: Some("console.log('hello');".to_string()), @@ -4845,7 +4838,6 @@ mod tests { assert_eq!( r.unwrap(), Flags { - repl: true, subcommand: DenoSubcommand::Repl(ReplFlags { eval_files: None, eval: None diff --git a/cli/args/mod.rs b/cli/args/mod.rs index 375a53122..757c6a8f4 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -5,5 +5,441 @@ pub mod flags; mod flags_allow_net; -pub use config_file::*; +pub use config_file::CompilerOptions; +pub use config_file::ConfigFile; +pub use config_file::EmitConfigOptions; +pub use config_file::FmtConfig; +pub use config_file::FmtOptionsConfig; +pub use config_file::LintConfig; +pub use config_file::LintRulesConfig; +pub use config_file::MaybeImportsResult; +pub use config_file::ProseWrap; +pub use config_file::TsConfig; pub use flags::*; + +use deno_ast::ModuleSpecifier; +use deno_core::anyhow::anyhow; +use deno_core::anyhow::bail; +use deno_core::anyhow::Context; +use deno_core::error::AnyError; +use deno_core::normalize_path; +use deno_core::url::Url; +use deno_runtime::colors; +use deno_runtime::deno_tls::rustls::RootCertStore; +use deno_runtime::inspector_server::InspectorServer; +use deno_runtime::permissions::PermissionsOptions; +use std::collections::BTreeMap; +use std::env; +use std::net::SocketAddr; +use std::path::PathBuf; + +use crate::compat; +use crate::deno_dir::DenoDir; +use crate::emit::get_ts_config_for_emit; +use crate::emit::TsConfigType; +use crate::emit::TsConfigWithIgnoredOptions; +use crate::emit::TsTypeLib; +use crate::file_fetcher::get_root_cert_store; +use crate::file_fetcher::CacheSetting; +use crate::lockfile::Lockfile; +use crate::version; + +/// Holds the common configuration used by many sub commands +/// and provides some helper function for creating common objects. +pub struct RootConfig { + // the source of the configuration is a detail the rest of the + // application need not concern itself with, so keep these private + flags: Flags, + maybe_config_file: Option<ConfigFile>, +} + +impl RootConfig { + pub fn from_flags(flags: Flags) -> 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); + eprintln!("{}", colors::yellow(msg)); + } + + let maybe_config_file = ConfigFile::discover(&flags)?; + Ok(Self { + maybe_config_file, + flags, + }) + } + + pub fn maybe_config_file_specifier(&self) -> Option<ModuleSpecifier> { + self.maybe_config_file.as_ref().map(|f| f.specifier.clone()) + } + + pub fn ts_type_lib_window(&self) -> TsTypeLib { + if self.flags.unstable { + TsTypeLib::UnstableDenoWindow + } else { + TsTypeLib::DenoWindow + } + } + + pub fn ts_type_lib_worker(&self) -> TsTypeLib { + if self.flags.unstable { + TsTypeLib::UnstableDenoWorker + } else { + 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 resolve_deno_dir(&self) -> Result<DenoDir, AnyError> { + Ok(DenoDir::new(self.maybe_custom_root())?) + } + + /// Based on an optional command line import map path and an optional + /// configuration file, return a resolved module specifier to an import map. + pub fn resolve_import_map_path( + &self, + ) -> Result<Option<ModuleSpecifier>, AnyError> { + resolve_import_map_specifier( + self.flags.import_map_path.as_deref(), + self.maybe_config_file.as_ref(), + ) + } + + pub fn resolve_root_cert_store(&self) -> Result<RootCertStore, AnyError> { + get_root_cert_store( + None, + self.flags.ca_stores.clone(), + self.flags.ca_file.clone(), + ) + } + + pub fn resolve_ts_config_for_emit( + &self, + config_type: TsConfigType, + ) -> Result<TsConfigWithIgnoredOptions, AnyError> { + get_ts_config_for_emit(config_type, self.maybe_config_file.as_ref()) + } + + /// 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> { + if let Some(location) = &self.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().ascii_serialization(); + if storage_origin == "null" { + None + } else { + Some(storage_origin) + } + } else if let Some(config_file) = &self.maybe_config_file { + // otherwise we will use the path to the config file + Some(config_file.specifier.to_string()) + } else { + // otherwise we will use the path to the main module + Some(main_module.to_string()) + } + } + + pub fn resolve_inspector_server(&self) -> Option<InspectorServer> { + let maybe_inspect_host = self.flags.inspect.or(self.flags.inspect_brk); + maybe_inspect_host + .map(|host| InspectorServer::new(host, version::get_user_agent())) + } + + pub fn resolve_lock_file(&self) -> Result<Option<Lockfile>, AnyError> { + if let Some(filename) = &self.flags.lock { + let lockfile = Lockfile::new(filename.clone(), self.flags.lock_write)?; + Ok(Some(lockfile)) + } else { + Ok(None) + } + } + + pub fn resolve_tasks_config( + &self, + ) -> Result<BTreeMap<String, String>, AnyError> { + if let Some(config_file) = &self.maybe_config_file { + config_file.resolve_tasks_config() + } else { + bail!("No config file found") + } + } + + /// Return the implied JSX import source module. + pub fn to_maybe_jsx_import_source_module(&self) -> Option<String> { + self + .maybe_config_file + .as_ref() + .and_then(|c| c.to_maybe_jsx_import_source_module()) + } + + /// Return any imports that should be brought into the scope of the module + /// graph. + pub fn to_maybe_imports(&self) -> MaybeImportsResult { + let mut imports = Vec::new(); + if let Some(config_file) = &self.maybe_config_file { + if let Some(config_imports) = config_file.to_maybe_imports()? { + imports.extend(config_imports); + } + } + if self.flags.compat { + imports.extend(compat::get_node_imports()); + } + if imports.is_empty() { + Ok(None) + } else { + Ok(Some(imports)) + } + } + + pub fn to_lint_config(&self) -> Result<Option<LintConfig>, AnyError> { + if let Some(config_file) = &self.maybe_config_file { + config_file.to_lint_config() + } else { + Ok(None) + } + } + + pub fn to_fmt_config(&self) -> Result<Option<FmtConfig>, AnyError> { + if let Some(config) = &self.maybe_config_file { + config.to_fmt_config() + } else { + Ok(None) + } + } + + /// Vector of user script CLI arguments. + pub fn argv(&self) -> &Vec<String> { + &self.flags.argv + } + + pub fn check_js(&self) -> bool { + self + .maybe_config_file + .as_ref() + .map(|cf| cf.get_check_js()) + .unwrap_or(false) + } + + pub fn compat(&self) -> bool { + self.flags.compat + } + + pub fn coverage_dir(&self) -> Option<&String> { + self.flags.coverage_dir.as_ref() + } + + pub fn enable_testing_features(&self) -> bool { + self.flags.enable_testing_features + } + + pub fn inspect_brk(&self) -> Option<SocketAddr> { + self.flags.inspect_brk + } + + pub fn log_level(&self) -> Option<log::Level> { + self.flags.log_level + } + + pub fn location_flag(&self) -> Option<&Url> { + self.flags.location.as_ref() + } + + pub fn maybe_custom_root(&self) -> Option<PathBuf> { + self + .flags + .cache_path + .clone() + .or_else(|| env::var("DENO_DIR").map(String::into).ok()) + } + + pub fn no_clear_screen(&self) -> bool { + self.flags.no_clear_screen + } + + pub fn no_remote(&self) -> bool { + self.flags.no_remote + } + + pub fn permissions_options(&self) -> PermissionsOptions { + self.flags.permissions_options() + } + + 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 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.as_ref() + } + + pub fn unstable(&self) -> bool { + self.flags.unstable + } + + pub fn watch_paths(&self) -> Option<&Vec<PathBuf>> { + self.flags.watch.as_ref() + } +} + +fn resolve_import_map_specifier( + maybe_import_map_path: Option<&str>, + maybe_config_file: Option<&ConfigFile>, +) -> 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.to_import_map_path().is_some() { + log::warn!("{} the configuration file \"{}\" contains an entry for \"importMap\" that is being ignored.", crate::colors::yellow("Warning"), config_file.specifier); + } + } + let specifier = deno_core::resolve_url_or_path(import_map_path) + .context(format!("Bad URL (\"{}\") for import map.", import_map_path))?; + return Ok(Some(specifier)); + } else if let Some(config_file) = &maybe_config_file { + // when the import map is specifier in a config file, it needs to be + // resolved relative to the config file, versus the CWD like with the flag + // and with config files, we support both local and remote config files, + // so we have treat them differently. + if let Some(import_map_path) = config_file.to_import_map_path() { + let specifier = + // with local config files, it might be common to specify an import + // map like `"importMap": "import-map.json"`, which is resolvable if + // the file is resolved like a file path, so we will coerce the config + // file into a file path if possible and join the import map path to + // the file path. + if let Ok(config_file_path) = config_file.specifier.to_file_path() { + let import_map_file_path = normalize_path(config_file_path + .parent() + .ok_or_else(|| { + anyhow!("Bad config file specifier: {}", config_file.specifier) + })? + .join(&import_map_path)); + ModuleSpecifier::from_file_path(import_map_file_path).unwrap() + // otherwise if the config file is remote, we have no choice but to + // use "import resolution" with the config file as the base. + } else { + deno_core::resolve_import(&import_map_path, config_file.specifier.as_str()) + .context(format!( + "Bad URL (\"{}\") for import map.", + import_map_path + ))? + }; + return Ok(Some(specifier)); + } + } + Ok(None) +} + +#[cfg(test)] +mod test { + use super::*; + + #[cfg(not(windows))] + #[test] + fn resolve_import_map_config_file() { + let config_text = r#"{ + "importMap": "import_map.json" + }"#; + let config_specifier = + ModuleSpecifier::parse("file:///deno/deno.jsonc").unwrap(); + let config_file = ConfigFile::new(config_text, &config_specifier).unwrap(); + let actual = resolve_import_map_specifier(None, Some(&config_file)); + assert!(actual.is_ok()); + let actual = actual.unwrap(); + assert_eq!( + actual, + Some(ModuleSpecifier::parse("file:///deno/import_map.json").unwrap()) + ); + } + + #[test] + fn resolve_import_map_config_file_remote() { + let config_text = r#"{ + "importMap": "./import_map.json" + }"#; + let config_specifier = + ModuleSpecifier::parse("https://example.com/deno.jsonc").unwrap(); + let config_file = ConfigFile::new(config_text, &config_specifier).unwrap(); + let actual = resolve_import_map_specifier(None, Some(&config_file)); + assert!(actual.is_ok()); + let actual = actual.unwrap(); + assert_eq!( + actual, + Some( + ModuleSpecifier::parse("https://example.com/import_map.json").unwrap() + ) + ); + } + + #[test] + fn resolve_import_map_flags_take_precedence() { + let config_text = r#"{ + "importMap": "import_map.json" + }"#; + let config_specifier = + ModuleSpecifier::parse("file:///deno/deno.jsonc").unwrap(); + let config_file = ConfigFile::new(config_text, &config_specifier).unwrap(); + let actual = + resolve_import_map_specifier(Some("import-map.json"), Some(&config_file)); + let import_map_path = + std::env::current_dir().unwrap().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).unwrap(); + let actual = resolve_import_map_specifier(None, Some(&config_file)); + 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); + assert!(actual.is_ok()); + let actual = actual.unwrap(); + assert_eq!(actual, None); + } +} |