use std::path::Path;
use brk_rolldown::{Bundler, BundlerOptions};
use brk_rolldown_common::Output;
use super::{Asset, Context, MediaType, ProcessesAssets, ProcessingError};
pub struct JsBundleProcessor {
minify: bool,
}
impl JsBundleProcessor {
pub fn new(minify: bool) -> Self {
Self { minify }
}
}
impl JsBundleProcessor {
fn bundle_js(&self, entry_path: &Path) -> Result<String, ProcessingError> {
let file_name = entry_path
.file_name()
.ok_or_else(|| ProcessingError::Compilation {
message: format!(
"Invalid entry path '{}': must be a file path, not a directory or root",
entry_path.display()
)
.into(),
})?;
let cwd = entry_path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(|p| p.to_path_buf());
let input_path = format!("./{}", file_name.to_string_lossy());
let options = BundlerOptions {
input: Some(vec![input_path.into()]),
cwd,
minify: if self.minify {
Some(brk_rolldown::RawMinifyOptions::Bool(true))
} else {
None
},
..Default::default()
};
let rt = tokio::runtime::Runtime::new().map_err(|e| ProcessingError::Compilation {
message: format!("Failed to create async runtime: {}", e).into(),
})?;
rt.block_on(async {
let mut bundler = Bundler::new(options).map_err(|e| ProcessingError::Compilation {
message: format!("Failed to create bundler: {:?}", e).into(),
})?;
let output = bundler
.generate()
.await
.map_err(|e| ProcessingError::Compilation {
message: format!("Bundling failed: {:?}", e).into(),
})?;
for asset in output.assets {
if let Output::Chunk(chunk) = asset {
return Ok(chunk.code.clone());
}
}
Err(ProcessingError::Compilation {
message: "Bundling produced no output chunks".into(),
})
})
}
}
impl ProcessesAssets for JsBundleProcessor {
fn process(&self, _context: &mut Context, asset: &mut Asset) -> Result<(), ProcessingError> {
if *asset.media_type() != MediaType::JavaScript {
return Ok(());
}
tracing::trace!("js_bundle: {}", asset.path());
let entry_path_str = asset.path().clone();
let entry_path = Path::new(entry_path_str.as_str());
let bundled_code = self.bundle_js(entry_path)?;
asset.replace_with_text(bundled_code.into(), MediaType::JavaScript);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skips_non_javascript_assets() {
let processor = JsBundleProcessor { minify: false };
let mut css_asset = Asset::new("style.css".into(), "body {}".as_bytes().to_vec());
let result = processor.process(&mut Context::default(), &mut css_asset);
assert!(result.is_ok());
}
#[test]
fn bundles_javascript() {
let processor = JsBundleProcessor { minify: false };
let mut js_asset = Asset::new("test/js_bundle/entry.js".into(), "".as_bytes().to_vec());
let result = processor.process(&mut Context::default(), &mut js_asset);
assert!(result.is_ok());
let bundled = js_asset.as_text().unwrap();
assert!(bundled.contains("Hello from bundled JavaScript!"));
assert!(bundled.contains("greet"));
assert!(bundled.contains("HELPER_VERSION"));
assert!(bundled.contains("formatMessage"));
}
}