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}