Skip to main content

rust_nrm/utils/
registries.rs

1use colored::*;
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use tokio::fs;
7use toml;
8
9use super::speed_test::{SpeedTestResult, SpeedTester};
10use super::Logger;
11
12#[derive(Debug, Serialize, Deserialize, Clone)]
13pub struct Registry {
14    pub registry: String,
15    pub home: Option<String>,
16}
17
18#[derive(Debug, Clone)]
19pub struct Store {
20    pub registries: HashMap<String, Registry>,
21}
22
23impl Store {
24    pub async fn load() -> Self {
25        let config_path = get_config_path();
26        Logger::info(&format!("Config file path: {}", config_path.display()));
27
28        if !config_path.exists() {
29            Logger::info("Config file not found, creating default configuration...");
30            Self::create_default_config(&config_path).await;
31        }
32
33        match fs::read_to_string(&config_path).await {
34            Ok(contents) => {
35                match toml::from_str(&contents) {
36                    Ok(registries) => Self { registries },
37                    Err(e) => {
38                        Logger::error(&format!("Failed to parse config file: {}", e));
39                        Self::create_default_store()
40                    }
41                }
42            }
43            Err(e) => {
44                Logger::error(&format!("Failed to read config file: {}", e));
45                Self::create_default_store()
46            }
47        }
48    }
49
50    async fn create_default_config(config_path: &Path) {
51        let default_registries = HashMap::from([
52            (
53                "npm".to_string(),
54                Registry {
55                    registry: "https://registry.npmjs.org/".to_string(),
56                    home: Some("https://www.npmjs.org".to_string()),
57                },
58            ),
59            (
60                "yarn".to_string(),
61                Registry {
62                    registry: "https://registry.yarnpkg.com/".to_string(),
63                    home: Some("https://yarnpkg.com".to_string()),
64                },
65            ),
66            (
67                "taobao".to_string(),
68                Registry {
69                    registry: "https://registry.npmmirror.com/".to_string(),
70                    home: Some("https://npmmirror.com/".to_string()),
71                },
72            ),
73            (
74                "tencent".to_string(),
75                Registry {
76                    registry: "https://mirrors.cloud.tencent.com/npm/".to_string(),
77                    home: Some("https://mirrors.cloud.tencent.com/npm/".to_string()),
78                },
79            ),
80            (
81                "npmMirror".to_string(),
82                Registry {
83                    registry: "https://skimdb.npmjs.com/registry/".to_string(),
84                    home: Some("https://skimdb.npmjs.com".to_string()),
85                },
86            ),
87            (
88                "github".to_string(),
89                Registry {
90                    registry: "https://npm.pkg.github.com/".to_string(),
91                    home: Some("https://github.com".to_string()),
92                },
93            ),
94        ]);
95
96        if let Some(parent) = config_path.parent() {
97            if !parent.exists() {
98                if let Err(e) = fs::create_dir_all(parent).await {
99                    Logger::error(&format!("Failed to create config directory: {}", e));
100                    return;
101                }
102            }
103        }
104
105        let toml = toml::to_string(&default_registries).unwrap();
106        if let Err(e) = fs::write(config_path, toml).await {
107            Logger::error(&format!("Failed to write default config: {}", e));
108        }
109    }
110
111    fn create_default_store() -> Self {
112        Self {
113            registries: HashMap::from([
114                (
115                    "npm".to_string(),
116                    Registry {
117                        registry: "https://registry.npmjs.org/".to_string(),
118                        home: Some("https://www.npmjs.org".to_string()),
119                    },
120                ),
121                (
122                    "yarn".to_string(),
123                    Registry {
124                        registry: "https://registry.yarnpkg.com/".to_string(),
125                        home: Some("https://yarnpkg.com".to_string()),
126                    },
127                ),
128                (
129                    "taobao".to_string(),
130                    Registry {
131                        registry: "https://registry.npmmirror.com/".to_string(),
132                        home: Some("https://npmmirror.com/".to_string()),
133                    },
134                ),
135                (
136                    "tencent".to_string(),
137                    Registry {
138                        registry: "https://mirrors.cloud.tencent.com/npm/".to_string(),
139                        home: Some("https://mirrors.cloud.tencent.com/npm/".to_string()),
140                    },
141                ),
142                (
143                    "npmMirror".to_string(),
144                    Registry {
145                        registry: "https://skimdb.npmjs.com/registry/".to_string(),
146                        home: Some("https://skimdb.npmjs.com".to_string()),
147                    },
148                ),
149                (
150                    "github".to_string(),
151                    Registry {
152                        registry: "https://npm.pkg.github.com/".to_string(),
153                        home: Some("https://github.com".to_string()),
154                    },
155                ),
156            ])
157        }
158    }
159
160    pub async fn save(&self) {
161        let config_path = get_config_path();
162        let content = toml::to_string_pretty(&self.registries).unwrap();
163        if let Err(e) = fs::write(&config_path, content).await {
164            Logger::error(&format!("Failed to save config: {}", e));
165        }
166    }
167
168    pub async fn get_current_registry(&self, is_local: bool) -> Option<String> {
169        let npmrc_path = if is_local {
170            ".npmrc".to_string()
171        } else {
172            dirs::home_dir()
173                .map(|path| path.join(".npmrc").to_string_lossy().to_string())
174                .expect("Failed to get home directory")
175        };
176
177        if Path::new(&npmrc_path).exists() {
178            if let Ok(content) = fs::read_to_string(&npmrc_path).await {
179                let re = Regex::new(r"(?m)^\s*registry\s*=\s*(.+?)\s*$").unwrap();
180                if let Some(captures) = re.captures(&content) {
181                    let registry_url = captures.get(1).unwrap().as_str().to_string();
182                    // Find registry name by URL
183                    for (name, registry) in &self.registries {
184                        if registry.registry == registry_url {
185                            return Some(name.clone());
186                        }
187                    }
188                }
189            }
190        }
191        None
192    }
193
194    pub async fn list_registries(&self) {
195        Logger::list("Available registries:");
196
197        // Get current registries
198        let current_global = self.get_current_registry(false).await;
199        let current_local = self.get_current_registry(true).await;
200
201        for (name, registry) in self.registries.iter() {
202            let mut tags = Vec::new();
203
204            if let Some(current_global_name) = &current_global {
205                if name == current_global_name {
206                    tags.push("[GLOBAL]".white().on_blue());
207                }
208            }
209
210            if let Some(current_local_name) = &current_local {
211                if name == current_local_name {
212                    tags.push("[LOCAL]".white().on_green());
213                }
214            }
215
216            let tags_str = if !tags.is_empty() {
217                format!(
218                    " {}",
219                    tags.iter()
220                        .map(|t| t.to_string())
221                        .collect::<Vec<_>>()
222                        .join(" ")
223                )
224            } else {
225                String::new()
226            };
227
228            println!(
229                "{} -> {}{}",
230                name.green().bold(),
231                registry.registry.yellow(),
232                tags_str
233            );
234        }
235    }
236
237    pub fn set_current_use(&mut self, name: &str, is_local: bool) {
238        if self.registries.contains_key(name) {
239            Logger::info(&format!(
240                "Switched to registry: {} ({})",
241                name.green().bold(),
242                if is_local {
243                    "local".yellow()
244                } else {
245                    "global".yellow()
246                }
247            ));
248        } else {
249            Logger::error(&format!("Registry not found: {}", name));
250        }
251    }
252
253    pub async fn test_registry_speed(&self) -> Vec<SpeedTestResult> {
254        let tester = SpeedTester::new();
255        let registries: Vec<(String, String)> = self
256            .registries
257            .iter()
258            .map(|(name, reg)| (name.clone(), reg.registry.clone()))
259            .collect();
260
261        tester.test_all(&registries).await
262    }
263}
264
265fn get_config_path() -> PathBuf {
266    let home = dirs::home_dir().expect("Could not find home directory");
267    home.join(".config")
268        .join("rust-nrm")
269        .join("registries.toml")
270}