Use minify-js as JS minifier

This commit is contained in:
Wilson Lin 2022-06-21 17:29:12 +10:00
parent fd60983516
commit c7d0652fbc
18 changed files with 82 additions and 171 deletions

View File

@ -13,7 +13,7 @@ A Rust HTML minifier meticulously optimised for speed and effectiveness, with bi
- Advanced minification strategy beats other minifiers while being much faster.
- Uses SIMD searching, direct tries, and lookup tables.
- Handles [invalid HTML](./notes/Parsing.md), with extensive testing and [fuzzing](./fuzz).
- Natively binds to [esbuild](https://github.com/wilsonzlin/esbuild-rs) for super fast JS and CSS minification.
- Uses [minify-js](https://github.com/wilsonzlin/minify-js) for super fast JS minification.
## Performance
@ -55,13 +55,9 @@ minify-html --output /path/to/output.min.html --keep-closing-tags --minify-css /
```toml
[dependencies]
minify-html = { version = "0.8.1", features = ["js-esbuild"] }
minify-html = { version = "0.8.1" }
```
Building with the `js-esbuild` feature requires the Go compiler to be installed as well, to build the [JS and CSS minifier](https://github.com/wilsonzlin/esbuild-rs).
If the `js-esbuild` feature is not enabled, `cfg.minify_js` and `cfg.minify_css` will have no effect.
### Use
Check out the [docs](https://docs.rs/minify-html) for API and usage examples.
@ -190,7 +186,11 @@ All [`Cfg` fields](https://docs.rs/minify-html/latest/minify_html/struct.Cfg.htm
## Minification
Note that some of the minification done can result in HTML that will not pass validation, but remain interpreted and rendered correctly by the browser; essentially, the laxness of the browser is taken advantage of for better minification. These can be turned off via the `Cfg` object.
Note that some of the minification done can result in HTML that will not pass validation, but remain interpreted and rendered correctly by the browser; essentially, the laxness of the browser is taken advantage of for better minification. To prevent this, refer to these configuration options:
- `do_not_minify_doctype`
- `ensure_spec_compliant_unquoted_attribute_values`
- `keep_spaces_between_attributes`
### Whitespace

View File

@ -6,7 +6,7 @@ authors = ["Wilson Lin <code@wilsonl.in>"]
edition = "2018"
[dependencies]
minify-html = { path = "../../../rust/main", features = ["js-esbuild"] }
minify-html = { path = "../../../rust/main" }
serde = { version = "1.0.104", features = ["derive"] }
serde_json = "1.0.44"

View File

@ -7,5 +7,5 @@ authors = ["Wilson Lin <code@wilsonl.in>"]
edition = "2018"
[dependencies]
minify-html = { path = "../rust/main", features = ["js-esbuild"] }
minify-html = { path = "../rust/main" }
structopt = "0.3"

View File

@ -6,7 +6,7 @@ authors = ["Wilson Lin <code@wilsonl.in>"]
edition = "2018"
[dependencies]
minify-html = { path = "../rust/main", features = ["js-esbuild"] }
minify-html = { path = "../rust/main" }
jni = "0.14.0"
[lib]

4
nodejs/index.d.ts vendored
View File

@ -21,11 +21,11 @@ export function createConfiguration (options: {
/** Keep all comments. */
keep_comments?: boolean;
/**
* If enabled, content in `<script>` tags with a JS or no [MIME type](https://mimesniff.spec.whatwg.org/#javascript-mime-type) will be minified using [esbuild-rs](https://github.com/wilsonzlin/esbuild-rs).
* If enabled, content in `<script>` tags with a JS or no [MIME type](https://mimesniff.spec.whatwg.org/#javascript-mime-type) will be minified using [minify-js](https://github.com/wilsonzlin/minify-js).
*/
minify_js?: boolean;
/**
* If enabled, CSS in `<style>` tags will be minified using [esbuild-rs](https://github.com/wilsonzlin/esbuild-rs).
* If enabled, CSS in `<style>` tags and `style` attributes will be minified.
*/
minify_css?: boolean;
/** Remove all bangs. */

View File

@ -10,13 +10,9 @@ edition = "2018"
name = "minify_html_ffi"
crate-type = ["staticlib"]
[features]
core = ["minify-html"]
js = ["minify-html/js-esbuild"]
[build-dependencies]
cbindgen = "0.14"
[dependencies]
libc = "0.2"
minify-html = { path = "../../rust/main", optional = true }
minify-html = { path = "../../rust/main" }

View File

@ -15,7 +15,7 @@ name = "minify_html"
crate-type = ["cdylib"]
[dependencies]
minify-html = { path = "../rust/main", features = ["js-esbuild"] }
minify-html = { path = "../rust/main" }
[dependencies.pyo3]
version = "0.13.0"
features = ["extension-module"]

View File

@ -10,5 +10,5 @@ name = "minify_html_ruby_lib"
crate-type = ["cdylib"]
[dependencies]
minify-html = { path = "../rust/main", features = ["js-esbuild"] }
minify-html = { path = "../rust/main" }
rutie = "0.7.0"

View File

@ -1,6 +1,4 @@
use crate::tests::eval;
#[cfg(feature = "js-esbuild")]
use crate::tests::{eval_with_css_min, eval_with_js_min};
#[test]
@ -460,25 +458,19 @@ fn test_processing_instructions() {
eval(b"av<?xml 1.0 ?>g", b"av<?xml 1.0 ?>g");
}
#[cfg(feature = "js-esbuild")]
#[test]
fn test_js_minification() {
eval_with_js_min(b"<script>let a = 1;</script>", b"<script>let a=1;</script>");
eval_with_js_min(b"<script>let a = 1;</script>", b"<script>let a=1</script>");
eval_with_js_min(
b"<script type=text/javascript>let a = 1;</script>",
b"<script>let a=1;</script>",
);
// `export` statements are not allowed inline.
eval_with_js_min(
b"<script type=module>let a = 1; export a;</script>",
b"<script type=module></script>",
b"<script>let a=1</script>",
);
eval_with_js_min(
br#"
<script>let a = 1;</script>
<script>let b = 2;</script>
"#,
b"<script>let a=1;</script><script>let b=2;</script>",
b"<script>let a=1</script><script>let b=2</script>",
);
eval_with_js_min(
b"<scRIPt type=text/plain> alert(1.00000); </scripT>",
@ -491,39 +483,37 @@ fn test_js_minification() {
let a = 1;
</script>
"#,
b"<script>let a=1;</script>",
b"<script>let a=1</script>",
);
}
#[cfg(feature = "js-esbuild")]
/* TODO Reenable once unintentional script closing tag escaping is implemented in minify-js.
#[test]
fn test_js_minification_unintentional_closing_tag() {
eval_with_js_min(
br#"<script>let a = "</" + "script>";</script>"#,
br#"<script>let a="<\/script>";</script>"#,
);
// TODO Reenable once esbuild handles closing tags case insensitively (evanw/esbuild#1509).
// eval_with_js_min(
// br#"<script>let a = "</S" + "cRiPT>";</script>"#,
// br#"<script>let a="<\/ScRiPT>";</script>"#,
// );
eval_with_js_min(
br#"<script>let a = "</S" + "cRiPT>";</script>"#,
br#"<script>let a="<\/ScRiPT>";</script>"#,
);
eval_with_js_min(
br#"<script>let a = "\u003c/script>";</script>"#,
br#"<script>let a="<\/script>";</script>"#,
);
// TODO Reenable once esbuild handles closing tags case insensitively (evanw/esbuild#1509).
// eval_with_js_min(
// br#"<script>let a = "\u003c/scrIPt>";</script>"#,
// br#"<script>let a="<\/scrIPt>";</script>"#,
// );
eval_with_js_min(
br#"<script>let a = "\u003c/scrIPt>";</script>"#,
br#"<script>let a="<\/scrIPt>";</script>"#,
);
}
*/
#[cfg(feature = "js-esbuild")]
#[test]
fn test_style_element_minification() {
// `<style>` contents.
eval_with_css_min(
b"<style>div { color: yellow }</style>",
b"<style>div{color:#ff0}</style>",
b"<style>div{color:yellow}</style>",
);
}

View File

@ -15,13 +15,9 @@ include = ["/src/**/*", "/Cargo.toml", "/LICENSE", "/README.md"]
[badges]
maintenance = { status = "actively-developed" }
[features]
default = []
js-esbuild = ["crossbeam", "esbuild-rs"]
[dependencies]
aho-corasick = "0.7"
crossbeam = { version = "0.7", optional = true }
esbuild-rs = { version = "0.13.8", optional = true }
css-minify = "0.2.2"
minify-js = "0.1.0"
lazy_static = "1.4"
memchr = "2"

View File

@ -14,13 +14,10 @@ pub struct Cfg {
pub keep_spaces_between_attributes: bool,
/// Keep all comments.
pub keep_comments: bool,
/// If enabled, CSS in `<style>` tags are minified using
/// [esbuild-rs](https://github.com/wilsonzlin/esbuild-rs). The `js-esbuild` feature must be
/// enabled; otherwise, this value has no effect.
/// If enabled, CSS in `<style>` tags and `style` attributes are minified.
pub minify_css: bool,
/// If enabled, JavaScript in `<script>` tags are minified using
/// [esbuild-rs](https://github.com/wilsonzlin/esbuild-rs). The `js-esbuild` feature must be
/// enabled; otherwise, this value has no effect.
/// [minify-js](https://github.com/wilsonzlin/minify-js).
///
/// Only `<script>` tags with a valid or no
/// [MIME type](https://mimesniff.spec.whatwg.org/#javascript-mime-type) is considered to

View File

@ -1,10 +1,8 @@
use aho_corasick::{AhoCorasickBuilder, MatchKind};
use lazy_static::lazy_static;
use std::str::from_utf8_unchecked;
#[cfg(feature = "js-esbuild")]
use {
crate::minify::css::MINIFY_CSS_TRANSFORM_OPTIONS, crate::minify::esbuild::minify_using_esbuild,
};
use aho_corasick::{AhoCorasickBuilder, MatchKind};
use css_minify::optimizations::{Level, Minifier};
use lazy_static::lazy_static;
use crate::common::gen::attrs::ATTRS;
use crate::common::gen::codepoints::DIGIT;
@ -312,27 +310,25 @@ pub fn minify_attr(
};
};
#[cfg(feature = "js-esbuild")]
if name == b"style" && cfg.minify_css {
let mut value_raw_wrapped = Vec::with_capacity(value_raw.len() + 3);
let mut value_raw_wrapped = String::with_capacity(value_raw.len() + 3);
// TODO This isn't safe for invalid input e.g. `a}/*`.
value_raw_wrapped.extend_from_slice(b"x{");
value_raw_wrapped.extend_from_slice(&value_raw);
value_raw_wrapped.push(b'}');
let mut value_raw_wrapped_min = Vec::with_capacity(value_raw_wrapped.len());
minify_using_esbuild(
&mut value_raw_wrapped_min,
&value_raw_wrapped,
&MINIFY_CSS_TRANSFORM_OPTIONS.clone(),
);
// TODO If input was invalid, wrapper syntax may not exist anymore.
if value_raw_wrapped_min.starts_with(b"x{") {
value_raw_wrapped_min.drain(0..2);
value_raw_wrapped.push_str("x{");
value_raw_wrapped.push_str(unsafe { from_utf8_unchecked(&value_raw) });
value_raw_wrapped.push('}');
let result = Minifier::default().minify(&value_raw_wrapped, Level::Three);
// TODO Collect error as warning.
if let Ok(min) = result {
let mut value_raw_wrapped_min = min.into_bytes();
// TODO If input was invalid, wrapper syntax may not exist anymore.
if value_raw_wrapped_min.starts_with(b"x{") {
value_raw_wrapped_min.drain(0..2);
};
if value_raw_wrapped_min.ends_with(b"}") {
value_raw_wrapped_min.pop();
};
value_raw = value_raw_wrapped_min;
};
if value_raw_wrapped_min.ends_with(b"}") {
value_raw_wrapped_min.pop();
};
value_raw = value_raw_wrapped_min;
}
// Make lowercase before checking against default value or JAVASCRIPT_MIME_TYPES.

View File

@ -1,41 +1,19 @@
#[cfg(feature = "js-esbuild")]
use {
crate::minify::esbuild::minify_using_esbuild,
esbuild_rs::{
Charset, LegalComments, Loader, SourceMap, TransformOptions, TransformOptionsBuilder,
},
lazy_static::lazy_static,
std::sync::Arc,
};
use std::str::from_utf8_unchecked;
use crate::cfg::Cfg;
use crate::common::whitespace::trimmed;
use css_minify::optimizations::{Level, Minifier};
#[cfg(feature = "js-esbuild")]
lazy_static! {
pub static ref MINIFY_CSS_TRANSFORM_OPTIONS: Arc<TransformOptions> = {
let mut builder = TransformOptionsBuilder::new();
builder.charset = Charset::UTF8;
builder.legal_comments = LegalComments::None;
builder.loader = Loader::CSS;
builder.minify_identifiers = true;
builder.minify_syntax = true;
builder.minify_whitespace = true;
builder.source_map = SourceMap::None;
builder.build()
};
}
#[cfg(not(feature = "js-esbuild"))]
pub fn minify_css(_cfg: &Cfg, out: &mut Vec<u8>, code: &[u8]) {
pub fn minify_css(cfg: &Cfg, out: &mut Vec<u8>, code: &[u8]) {
if cfg.minify_css {
let result = Minifier::default().minify(unsafe { from_utf8_unchecked(code) }, Level::Three);
// TODO Collect error as warning.
if let Ok(min) = result {
if min.len() < code.len() {
out.extend_from_slice(min.as_bytes());
return;
};
};
}
out.extend_from_slice(trimmed(code));
}
#[cfg(feature = "js-esbuild")]
pub fn minify_css(cfg: &Cfg, out: &mut Vec<u8>, code: &[u8]) {
if !cfg.minify_css {
out.extend_from_slice(trimmed(code));
} else {
minify_using_esbuild(out, code, &MINIFY_CSS_TRANSFORM_OPTIONS.clone());
}
}

View File

@ -1,18 +0,0 @@
#[cfg(feature = "js-esbuild")]
use {crossbeam::sync::WaitGroup, esbuild_rs::TransformOptions};
#[cfg(feature = "js-esbuild")]
// TODO The use of WG is ugly and we don't want to be multi-threaded; wait for Rust port esbuild-transform-rs.
pub fn minify_using_esbuild(out: &mut Vec<u8>, code: &[u8], transform_options: &TransformOptions) {
let wg = WaitGroup::new();
unsafe {
let wg = wg.clone();
// esbuild now officially handles escaping `</script` and `</style`.
esbuild_rs::transform_direct_unmanaged(code, transform_options, move |result| {
let min_code = result.code.as_str().trim().as_bytes();
out.extend_from_slice(min_code);
drop(wg);
});
};
wg.wait();
}

View File

@ -1,38 +1,18 @@
#[cfg(feature = "js-esbuild")]
use {
crate::minify::esbuild::minify_using_esbuild,
esbuild_rs::{Charset, LegalComments, SourceMap, TransformOptions, TransformOptionsBuilder},
lazy_static::lazy_static,
std::sync::Arc,
};
use crate::cfg::Cfg;
use crate::common::whitespace::trimmed;
use minify_js::minify as minifier;
#[cfg(feature = "js-esbuild")]
lazy_static! {
static ref TRANSFORM_OPTIONS: Arc<TransformOptions> = {
let mut builder = TransformOptionsBuilder::new();
builder.charset = Charset::UTF8;
builder.legal_comments = LegalComments::None;
builder.minify_identifiers = true;
builder.minify_syntax = true;
builder.minify_whitespace = true;
builder.source_map = SourceMap::None;
builder.build()
};
}
#[cfg(not(feature = "js-esbuild"))]
pub fn minify_js(_cfg: &Cfg, out: &mut Vec<u8>, code: &[u8]) {
pub fn minify_js(cfg: &Cfg, out: &mut Vec<u8>, code: &[u8]) {
if cfg.minify_js {
let source = code.to_vec();
// TODO Write to `out` directly, but only if we can guarantee that the length will never exceed the input.
let mut output = Vec::new();
let result = minifier(source, &mut output);
// TODO Collect error as warning.
if !result.is_err() && output.len() < code.len() {
out.extend_from_slice(output.as_slice());
return;
};
}
out.extend_from_slice(trimmed(code));
}
#[cfg(feature = "js-esbuild")]
pub fn minify_js(cfg: &Cfg, out: &mut Vec<u8>, code: &[u8]) {
if !cfg.minify_js {
out.extend_from_slice(trimmed(code));
} else {
minify_using_esbuild(out, code, &TRANSFORM_OPTIONS.clone());
}
}

View File

@ -5,7 +5,6 @@ pub mod content;
pub mod css;
pub mod doctype;
pub mod element;
pub mod esbuild;
pub mod instruction;
pub mod js;
#[cfg(test)]

View File

@ -8,14 +8,12 @@ pub fn eval_with_cfg(src: &'static [u8], expected: &'static [u8], cfg: &Cfg) {
assert_eq!(from_utf8(&min).unwrap(), from_utf8(expected).unwrap(),);
}
#[cfg(feature = "js-esbuild")]
pub fn eval_with_js_min(src: &'static [u8], expected: &'static [u8]) -> () {
let mut cfg = Cfg::new();
cfg.minify_js = true;
eval_with_cfg(src, expected, &cfg);
}
#[cfg(feature = "js-esbuild")]
pub fn eval_with_css_min(src: &'static [u8], expected: &'static [u8]) -> () {
let mut cfg = Cfg::new();
cfg.minify_css = true;
@ -146,12 +144,11 @@ fn test_viewport_attr_minification() {
);
}
#[cfg(feature = "js-esbuild")]
#[test]
fn test_style_attr_minification() {
eval_with_css_min(
br#"<div style="color: yellow;"></div>"#,
br#"<div style=color:#ff0></div>"#,
br#"<div style=color:yellow></div>"#,
);
// `style` attributes are removed if fully minified away.
eval_with_css_min(br#"<div style=" /* */ "></div>"#, br#"<div></div>"#);

View File

@ -92,7 +92,7 @@ if (
}
cmd("git", "pull");
cmd("bash", "./prebuild.sh");
cmd("cargo", "test", "--features", "js-esbuild", { workingDir: RUST_MAIN_DIR });
cmd("cargo", "test", { workingDir: RUST_MAIN_DIR });
cmd("cargo", "test", "--features", "js-esbuild", {
workingDir: RUST_ONEPASS_DIR,
});