use std::path::Path;
use rolldown::{Bundler, BundlerOptions};
use rolldown_common::Output;
use super::{Asset, Environment, LayeredContext, 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| {
if p.is_absolute() {
p.to_path_buf()
} else {
std::env::current_dir().unwrap_or_default().join(p)
}
});
let input_path = format!("./{}", file_name.to_string_lossy());
let options = BundlerOptions {
input: Some(vec![input_path.into()]),
cwd,
minify: if self.minify {
Some(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,
_env: &Environment,
_context: &LayeredContext,
asset: &mut Asset,
) -> Result<bool, ProcessingError> {
if *asset.media_type() != MediaType::JavaScript {
return Ok(false);
}
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(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proc::LayeredContext;
#[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 modified = processor
.process(
&Environment::test(),
&LayeredContext::from_flat(Default::default()),
&mut css_asset,
)
.unwrap();
assert!(!modified);
}
#[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(
&Environment::test(),
&LayeredContext::from_flat(Default::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("1.0.0"));
assert!(bundled.contains("formatMessage"));
}
}