use std::path::{Path, PathBuf};
use super::env::npm_config_env_entries_from;
use super::npmrc::{parse_npmrc, parse_npmrc_untrusted};
use super::types::{NpmConfig, NpmrcSource};
impl NpmConfig {
pub fn load(project_dir: &Path) -> Self {
let env: Vec<(String, String)> = std::env::vars_os()
.filter_map(|(k, v)| Some((k.into_string().ok()?, v.into_string().ok()?)))
.collect();
Self::load_with_env(project_dir, &env)
}
#[cfg(test)]
pub(crate) fn load_isolated(project_dir: &Path) -> Self {
let home = tempfile::tempdir().expect("tempdir for isolated config load");
let mut config = Self {
registry: "https://registry.npmjs.org/".to_string(),
..Default::default()
};
config.apply(load_npmrc_entries_with_home(
Some(home.path()),
None,
project_dir,
None,
));
config.apply_builtin_scoped_defaults();
config
}
pub(crate) fn load_with_env(project_dir: &Path, env: &[(String, String)]) -> Self {
let mut config = Self {
registry: "https://registry.npmjs.org/".to_string(),
..Default::default()
};
let xdg = aube_util::env::xdg_config_home();
let home = home_dir();
let user_rc_override = userconfig_override_from_env(env, home.as_deref());
let mut tagged = load_npmrc_entries_tagged_with_home(
home.as_deref(),
xdg.as_deref(),
project_dir,
user_rc_override.as_deref(),
);
tagged.extend(
npm_config_env_entries_from(env)
.into_iter()
.map(|(k, v)| (NpmrcSource::Env, k, v)),
);
config.apply_tagged(tagged);
config.apply_proxy_env();
config.apply_builtin_scoped_defaults();
config
}
}
pub fn load_npmrc_entries_split(project_dir: &Path) -> SplitNpmrcEntries {
use std::sync::{Mutex, OnceLock};
type CacheMap = std::collections::HashMap<PathBuf, SplitNpmrcEntries>;
static CACHE: OnceLock<Mutex<CacheMap>> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(std::collections::HashMap::new()));
if let Ok(map) = cache.lock()
&& let Some(hit) = map.get(project_dir)
{
return hit.clone();
}
let xdg = aube_util::env::xdg_config_home();
let home = home_dir();
let user_rc_override = std::env::var("NPM_CONFIG_USERCONFIG")
.ok()
.or_else(|| std::env::var("npm_config_userconfig").ok())
.and_then(|raw| expand_userconfig_path(&raw, home.as_deref()));
let tagged = load_npmrc_entries_tagged_with_home(
home.as_deref(),
xdg.as_deref(),
project_dir,
user_rc_override.as_deref(),
);
let mut split = SplitNpmrcEntries::default();
for (src, k, v) in tagged {
match src {
NpmrcSource::User | NpmrcSource::PnpmAuth | NpmrcSource::UserNpmrcAuthFile => {
split.user.push((k, v));
}
NpmrcSource::Project | NpmrcSource::ProjectNpmrcAuthFile => split.project.push((k, v)),
NpmrcSource::Env => continue,
}
}
if let Ok(mut map) = cache.lock() {
map.insert(project_dir.to_path_buf(), split.clone());
}
split
}
#[derive(Default, Clone)]
pub struct SplitNpmrcEntries {
pub user: Vec<(String, String)>,
pub project: Vec<(String, String)>,
}
pub fn load_npmrc_entries(project_dir: &Path) -> Vec<(String, String)> {
use std::sync::{Mutex, OnceLock};
type CacheMap = std::collections::HashMap<PathBuf, Vec<(String, String)>>;
static CACHE: OnceLock<Mutex<CacheMap>> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(std::collections::HashMap::new()));
if let Ok(map) = cache.lock()
&& let Some(hit) = map.get(project_dir)
{
return hit.clone();
}
let xdg = aube_util::env::xdg_config_home();
let home = home_dir();
let user_rc_override = std::env::var("NPM_CONFIG_USERCONFIG")
.ok()
.or_else(|| std::env::var("npm_config_userconfig").ok())
.and_then(|raw| expand_userconfig_path(&raw, home.as_deref()));
let entries = load_npmrc_entries_with_home(
home.as_deref(),
xdg.as_deref(),
project_dir,
user_rc_override.as_deref(),
);
if let Ok(mut map) = cache.lock() {
map.insert(project_dir.to_path_buf(), entries.clone());
}
entries
}
pub(super) fn load_npmrc_entries_tagged_with_home(
home: Option<&Path>,
xdg_config_home: Option<&Path>,
project_dir: &Path,
user_rc_override: Option<&Path>,
) -> Vec<(NpmrcSource, String, String)> {
let mut out: Vec<(NpmrcSource, String, String)> = Vec::new();
let user_rc = user_rc_override
.map(PathBuf::from)
.or_else(|| home.map(|h| h.join(".npmrc")));
if let Some(user_rc) = user_rc
&& user_rc.exists()
&& let Ok(entries) = parse_npmrc(&user_rc)
{
out.extend(entries.into_iter().map(|(k, v)| (NpmrcSource::User, k, v)));
}
if let Some(home) = home {
let auth_ini = pnpm_global_auth_ini_path(home, xdg_config_home);
if auth_ini.exists()
&& let Ok(entries) = parse_npmrc(&auth_ini)
{
out.extend(
entries
.into_iter()
.map(|(k, v)| (NpmrcSource::PnpmAuth, k, v)),
);
}
}
if let Some((auth_path, auth_source)) = resolve_npmrc_auth_file_tagged(home, project_dir, &out)
&& !auth_source.is_project_controlled()
&& auth_path.exists()
&& let Ok(entries) = parse_npmrc(&auth_path)
{
out.extend(
entries
.into_iter()
.map(|(k, v)| (NpmrcSource::UserNpmrcAuthFile, k, v)),
);
}
let project_rc = project_dir.join(".npmrc");
if project_rc.exists()
&& let Ok(entries) = parse_npmrc_untrusted(&project_rc)
{
out.extend(
entries
.into_iter()
.map(|(k, v)| (NpmrcSource::Project, k, v)),
);
}
if let Some((auth_path, auth_source)) = resolve_npmrc_auth_file_tagged(home, project_dir, &out)
&& auth_source.is_project_controlled()
&& auth_path.exists()
&& let Ok(entries) = parse_npmrc_untrusted(&auth_path)
{
out.extend(
entries
.into_iter()
.map(|(k, v)| (NpmrcSource::ProjectNpmrcAuthFile, k, v)),
);
}
out
}
pub(super) fn load_npmrc_entries_with_home(
home: Option<&Path>,
xdg_config_home: Option<&Path>,
project_dir: &Path,
user_rc_override: Option<&Path>,
) -> Vec<(String, String)> {
load_npmrc_entries_tagged_with_home(home, xdg_config_home, project_dir, user_rc_override)
.into_iter()
.map(|(_, k, v)| (k, v))
.collect()
}
fn resolve_npmrc_auth_file_path(
home: Option<&Path>,
project_dir: &Path,
raw: &str,
) -> Option<PathBuf> {
let expanded = if let Some(rest) = raw.strip_prefix("~/") {
home.map(|h| h.join(rest))?
} else if raw == "~" {
home.map(PathBuf::from)?
} else {
PathBuf::from(raw)
};
if expanded.is_absolute() {
Some(expanded)
} else {
Some(project_dir.join(expanded))
}
}
fn resolve_npmrc_auth_file_tagged(
home: Option<&Path>,
project_dir: &Path,
entries: &[(NpmrcSource, String, String)],
) -> Option<(PathBuf, NpmrcSource)> {
let (source, _, raw) = entries
.iter()
.rev()
.find(|(_, k, _)| matches!(k.as_str(), "npmrcAuthFile" | "npmrc-auth-file"))?;
let path = resolve_npmrc_auth_file_path(home, project_dir, raw)?;
Some((path, *source))
}
pub(super) fn expand_userconfig_path(raw: &str, home: Option<&Path>) -> Option<PathBuf> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
if let Some(rest) = trimmed.strip_prefix("~/") {
return home.map(|h| h.join(rest));
}
if trimmed == "~" {
return home.map(PathBuf::from);
}
Some(PathBuf::from(trimmed))
}
pub(super) fn userconfig_override_from_env(
env: &[(String, String)],
home: Option<&Path>,
) -> Option<PathBuf> {
let raw = env
.iter()
.find(|(name, _)| name == "NPM_CONFIG_USERCONFIG")
.or_else(|| env.iter().find(|(name, _)| name == "npm_config_userconfig"))?;
expand_userconfig_path(&raw.1, home)
}
pub(super) fn home_dir() -> Option<PathBuf> {
aube_util::env::home_dir()
}
fn pnpm_global_auth_ini_path(home: &Path, xdg_config_home: Option<&Path>) -> PathBuf {
let config_root = xdg_config_home
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".config"));
config_root.join("pnpm").join("auth.ini")
}