use camino::Utf8Path;
use glob::Pattern;
use rolldown::{BundlerOptions, CodeSplittingMode, InputItem, RawMinifyOptions};
use thiserror::Error;
use crate::core::Hash32;
use crate::{Blueprint, engine::Many, error::HauchiwaError, loader::GlobBundle};
#[derive(Debug, Error)]
pub enum ScriptError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Rolldown execution failed: {0}")]
Rolldown(String),
#[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("Build error: {0}")]
Build(#[from] crate::error::BuildError),
}
pub struct ScriptLoader<'a, G>
where
G: Send + Sync,
{
blueprint: &'a mut Blueprint<G>,
entry_globs: Vec<String>,
entry_patterns: Vec<Pattern>,
watch_globs: Vec<Pattern>,
bundle: bool,
minify: bool,
externals: Vec<String>,
}
impl<'a, G> ScriptLoader<'a, G>
where
G: Send + Sync + 'static,
{
pub(crate) fn new(blueprint: &'a mut Blueprint<G>) -> Self {
Self {
blueprint,
entry_globs: Vec::new(),
entry_patterns: Vec::new(),
watch_globs: Vec::new(),
bundle: true,
minify: true,
externals: Vec::new(),
}
}
pub fn entry(mut self, glob: impl Into<String>) -> Result<Self, HauchiwaError> {
let glob = glob.into();
let pattern = Pattern::new(&glob)?;
self.entry_globs.push(glob);
self.entry_patterns.push(pattern);
Ok(self)
}
pub fn watch(mut self, glob: impl Into<String>) -> Result<Self, HauchiwaError> {
let glob = glob.into();
let pattern = Pattern::new(&glob)?;
self.watch_globs.push(pattern);
Ok(self)
}
pub fn bundle(mut self, bundle: bool) -> Self {
self.bundle = bundle;
self
}
pub fn minify(mut self, minify: bool) -> Self {
self.minify = minify;
self
}
pub fn external(mut self, package: impl Into<String>) -> Self {
self.externals.push(package.into());
self
}
pub fn register(self) -> Many<super::Script> {
let watch_globs = if self.watch_globs.is_empty() {
self.entry_patterns
} else {
self.watch_globs
};
let bundle = self.bundle;
let minify = self.minify;
let externals = self.externals;
let externals_clone = externals.clone();
let task = GlobBundle::new(self.entry_globs, watch_globs, move |_, store, input| {
let data = compile_rolldown(&input.path, bundle, minify, Some(externals_clone.clone()))?;
let hash = Hash32::hash(&data);
let path = store.save(&data, "js").map_err(ScriptError::Build)?;
Ok((hash, input.path, super::Script { path }))
})
.pre_run(move |_, store| {
for package in &externals {
let data = bundle_package_rolldown(&store.cache_dir, package, minify)?;
let path = store.save(&data, "js").map_err(ScriptError::Build)?;
store.register(package.as_str(), path.as_str());
}
Ok(())
});
self.blueprint.add_task_fine(task)
}
}
impl<G> Blueprint<G>
where
G: Send + Sync + 'static,
{
pub fn load_rolldown(&mut self) -> ScriptLoader<'_, G> {
ScriptLoader::new(self)
}
}
fn bundle_package_rolldown(cache_dir: &Utf8Path, package: &str, minify: bool) -> Result<Vec<u8>, ScriptError> {
let temp_dir = cache_dir.join("tmp");
std::fs::create_dir_all(&temp_dir)?;
let hash = Hash32::hash(package.as_bytes()).to_hex();
let temp_file_path = temp_dir.join(format!("rolldown_temp_{hash}.js"));
std::fs::write(&temp_file_path, format!("export * from '{package}';\n"))?;
let res = compile_rolldown(&temp_file_path, true, minify, None);
let _ = std::fs::remove_file(&temp_file_path);
res
}
fn compile_rolldown(
file: &Utf8Path,
_bundle: bool,
minify: bool,
externals: Option<Vec<String>>,
) -> Result<Vec<u8>, ScriptError> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
rt.block_on(async {
let external = externals.map(rolldown::IsExternal::from);
let file_str = file.as_str();
let import_path =
if file.is_absolute() || file_str.starts_with("./") || file_str.starts_with("../") {
file_str.to_string()
} else {
format!("./{file_str}")
};
let options = BundlerOptions {
input: Some(vec![InputItem {
name: file.file_stem().map(String::from),
import: import_path,
}]),
minify: Some(RawMinifyOptions::Bool(minify)),
code_splitting: Some(CodeSplittingMode::Bool(false)),
external,
..Default::default()
};
let mut bundler =
rolldown::Bundler::new(options).map_err(|e| ScriptError::Rolldown(e.to_string()))?;
let output = bundler
.generate()
.await
.map_err(|e| ScriptError::Rolldown(e.to_string()))?;
if let Some(chunk) = output.assets.into_iter().next() {
return Ok(chunk.content_as_bytes().to_vec());
}
Err(ScriptError::Rolldown("No output chunks generated".into()))
})
}