use oxc_allocator::Allocator;
use oxc_codegen::{Codegen, CodegenOptions};
use oxc_minifier::{Minifier, MinifierOptions};
use oxc_parser::Parser;
use oxc_span::SourceType;
use super::{Asset, Environment, LayeredContext, MediaType, ProcessesAssets, ProcessingError};
pub struct MinifyJsProcessor;
impl ProcessesAssets for MinifyJsProcessor {
fn process(
&self,
_env: &Environment,
_context: &LayeredContext,
asset: &mut Asset,
) -> Result<bool, ProcessingError> {
if asset.media_type() != &MediaType::JavaScript {
return Ok(false);
}
if asset.path().ends_with(".min.js") {
return Ok(false);
}
tracing::trace!("minify_js: {}", asset.path());
let source = asset.as_text()?;
let allocator = Allocator::default();
let source_type = if asset.path().ends_with(".mjs") {
SourceType::mjs()
} else {
SourceType::unambiguous()
};
let ret = Parser::new(&allocator, source, source_type).parse();
if ret.program.is_empty() {
let error_messages: Vec<_> = ret.diagnostics.iter().map(|d| d.to_string()).collect();
return Err(ProcessingError::Compilation {
message: format!("JS parse errors: {}", error_messages.join("; ")).into(),
});
}
let mut program = ret.program;
let options = MinifierOptions::default();
Minifier::new(options).minify(&allocator, &mut program);
let output = Codegen::new()
.with_options(CodegenOptions::minify())
.build(&program);
asset.replace_with_text(output.code.into(), MediaType::JavaScript);
Ok(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proc::LayeredContext;
#[test]
fn minifies_js() {
let js = r#"
// This is a comment
function hello(name) {
/* Another comment */
console.log("Hello, " + name);
}
hello("world");
"#;
let mut asset = Asset::new("script.js".into(), js.as_bytes().to_vec());
MinifyJsProcessor
.process(
&Environment::test(),
&LayeredContext::from_flat(Default::default()),
&mut asset,
)
.unwrap();
let result = asset.as_text().unwrap();
assert!(!result.contains("// This is a comment"));
assert!(!result.contains("/* Another comment */"));
assert!(result.contains("Hello, "));
assert!(result.contains("console.log"));
assert!(result.len() < js.len());
}
#[test]
fn minifies_classic_scripts() {
let js = r#"
// A global consumed by another <script>.
let wasm_bindgen = (function(exports) {
return Object.assign(function init() {}, exports);
})({});
"#;
let mut asset = Asset::new("js/emhash.js".into(), js.as_bytes().to_vec());
MinifyJsProcessor
.process(
&Environment::test(),
&LayeredContext::from_flat(Default::default()),
&mut asset,
)
.unwrap();
let result = asset.as_text().unwrap();
assert!(result.contains("wasm_bindgen"));
assert!(!result.contains("// A global"));
}
#[test]
fn minifies_modules() {
let js = r#"
import { helper } from "./helper.js";
export function run() { helper(); }
"#;
let mut asset = Asset::new("editor.js".into(), js.as_bytes().to_vec());
MinifyJsProcessor
.process(
&Environment::test(),
&LayeredContext::from_flat(Default::default()),
&mut asset,
)
.unwrap();
let result = asset.as_text().unwrap();
assert!(result.contains("import"));
assert!(result.contains("export"));
}
#[test]
fn skips_non_js() {
let mut asset = Asset::new("index.html".into(), b"<html></html>".to_vec());
let modified = MinifyJsProcessor
.process(
&Environment::test(),
&LayeredContext::from_flat(Default::default()),
&mut asset,
)
.unwrap();
assert!(!modified);
assert_eq!(asset.as_text().unwrap(), "<html></html>");
}
#[test]
fn skips_already_minified() {
let js = "function test(){console.log('already minified')}";
let mut asset = Asset::new("vendor.min.js".into(), js.as_bytes().to_vec());
let modified = MinifyJsProcessor
.process(
&Environment::test(),
&LayeredContext::from_flat(Default::default()),
&mut asset,
)
.unwrap();
assert!(!modified);
assert_eq!(asset.as_text().unwrap(), js);
}
#[test]
fn rejects_invalid_js() {
let js = "function {{{ invalid syntax";
let mut asset = Asset::new("bad.js".into(), js.as_bytes().to_vec());
let result = MinifyJsProcessor.process(
&Environment::test(),
&LayeredContext::from_flat(Default::default()),
&mut asset,
);
assert!(result.is_err());
}
}