diff --git a/Cargo.lock b/Cargo.lock index 29263b6..a9230f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", + "url", ] [[package]] @@ -142,9 +143,9 @@ dependencies = [ [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -265,9 +266,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -388,9 +389,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" @@ -648,9 +649,9 @@ dependencies = [ [[package]] name = "url" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", diff --git a/Cargo.toml b/Cargo.toml index 894bbaf..4715987 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ log = "0.4" serde = { version = "1.0.130", features = ["derive", "rc"] } thiserror = "1.0.24" futures = "0.3.28" +url = "2" [dev-dependencies] pretty_assertions = "1.0.0" diff --git a/src/npm_rc/mod.rs b/src/npm_rc/mod.rs index 1b9c70d..51d0bd5 100644 --- a/src/npm_rc/mod.rs +++ b/src/npm_rc/mod.rs @@ -1,8 +1,10 @@ // Copyright 2018-2024 the Deno authors. MIT license. +use anyhow::Context; use monch::*; use std::borrow::Cow; use std::collections::HashMap; +use url::Url; use self::ini::Key; use self::ini::KeyValueOrSection; @@ -31,6 +33,18 @@ pub struct RegistryConfig { pub keyfile: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RegistryConfigWithUrl { + pub registry_url: Url, + pub config: RegistryConfig, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedNpmRc { + pub default_config: RegistryConfigWithUrl, + pub scopes: HashMap, +} + #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct NpmRc { pub registry: Option, @@ -108,17 +122,69 @@ impl NpmRc { Ok(rc_file) } + pub fn as_resolved( + &self, + env_registry_url: &Url, + ) -> Result { + let mut scopes = HashMap::with_capacity(self.scope_registries.len()); + for scope in self.scope_registries.keys() { + let (url, config) = match self.registry_url_and_config_for_maybe_scope( + Some(scope.as_str()), + env_registry_url.as_str(), + ) { + Some((url, config)) => ( + Url::parse(&url).with_context(|| { + format!("failed parsing npm registry url for scope '{}'", scope) + })?, + config.clone(), + ), + None => { + anyhow::bail!("failed resolving .npmrc config for scope '{}'", scope) + } + }; + scopes.insert( + scope.clone(), + RegistryConfigWithUrl { + registry_url: url, + config, + }, + ); + } + let (default_url, default_config) = match self + .registry_url_and_config_for_maybe_scope(None, env_registry_url.as_str()) + { + Some((default_url, default_config)) => ( + Url::parse(&default_url).context("failed parsing npm registry url")?, + default_config.clone(), + ), + None => (env_registry_url.clone(), RegistryConfig::default()), + }; + Ok(ResolvedNpmRc { + default_config: RegistryConfigWithUrl { + registry_url: default_url, + config: default_config, + }, + scopes, + }) + } + pub fn registry_url_and_config_for_package<'a>( &'a self, package_name: &str, env_registry_url: &str, ) -> Option<(String, &'a RegistryConfig)> { - fn get_scope_name(package_name: &str) -> Option<&str> { - let no_at_pkg_name = package_name.strip_prefix('@')?; - no_at_pkg_name.split_once('/').map(|(scope, _)| scope) - } - let maybe_scope_name = get_scope_name(package_name); + self.registry_url_and_config_for_maybe_scope( + maybe_scope_name, + env_registry_url, + ) + } + + pub fn registry_url_and_config_for_maybe_scope<'a>( + &'a self, + maybe_scope_name: Option<&str>, + env_registry_url: &str, + ) -> Option<(String, &'a RegistryConfig)> { let registry_url = maybe_scope_name .and_then(|scope| self.scope_registries.get(scope).map(|s| s.as_str())) .or(self.registry.as_deref()) @@ -162,6 +228,45 @@ impl NpmRc { } } +fn get_scope_name(package_name: &str) -> Option<&str> { + let no_at_pkg_name = package_name.strip_prefix('@')?; + no_at_pkg_name.split_once('/').map(|(scope, _)| scope) +} + +impl ResolvedNpmRc { + pub fn get_registry_url(&self, package_name: &str) -> &Url { + let Some(scope_name) = get_scope_name(package_name) else { + return &self.default_config.registry_url; + }; + + match self.scopes.get(scope_name) { + Some(registry_config) => ®istry_config.registry_url, + None => &self.default_config.registry_url, + } + } + + pub fn get_registry_config(&self, package_name: &str) -> &RegistryConfig { + let Some(scope_name) = get_scope_name(package_name) else { + return &self.default_config.config; + }; + + match self.scopes.get(scope_name) { + Some(registry_config) => ®istry_config.config, + None => &self.default_config.config, + } + } + + pub fn get_all_known_registries_urls(&self) -> Vec { + let mut urls = Vec::with_capacity(1 + self.scopes.len()); + + urls.push(self.default_config.registry_url.clone()); + for scope_config in self.scopes.values() { + urls.push(scope_config.registry_url.clone()); + } + urls + } +} + fn expand_vars( input: &str, get_env_var: &impl Fn(&str) -> Option, @@ -311,6 +416,111 @@ registry=https://registry.npmjs.org/ assert_eq!(registry_url, "https://example.com/myorg/"); assert_eq!(config.auth_token, Some("MYTOKEN1".to_string())); } + + let resolved_npm_rc = npm_rc + .as_resolved(&Url::parse("https://deno.land/npm/").unwrap()) + .unwrap(); + assert_eq!( + resolved_npm_rc, + ResolvedNpmRc { + default_config: RegistryConfigWithUrl { + registry_url: Url::parse("https://registry.npmjs.org/").unwrap(), + config: RegistryConfig { + auth_token: Some("MYTOKEN".to_string()), + ..Default::default() + }, + }, + scopes: HashMap::from([ + ( + "myorg".to_string(), + RegistryConfigWithUrl { + registry_url: Url::parse("https://example.com/myorg/").unwrap(), + config: RegistryConfig { + auth_token: Some("MYTOKEN1".to_string()), + ..Default::default() + } + } + ), + ( + "another".to_string(), + RegistryConfigWithUrl { + registry_url: Url::parse("https://example.com/another/").unwrap(), + config: RegistryConfig { + auth_token: Some("MYTOKEN2".to_string()), + ..Default::default() + } + } + ), + ( + "example".to_string(), + RegistryConfigWithUrl { + registry_url: Url::parse("https://example.com/example/").unwrap(), + config: RegistryConfig { + auth: Some("AUTH".to_string()), + auth_token: Some("MYTOKEN0".to_string()), + username: Some("USERNAME".to_string()), + password: Some("PASSWORD".to_string()), + email: Some("EMAIL".to_string()), + certfile: Some("CERTFILE".to_string()), + keyfile: Some("KEYFILE".to_string()), + } + } + ), + ]) + } + ); + + assert_eq!( + resolved_npm_rc.get_registry_url("@deno/test").as_str(), + "https://registry.npmjs.org/" + ); + assert_eq!( + resolved_npm_rc + .get_registry_config("@deno/test") + .auth_token + .as_ref() + .unwrap(), + "MYTOKEN" + ); + + assert_eq!( + resolved_npm_rc.get_registry_url("@myorg/test").as_str(), + "https://example.com/myorg/" + ); + assert_eq!( + resolved_npm_rc + .get_registry_config("@myorg/test") + .auth_token + .as_ref() + .unwrap(), + "MYTOKEN1" + ); + + assert_eq!( + resolved_npm_rc.get_registry_url("@another/test").as_str(), + "https://example.com/another/" + ); + assert_eq!( + resolved_npm_rc + .get_registry_config("@another/test") + .auth_token + .as_ref() + .unwrap(), + "MYTOKEN2" + ); + + assert_eq!( + resolved_npm_rc.get_registry_url("@example/test").as_str(), + "https://example.com/example/" + ); + let config = resolved_npm_rc.get_registry_config("@example/test"); + assert_eq!(config.auth.as_ref().unwrap(), "AUTH"); + assert_eq!(config.auth_token.as_ref().unwrap(), "MYTOKEN0"); + assert_eq!(config.username.as_ref().unwrap(), "USERNAME"); + assert_eq!(config.password.as_ref().unwrap(), "PASSWORD"); + assert_eq!(config.email.as_ref().unwrap(), "EMAIL"); + assert_eq!(config.certfile.as_ref().unwrap(), "CERTFILE"); + assert_eq!(config.keyfile.as_ref().unwrap(), "KEYFILE"); } #[test]