Skip to main content

fob_bundler/builders/unified/
primitives.rs

1//! Composable primitives for configuring bundler behavior.
2//!
3//! This module provides three orthogonal axes for controlling the bundler:
4//!
5//! 1. [`EntryMode`] - Do entries share code? (Shared vs Isolated)
6//! 2. [`CodeSplittingConfig`] - Code splitting configuration (Option = on/off)
7//! 3. [`ExternalConfig`] - External dependencies (None, List, FromManifest)
8//! 4. [`IncrementalConfig`] - Incremental module graph caching (optional)
9//!
10//! These primitives can be composed explicitly to achieve any valid build configuration.
11
12use std::path::PathBuf;
13
14/// Controls whether entry points share code or are isolated.
15///
16/// This determines whether multiple entry points share a single bundle context
17/// (allowing code splitting and shared chunks) or produce completely independent bundles.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum EntryMode {
20    /// Entries can share chunks (was: Unified).
21    ///
22    /// Entry points share a single output context and can reference shared chunks.
23    /// This is the only valid option for single-entry builds, and required when
24    /// code splitting is enabled.
25    ///
26    /// Returns [`BuildOutput::Single`](super::BuildOutput::Single).
27    #[default]
28    Shared,
29
30    /// Each entry stands alone (was: Separate).
31    ///
32    /// No code sharing between bundles; each is self-contained. This is useful
33    /// for component libraries where each component should be independently
34    /// consumable without requiring shared runtime chunks.
35    ///
36    /// Requires multiple entry points and is incompatible with code splitting.
37    ///
38    /// Returns [`BuildOutput::Multiple`](super::BuildOutput::Multiple).
39    Isolated,
40}
41
42/// Configuration for code splitting.
43///
44/// Code splitting extracts shared dependencies into separate chunks that can be
45/// loaded on-demand or preloaded, reducing initial bundle size and improving
46/// caching efficiency.
47///
48/// When `None`, code splitting is disabled. When `Some(config)`, code splitting
49/// is enabled with the specified thresholds.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct CodeSplittingConfig {
52    /// Minimum chunk size in bytes (e.g., 20000 = 20KB).
53    pub min_size: u32,
54
55    /// Minimum number of entry points that must import the same module (minimum: 2).
56    ///
57    /// This was previously called `min_share_count` but `min_imports` is clearer
58    /// about what's being counted.
59    pub min_imports: u32,
60}
61
62/// Controls external dependency handling.
63///
64/// This determines which dependencies (from node_modules) are externalized
65/// (not bundled) vs included in the bundle.
66#[derive(Debug, Clone, PartialEq, Eq, Default)]
67pub enum ExternalConfig {
68    /// Bundle everything (was: BundlingMode::Full).
69    ///
70    /// All imports, including from node_modules, are included in the bundle.
71    /// This produces self-contained output that can run without external dependencies.
72    #[default]
73    None,
74
75    /// Externalize specific packages (explicit list).
76    ///
77    /// Only the packages in this list are marked as external.
78    /// All other dependencies are bundled.
79    List(Vec<String>),
80
81    /// Externalize dependencies from package.json manifest.
82    ///
83    /// Reads the `dependencies` and `peerDependencies` fields from the
84    /// specified package.json file and externalizes those packages.
85    FromManifest(std::path::PathBuf),
86}
87
88impl CodeSplittingConfig {
89    /// Create code splitting config with custom thresholds.
90    #[inline]
91    pub fn new(min_size: u32, min_imports: u32) -> Self {
92        Self {
93            min_size,
94            min_imports,
95        }
96    }
97
98    /// Convert to Rolldown's AdvancedChunksOptions.
99    pub(crate) fn to_rolldown_options(&self) -> rolldown::AdvancedChunksOptions {
100        rolldown::AdvancedChunksOptions {
101            min_size: Some(self.min_size as f64),
102            min_share_count: Some(self.min_imports),
103            max_size: None,
104            min_module_size: None,
105            max_module_size: None,
106            include_dependencies_recursively: None,
107            groups: Some(vec![]),
108        }
109    }
110}
111
112impl Default for CodeSplittingConfig {
113    fn default() -> Self {
114        Self {
115            min_size: 20_000,
116            min_imports: 2,
117        }
118    }
119}
120
121impl std::fmt::Display for EntryMode {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        match self {
124            EntryMode::Shared => write!(f, "shared"),
125            EntryMode::Isolated => write!(f, "isolated"),
126        }
127    }
128}
129
130impl std::fmt::Display for CodeSplittingConfig {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        write!(
133            f,
134            "code_splitting(min_size={}, min_imports={})",
135            self.min_size, self.min_imports
136        )
137    }
138}
139
140impl std::fmt::Display for ExternalConfig {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        match self {
143            ExternalConfig::None => write!(f, "none"),
144            ExternalConfig::List(packages) => {
145                write!(f, "list({})", packages.join(", "))
146            }
147            ExternalConfig::FromManifest(path) => {
148                write!(f, "from_manifest({})", path.display())
149            }
150        }
151    }
152}
153
154/// Configuration for incremental module graph caching.
155///
156/// When enabled, the module graph is cached between builds and only
157/// reprocessed when files change. This can significantly speed up
158/// watch mode and incremental rebuilds.
159///
160/// # Cache Strategy
161///
162/// The cache stores:
163/// - Serialized module graph (post-Rolldown analysis)
164/// - BLAKE3 hashes of all module file contents
165/// - Rolldown version string (for invalidation on upgrades)
166///
167/// On subsequent builds:
168/// 1. Load cached graph and hashes
169/// 2. Compute current file hashes
170/// 3. If no changes detected, reuse cached graph
171/// 4. Otherwise, run full analysis and update cache
172///
173/// # Limitations
174///
175/// Rolldown still runs every time (it's a black box we can't cache).
176/// We only cache the post-Rolldown graph analysis phase.
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct IncrementalConfig {
179    /// Directory where incremental cache is stored.
180    ///
181    /// The cache uses a single binary file at `<cache_dir>/incremental.bin`.
182    /// Parent directories are created automatically if they don't exist.
183    pub cache_dir: PathBuf,
184}
185
186impl IncrementalConfig {
187    /// Create incremental config with the given cache directory.
188    ///
189    /// # Examples
190    ///
191    /// ```rust
192    /// use fob_bundler::IncrementalConfig;
193    ///
194    /// let config = IncrementalConfig::new(".fob-cache");
195    /// ```
196    pub fn new(cache_dir: impl Into<PathBuf>) -> Self {
197        Self {
198            cache_dir: cache_dir.into(),
199        }
200    }
201}
202
203impl Default for IncrementalConfig {
204    fn default() -> Self {
205        Self::new(".fob-cache")
206    }
207}
208
209impl std::fmt::Display for IncrementalConfig {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        write!(f, "incremental({})", self.cache_dir.display())
212    }
213}