use crate::config::{BundleConfig, ProjectConfig};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("nothing to bundle: set bundle.js / bundle.css in project.cfg or pass --js / --css")]
NothingToBundle,
#[cfg(not(feature = "bundler"))]
#[error("this build has no embedded bundler; rebuild with `--features bundler`")]
NoBundler,
#[cfg(feature = "bundler")]
#[error("rolldown init: {0}")]
RolldownInit(String),
#[cfg(feature = "bundler")]
#[error("rolldown bundle failed: {0}")]
RolldownBundle(String),
#[cfg(feature = "bundler")]
#[error("css bundle {0}: {1}")]
CssBundle(String, String),
#[cfg(feature = "bundler")]
#[error("css minify {0}: {1}")]
CssMinify(String, String),
#[cfg(feature = "bundler")]
#[error("css print {0}: {1}")]
CssPrint(String, String),
#[cfg(feature = "bundler")]
#[error(transparent)]
Io(#[from] std::io::Error),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, serde::Serialize)]
pub struct BundleOutput {
#[serde(skip_serializing_if = "Option::is_none")]
js: Option<JsBundled>,
#[serde(skip_serializing_if = "Vec::is_empty")]
css: Vec<CssBundled>,
}
#[derive(Debug, serde::Serialize)]
struct JsBundled {
entries: usize,
outdir: String,
}
#[derive(Debug, serde::Serialize)]
struct CssBundled {
entry: String,
dest: String,
bytes: usize,
}
impl crate::output::Rendered for BundleOutput {
fn render_human(&self, w: &mut dyn std::io::Write) -> std::io::Result<()> {
if let Some(js) = &self.js {
writeln!(
w,
"bundled {} JS entry point(s) -> {}",
js.entries, js.outdir
)?;
}
for c in &self.css {
writeln!(
w,
"bundled CSS {} -> {} ({} bytes)",
c.entry, c.dest, c.bytes
)?;
}
Ok(())
}
}
#[derive(Debug, clap::Args)]
pub struct BundleArgs {
#[arg(long)]
outdir: Option<String>,
#[arg(long = "js")]
js: Vec<String>,
#[arg(long = "css")]
css: Vec<String>,
#[arg(long)]
no_minify: bool,
}
impl BundleArgs {
fn resolve(&self, config: &ProjectConfig) -> BundleConfig {
let base = config.bundle.clone().unwrap_or_default();
BundleConfig {
outdir: self.outdir.clone().unwrap_or(base.outdir),
js: if self.js.is_empty() {
base.js
} else {
self.js.clone()
},
css: if self.css.is_empty() {
base.css
} else {
self.css.clone()
},
minify: base.minify && !self.no_minify,
}
}
}
pub async fn run(args: BundleArgs, config: &ProjectConfig) -> Result<BundleOutput> {
let bundle = args.resolve(config);
if bundle.js.is_empty() && bundle.css.is_empty() {
return Err(Error::NothingToBundle);
}
run_bundle(&bundle).await
}
#[cfg(feature = "bundler")]
async fn run_bundle(bundle: &BundleConfig) -> Result<BundleOutput> {
let outdir = std::path::PathBuf::from(&bundle.outdir);
std::fs::create_dir_all(&outdir)?;
let mut out = BundleOutput {
js: None,
css: Vec::new(),
};
if !bundle.js.is_empty() {
bundle_js(&bundle.js, &bundle.outdir, bundle.minify).await?;
out.js = Some(JsBundled {
entries: bundle.js.len(),
outdir: bundle.outdir.clone(),
});
}
for entry in &bundle.css {
let css = bundle_css(entry, bundle.minify)?;
let name = std::path::Path::new(entry)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "bundle.css".to_string());
let dest = outdir.join(&name);
std::fs::write(&dest, css.as_bytes())?;
out.css.push(CssBundled {
entry: entry.clone(),
dest: dest.display().to_string(),
bytes: css.len(),
});
}
Ok(out)
}
#[cfg(feature = "bundler")]
async fn bundle_js(entries: &[String], outdir: &str, minify: bool) -> Result<()> {
use rolldown::{Bundler, BundlerOptions, InputItem, OutputFormat, RawMinifyOptions};
let options = BundlerOptions {
input: Some(
entries
.iter()
.map(|import| InputItem {
name: None,
import: import.clone(),
})
.collect(),
),
dir: Some(outdir.to_string()),
format: Some(OutputFormat::Esm),
minify: Some(RawMinifyOptions::Bool(minify)),
..Default::default()
};
let mut bundler =
Bundler::new(options).map_err(|err| Error::RolldownInit(format!("{err:?}")))?;
bundler
.write()
.await
.map_err(|err| Error::RolldownBundle(format!("{err:?}")))?;
Ok(())
}
#[cfg(feature = "bundler")]
fn bundle_css(entry: &str, minify: bool) -> Result<String> {
use lightningcss::bundler::{Bundler, FileProvider};
use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions};
let fs = FileProvider::new();
let mut bundler = Bundler::new(&fs, None, ParserOptions::default());
let mut stylesheet = bundler
.bundle(std::path::Path::new(entry))
.map_err(|err| Error::CssBundle(entry.to_string(), format!("{err:?}")))?;
if minify {
stylesheet
.minify(MinifyOptions::default())
.map_err(|err| Error::CssMinify(entry.to_string(), format!("{err:?}")))?;
}
let result = stylesheet
.to_css(PrinterOptions {
minify,
..Default::default()
})
.map_err(|err| Error::CssPrint(entry.to_string(), format!("{err:?}")))?;
Ok(result.code)
}
#[cfg(not(feature = "bundler"))]
async fn run_bundle(_bundle: &BundleConfig) -> Result<BundleOutput> {
Err(Error::NoBundler)
}