use swc_core::common::comments::{Comments, SingleThreadedComments};
use swc_core::common::{FileName, Globals, Mark, SourceMap, GLOBALS, sync::Lrc};
use swc_core::ecma::ast::{Callee, EsVersion, Expr, Function, Program, Script, Stmt, UnaryOp};
use swc_core::ecma::codegen::{Config, Emitter, text_writer::JsWriter};
use swc_core::ecma::minifier::optimize as swc_optimize;
use swc_core::ecma::minifier::option::{CompressOptions, ExtraOptions, MinifyOptions};
use swc_core::ecma::parser::{EsSyntax, Parser, StringInput, Syntax, lexer::Lexer};
use swc_core::ecma::transforms::base::fixer::{fixer, paren_remover};
use swc_core::ecma::transforms::base::hygiene::hygiene;
use swc_core::ecma::transforms::base::resolver;
pub fn optimize(text: &str) -> Option<String> {
if std::env::var_os("JSCD_NO_OPT").is_some() {
return None;
}
if text.trim().is_empty() {
return None;
}
let src = text.to_string();
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Anon.into(), src);
let comments = SingleThreadedComments::default();
let lexer = Lexer::new(
Syntax::Es(EsSyntax::default()),
EsVersion::EsNext,
StringInput::from(&*fm),
Some(&comments),
);
let mut parser = Parser::new_from(lexer);
GLOBALS.set(&Globals::new(), || {
let script = parser.parse_script().ok()?;
if !parser.take_errors().is_empty() {
return None;
}
let mut program = Program::Script(script);
let unresolved_mark = Mark::new();
let top_level_mark = Mark::new();
program.mutate(&mut paren_remover(Some(&comments as &dyn Comments)));
program.mutate(&mut resolver(unresolved_mark, top_level_mark, false));
let options = MinifyOptions {
compress: Some(compress_options()),
mangle: None,
..Default::default()
};
let extra = ExtraOptions {
unresolved_mark,
top_level_mark,
mangle_name_cache: None,
};
let mut program =
swc_optimize(program, cm.clone(), Some(&comments), None, &options, &extra);
program.mutate(&mut hygiene());
program.mutate(&mut fixer(None));
let body = unwrap_program(program)?;
let code = print_stmts(&cm, body)?;
if code.trim().is_empty() {
return None;
}
let check_fm = cm.new_source_file(FileName::Anon.into(), code.clone());
let check_lexer = Lexer::new(
Syntax::Es(EsSyntax::default()),
EsVersion::EsNext,
StringInput::from(&*check_fm),
None,
);
let mut check = Parser::new_from(check_lexer);
check.parse_script().ok()?;
if !check.take_errors().is_empty() {
return None;
}
Some(code)
})
}
fn compress_options() -> CompressOptions {
CompressOptions {
reduce_vars: true, collapse_vars: true,
unused: true,
inline: 3,
arrows: false, negate_iife: false, join_vars: false, sequences: 0, keep_fnames: true,
keep_classnames: true,
keep_fargs: true, keep_infinity: true, drop_console: false,
..Default::default()
}
}
fn unwrap_program(program: Program) -> Option<Vec<Stmt>> {
let Program::Script(script) = program else {
return None;
};
let mut body = script.body;
if body.len() == 1 {
match body.into_iter().next()? {
Stmt::Expr(mut expr_stmt) => {
if let Some(func) = find_wrapper(&mut expr_stmt.expr) {
if let Some(fb) = func.body.as_mut() {
return Some(std::mem::take(&mut fb.stmts));
}
}
body = vec![Stmt::Expr(expr_stmt)];
}
other => body = vec![other],
}
}
Some(body)
}
fn find_wrapper(expr: &mut Expr) -> Option<&mut Function> {
match expr {
Expr::Paren(p) => find_wrapper(&mut p.expr),
Expr::Unary(u) if u.op == UnaryOp::Bang => find_wrapper(&mut u.arg),
Expr::Call(call) => match &mut call.callee {
Callee::Expr(callee) => find_wrapper(callee),
_ => None,
},
Expr::Fn(f) => Some(&mut f.function),
_ => None,
}
}
fn print_stmts(cm: &Lrc<SourceMap>, body: Vec<Stmt>) -> Option<String> {
let script = Script {
span: Default::default(),
body,
shebang: None,
};
let mut buf = Vec::new();
{
let wr = JsWriter::new(cm.clone(), "\n", &mut buf, None);
let mut emitter = Emitter {
cfg: Config::default().with_minify(false),
cm: cm.clone(),
comments: None,
wr,
};
emitter.emit_script(&script).ok()?;
}
String::from_utf8(buf).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collapses_single_use_copies() {
let src = "let r0, r1, r2, r3;\nr0 = greet;\nr2 = console;\nr1 = r2.log;\nr3 = r0(\"world\");\nr2.log(r3);\nfunction greet(a0) {\n return \"hello \" + a0;\n}\n";
let text = crate::opt::optimize(src);
let out = optimize(&text).unwrap_or(text);
assert!(out.contains("console.log(greet(\"world\"))"), "{out}");
assert!(out.contains("function greet"), "函数本体要留着:{out}");
}
#[test]
fn keeps_unreferenced_top_level_declarations() {
let src = "someCall();\nfunction target(a0) { return a0; }\nvar Counter;\nCounter = target;\n";
let out = optimize(src).expect("parse+optimize");
assert!(out.contains("function target"), "顶层函数声明不许丢:{out}");
assert!(out.contains("Counter = target"), "顶层赋值不许丢:{out}");
assert!(out.contains("someCall()"), "{out}");
}
#[test]
fn keeps_registers_with_multiple_uses() {
let src = "let r0 = greet;\nuse(r0);\nuse(r0);\nfunction greet(a0) { return a0; }\n";
let out = optimize(src).expect("parse+optimize");
assert!(out.contains("r0"), "多次读的寄存器不许折叠掉:{out}");
}
#[test]
fn returns_none_on_unparseable_input() {
assert!(optimize("let r0 = ;;;").is_none());
}
#[test]
fn returns_none_on_empty_input() {
assert!(optimize("").is_none());
}
#[test]
fn does_not_rename() {
let src = "let r0 = 1;\nlet r1 = r0 + 1;\nexports.x = r1;\n";
let out = optimize(src).expect("parse+optimize");
assert!(out.contains("exports.x"), "全局引用不许改名:{out}");
}
}