fob_cli/config/
loading.rs

1use crate::config::FobConfig;
2use crate::error::{ConfigError, Result};
3use figment::{
4    providers::{Env, Format as _, Json, Serialized},
5    Error as FigmentError, Figment,
6};
7use std::path::Path;
8
9impl FobConfig {
10    /// Load configuration from multiple sources.
11    /// Priority: CLI args > environment variables > config file > defaults
12    pub fn load(args: &crate::cli::BuildArgs, config_path: Option<&Path>) -> Result<Self> {
13        let defaults = Self::default_config();
14        let default_entry = defaults.entry.clone();
15        let mut figment = Figment::new().merge(Serialized::defaults(defaults));
16
17        // Load fob.config.json if it exists
18        let mut config_file = config_path
19            .map(|p| p.to_path_buf())
20            .or_else(|| {
21                args.cwd.as_ref().and_then(|cwd| {
22                    let cwd_path = if cwd.is_absolute() {
23                        cwd.clone()
24                    } else {
25                        std::env::current_dir().ok()?.join(cwd)
26                    };
27                    let candidate = cwd_path.join("fob.config.json");
28                    candidate.exists().then_some(candidate)
29                })
30            })
31            .or_else(|| {
32                let default_path = Path::new("fob.config.json");
33                default_path.exists().then_some(default_path.to_path_buf())
34            });
35        let has_config_file = config_file.is_some();
36
37        if let Some(path) = config_file.take() {
38            figment = figment.merge(Json::file(path));
39        }
40
41        // Merge environment variables (FOB_FORMAT, FOB_OUT_DIR, etc.)
42        let env_provider = Env::prefixed("FOB_")
43            .map(|key| env_key_to_camel_case(key.as_str()).into())
44            .lowercase(false);
45        figment = figment.merge(env_provider);
46
47        // Preserve existing entry when CLI explicitly omits entries so other CLI flags still apply.
48        let base_entry =
49            if args.entry.is_none() || args.entry.as_ref().is_some_and(|e| e.is_empty()) {
50                let base: Self = figment.clone().extract().map_err(convert_figment_error)?;
51                Some(base.entry)
52            } else {
53                None
54            };
55
56        let cli_config = Self::from_build_args(args);
57        figment = figment.merge(Serialized::defaults(cli_config));
58
59        let mut config: Self = figment.extract().map_err(convert_figment_error)?;
60
61        if let Some(entry) = base_entry {
62            config.entry = entry;
63        }
64
65        if args.entry.is_none()
66            && !has_config_file
67            && config.entry == default_entry
68            && !std::env::vars().any(|(key, _)| key == "FOB_ENTRY" || key.starts_with("FOB_ENTRY_"))
69        {
70            return Err(ConfigError::MissingField {
71                field: "entry".to_string(),
72                hint: "Specify at least one entry point with --entry or fob.config.json"
73                    .to_string(),
74            }
75            .into());
76        }
77
78        Ok(config)
79    }
80
81    /// Convert CLI BuildArgs to FobConfig.
82    fn from_build_args(args: &crate::cli::BuildArgs) -> Self {
83        Self {
84            entry: args.entry.clone().unwrap_or_default(),
85            format: args.format.into(),
86            out_dir: args.out_dir.clone(),
87            dts: args.dts,
88            dts_bundle: if args.dts_bundle { Some(true) } else { None },
89            external: args.external.clone(),
90            platform: args.platform.into(),
91            sourcemap: args.sourcemap.map(Into::into),
92            minify: args.minify,
93            target: args.target,
94            global_name: args.global_name.clone(),
95            bundle: args.bundle,
96            splitting: args.splitting,
97            no_treeshake: args.no_treeshake,
98            clean: args.clean,
99            cwd: args.cwd.clone(),
100        }
101    }
102
103    /// Get default configuration values.
104    pub(crate) fn default_config() -> Self {
105        use crate::config::types::*;
106        use std::path::PathBuf;
107
108        Self {
109            entry: vec!["src/index.ts".to_string()],
110            format: Format::Esm,
111            out_dir: PathBuf::from("dist"),
112            dts: false,
113            dts_bundle: None,
114            external: vec![],
115            platform: Platform::Browser,
116            sourcemap: None,
117            minify: false,
118            target: EsTarget::Es2020,
119            global_name: None,
120            bundle: true, // Bundle by default
121            splitting: false,
122            no_treeshake: false,
123            clean: false,
124            cwd: None,
125        }
126    }
127}
128
129fn convert_figment_error(e: FigmentError) -> crate::error::CliError {
130    ConfigError::InvalidValue {
131        field: "configuration".to_string(),
132        value: e.to_string(),
133        hint: "Check fob.config.json syntax and field types".to_string(),
134    }
135    .into()
136}
137
138fn env_key_to_camel_case(key: &str) -> String {
139    let mut parts = key.split('_').filter(|part| !part.is_empty());
140    let mut result = String::new();
141    if let Some(first) = parts.next() {
142        result.push_str(&first.to_ascii_lowercase());
143    }
144
145    for part in parts {
146        let mut chars = part.chars();
147        if let Some(first_char) = chars.next() {
148            result.push(first_char.to_ascii_uppercase());
149            for c in chars {
150                result.push(c.to_ascii_lowercase());
151            }
152        }
153    }
154
155    result
156}