use std::borrow::Cow;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use boxed_error::Boxed;
use deno_config::workspace::Workspace;
use deno_npmrc::NpmRc;
use deno_npmrc::NpmRegistryUrl;
use deno_npmrc::RegistryConfigWithUrl;
use deno_npmrc::ResolvedNpmRc;
use sys_traits::EnvHomeDir;
use sys_traits::EnvVar;
use sys_traits::FsRead;
use thiserror::Error;
#[allow(clippy::disallowed_types, reason = "definition")]
pub type ResolvedNpmRcRc = deno_maybe_sync::MaybeArc<ResolvedNpmRc>;
#[derive(Debug, Boxed)]
pub struct NpmRcDiscoverError(pub Box<NpmRcDiscoverErrorKind>);
#[derive(Debug, Error)]
pub enum NpmRcDiscoverErrorKind {
#[error(transparent)]
Load(#[from] NpmRcLoadError),
#[error(transparent)]
Parse(#[from] NpmRcParseError),
#[error(transparent)]
Resolve(#[from] NpmRcOptionsResolveError),
#[error(transparent)]
UrlToFilePath(#[from] deno_path_util::UrlToFilePathError),
}
#[derive(Debug, Error)]
#[error("Error loading .npmrc at {}.", path.display())]
pub struct NpmRcLoadError {
path: PathBuf,
#[source]
source: std::io::Error,
}
#[derive(Debug, Error)]
#[error("Failed to parse .npmrc at {}.", path.display())]
pub struct NpmRcParseError {
path: PathBuf,
#[source]
source: deno_npmrc::NpmRcParseError,
}
#[derive(Debug, Error)]
#[error("Failed to resolve .npmrc options at {}.", path.display())]
pub struct NpmRcOptionsResolveError {
path: PathBuf,
#[source]
source: deno_npmrc::ResolveError,
}
pub fn discover_npmrc_from_workspace<TSys: EnvVar + EnvHomeDir + FsRead>(
sys: &TSys,
workspace: &Workspace,
) -> Result<(ResolvedNpmRc, Option<PathBuf>), NpmRcDiscoverError> {
let root_folder = workspace.root_folder_configs();
discover_npmrc(
sys,
root_folder.pkg_json.as_ref().map(|p| p.path.clone()),
match &root_folder.deno_json {
Some(cf) if cf.specifier.scheme() == "file" => {
Some(deno_path_util::url_to_file_path(&cf.specifier)?)
}
_ => None,
},
)
}
pub fn discover_npmrc_from_home<TSys: EnvVar + EnvHomeDir + FsRead>(
sys: &TSys,
) -> Result<(ResolvedNpmRc, Option<PathBuf>), NpmRcDiscoverError> {
discover_npmrc(sys, None, None)
}
fn discover_npmrc<TSys: EnvVar + EnvHomeDir + FsRead>(
sys: &TSys,
maybe_package_json_path: Option<PathBuf>,
maybe_deno_json_path: Option<PathBuf>,
) -> Result<(ResolvedNpmRc, Option<PathBuf>), NpmRcDiscoverError> {
const NPMRC_NAME: &str = ".npmrc";
fn try_to_read_npmrc(
sys: &impl FsRead,
dir: &Path,
) -> Result<Option<(Cow<'static, str>, PathBuf)>, NpmRcLoadError> {
let path = dir.join(NPMRC_NAME);
let maybe_source = match sys.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(
sys: &impl EnvVar,
source: &str,
path: &Path,
) -> Result<NpmRc, NpmRcDiscoverError> {
let npmrc =
NpmRc::parse(sys, source).map_err(|source| NpmRcParseError {
path: path.to_path_buf(),
source,
})?;
log::debug!(".npmrc found at: '{}'", path.display());
Ok(npmrc)
}
fn merge_npm_rc(project_rc: NpmRc, home_rc: NpmRc) -> NpmRc {
fn merge_maps<TValue>(
mut project: HashMap<String, TValue>,
home: HashMap<String, TValue>,
) -> HashMap<String, TValue> {
for (key, value) in home {
project.entry(key).or_insert(value);
}
project
}
NpmRc {
registry: project_rc.registry.or(home_rc.registry),
scope_registries: merge_maps(
project_rc.scope_registries,
home_rc.scope_registries,
),
registry_configs: merge_maps(
project_rc.registry_configs,
home_rc.registry_configs,
),
replace_registry_host: project_rc
.replace_registry_host
.or(home_rc.replace_registry_host),
min_release_age_days: project_rc
.min_release_age_days
.or(home_rc.min_release_age_days),
trust_policy: if project_rc.trust_policy
!= deno_npmrc::TrustPolicyConfig::Off
{
project_rc.trust_policy
} else {
home_rc.trust_policy
},
trust_policy_ignore_after_minutes: project_rc
.trust_policy_ignore_after_minutes
.or(home_rc.trust_policy_ignore_after_minutes),
trust_policy_exclude: {
let mut excludes = project_rc.trust_policy_exclude;
for pkg in home_rc.trust_policy_exclude {
if !excludes.contains(&pkg) {
excludes.push(pkg);
}
}
excludes
},
}
}
let mut home_npmrc = None;
let mut project_npmrc = None;
if let Some(home_dir) = sys.env_home_dir() {
match try_to_read_npmrc(sys, &home_dir) {
Ok(Some((source, path))) => {
let npmrc = try_to_parse_npmrc(sys, &source, &path)?;
home_npmrc = Some((path, npmrc));
}
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());
}
}
}
if let Some(package_json_path) = maybe_package_json_path
&& let Some(package_json_dir) = package_json_path.parent()
&& let Some((source, path)) = try_to_read_npmrc(sys, package_json_dir)?
{
let npmrc = try_to_parse_npmrc(sys, &source, &path)?;
project_npmrc = Some((path, npmrc));
}
if project_npmrc.is_none()
&& let Some(deno_json_path) = maybe_deno_json_path
&& let Some(deno_json_dir) = deno_json_path.parent()
&& let Some((source, path)) = try_to_read_npmrc(sys, deno_json_dir)?
{
let npmrc = try_to_parse_npmrc(sys, &source, &path)?;
project_npmrc = Some((path, npmrc));
}
let resolve_npmrc = |path: PathBuf, npm_rc: NpmRc| {
let registry_url = NpmRegistryUrl::for_npm(sys);
let mut resolved_npm_rc =
npm_rc.as_resolved(®istry_url).map_err(|source| {
NpmRcOptionsResolveError {
path: path.to_path_buf(),
source,
}
})?;
resolved_npm_rc
.scopes
.entry("jsr".to_string())
.or_insert_with(|| RegistryConfigWithUrl {
registry_url: NpmRegistryUrl::for_jsr(sys).url,
config: Default::default(),
});
Ok((resolved_npm_rc, Some(path)))
};
match (home_npmrc, project_npmrc) {
(None, None) => {
log::debug!("No .npmrc file found");
Ok((create_default_npmrc(sys), None))
}
(None, Some((npmrc_path, project_rc))) => {
log::debug!("Only project .npmrc file found");
resolve_npmrc(npmrc_path, project_rc)
}
(Some((npmrc_path, home_rc)), None) => {
log::debug!("Only home .npmrc file found");
resolve_npmrc(npmrc_path, home_rc)
}
(Some((_, home_rc)), Some((npmrc_path, project_rc))) => {
log::debug!("Both home and project .npmrc files found");
let merged_npmrc = merge_npm_rc(project_rc, home_rc);
resolve_npmrc(npmrc_path, merged_npmrc)
}
}
}
pub fn create_default_npmrc(sys: &impl EnvVar) -> ResolvedNpmRc {
ResolvedNpmRc {
default_config: deno_npmrc::RegistryConfigWithUrl {
registry_url: NpmRegistryUrl::for_npm(sys).url,
config: Default::default(),
},
scopes: HashMap::from([(
"jsr".to_string(),
RegistryConfigWithUrl {
registry_url: NpmRegistryUrl::for_jsr(sys).url,
config: Default::default(),
},
)]),
registry_configs: Default::default(),
replace_registry_host: deno_npmrc::ReplaceRegistryHost::for_npm(sys)
.unwrap_or_default(),
min_release_age_days: deno_npmrc::min_release_age_days_from_env(sys),
trust_policy: Default::default(),
trust_policy_ignore_after_minutes: None,
trust_policy_exclude: Vec::new(),
}
}
#[cfg(test)]
mod test {
use sys_traits::EnvSetVar;
use sys_traits::impls::InMemorySys;
use super::*;
#[test]
fn discover_npmrc_from_home_ignores_workspace_config() {
let sys = InMemorySys::new_with_cwd("/workspace");
sys.env_set_var("HOME", "/home/test");
sys.fs_insert(
"/home/test/.npmrc",
concat!(
"registry=http://home.example/\n",
"//home.example/:_authToken=test-token\n",
"trust-policy=no-downgrade\n",
),
);
sys.fs_insert("/workspace/.npmrc", "registry=http://workspace.example/\n");
let (npmrc, path) = discover_npmrc_from_home(&sys).unwrap();
assert_eq!(path, Some(PathBuf::from("/home/test/.npmrc")));
assert_eq!(
npmrc.get_registry_url("@esbuild/linux-x64").as_str(),
"http://home.example/"
);
assert_eq!(
npmrc
.get_registry_config("@esbuild/linux-x64")
.auth_token
.as_deref(),
Some("test-token")
);
assert_eq!(
npmrc.trust_policy,
deno_npmrc::TrustPolicyConfig::NoDowngrade
);
}
}