@@ -8,11 +8,21 @@ use std::sync::OnceLock;
88
99pub type ConfigResult < T > = Result < T > ;
1010
11- #[ derive( Debug , Serialize , Deserialize , Default ) ]
11+ /// Cached merged config (global + local). Set on first `Config::load(false)`.
12+ static MERGED_CONFIG : OnceLock < Config > = OnceLock :: new ( ) ;
13+
14+ #[ derive( Debug , Clone , Serialize , Deserialize , Default ) ]
1215pub struct Config {
16+ #[ serde( default ) ]
1317 values : HashMap < String , String > ,
1418 #[ serde( default , skip_serializing_if = "HashMap::is_empty" ) ]
1519 arrays : HashMap < String , Vec < String > > ,
20+ /// Default catalog: `[catalog]` section.
21+ #[ serde( default , skip_serializing_if = "HashMap::is_empty" ) ]
22+ catalog : HashMap < String , String > ,
23+ /// Named catalogs: `[catalogs.<name>]` sections.
24+ #[ serde( default , skip_serializing_if = "HashMap::is_empty" ) ]
25+ catalogs : HashMap < String , HashMap < String , String > > ,
1626}
1727
1828// global config path is ~/.utoo/config.toml
@@ -23,13 +33,30 @@ impl Config {
2333 return Self :: load_from_path ( & Self :: global_config_path ( ) ?) . await ;
2434 }
2535
36+ // Return cached merged config if available
37+ if let Some ( config) = MERGED_CONFIG . get ( ) {
38+ return Ok ( config. clone ( ) ) ;
39+ }
40+
2641 let mut config = Self :: load_from_path ( & Self :: global_config_path ( ) ?) . await ?;
27- let local_path = Self :: local_config_path ( ) ?;
28- if crate :: fs:: try_exists ( & local_path) . await ? {
29- let local_config = Self :: load_from_path ( & local_path) . await ?;
30- config. values . extend ( local_config. values ) ;
31- config. arrays . extend ( local_config. arrays ) ;
42+
43+ let local_config = {
44+ let local_path = Self :: local_config_path ( ) ?;
45+ if crate :: fs:: try_exists ( & local_path) . await ? {
46+ Some ( Self :: load_from_path ( & local_path) . await ?)
47+ } else {
48+ None
49+ }
50+ } ;
51+ if let Some ( local) = local_config {
52+ config. values . extend ( local. values ) ;
53+ config. arrays . extend ( local. arrays ) ;
54+ // Catalogs are project-local only; take them from the local config
55+ config. catalog = local. catalog ;
56+ config. catalogs = local. catalogs ;
3257 }
58+
59+ let _ = MERGED_CONFIG . set ( config. clone ( ) ) ;
3360 Ok ( config)
3461 }
3562
@@ -56,14 +83,22 @@ impl Config {
5683 self . save ( global)
5784 }
5885
59- pub ( crate ) async fn load_from_path ( path : & Path ) -> ConfigResult < Self > {
60- if !crate :: fs:: try_exists ( path) . await ? {
61- return Ok ( Config :: default ( ) ) ;
86+ /// Build a `Catalogs` map from the parsed `[catalog]` and `[catalogs.*]` sections.
87+ #[ allow( dead_code) ] // used by catalog protocol (upcoming)
88+ pub fn catalogs ( & self ) -> HashMap < String , HashMap < String , String > > {
89+ let mut result = self . catalogs . clone ( ) ;
90+ if !self . catalog . is_empty ( ) {
91+ result. insert ( String :: new ( ) , self . catalog . clone ( ) ) ;
6292 }
93+ result
94+ }
6395
64- let content = fs:: read_to_string ( path) ?;
65- let config = toml:: from_str ( & content) ?;
66- Ok ( config)
96+ pub ( crate ) async fn load_from_path ( path : & Path ) -> ConfigResult < Self > {
97+ match crate :: fs:: read_to_string ( path) . await {
98+ Ok ( content) => Ok ( toml:: from_str ( & content) ?) ,
99+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => Ok ( Config :: default ( ) ) ,
100+ Err ( e) => Err ( e. into ( ) ) ,
101+ }
67102 }
68103
69104 pub fn save ( & self , global : bool ) -> ConfigResult < ( ) > {
@@ -147,15 +182,18 @@ impl<T: Clone + Debug + 'static> ConfigValue<T> {
147182 return value. clone ( ) ;
148183 }
149184
150- // load from config
151- let config_result = Config :: load ( false ) . await ;
152- if let Ok ( config) = config_result {
153- let value_result = config. get ( self . key ) ;
154- if let Ok ( Some ( value) ) = value_result {
155- let parsed_value = self . parse_config_value ( & value) ;
156- let _ = self . value . set ( parsed_value. clone ( ) ) ;
157- return parsed_value;
158- }
185+ // Ensure merged config is loaded and cached
186+ if MERGED_CONFIG . get ( ) . is_none ( ) {
187+ let _ = Config :: load ( false ) . await ; // populates MERGED_CONFIG
188+ }
189+
190+ // Read from cached merged config (no clone of Config itself)
191+ if let Some ( config) = MERGED_CONFIG . get ( )
192+ && let Ok ( Some ( value) ) = config. get ( self . key )
193+ {
194+ let parsed_value = self . parse_config_value ( & value) ;
195+ let _ = self . value . set ( parsed_value. clone ( ) ) ;
196+ return parsed_value;
159197 }
160198
161199 self . default . clone ( )
@@ -254,4 +292,57 @@ mod tests {
254292 } ) ;
255293 } ) ;
256294 }
295+
296+ #[ test]
297+ fn test_catalogs_default_and_named ( ) {
298+ let config: Config = toml:: from_str (
299+ r#"
300+ [catalog]
301+ lodash = "^4.17.21"
302+ react = "^18.0.0"
303+
304+ [catalogs.legacy]
305+ path-to-regexp = "^1.9.0"
306+ "# ,
307+ )
308+ . unwrap ( ) ;
309+
310+ let catalogs = config. catalogs ( ) ;
311+ let default = catalogs. get ( "" ) . unwrap ( ) ;
312+ assert_eq ! ( default . get( "lodash" ) , Some ( & "^4.17.21" . to_string( ) ) ) ;
313+ assert_eq ! ( default . get( "react" ) , Some ( & "^18.0.0" . to_string( ) ) ) ;
314+
315+ let legacy = catalogs. get ( "legacy" ) . unwrap ( ) ;
316+ assert_eq ! ( legacy. get( "path-to-regexp" ) , Some ( & "^1.9.0" . to_string( ) ) ) ;
317+ }
318+
319+ #[ test]
320+ fn test_catalogs_empty ( ) {
321+ let config: Config = toml:: from_str ( "" ) . unwrap ( ) ;
322+ assert ! ( config. catalogs( ) . is_empty( ) ) ;
323+ }
324+
325+ #[ test]
326+ fn test_catalogs_coexists_with_config_values ( ) {
327+ let config: Config = toml:: from_str (
328+ r#"
329+ [values]
330+ registry = "https://registry.npmmirror.com"
331+
332+ [catalog]
333+ lodash = "^4.17.21"
334+ "# ,
335+ )
336+ . unwrap ( ) ;
337+
338+ assert_eq ! (
339+ config. get( "registry" ) . unwrap( ) ,
340+ Some ( "https://registry.npmmirror.com" . to_string( ) )
341+ ) ;
342+ let catalogs = config. catalogs ( ) ;
343+ assert_eq ! (
344+ catalogs. get( "" ) . unwrap( ) . get( "lodash" ) ,
345+ Some ( & "^4.17.21" . to_string( ) )
346+ ) ;
347+ }
257348}
0 commit comments