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. - Advanced minification strategy beats other minifiers while being much faster.
- Uses SIMD searching, direct tries, and lookup tables. - Uses SIMD searching, direct tries, and lookup tables.
- Handles [invalid HTML](./notes/Parsing.md), with extensive testing and [fuzzing](./fuzz). - 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 ## Performance
@ -55,13 +55,9 @@ minify-html --output /path/to/output.min.html --keep-closing-tags --minify-css /
```toml ```toml
[dependencies] [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 ### Use
Check out the [docs](https://docs.rs/minify-html) for API and usage examples. 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 ## 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 ### Whitespace

View File

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

View File

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

View File

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

4
nodejs/index.d.ts vendored
View File

@ -21,11 +21,11 @@ export function createConfiguration (options: {
/** Keep all comments. */ /** Keep all comments. */
keep_comments?: boolean; 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; 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; minify_css?: boolean;
/** Remove all bangs. */ /** Remove all bangs. */

View File

@ -10,13 +10,9 @@ edition = "2018"
name = "minify_html_ffi" name = "minify_html_ffi"
crate-type = ["staticlib"] crate-type = ["staticlib"]
[features]
core = ["minify-html"]
js = ["minify-html/js-esbuild"]
[build-dependencies] [build-dependencies]
cbindgen = "0.14" cbindgen = "0.14"
[dependencies] [dependencies]
libc = "0.2" 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"] crate-type = ["cdylib"]
[dependencies] [dependencies]
minify-html = { path = "../rust/main", features = ["js-esbuild"] } minify-html = { path = "../rust/main" }
[dependencies.pyo3] [dependencies.pyo3]
version = "0.13.0" version = "0.13.0"
features = ["extension-module"] features = ["extension-module"]

View File

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

View File

@ -1,6 +1,4 @@
use crate::tests::eval; use crate::tests::eval;
#[cfg(feature = "js-esbuild")]
use crate::tests::{eval_with_css_min, eval_with_js_min}; use crate::tests::{eval_with_css_min, eval_with_js_min};
#[test] #[test]
@ -460,25 +458,19 @@ fn test_processing_instructions() {
eval(b"av<?xml 1.0 ?>g", b"av<?xml 1.0 ?>g"); eval(b"av<?xml 1.0 ?>g", b"av<?xml 1.0 ?>g");
} }
#[cfg(feature = "js-esbuild")]
#[test] #[test]
fn test_js_minification() { 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( eval_with_js_min(
b"<script type=text/javascript>let a = 1;</script>", b"<script type=text/javascript>let a = 1;</script>",
b"<script>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>",
); );
eval_with_js_min( eval_with_js_min(
br#" br#"
<script>let a = 1;</script> <script>let a = 1;</script>
<script>let b = 2;</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( eval_with_js_min(
b"<scRIPt type=text/plain> alert(1.00000); </scripT>", b"<scRIPt type=text/plain> alert(1.00000); </scripT>",
@ -491,39 +483,37 @@ fn test_js_minification() {
let a = 1; let a = 1;
</script> </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] #[test]
fn test_js_minification_unintentional_closing_tag() { fn test_js_minification_unintentional_closing_tag() {
eval_with_js_min( eval_with_js_min(
br#"<script>let a = "</" + "script>";</script>"#, br#"<script>let a = "</" + "script>";</script>"#,
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(
// eval_with_js_min( br#"<script>let a = "</S" + "cRiPT>";</script>"#,
// br#"<script>let a = "</S" + "cRiPT>";</script>"#, br#"<script>let a="<\/ScRiPT>";</script>"#,
// br#"<script>let a="<\/ScRiPT>";</script>"#, );
// );
eval_with_js_min( eval_with_js_min(
br#"<script>let a = "\u003c/script>";</script>"#, br#"<script>let a = "\u003c/script>";</script>"#,
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(
// eval_with_js_min( br#"<script>let a = "\u003c/scrIPt>";</script>"#,
// br#"<script>let a = "\u003c/scrIPt>";</script>"#, br#"<script>let a="<\/scrIPt>";</script>"#,
// br#"<script>let a="<\/scrIPt>";</script>"#, );
// );
} }
*/
#[cfg(feature = "js-esbuild")]
#[test] #[test]
fn test_style_element_minification() { fn test_style_element_minification() {
// `<style>` contents. // `<style>` contents.
eval_with_css_min( eval_with_css_min(
b"<style>div { color: yellow }</style>", 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] [badges]
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[features]
default = []
js-esbuild = ["crossbeam", "esbuild-rs"]
[dependencies] [dependencies]
aho-corasick = "0.7" aho-corasick = "0.7"
crossbeam = { version = "0.7", optional = true } css-minify = "0.2.2"
esbuild-rs = { version = "0.13.8", optional = true } minify-js = "0.1.0"
lazy_static = "1.4" lazy_static = "1.4"
memchr = "2" memchr = "2"

View File

@ -14,13 +14,10 @@ pub struct Cfg {
pub keep_spaces_between_attributes: bool, pub keep_spaces_between_attributes: bool,
/// Keep all comments. /// Keep all comments.
pub keep_comments: bool, pub keep_comments: bool,
/// If enabled, CSS in `<style>` tags are minified using /// If enabled, CSS in `<style>` tags and `style` attributes are minified.
/// [esbuild-rs](https://github.com/wilsonzlin/esbuild-rs). The `js-esbuild` feature must be
/// enabled; otherwise, this value has no effect.
pub minify_css: bool, pub minify_css: bool,
/// If enabled, JavaScript in `<script>` tags are minified using /// If enabled, JavaScript in `<script>` tags are minified using
/// [esbuild-rs](https://github.com/wilsonzlin/esbuild-rs). The `js-esbuild` feature must be /// [minify-js](https://github.com/wilsonzlin/minify-js).
/// enabled; otherwise, this value has no effect.
/// ///
/// Only `<script>` tags with a valid or no /// Only `<script>` tags with a valid or no
/// [MIME type](https://mimesniff.spec.whatwg.org/#javascript-mime-type) is considered to /// [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 std::str::from_utf8_unchecked;
use lazy_static::lazy_static;
#[cfg(feature = "js-esbuild")] use aho_corasick::{AhoCorasickBuilder, MatchKind};
use { use css_minify::optimizations::{Level, Minifier};
crate::minify::css::MINIFY_CSS_TRANSFORM_OPTIONS, crate::minify::esbuild::minify_using_esbuild, use lazy_static::lazy_static;
};
use crate::common::gen::attrs::ATTRS; use crate::common::gen::attrs::ATTRS;
use crate::common::gen::codepoints::DIGIT; 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 { 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}/*`. // TODO This isn't safe for invalid input e.g. `a}/*`.
value_raw_wrapped.extend_from_slice(b"x{"); value_raw_wrapped.push_str("x{");
value_raw_wrapped.extend_from_slice(&value_raw); value_raw_wrapped.push_str(unsafe { from_utf8_unchecked(&value_raw) });
value_raw_wrapped.push(b'}'); value_raw_wrapped.push('}');
let mut value_raw_wrapped_min = Vec::with_capacity(value_raw_wrapped.len()); let result = Minifier::default().minify(&value_raw_wrapped, Level::Three);
minify_using_esbuild( // TODO Collect error as warning.
&mut value_raw_wrapped_min, if let Ok(min) = result {
&value_raw_wrapped, let mut value_raw_wrapped_min = min.into_bytes();
&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{") {
// TODO If input was invalid, wrapper syntax may not exist anymore. value_raw_wrapped_min.drain(0..2);
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. // Make lowercase before checking against default value or JAVASCRIPT_MIME_TYPES.

View File

@ -1,41 +1,19 @@
#[cfg(feature = "js-esbuild")] use std::str::from_utf8_unchecked;
use {
crate::minify::esbuild::minify_using_esbuild,
esbuild_rs::{
Charset, LegalComments, Loader, SourceMap, TransformOptions, TransformOptionsBuilder,
},
lazy_static::lazy_static,
std::sync::Arc,
};
use crate::cfg::Cfg; use crate::cfg::Cfg;
use crate::common::whitespace::trimmed; use crate::common::whitespace::trimmed;
use css_minify::optimizations::{Level, Minifier};
#[cfg(feature = "js-esbuild")] pub fn minify_css(cfg: &Cfg, out: &mut Vec<u8>, code: &[u8]) {
lazy_static! { if cfg.minify_css {
pub static ref MINIFY_CSS_TRANSFORM_OPTIONS: Arc<TransformOptions> = { let result = Minifier::default().minify(unsafe { from_utf8_unchecked(code) }, Level::Three);
let mut builder = TransformOptionsBuilder::new(); // TODO Collect error as warning.
builder.charset = Charset::UTF8; if let Ok(min) = result {
builder.legal_comments = LegalComments::None; if min.len() < code.len() {
builder.loader = Loader::CSS; out.extend_from_slice(min.as_bytes());
builder.minify_identifiers = true; return;
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]) {
out.extend_from_slice(trimmed(code)); 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::cfg::Cfg;
use crate::common::whitespace::trimmed; use crate::common::whitespace::trimmed;
use minify_js::minify as minifier;
#[cfg(feature = "js-esbuild")] pub fn minify_js(cfg: &Cfg, out: &mut Vec<u8>, code: &[u8]) {
lazy_static! { if cfg.minify_js {
static ref TRANSFORM_OPTIONS: Arc<TransformOptions> = { let source = code.to_vec();
let mut builder = TransformOptionsBuilder::new(); // TODO Write to `out` directly, but only if we can guarantee that the length will never exceed the input.
builder.charset = Charset::UTF8; let mut output = Vec::new();
builder.legal_comments = LegalComments::None; let result = minifier(source, &mut output);
builder.minify_identifiers = true; // TODO Collect error as warning.
builder.minify_syntax = true; if !result.is_err() && output.len() < code.len() {
builder.minify_whitespace = true; out.extend_from_slice(output.as_slice());
builder.source_map = SourceMap::None; return;
builder.build() };
}; }
}
#[cfg(not(feature = "js-esbuild"))]
pub fn minify_js(_cfg: &Cfg, out: &mut Vec<u8>, code: &[u8]) {
out.extend_from_slice(trimmed(code)); 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 css;
pub mod doctype; pub mod doctype;
pub mod element; pub mod element;
pub mod esbuild;
pub mod instruction; pub mod instruction;
pub mod js; pub mod js;
#[cfg(test)] #[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(),); 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]) -> () { pub fn eval_with_js_min(src: &'static [u8], expected: &'static [u8]) -> () {
let mut cfg = Cfg::new(); let mut cfg = Cfg::new();
cfg.minify_js = true; cfg.minify_js = true;
eval_with_cfg(src, expected, &cfg); eval_with_cfg(src, expected, &cfg);
} }
#[cfg(feature = "js-esbuild")]
pub fn eval_with_css_min(src: &'static [u8], expected: &'static [u8]) -> () { pub fn eval_with_css_min(src: &'static [u8], expected: &'static [u8]) -> () {
let mut cfg = Cfg::new(); let mut cfg = Cfg::new();
cfg.minify_css = true; cfg.minify_css = true;
@ -146,12 +144,11 @@ fn test_viewport_attr_minification() {
); );
} }
#[cfg(feature = "js-esbuild")]
#[test] #[test]
fn test_style_attr_minification() { fn test_style_attr_minification() {
eval_with_css_min( eval_with_css_min(
br#"<div style="color: yellow;"></div>"#, 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. // `style` attributes are removed if fully minified away.
eval_with_css_min(br#"<div style=" /* */ "></div>"#, br#"<div></div>"#); eval_with_css_min(br#"<div style=" /* */ "></div>"#, br#"<div></div>"#);

View File

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