From 4c0eb3ed281b4422af90a3343c5cacd217dc92a2 Mon Sep 17 00:00:00 2001 From: Wilson Lin Date: Sat, 7 Aug 2021 18:51:22 +1000 Subject: [PATCH] Implement JS and CSS minification; add tests --- Cargo.toml | 2 +- src/minify/css.rs | 39 +++++++++++++++++++++++-- src/minify/esbuild.rs | 42 +++++++++++++++++++++++++++ src/minify/js.rs | 40 +++++++++++++++++++++++--- src/minify/mod.rs | 1 + src/pattern.rs | 3 +- src/tests/mod.rs | 66 +++++++++++++++++++++++-------------------- 7 files changed, 154 insertions(+), 39 deletions(-) create mode 100644 src/minify/esbuild.rs diff --git a/Cargo.toml b/Cargo.toml index cdcf43f..00cbb8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,6 @@ js-esbuild = ["crossbeam", "esbuild-rs"] [dependencies] aho-corasick = "0.7" crossbeam = { version = "0.7", optional = true } -esbuild-rs = { version = "0.8.30", optional = true } +esbuild-rs = { version = "0.12.18", optional = true } lazy_static = "1.4" memchr = "2" diff --git a/src/minify/css.rs b/src/minify/css.rs index 9618d8a..238dd28 100644 --- a/src/minify/css.rs +++ b/src/minify/css.rs @@ -1,6 +1,39 @@ +#[cfg(feature = "js-esbuild")] +use { + crate::minify::esbuild::minify_using_esbuild, + aho_corasick::{AhoCorasick, AhoCorasickBuilder}, + esbuild_rs::{Loader, TransformOptions, TransformOptionsBuilder}, + lazy_static::lazy_static, + std::sync::Arc, +}; + use crate::cfg::Cfg; -pub fn minify_css(_cfg: &Cfg, out: &mut Vec, code: &[u8]) { - // TODO - out.extend_from_slice(code); +#[cfg(feature = "js-esbuild")] +lazy_static! { + static ref STYLE_END: AhoCorasick = AhoCorasickBuilder::new() + .ascii_case_insensitive(true) + .build(&[" = { + let mut builder = TransformOptionsBuilder::new(); + builder.loader = Loader::CSS; + builder.minify_identifiers = true; + builder.minify_syntax = true; + builder.minify_whitespace = true; + builder.build() + }; +} + +#[cfg(not(feature = "js-esbuild"))] +pub fn minify_css(_cfg: &Cfg, out: &mut Vec, code: &[u8]) { + out.extend_from_slice(&code); +} + +#[cfg(feature = "js-esbuild")] +pub fn minify_css(cfg: &Cfg, out: &mut Vec, code: &[u8]) { + if !cfg.minify_css { + out.extend_from_slice(&code); + } else { + minify_using_esbuild(out, code, &TRANSFORM_OPTIONS.clone(), &STYLE_END); + } } diff --git a/src/minify/esbuild.rs b/src/minify/esbuild.rs new file mode 100644 index 0000000..953a034 --- /dev/null +++ b/src/minify/esbuild.rs @@ -0,0 +1,42 @@ +#[cfg(feature = "js-esbuild")] +use {aho_corasick::AhoCorasick, 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. +// `tag_to_escape` must be case insensitive. +pub fn minify_using_esbuild( + out: &mut Vec, + code: &[u8], + transform_options: &TransformOptions, + tag_to_escape: &'static AhoCorasick, +) { + let wg = WaitGroup::new(); + unsafe { + let wg = wg.clone(); + esbuild_rs::transform_direct_unmanaged(code, transform_options, move |result| { + // TODO (JS) Handle other forms: + // 1 < /script/.exec(a).length + // ` ${` ${a + // /* + // Considerations: + // - Need to parse strings (e.g. "", '', ``) so syntax within strings aren't mistakenly interpreted as code. + // - Need to be able to parse regex literals to determine string delimiters aren't actually characters in the regex. + // - Determining whether a slash is division or regex requires a full-blown JS parser to handle all cases (this is a well-known JS parsing problem). + // - `/, code: &[u8]) { - // TODO - out.extend_from_slice(code); +use crate::Cfg; + +#[cfg(feature = "js-esbuild")] +lazy_static! { + static ref SCRIPT_END: AhoCorasick = AhoCorasickBuilder::new() + .ascii_case_insensitive(true) + .build(&[" = { + let mut builder = TransformOptionsBuilder::new(); + builder.minify_identifiers = true; + builder.minify_syntax = true; + builder.minify_whitespace = true; + builder.build() + }; +} + +#[cfg(not(feature = "js-esbuild"))] +pub fn minify_js(_cfg: &Cfg, out: &mut Vec, code: &[u8]) { + out.extend_from_slice(&code); +} + +#[cfg(feature = "js-esbuild")] +pub fn minify_js(cfg: &Cfg, out: &mut Vec, code: &[u8]) { + if !cfg.minify_js { + out.extend_from_slice(&code); + } else { + minify_using_esbuild(out, code, &TRANSFORM_OPTIONS.clone(), &SCRIPT_END); + } } diff --git a/src/minify/mod.rs b/src/minify/mod.rs index 25f43be..559c092 100644 --- a/src/minify/mod.rs +++ b/src/minify/mod.rs @@ -4,6 +4,7 @@ pub mod comment; pub mod content; pub mod css; pub mod element; +pub mod esbuild; pub mod instruction; pub mod js; #[cfg(test)] diff --git a/src/pattern.rs b/src/pattern.rs index 94f6e29..db09c69 100644 --- a/src/pattern.rs +++ b/src/pattern.rs @@ -15,6 +15,7 @@ pub enum TrieNodeMatch { NotFound { reached: usize }, } +#[allow(dead_code)] impl TrieNode { // Find the node that matches the shortest prefix of {@param text} that: // - has a value (except the start node if it has a value); @@ -32,7 +33,7 @@ impl TrieNode { // - "&amx" will return node `m`. // - "&ax" will return node `a`. // - "+ax" will return itself. - // - "" will return the itself. + // - "" will return itself. pub fn shortest_matching_prefix(&self, text: &[u8], from: usize) -> (&TrieNode, usize) { let mut node: &TrieNode = self; let mut pos = from; diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 3506980..510f6b3 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,4 +1,4 @@ -fn _eval(src: &'static [u8], expected: &'static [u8], cfg: &super::Cfg) { +fn eval_with_cfg(src: &'static [u8], expected: &'static [u8], cfg: &super::Cfg) { let mut code = src.to_vec(); let min = super::minify(&mut code, cfg); assert_eq!( @@ -8,21 +8,27 @@ fn _eval(src: &'static [u8], expected: &'static [u8], cfg: &super::Cfg) { } fn eval(src: &'static [u8], expected: &'static [u8]) { - _eval(src, expected, &super::Cfg::new()); + eval_with_cfg(src, expected, &super::Cfg::new()); +} + +fn eval_with_keep_html_head(src: &'static [u8], expected: &'static [u8]) -> () { + let mut cfg = super::Cfg::new(); + cfg.keep_html_and_head_opening_tags = true; + eval_with_cfg(src, expected, &cfg); } #[cfg(feature = "js-esbuild")] fn eval_with_js_min(src: &'static [u8], expected: &'static [u8]) -> () { let mut cfg = super::Cfg::new(); cfg.minify_js = true; - _eval(src, expected, &cfg); + eval_with_cfg(src, expected, &cfg); } #[cfg(feature = "js-esbuild")] fn eval_with_css_min(src: &'static [u8], expected: &'static [u8]) -> () { let mut cfg = super::Cfg::new(); cfg.minify_css = true; - _eval(src, expected, &cfg); + eval_with_cfg(src, expected, &cfg); } #[test] @@ -97,17 +103,17 @@ fn test_no_whitespace_minification() { #[test] fn test_parsing_extra_head_tag() { // Extra `` in `