Skip to main content

fob_bundler/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! # fob-bundler
4//!
5//! Fob bundler - Rolldown-based bundling on top of fob foundation.
6//!
7//! This crate provides full bundling capabilities using Rolldown, building on top
8//! of the `fob` foundation crate for graph analysis and runtime abstraction.
9//!
10//! ## Quick Start
11//!
12//! ### Bundle a library
13//!
14//! ```no_run
15//! use fob_bundler as fob;
16//!
17//! # #[tokio::main]
18//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
19//! let result = fob::BuildOptions::new("./src/index.js")
20//!     .platform(fob::Platform::Node)  // Uses Node.js export conditions
21//!     .externalize(["react", "react-dom"])
22//!     .sourcemap(true)
23//!     .build()
24//!     .await?;
25//!
26//! let bundle = result.output.as_single().expect("single bundle");
27//! for asset in bundle.assets.iter() {
28//!     std::fs::write(format!("dist/{}", asset.filename()), asset.content_as_bytes())?;
29//! }
30//! # Ok(()) }
31//! ```
32//!
33//! ### Using BuildConfig (Recommended for New Code)
34//!
35//! `BuildConfig` provides a cleaner API with better type safety:
36//!
37//! ```no_run
38//! use fob_bundler::BuildConfig;
39//!
40//! # #[tokio::main]
41//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
42//! let result = BuildConfig::new("./src/index.js")
43//!     .bundle(false)
44//!     .output_dir("dist")
45//!     .build()
46//!     .await?;
47//!
48//! result.write_to("dist", true)?;
49//! # Ok(()) }
50//! ```
51//!
52//! ### Analyze without bundling
53//!
54//! ```no_run
55//! use fob_bundler as fob;
56//!
57//! # #[tokio::main]
58//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
59//! let analysis = fob_graph::analyze(["./src/index.js"]).await?;
60//! for unused in analysis.graph.unused_exports()? {
61//!     println!("unused: {} from {}", unused.export.name, unused.module_id);
62//! }
63//! # Ok(()) }
64//! ```
65
66// Re-export everything from foundation crate
67pub use fob_graph::*;
68
69// Bundler-specific modules
70pub mod builders;
71pub mod builtins;
72pub mod cache;
73pub mod config;
74pub mod output;
75pub mod plugins;
76pub mod runtime;
77pub mod target;
78
79// Bundler-specific graph modules
80pub mod from_rolldown;
81pub mod module_collection_plugin;
82
83// Bundler-specific analysis
84pub mod analysis;
85
86// Diagnostic extraction
87pub mod diagnostics;
88
89// Re-export core Rolldown types for library users
90pub use rolldown::{
91    BundleOutput, Bundler, BundlerBuilder, BundlerOptions, GlobalsOutputOption, InputItem,
92    IsExternal, OutputFormat, Platform, RawMinifyOptions, ResolveOptions, SourceMapType,
93};
94
95// Re-export common types (CRITICAL: ModuleType for plugins)
96pub use rolldown_common::{
97    BundlerTransformOptions, DecoratorOptions, EmittedAsset, IsolatedDeclarationsOptions,
98    LogWithoutPlugin, ModuleType, Output, OutputAsset, OutputChunk, TypeScriptOptions,
99};
100
101// Re-export plugin types (CRITICAL for plugin authors)
102pub use rolldown_plugin::{
103    __inner::SharedPluginable, HookGenerateBundleArgs, HookLoadArgs, HookLoadOutput,
104    HookLoadReturn, HookNoopReturn, HookResolveIdArgs, HookResolveIdOutput, HookResolveIdReturn,
105    HookTransformArgs, HookTransformOutput, HookTransformReturn, HookUsage, Plugin, PluginContext,
106    SharedTransformPluginContext,
107};
108
109// Re-export bundler APIs
110pub use builders::{
111    BuildOptions, BuildOutput, BuildResult, CodeSplittingConfig, EntryMode, EntryPoints,
112    ExternalConfig, IncrementalConfig, MinifyLevel, build,
113};
114pub use builtins::CssPlugin;
115pub use cache::{CacheConfig, CacheKey};
116pub use config::{
117    BuildConfig, ExternalPattern, OptimizationConfig, OutputConfig, ResolutionConfig,
118};
119
120#[cfg(feature = "dts-generation")]
121pub use builders::DtsOptions;
122
123// Logging utilities (optional, enabled with "logging" feature)
124#[cfg(feature = "logging")]
125#[cfg_attr(docsrs, doc(cfg(feature = "logging")))]
126pub mod logging;
127
128#[cfg(feature = "logging")]
129#[cfg_attr(docsrs, doc(cfg(feature = "logging")))]
130pub use logging::{LogLevel, init_logging, init_logging_from_env};
131
132pub use output::{AppBuild, Bundle as JoyBundle, ComponentBuild, ImportMap};
133pub use target::{DeploymentTarget, ExportConditions, NodeBuiltins, RuntimeEnvironment};
134
135// Re-export AnalyzedBundle (bundler-specific analysis result)
136pub use analysis::AnalyzedBundle;
137
138// Test utilities (available in test builds for both unit and integration tests)
139// Re-export from fob foundation crate
140#[cfg(all(any(test, doctest), not(target_family = "wasm")))]
141pub use fob_graph::test_utils;
142
143/// Error types for fob-bundler operations.
144#[derive(Debug, thiserror::Error)]
145pub enum Error {
146    /// Error from Rolldown bundler.
147    #[error("Rolldown bundler error: {}", format_bundler_error(.0))]
148    Bundler(Vec<diagnostics::ExtractedDiagnostic>),
149
150    /// Invalid configuration provided.
151    #[error("Invalid configuration: {0}")]
152    InvalidConfig(String),
153
154    /// I/O error.
155    #[error("I/O error: {0}")]
156    Io(#[from] std::io::Error),
157
158    /// Invalid output path (e.g., directory traversal attempt).
159    #[error("Invalid output path: {0}")]
160    InvalidOutputPath(String),
161
162    /// File write operation failed.
163    #[error("Write failure: {0}")]
164    WriteFailure(String),
165
166    /// Output file already exists and overwrite is disabled.
167    #[error("Output exists: {0}")]
168    OutputExists(String),
169
170    /// Asset not found during resolution.
171    #[error("Asset not found: {specifier} (searched from: {searched_from})")]
172    AssetNotFound {
173        specifier: String,
174        searched_from: String,
175    },
176
177    /// Asset security violation (e.g., directory traversal attempt).
178    #[error("Asset security violation: {path} - {reason}")]
179    AssetSecurityViolation { path: String, reason: String },
180
181    /// Asset file is too large.
182    #[error("Asset too large: {path} ({size} bytes exceeds limit of {max_size} bytes)")]
183    AssetTooLarge {
184        path: String,
185        size: u64,
186        max_size: u64,
187    },
188
189    /// I/O error with context message.
190    #[error("{message}")]
191    IoError {
192        message: String,
193        #[source]
194        source: std::io::Error,
195    },
196
197    /// Error from foundation crate.
198    #[error("Foundation error: {0}")]
199    Foundation(#[from] fob_graph::Error),
200}
201
202/// Result type alias for fob-bundler operations.
203pub type Result<T> = std::result::Result<T, Error>;
204
205impl Error {
206    /// Create a bundler error from a Rolldown error.
207    ///
208    /// Extracts structured diagnostics from Rolldown's error types.
209    pub fn from_rolldown_batch(error: &dyn std::fmt::Debug) -> Self {
210        Error::Bundler(diagnostics::extract_from_rolldown_error(error))
211    }
212}
213
214/// Format bundler error diagnostics for display.
215fn format_bundler_error(diagnostics: &[diagnostics::ExtractedDiagnostic]) -> String {
216    if diagnostics.is_empty() {
217        return "Unknown bundler error".to_string();
218    }
219
220    if diagnostics.len() == 1 {
221        let diag = &diagnostics[0];
222        format!("{}: {}", diag.kind, diag.message)
223    } else {
224        format!(
225            "{} errors: {}",
226            diagnostics.len(),
227            diagnostics
228                .iter()
229                .map(|d| format!("{}: {}", d.kind, d.message))
230                .collect::<Vec<_>>()
231                .join("; ")
232        )
233    }
234}
235
236impl std::fmt::Display for diagnostics::DiagnosticKind {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        match self {
239            diagnostics::DiagnosticKind::MissingExport => write!(f, "MissingExport"),
240            diagnostics::DiagnosticKind::ParseError => write!(f, "ParseError"),
241            diagnostics::DiagnosticKind::CircularDependency => write!(f, "CircularDependency"),
242            diagnostics::DiagnosticKind::UnresolvedEntry => write!(f, "UnresolvedEntry"),
243            diagnostics::DiagnosticKind::UnresolvedImport => write!(f, "UnresolvedImport"),
244            diagnostics::DiagnosticKind::InvalidOption => write!(f, "InvalidOption"),
245            diagnostics::DiagnosticKind::Plugin => write!(f, "Plugin"),
246            diagnostics::DiagnosticKind::Transform => write!(f, "Transform"),
247            diagnostics::DiagnosticKind::Other(s) => write!(f, "{}", s),
248        }
249    }
250}
251
252impl miette::Diagnostic for Error {
253    fn code(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
254        Some(Box::new(match self {
255            Error::Bundler(_) => "BUNDLER_ERROR",
256            Error::InvalidConfig(_) => "INVALID_CONFIG",
257            Error::Io(_) => "IO_ERROR",
258            Error::InvalidOutputPath(_) => "INVALID_OUTPUT_PATH",
259            Error::WriteFailure(_) => "WRITE_FAILURE",
260            Error::OutputExists(_) => "OUTPUT_EXISTS",
261            Error::AssetNotFound { .. } => "ASSET_NOT_FOUND",
262            Error::AssetSecurityViolation { .. } => "ASSET_SECURITY_VIOLATION",
263            Error::AssetTooLarge { .. } => "ASSET_TOO_LARGE",
264            Error::IoError { .. } => "IO_ERROR",
265            Error::Foundation(_) => "FOUNDATION_ERROR",
266        }))
267    }
268
269    fn severity(&self) -> Option<miette::Severity> {
270        Some(miette::Severity::Error)
271    }
272
273    fn help(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
274        match self {
275            Error::InvalidConfig(msg) => Some(Box::new(format!(
276                "Check your configuration file for syntax errors.\nError: {}",
277                msg
278            ))),
279            Error::InvalidOutputPath(path) => Some(Box::new(format!(
280                "The output path '{}' is invalid. Ensure it's within the project directory and doesn't contain '..' components.",
281                path
282            ))),
283            Error::WriteFailure(msg) => Some(Box::new(format!(
284                "Failed to write file. Check disk space and permissions.\nError: {}",
285                msg
286            ))),
287            Error::OutputExists(msg) => Some(Box::new(format!(
288                "Output file already exists: {}\nUse --overwrite flag to replace existing files.",
289                msg
290            ))),
291            Error::AssetNotFound {
292                specifier,
293                searched_from,
294            } => Some(Box::new(format!(
295                "Could not find asset '{}'.\nSearched from: {}\nCheck that the file exists and the path is correct.",
296                specifier, searched_from
297            ))),
298            Error::AssetSecurityViolation { path, reason } => Some(Box::new(format!(
299                "Security violation detected for path '{}': {}\nPaths must be within the project directory.",
300                path, reason
301            ))),
302            Error::AssetTooLarge {
303                path,
304                size,
305                max_size,
306            } => Some(Box::new(format!(
307                "Asset '{}' is too large: {} bytes (max: {} bytes).\nConsider splitting large assets or increasing the size limit.",
308                path, size, max_size
309            ))),
310            Error::Bundler(diagnostics) => {
311                if diagnostics.len() == 1 {
312                    diagnostics[0]
313                        .help
314                        .as_ref()
315                        .map(|h| Box::new(h.clone()) as Box<dyn std::fmt::Display>)
316                } else {
317                    Some(Box::new(
318                        "Multiple bundler errors occurred. See details below.".to_string(),
319                    ))
320                }
321            }
322            _ => None,
323        }
324    }
325
326    fn related(&self) -> Option<Box<dyn Iterator<Item = &dyn miette::Diagnostic> + '_>> {
327        match self {
328            Error::Bundler(diagnostics) if diagnostics.len() > 1 => {
329                // Return all diagnostics as related errors
330                // This requires converting each diagnostic, which is complex
331                // For now, return None - the main error message will show the count
332                None
333            }
334            _ => None,
335        }
336    }
337}