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 `
Goodbye", b"HelloGoodbye");
- eval(b"Hello
Goodbye", b"Hello
Goodbye");
- eval(b"
HelloGoodbye", b"
Hello
Goodbye");
- eval(b"
- a", b"
- a
");
- eval(b"