use std::{
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::{Context, Result};
use clap::Args;
use rayon::prelude::*;
use swc_common::{
errors::{ColorConfig, Handler, HANDLER},
SourceFile, SourceMap, GLOBALS,
};
use tracing::info;
use crate::util::{
all_js_files, gzipped_size,
minifier::{get_esbuild_output, get_minified, get_terser_output},
print_js, wrap_task,
};
#[derive(Debug, Args)]
pub struct EnsureSize {
#[clap(long)]
pub no_terser: bool,
#[clap(long)]
pub no_esbuild: bool,
pub path: PathBuf,
}
impl EnsureSize {
pub fn run(self, cm: Arc<SourceMap>) -> Result<()> {
let all_files = all_js_files(&self.path)?;
info!("Using {} files", all_files.len());
let mut results = GLOBALS.with(|globals| {
all_files
.par_iter()
.map(|js_file| GLOBALS.set(globals, || self.check_file(cm.clone(), js_file)))
.filter_map(|v| v.transpose())
.collect::<Result<Vec<_>>>()
})?;
results.sort_by_key(|f| {
if let Some(terser) = &f.terser {
f.swc.mangled_size as isize - terser.mangled_size as isize
} else {
0
}
});
for f in &results {
if let Some(terser) = &f.terser {
if f.swc.mangled_size > terser.mangled_size
|| f.swc.no_mangle_size > terser.no_mangle_size
|| f.swc.gzipped_size > terser.gzipped_size
{
println!();
println!("{}", f.fm.name);
}
if f.swc.gzipped_size > terser.gzipped_size {
println!(" Gzipped");
println!(" swc: {} bytes", f.swc.gzipped_size);
println!(" terser: {} bytes", terser.gzipped_size);
}
if f.swc.mangled_size > terser.mangled_size {
println!(" Mangled");
println!(" swc: {} bytes", f.swc.mangled_size);
println!(" terser: {} bytes", terser.mangled_size);
}
if f.swc.no_mangle_size > terser.no_mangle_size {
println!(" No-mangle");
println!(" swc: {} bytes", f.swc.no_mangle_size);
println!(" terser: {} bytes", terser.no_mangle_size);
}
}
}
{
let swc_total = results.iter().map(|f| f.swc.mangled_size).sum::<usize>();
let terser_total = results
.iter()
.flat_map(|f| f.terser.map(|v| v.mangled_size))
.sum::<usize>();
println!("Total");
println!(" swc: {} bytes", swc_total);
println!(" terser: {} bytes", terser_total);
println!(" Size ratio: {}", swc_total as f64 / terser_total as f64);
let swc_smaller_file_count = results
.iter()
.filter(|f| {
if let Some(terser) = &f.terser {
f.swc.mangled_size <= terser.mangled_size
} else {
false
}
})
.count();
println!(
"swc produced smaller or equal output for {} files out of {} files, {:.2}%",
swc_smaller_file_count,
all_files.len(),
100.0 * swc_smaller_file_count as f64 / results.len() as f64
);
}
{
let swc_total = results.iter().map(|f| f.swc.gzipped_size).sum::<usize>();
let terser_total = results
.iter()
.flat_map(|f| f.terser.map(|v| v.gzipped_size))
.sum::<usize>();
println!("Total (gzipped)");
println!(" swc: {} bytes", swc_total);
println!(" terser: {} bytes", terser_total);
println!(" Size ratio: {}", swc_total as f64 / terser_total as f64);
let swc_smaller_file_count = results
.iter()
.filter(|f| {
if let Some(terser) = &f.terser {
f.swc.gzipped_size <= terser.gzipped_size
} else {
false
}
})
.count();
println!(
"swc produced smaller or equal output for {} files out of {} files, {:.2}%",
swc_smaller_file_count,
all_files.len(),
100.0 * swc_smaller_file_count as f64 / results.len() as f64
);
}
Ok(())
}
fn check_file(&self, cm: Arc<SourceMap>, js_file: &Path) -> Result<Option<FileSize>> {
wrap_task(|| {
info!("Checking {}", js_file.display());
let fm = cm.load_file(js_file).context("failed to load file")?;
let handler =
Handler::with_tty_emitter(ColorConfig::Never, true, false, Some(cm.clone()));
HANDLER.set(&handler, || {
let code_mangled = {
let minified_mangled = get_minified(cm.clone(), js_file, true, true)?;
print_js(cm.clone(), &minified_mangled.module, true)
.context("failed to convert ast to code")?
};
let swc_no_mangle = {
let minified_no_mangled = get_minified(cm.clone(), js_file, true, false)?;
print_js(cm, &minified_no_mangled.module, true)
.context("failed to convert ast to code")?
};
let mut file_size = FileSize {
fm,
swc: MinifierOutput {
mangled_size: code_mangled.len(),
no_mangle_size: swc_no_mangle.len(),
gzipped_size: gzipped_size(&code_mangled),
},
terser: Default::default(),
esbuild: Default::default(),
};
if !self.no_terser {
let terser_mangled = get_terser_output(js_file, true, true)?;
let terser_no_mangle = get_terser_output(js_file, true, false)?;
file_size.terser = Some(MinifierOutput {
mangled_size: terser_mangled.len(),
no_mangle_size: terser_no_mangle.len(),
gzipped_size: gzipped_size(&terser_mangled),
});
}
if !self.no_esbuild {
let esbuild_mangled = get_esbuild_output(js_file, true)?;
let esbuild_no_mangle = get_esbuild_output(js_file, false)?;
file_size.esbuild = Some(MinifierOutput {
mangled_size: esbuild_mangled.len(),
no_mangle_size: esbuild_no_mangle.len(),
gzipped_size: gzipped_size(&esbuild_mangled),
});
}
if file_size.terser.is_none() && file_size.esbuild.is_none() {
return Ok(None);
}
Ok(Some(file_size))
})
})
.with_context(|| format!("failed to check file: {}", js_file.display()))
}
}
#[allow(unused)]
#[derive(Debug)]
struct FileSize {
fm: Arc<SourceFile>,
swc: MinifierOutput,
terser: Option<MinifierOutput>,
esbuild: Option<MinifierOutput>,
}
#[allow(unused)]
#[derive(Debug, Clone, Copy)]
struct MinifierOutput {
mangled_size: usize,
no_mangle_size: usize,
gzipped_size: usize,
}