1use serde::Deserialize;
2use std::collections::HashMap;
3use std::fs;
4use std::path::Path;
5
6#[derive(Debug, Clone, Deserialize, Default)]
12#[serde(default)]
13pub struct DnxToml {
14 pub workspace: WorkspaceSection,
15 pub settings: SettingsSection,
16 pub registries: RegistriesSection,
17 pub catalog: HashMap<String, String>,
18 pub catalogs: HashMap<String, HashMap<String, String>>,
19 pub overrides: HashMap<String, String>,
20 #[serde(rename = "peer-dependency-rules")]
21 pub peer_dependency_rules: PeerDependencyRulesToml,
22 #[serde(rename = "patched-dependencies")]
23 pub patched_dependencies: HashMap<String, String>,
24 #[serde(rename = "only-built-dependencies")]
25 pub only_built_dependencies: OnlyBuiltDependencies,
26 #[serde(rename = "never-built-dependencies")]
27 pub never_built_dependencies: NeverBuiltDependencies,
28}
29
30#[derive(Debug, Clone, Deserialize, Default)]
31#[serde(default)]
32pub struct WorkspaceSection {
33 pub packages: Option<Vec<String>>,
34}
35
36#[derive(Debug, Clone, Deserialize)]
37#[serde(default)]
38pub struct SettingsSection {
39 pub registry: String,
40 #[serde(rename = "store-dir")]
41 pub store_dir: String,
42 #[serde(rename = "cache-dir")]
43 pub cache_dir: String,
44 pub concurrency: usize,
45 #[serde(rename = "prefer-offline")]
46 pub prefer_offline: bool,
47 #[serde(rename = "frozen-lockfile")]
48 pub frozen_lockfile: bool,
49 #[serde(rename = "shamefully-hoist")]
50 pub shamefully_hoist: bool,
51 pub hoist: bool,
52 #[serde(rename = "hoist-pattern")]
53 pub hoist_pattern: Vec<String>,
54 #[serde(rename = "public-hoist-pattern")]
55 pub public_hoist_pattern: Vec<String>,
56 #[serde(rename = "auto-install-peers")]
57 pub auto_install_peers: bool,
58 #[serde(rename = "strict-peer-dependencies")]
59 pub strict_peer_dependencies: bool,
60 #[serde(rename = "node-linker")]
61 pub node_linker: String,
62 #[serde(rename = "side-effects-cache")]
63 pub side_effects_cache: bool,
64 #[serde(rename = "verify-store-integrity")]
65 pub verify_store_integrity: bool,
66 #[serde(rename = "ignore-scripts")]
67 pub ignore_scripts: bool,
68 #[serde(rename = "enable-pre-post-scripts")]
69 pub enable_pre_post_scripts: bool,
70 #[serde(rename = "resolution-mode")]
71 pub resolution_mode: String,
72 #[serde(rename = "shell-emulator")]
73 pub shell_emulator: bool,
74 #[serde(rename = "use-node-version")]
75 pub use_node_version: String,
76 #[serde(default)]
78 pub pnpmfile: String,
79 #[serde(rename = "global-dir", default)]
81 pub global_dir: String,
82 #[serde(rename = "global-bin-dir", default)]
84 pub global_bin_dir: String,
85 pub proxy: ProxySection,
86}
87
88impl Default for SettingsSection {
89 fn default() -> Self {
90 Self {
91 registry: "https://registry.npmjs.org".to_string(),
92 store_dir: "~/.dnx/store".to_string(),
93 cache_dir: "~/.dnx/cache".to_string(),
94 concurrency: 64,
95 prefer_offline: false,
96 frozen_lockfile: false,
97 shamefully_hoist: false,
98 hoist: true,
99 hoist_pattern: vec!["*".to_string()],
100 public_hoist_pattern: vec!["@types/*".to_string()],
101 auto_install_peers: true,
102 strict_peer_dependencies: false,
103 node_linker: "isolated".to_string(),
104 side_effects_cache: true,
105 verify_store_integrity: true,
106 ignore_scripts: false,
107 enable_pre_post_scripts: true,
108 resolution_mode: "highest".to_string(),
109 shell_emulator: false,
110 use_node_version: String::new(),
111 pnpmfile: String::new(),
112 global_dir: String::new(),
113 global_bin_dir: String::new(),
114 proxy: ProxySection::default(),
115 }
116 }
117}
118
119#[derive(Debug, Clone, Deserialize, Default)]
120#[serde(default)]
121pub struct ProxySection {
122 #[serde(rename = "https-proxy")]
123 pub https_proxy: String,
124 #[serde(rename = "http-proxy")]
125 pub http_proxy: String,
126 #[serde(rename = "no-proxy")]
127 pub no_proxy: String,
128}
129
130#[derive(Debug, Clone, Default)]
136pub struct RegistriesSection {
137 pub default: String,
138 pub scoped: HashMap<String, ScopedRegistryToml>,
139}
140
141#[derive(Debug, Clone, Deserialize)]
142pub struct ScopedRegistryToml {
143 pub url: String,
144 #[serde(rename = "auth-token")]
145 pub auth_token: Option<String>,
146}
147
148impl<'de> Deserialize<'de> for RegistriesSection {
149 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
150 where
151 D: serde::Deserializer<'de>,
152 {
153 use serde::de::{MapAccess, Visitor};
154
155 struct RegistriesVisitor;
156
157 impl<'de> Visitor<'de> for RegistriesVisitor {
158 type Value = RegistriesSection;
159
160 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
161 formatter.write_str(
162 "a registries table with a default string and optional scoped registry tables",
163 )
164 }
165
166 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
167 where
168 M: MapAccess<'de>,
169 {
170 let mut section = RegistriesSection {
171 default: "https://registry.npmjs.org".to_string(),
172 scoped: HashMap::new(),
173 };
174
175 while let Some(key) = map.next_key::<String>()? {
176 if key == "default" {
177 section.default = map.next_value::<String>()?;
178 } else {
179 let scoped: ScopedRegistryToml = map.next_value()?;
180 section.scoped.insert(key, scoped);
181 }
182 }
183
184 Ok(section)
185 }
186 }
187
188 deserializer.deserialize_map(RegistriesVisitor)
189 }
190}
191
192#[derive(Debug, Clone, Deserialize, Default)]
193#[serde(default)]
194pub struct PeerDependencyRulesToml {
195 #[serde(rename = "ignore-missing")]
196 pub ignore_missing: Vec<String>,
197 #[serde(rename = "allowed-versions")]
198 pub allowed_versions: HashMap<String, String>,
199 #[serde(rename = "allow-any")]
200 pub allow_any: Vec<String>,
201}
202
203#[derive(Debug, Clone, Deserialize, Default)]
204#[serde(default)]
205pub struct OnlyBuiltDependencies {
206 pub packages: Vec<String>,
207}
208
209#[derive(Debug, Clone, Deserialize, Default)]
210#[serde(default)]
211pub struct NeverBuiltDependencies {
212 pub packages: Vec<String>,
213}
214
215#[derive(Debug, Clone)]
221pub struct ScopedRegistry {
222 pub url: String,
223 pub auth_token: Option<String>,
224}
225
226#[derive(Debug, Clone, Default)]
228pub struct PeerDependencyRules {
229 pub ignore_missing: Vec<String>,
230 pub allowed_versions: HashMap<String, String>,
231 pub allow_any: Vec<String>,
232}
233
234#[derive(Debug, Clone)]
236pub struct DnxConfig {
237 pub registry_url: String,
239 pub auth_token: Option<String>,
240 pub concurrency: usize,
241 pub cache_dir: Option<String>,
242 pub prefer_offline: bool,
243 pub frozen: bool,
244 pub scoped_registries: HashMap<String, ScopedRegistry>,
245 pub proxy: Option<String>,
246
247 pub store_dir: Option<String>,
249 pub shamefully_hoist: bool,
250 pub hoist: bool,
251 pub hoist_pattern: Vec<String>,
252 pub public_hoist_pattern: Vec<String>,
253 pub auto_install_peers: bool,
254 pub strict_peer_dependencies: bool,
255 pub node_linker: String,
256 pub side_effects_cache: bool,
257 pub verify_store_integrity: bool,
258 pub ignore_scripts: bool,
259 pub enable_pre_post_scripts: bool,
260 pub resolution_mode: String,
261 pub shell_emulator: bool,
262 pub use_node_version: Option<String>,
263 pub frozen_lockfile: bool,
264 pub overrides: HashMap<String, String>,
265 pub peer_dependency_rules: PeerDependencyRules,
266 pub patched_dependencies: HashMap<String, String>,
267 pub only_built_dependencies: Vec<String>,
268 pub never_built_dependencies: Vec<String>,
269 pub workspace_patterns: Option<Vec<String>>,
270 pub catalog_default: HashMap<String, String>,
271 pub catalog_named: HashMap<String, HashMap<String, String>>,
272 pub pnpmfile: Option<String>,
273 pub global_dir: String,
274 pub global_bin_dir: String,
275}
276
277impl Default for DnxConfig {
278 fn default() -> Self {
279 Self {
280 registry_url: "https://registry.npmjs.org".to_string(),
281 auth_token: None,
282 concurrency: 64,
283 cache_dir: None,
284 prefer_offline: false,
285 frozen: false,
286 scoped_registries: HashMap::new(),
287 proxy: None,
288
289 store_dir: None,
290 shamefully_hoist: false,
291 hoist: true,
292 hoist_pattern: vec!["*".to_string()],
293 public_hoist_pattern: vec!["@types/*".to_string()],
294 auto_install_peers: true,
295 strict_peer_dependencies: false,
296 node_linker: "isolated".to_string(),
297 side_effects_cache: true,
298 verify_store_integrity: true,
299 ignore_scripts: false,
300 enable_pre_post_scripts: true,
301 resolution_mode: "highest".to_string(),
302 shell_emulator: false,
303 use_node_version: None,
304 frozen_lockfile: false,
305 overrides: HashMap::new(),
306 peer_dependency_rules: PeerDependencyRules::default(),
307 patched_dependencies: HashMap::new(),
308 only_built_dependencies: Vec::new(),
309 never_built_dependencies: Vec::new(),
310 workspace_patterns: None,
311 catalog_default: HashMap::new(),
312 catalog_named: HashMap::new(),
313 pnpmfile: None,
314 global_dir: default_global_dir(),
315 global_bin_dir: default_global_bin_dir(),
316 }
317 }
318}
319
320fn default_global_dir() -> String {
321 dirs::home_dir()
322 .map(|h| h.join(".dnx").join("global").to_string_lossy().to_string())
323 .unwrap_or_else(|| "~/.dnx/global".to_string())
324}
325
326fn default_global_bin_dir() -> String {
327 dirs::home_dir()
328 .map(|h| h.join(".dnx").join("bin").to_string_lossy().to_string())
329 .unwrap_or_else(|| "~/.dnx/bin".to_string())
330}
331
332impl DnxConfig {
333 pub fn load(project_dir: &Path) -> Self {
344 let mut config = Self::default();
345
346 let dnx_toml_path = project_dir.join("dnx.toml");
347
348 if dnx_toml_path.exists() {
349 if let Ok(content) = fs::read_to_string(&dnx_toml_path) {
351 if let Ok(toml) = toml::from_str::<DnxToml>(&content) {
352 apply_dnx_toml(&mut config, &toml);
353 }
354 }
355 } else {
356 let home = dirs::home_dir().unwrap_or_default();
358 let npmrc_paths = [home.join(".npmrc"), project_dir.join(".npmrc")];
359
360 for npmrc_path in &npmrc_paths {
361 if let Ok(content) = fs::read_to_string(npmrc_path) {
362 parse_npmrc(&content, &mut config);
363 }
364 }
365 }
366
367 if let Ok(registry) = std::env::var("DNX_REGISTRY") {
369 config.registry_url = registry;
370 }
371 if let Ok(concurrency) = std::env::var("DNX_CONCURRENCY") {
372 if let Ok(c) = concurrency.parse::<usize>() {
373 if c > 0 {
374 config.concurrency = c.min(256);
375 }
376 }
377 }
378 if let Ok(cache_dir) = std::env::var("DNX_CACHE_DIR") {
379 config.cache_dir = Some(cache_dir);
380 }
381 if std::env::var("DNX_OFFLINE").as_deref() == Ok("true") {
382 config.prefer_offline = true;
383 }
384 if std::env::var("DNX_FROZEN").as_deref() == Ok("true") {
385 config.frozen = true;
386 config.frozen_lockfile = true;
387 }
388
389 if config.proxy.is_none() {
391 for var in &["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"] {
392 if let Ok(proxy) = std::env::var(var) {
393 if !proxy.is_empty() {
394 config.proxy = Some(proxy);
395 break;
396 }
397 }
398 }
399 }
400
401 if config.registry_url.starts_with("http://")
403 && !config.registry_url.contains("localhost")
404 && !config.registry_url.contains("127.0.0.1")
405 {
406 config.registry_url = config.registry_url.replace("http://", "https://");
407 }
408
409 config
410 }
411}
412
413fn apply_dnx_toml(config: &mut DnxConfig, toml: &DnxToml) {
415 config.workspace_patterns = toml.workspace.packages.clone();
417
418 let s = &toml.settings;
420 config.registry_url = s.registry.clone();
421
422 if !s.store_dir.is_empty() {
423 config.store_dir = Some(s.store_dir.clone());
424 }
425 if !s.cache_dir.is_empty() {
426 config.cache_dir = Some(s.cache_dir.clone());
427 }
428
429 config.concurrency = s.concurrency;
430 config.prefer_offline = s.prefer_offline;
431 config.frozen = s.frozen_lockfile;
432 config.frozen_lockfile = s.frozen_lockfile;
433 config.shamefully_hoist = s.shamefully_hoist;
434 config.hoist = s.hoist;
435 config.hoist_pattern = s.hoist_pattern.clone();
436 config.public_hoist_pattern = s.public_hoist_pattern.clone();
437 config.auto_install_peers = s.auto_install_peers;
438 config.strict_peer_dependencies = s.strict_peer_dependencies;
439 config.node_linker = s.node_linker.clone();
440 config.side_effects_cache = s.side_effects_cache;
441 config.verify_store_integrity = s.verify_store_integrity;
442 config.ignore_scripts = s.ignore_scripts;
443 config.enable_pre_post_scripts = s.enable_pre_post_scripts;
444 config.resolution_mode = s.resolution_mode.clone();
445 config.shell_emulator = s.shell_emulator;
446
447 if !s.use_node_version.is_empty() {
448 config.use_node_version = Some(s.use_node_version.clone());
449 }
450
451 if !s.proxy.https_proxy.is_empty() {
453 config.proxy = Some(s.proxy.https_proxy.clone());
454 } else if !s.proxy.http_proxy.is_empty() {
455 config.proxy = Some(s.proxy.http_proxy.clone());
456 }
457
458 if !toml.registries.default.is_empty() {
460 config.registry_url = toml.registries.default.clone();
461 }
462 for (scope, scoped) in &toml.registries.scoped {
463 config.scoped_registries.insert(
464 scope.clone(),
465 ScopedRegistry {
466 url: scoped.url.clone(),
467 auth_token: scoped.auth_token.clone(),
468 },
469 );
470 }
471
472 config.catalog_default = toml.catalog.clone();
474 config.catalog_named = toml.catalogs.clone();
475
476 config.overrides = toml.overrides.clone();
478
479 config.peer_dependency_rules = PeerDependencyRules {
481 ignore_missing: toml.peer_dependency_rules.ignore_missing.clone(),
482 allowed_versions: toml.peer_dependency_rules.allowed_versions.clone(),
483 allow_any: toml.peer_dependency_rules.allow_any.clone(),
484 };
485
486 config.patched_dependencies = toml.patched_dependencies.clone();
488
489 config.only_built_dependencies = toml.only_built_dependencies.packages.clone();
491 config.never_built_dependencies = toml.never_built_dependencies.packages.clone();
492
493 if !s.pnpmfile.is_empty() {
495 if s.pnpmfile == "false" {
496 config.pnpmfile = Some("false".to_string());
497 } else {
498 config.pnpmfile = Some(s.pnpmfile.clone());
499 }
500 }
501
502 if !s.global_dir.is_empty() {
504 config.global_dir = s.global_dir.clone();
505 }
506 if !s.global_bin_dir.is_empty() {
507 config.global_bin_dir = s.global_bin_dir.clone();
508 }
509}
510
511fn parse_npmrc(content: &str, config: &mut DnxConfig) {
513 for line in content.lines() {
514 let trimmed = line.trim();
515 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with(';') {
516 continue;
517 }
518
519 if trimmed.starts_with('@') {
521 if let Some(idx) = trimmed.find(":registry") {
522 let scope = &trimmed[..idx]; let rest = &trimmed[idx + 9..]; let rest = rest.trim_start();
525 if let Some(url) = rest.strip_prefix('=') {
526 let url = url.trim().trim_matches(|c| c == '"' || c == '\'');
527 config.scoped_registries.insert(
528 scope.to_string(),
529 ScopedRegistry {
530 url: url.to_string(),
531 auth_token: None,
532 },
533 );
534 }
535 continue;
536 }
537 }
538
539 if let Some(rest) = trimmed.strip_prefix("//") {
541 if let Some(token_part) = rest.split("/:_authToken=").nth(1) {
542 let registry_host = rest.split("/:_authToken=").next().unwrap_or("");
543 let mut matched_scope = false;
545 for (_scope, scoped) in config.scoped_registries.iter_mut() {
546 if scoped.url.contains(registry_host) {
547 scoped.auth_token = Some(token_part.to_string());
548 matched_scope = true;
549 }
550 }
551 if !matched_scope {
552 config.auth_token = Some(token_part.to_string());
553 }
554 continue;
555 }
556 }
557
558 if let Some(rest) = trimmed.strip_prefix("proxy") {
560 let rest = rest.trim_start();
561 if let Some(url) = rest.strip_prefix('=') {
562 let url = url.trim().trim_matches(|c| c == '"' || c == '\'');
563 if !url.is_empty() {
564 config.proxy = Some(url.to_string());
565 }
566 }
567 continue;
568 }
569 if let Some(rest) = trimmed.strip_prefix("https-proxy") {
570 let rest = rest.trim_start();
571 if let Some(url) = rest.strip_prefix('=') {
572 let url = url.trim().trim_matches(|c| c == '"' || c == '\'');
573 if !url.is_empty() {
574 config.proxy = Some(url.to_string());
575 }
576 }
577 continue;
578 }
579
580 if let Some(rest) = trimmed.strip_prefix("registry") {
582 let rest = rest.trim_start();
583 if let Some(url) = rest.strip_prefix('=') {
584 let url = url.trim().trim_matches(|c| c == '"' || c == '\'');
585 config.registry_url = url.to_string();
586 }
587 }
588 }
589}