use std::path::{Path, PathBuf};
use super::env::npm_config_env_entries_from;
use super::npmrc::parse_npmrc;
use super::types::{NpmConfig, NpmrcSource};
impl NpmConfig {
pub fn load(project_dir: &Path) -> Self {
let env: Vec<(String, String)> = std::env::vars().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 => split.user.push((k, v)),
NpmrcSource::Project | NpmrcSource::NpmrcAuthFile => 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)),
);
}
}
let project_rc = project_dir.join(".npmrc");
if project_rc.exists()
&& let Ok(entries) = parse_npmrc(&project_rc)
{
out.extend(
entries
.into_iter()
.map(|(k, v)| (NpmrcSource::Project, k, v)),
);
}
if let Some(auth_path) = resolve_npmrc_auth_file(
home,
project_dir,
out.iter().map(|(_, k, v)| (k.as_str(), v.as_str())),
) && auth_path.exists()
&& let Ok(entries) = parse_npmrc(&auth_path)
{
out.extend(
entries
.into_iter()
.map(|(k, v)| (NpmrcSource::NpmrcAuthFile, 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)> {
let mut out = 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);
}
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);
}
}
let project_rc = project_dir.join(".npmrc");
if project_rc.exists()
&& let Ok(entries) = parse_npmrc(&project_rc)
{
out.extend(entries);
}
if let Some(auth_path) = resolve_npmrc_auth_file(
home,
project_dir,
out.iter().map(|(k, v)| (k.as_str(), v.as_str())),
) && auth_path.exists()
&& let Ok(entries) = parse_npmrc(&auth_path)
{
out.extend(entries);
}
out
}
pub(super) fn resolve_npmrc_auth_file<'a, I>(
home: Option<&Path>,
project_dir: &Path,
entries: I,
) -> Option<PathBuf>
where
I: DoubleEndedIterator<Item = (&'a str, &'a str)>,
{
let raw = entries
.rev()
.find(|(k, _)| matches!(*k, "npmrcAuthFile" | "npmrc-auth-file"))
.map(|(_, v)| v)?;
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))
}
}
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")
}