Skip to main content

fob_bundler/
config.rs

1//! Build configuration types.
2//!
3//! This module provides the new `BuildConfig` type with a cleaner, more extensible design.
4//! `BuildConfig` integrates with `DeploymentTarget` for platform-specific configuration.
5//!
6//! Note: `BuildOptions` remains the public API for backward compatibility.
7//! `BuildConfig` is available for advanced use cases and future migration.
8
9use crate::builders::unified::{
10    EntryPoints, MinifyLevel,
11    primitives::{CodeSplittingConfig, EntryMode, ExternalConfig},
12};
13use crate::target::{ExportConditions, NodeBuiltins};
14use crate::{OutputFormat, SourceMapType};
15use rustc_hash::FxHashMap;
16use std::path::PathBuf;
17use std::sync::Arc;
18
19/// External package pattern for resolution
20#[derive(Debug, Clone)]
21pub enum ExternalPattern {
22    /// Exact package name match
23    Exact(String),
24    /// Regex pattern match
25    Pattern(String),
26}
27
28impl ExternalPattern {
29    pub fn exact(name: impl Into<String>) -> Self {
30        Self::Exact(name.into())
31    }
32
33    pub fn pattern(pattern: impl Into<String>) -> Self {
34        Self::Pattern(pattern.into())
35    }
36}
37
38/// Output configuration
39#[derive(Debug, Clone)]
40pub struct OutputConfig {
41    pub dir: PathBuf,
42    pub format: OutputFormat,
43    pub sourcemap: Option<SourceMapType>,
44    pub splitting: bool,
45}
46
47/// Module resolution configuration
48#[derive(Debug, Clone)]
49pub struct ResolutionConfig {
50    /// Export conditions (from DeploymentTarget or custom)
51    pub conditions: ExportConditions,
52
53    /// Path aliases (@/ -> ./src/)
54    pub aliases: FxHashMap<String, String>,
55
56    /// External packages
57    pub external: Vec<ExternalPattern>,
58
59    /// How to handle Node.js built-ins
60    pub node_builtins: NodeBuiltins,
61}
62
63/// Optimization settings
64#[derive(Debug, Clone)]
65pub struct OptimizationConfig {
66    pub minify: MinifyLevel,
67    pub tree_shake: bool,
68    pub bundle: bool,
69}
70
71/// Build configuration
72///
73/// This replaces BuildOptions with a cleaner, more structured design.
74/// Use the builder pattern methods for ergonomic configuration.
75pub struct BuildConfig {
76    /// Entry points
77    pub entries: EntryPoints,
78
79    /// Output configuration
80    pub output: OutputConfig,
81
82    /// Module resolution
83    pub resolution: ResolutionConfig,
84
85    /// Optimization settings
86    pub optimization: OptimizationConfig,
87
88    /// Working directory for module resolution
89    pub cwd: Option<PathBuf>,
90
91    /// Runtime for filesystem operations
92    pub runtime: Option<Arc<dyn crate::Runtime>>,
93
94    /// Virtual files that don't exist on disk
95    pub virtual_files: FxHashMap<String, String>,
96}
97
98impl BuildConfig {
99    /// Create a new BuildConfig with a single entry point
100    pub fn new(entry: impl Into<EntryPoints>) -> Self {
101        let entries = entry.into();
102        Self {
103            entries,
104            output: OutputConfig {
105                dir: PathBuf::from("dist"),
106                format: OutputFormat::Esm,
107                sourcemap: Some(SourceMapType::File),
108                splitting: false,
109            },
110            resolution: ResolutionConfig {
111                conditions: ExportConditions::browser(),
112                aliases: FxHashMap::default(),
113                external: Vec::new(),
114                node_builtins: NodeBuiltins::Error,
115            },
116            optimization: OptimizationConfig {
117                minify: MinifyLevel::None,
118                tree_shake: true,
119                bundle: true,
120            },
121            cwd: None,
122            runtime: None,
123            virtual_files: FxHashMap::default(),
124        }
125    }
126
127    /// Apply settings from a deployment target
128    pub fn for_target(mut self, target: &dyn crate::DeploymentTarget) -> Self {
129        self.resolution.conditions = target.conditions();
130        self.resolution.node_builtins = target.node_builtins();
131        self.resolution.external.extend(
132            target
133                .external_packages()
134                .into_iter()
135                .map(ExternalPattern::exact),
136        );
137        self
138    }
139
140    /// Set the output directory
141    pub fn output_dir(mut self, dir: impl Into<PathBuf>) -> Self {
142        self.output.dir = dir.into();
143        self
144    }
145
146    /// Set the output format
147    pub fn format(mut self, format: OutputFormat) -> Self {
148        self.output.format = format;
149        self
150    }
151
152    /// Enable or disable source maps
153    pub fn sourcemap(mut self, enabled: bool) -> Self {
154        self.output.sourcemap = if enabled {
155            Some(SourceMapType::File)
156        } else {
157            None
158        };
159        self
160    }
161
162    /// Set source map type explicitly
163    pub fn sourcemap_type(mut self, sourcemap: Option<SourceMapType>) -> Self {
164        self.output.sourcemap = sourcemap;
165        self
166    }
167
168    /// Set the minification level
169    pub fn minify(mut self, level: MinifyLevel) -> Self {
170        self.optimization.minify = level;
171        self
172    }
173
174    /// Enable or disable bundling
175    pub fn bundle(mut self, enabled: bool) -> Self {
176        self.optimization.bundle = enabled;
177        self
178    }
179
180    /// Enable or disable code splitting
181    pub fn splitting(mut self, enabled: bool) -> Self {
182        self.output.splitting = enabled;
183        self
184    }
185
186    /// Add external packages
187    pub fn external(mut self, packages: impl IntoIterator<Item = impl Into<String>>) -> Self {
188        self.resolution.external.extend(
189            packages
190                .into_iter()
191                .map(|p| ExternalPattern::exact(p.into())),
192        );
193        self
194    }
195
196    /// Add a path alias
197    pub fn path_alias(mut self, alias: impl Into<String>, target: impl Into<String>) -> Self {
198        self.resolution.aliases.insert(alias.into(), target.into());
199        self
200    }
201
202    /// Set the working directory
203    pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
204        self.cwd = Some(cwd.into());
205        self
206    }
207
208    /// Set the runtime
209    pub fn runtime(mut self, runtime: Arc<dyn crate::Runtime>) -> Self {
210        self.runtime = Some(runtime);
211        self
212    }
213
214    /// Add a virtual file
215    pub fn virtual_file(mut self, path: impl Into<String>, content: impl Into<String>) -> Self {
216        self.virtual_files.insert(path.into(), content.into());
217        self
218    }
219
220    /// Validate the build configuration
221    ///
222    /// Checks for internal consistency and invalid combinations.
223    pub fn validate(&self) -> crate::Result<()> {
224        // Code splitting requires multiple entries and bundling
225        if self.output.splitting {
226            if matches!(self.entries, EntryPoints::Single(_)) {
227                return Err(crate::Error::InvalidConfig(
228                    "code splitting requires multiple entry points".into(),
229                ));
230            }
231            if !self.optimization.bundle {
232                return Err(crate::Error::InvalidConfig(
233                    "code splitting requires bundle: true".into(),
234                ));
235            }
236        }
237
238        // Entry count validation (DoS protection)
239        const MAX_ENTRY_POINTS: usize = 1000;
240        let entry_count = match &self.entries {
241            EntryPoints::Single(_) => 1,
242            EntryPoints::Multiple(v) => v.len(),
243            EntryPoints::Named(m) => m.len(),
244        };
245
246        if entry_count > MAX_ENTRY_POINTS {
247            return Err(crate::Error::InvalidConfig(format!(
248                "Too many entry points: {} (max {})",
249                entry_count, MAX_ENTRY_POINTS
250            )));
251        }
252
253        if entry_count == 0 {
254            return Err(crate::Error::InvalidConfig(
255                "At least one entry point is required".into(),
256            ));
257        }
258
259        Ok(())
260    }
261
262    /// Execute the build with this configuration
263    ///
264    /// Validates the configuration, converts to BuildOptions, and runs the build.
265    pub async fn build(self) -> crate::Result<crate::BuildResult> {
266        self.validate()?;
267        let options = self.into_build_options();
268        crate::builders::build(options).await
269    }
270
271    /// Convert BuildConfig to BuildOptions for compatibility with existing build pipeline
272    fn into_build_options(self) -> crate::BuildOptions {
273        use crate::Platform;
274
275        // Map ExportConditions to Platform for backward compatibility
276        let platform = match self.resolution.conditions {
277            ExportConditions::Node => Platform::Node,
278            ExportConditions::Edge | ExportConditions::Browser => Platform::Browser,
279        };
280
281        // Map old bundle/splitting to new primitives
282        let external = if self.optimization.bundle {
283            ExternalConfig::None
284        } else {
285            ExternalConfig::List(vec![])
286        };
287
288        // Determine entry mode and code splitting based on entries and splitting
289        let (entry_mode, code_splitting) = match &self.entries {
290            EntryPoints::Single(_) => (EntryMode::Shared, None),
291            EntryPoints::Multiple(_) | EntryPoints::Named(_) => {
292                if self.output.splitting {
293                    (EntryMode::Shared, Some(CodeSplittingConfig::default()))
294                } else {
295                    (EntryMode::Isolated, None)
296                }
297            }
298        };
299
300        // Convert external patterns to ExternalConfig
301        let external_config = if self.resolution.external.is_empty() {
302            external
303        } else {
304            let packages: Vec<String> = self
305                .resolution
306                .external
307                .into_iter()
308                .map(|p| match p {
309                    ExternalPattern::Exact(s) => s,
310                    ExternalPattern::Pattern(s) => s,
311                })
312                .collect();
313            ExternalConfig::List(packages)
314        };
315
316        crate::BuildOptions {
317            entry: self.entries,
318            entry_mode,
319            code_splitting,
320            external: external_config,
321            outdir: Some(self.output.dir),
322            outfile: None,
323            platform,
324            format: self.output.format,
325            sourcemap: self.output.sourcemap,
326            minify_level: match self.optimization.minify {
327                MinifyLevel::None => None,
328                MinifyLevel::Whitespace => Some("whitespace".to_string()),
329                MinifyLevel::Syntax => Some("syntax".to_string()),
330                MinifyLevel::Identifiers => Some("identifiers".to_string()),
331            },
332            globals: FxHashMap::default(),
333            virtual_files: self.virtual_files,
334            path_aliases: self.resolution.aliases,
335            cwd: self.cwd,
336            runtime: self.runtime,
337            decorator: None,
338            #[cfg(feature = "dts-generation")]
339            dts: None,
340            cache: None,
341            incremental: None,
342            max_parallel_builds: None,
343        }
344    }
345}