use crate::error::UpgradeError;
use std::collections::HashMap;
use std::path::Path;
use sublime_standard_tools::filesystem::AsyncFileSystem;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthType {
Basic,
Bearer,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthCredential {
pub auth_type: AuthType,
pub value: String,
}
#[derive(Debug, Clone, Default)]
pub struct NpmrcConfig {
pub registry: Option<String>,
pub scoped_registries: HashMap<String, String>,
pub auth_tokens: HashMap<String, AuthCredential>,
pub other: HashMap<String, String>,
}
impl NpmrcConfig {
pub async fn from_workspace<F>(workspace_root: &Path, fs: &F) -> Result<Self, UpgradeError>
where
F: AsyncFileSystem,
{
let mut config = Self::default();
if let Some(home_dir) = dirs::home_dir() {
let user_npmrc_path = home_dir.join(".npmrc");
if fs.exists(&user_npmrc_path).await {
match Self::parse_npmrc_file(&user_npmrc_path, fs).await {
Ok(user_config) => {
config.merge_with(user_config);
}
Err(e) => {
eprintln!("Warning: Failed to parse user .npmrc: {}", e);
}
}
}
}
let workspace_npmrc_path = workspace_root.join(".npmrc");
if fs.exists(&workspace_npmrc_path).await {
let workspace_config = Self::parse_npmrc_file(&workspace_npmrc_path, fs).await?;
config.merge_with(workspace_config);
}
Ok(config)
}
pub fn resolve_registry(&self, package_name: &str) -> Option<&str> {
if let Some(scope) = package_name.strip_prefix('@') {
if let Some(scope_end) = scope.find('/') {
let scope_name = &scope[..scope_end];
if let Some(registry) = self.scoped_registries.get(scope_name) {
return Some(registry.as_str());
}
}
}
self.registry.as_deref()
}
pub fn get_auth_token(&self, registry_url: &str) -> Option<&AuthCredential> {
if let Some(cred) = self.auth_tokens.get(registry_url) {
return Some(cred);
}
let normalized = registry_url
.trim_start_matches("https://")
.trim_start_matches("http://")
.trim_end_matches('/');
if let Some(cred) = self.auth_tokens.get(normalized) {
return Some(cred);
}
for (key, cred) in &self.auth_tokens {
let normalized_key = key
.trim_start_matches("https://")
.trim_start_matches("http://")
.trim_start_matches("//")
.trim_end_matches('/');
if normalized_key == normalized {
return Some(cred);
}
}
None
}
async fn parse_npmrc_file<F>(path: &Path, fs: &F) -> Result<Self, UpgradeError>
where
F: AsyncFileSystem,
{
let content =
fs.read_file_string(path).await.map_err(|e| UpgradeError::NpmrcParseError {
path: path.to_path_buf(),
reason: format!("Failed to read file: {}", e),
})?;
Self::parse_content(&content, path)
}
fn parse_content(content: &str, path: &Path) -> Result<Self, UpgradeError> {
let mut config = Self::default();
for (line_num, line) in content.lines().enumerate() {
let line = line.trim();
if line.is_empty() || (line.starts_with('#') && !line.contains('=')) {
continue;
}
if let Some(eq_pos) = line.find('=') {
let key = line[..eq_pos].trim();
let value_with_comment = &line[eq_pos + 1..];
let value = Self::remove_comment(value_with_comment).trim();
if value.is_empty() {
continue;
}
let value = Self::substitute_env_vars(value);
if let Err(e) = Self::parse_key_value(&mut config, key, &value) {
return Err(UpgradeError::NpmrcParseError {
path: path.to_path_buf(),
reason: format!("Line {}: {}", line_num + 1, e),
});
}
}
}
Ok(config)
}
fn remove_comment(value: &str) -> &str {
let hash_pos = value.find('#');
let double_slash_pos = Self::find_comment_double_slash(value);
match (hash_pos, double_slash_pos) {
(Some(h), Some(d)) => &value[..h.min(d)],
(Some(h), None) => &value[..h],
(None, Some(d)) => &value[..d],
(None, None) => value,
}
}
fn find_comment_double_slash(value: &str) -> Option<usize> {
let mut pos = 0;
while let Some(idx) = value[pos..].find("//") {
let absolute_pos = pos + idx;
if absolute_pos > 0
&& let Some(prev_char) = value[..absolute_pos].chars().last()
&& prev_char.is_whitespace()
{
return Some(absolute_pos);
}
pos = absolute_pos + 2;
}
None
}
fn substitute_env_vars(value: &str) -> String {
let mut result = value.to_string();
while let Some(start) = result.find("${") {
if let Some(end) = result[start..].find('}') {
let var_name = &result[start + 2..start + end];
if let Ok(var_value) = std::env::var(var_name) {
result.replace_range(start..start + end + 1, &var_value);
} else {
break;
}
} else {
break;
}
}
result
}
fn parse_key_value(config: &mut Self, key: &str, value: &str) -> Result<(), String> {
if let Some(scope_key) = key.strip_prefix('@')
&& let Some(colon_pos) = scope_key.find(':')
{
let scope_name = &scope_key[..colon_pos];
let property = &scope_key[colon_pos + 1..];
if property == "registry" {
config.scoped_registries.insert(scope_name.to_string(), value.to_string());
return Ok(());
}
}
if key.starts_with("//") && (key.contains(":_authToken") || key.contains(":_auth")) {
if let Some(auth_pos) = key.find("/:_authToken") {
let registry = &key[2..auth_pos];
config.auth_tokens.insert(
registry.to_string(),
AuthCredential { auth_type: AuthType::Bearer, value: value.to_string() },
);
return Ok(());
} else if let Some(auth_pos) = key.find(":_authToken") {
let registry = &key[2..auth_pos];
config.auth_tokens.insert(
registry.to_string(),
AuthCredential { auth_type: AuthType::Bearer, value: value.to_string() },
);
return Ok(());
} else if let Some(auth_pos) = key.find("/:_auth") {
let registry = &key[2..auth_pos];
config.auth_tokens.insert(
registry.to_string(),
AuthCredential { auth_type: AuthType::Basic, value: value.to_string() },
);
return Ok(());
} else if let Some(auth_pos) = key.find(":_auth") {
let registry = &key[2..auth_pos];
config.auth_tokens.insert(
registry.to_string(),
AuthCredential { auth_type: AuthType::Basic, value: value.to_string() },
);
return Ok(());
}
}
if key.ends_with(":_authToken") {
let registry = key.trim_end_matches(":_authToken").trim_end_matches('/');
config.auth_tokens.insert(
registry.to_string(),
AuthCredential { auth_type: AuthType::Bearer, value: value.to_string() },
);
return Ok(());
}
if key.ends_with(":_auth") {
let registry = key.trim_end_matches(":_auth").trim_end_matches('/');
config.auth_tokens.insert(
registry.to_string(),
AuthCredential { auth_type: AuthType::Basic, value: value.to_string() },
);
return Ok(());
}
if key == "registry" {
config.registry = Some(value.to_string());
return Ok(());
}
config.other.insert(key.to_string(), value.to_string());
Ok(())
}
pub(crate) fn merge_with(&mut self, other: Self) {
if other.registry.is_some() {
self.registry = other.registry;
}
self.scoped_registries.extend(other.scoped_registries);
self.auth_tokens.extend(other.auth_tokens);
self.other.extend(other.other);
}
}