8000 Make JS (oxc) and CSS (lightningcss) minification opt-in via `js`/`css` features by FalkWoldmann · Pull Request #289 · wilsonzlin/minify-html · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions minify-html/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,22 @@ edition = "2018"
[badges]
maintenance = { status = "actively-developed" }

[features]
default = ["css", "js"]
# CSS minification (`<style>` tags and `style` attributes) via lightningcss.
css = ["lightningcss"]
# JS minification (`<script>` tags) via the oxc toolchain.
js = ["oxc_allocator", "oxc_codegen", "oxc_minifier", "oxc_parser", "oxc_span"]

[dependencies]
ahash = "0.8"
aho-corasick = "1.1"
lightningcss = "1.0.0-alpha.68"
lightningcss = { version = "1.0.0-alpha.68", optional = true }
memchr = "2"
minify-html-common = { version = "0.0.3", path = "../minify-html-common" }
once_cell = "1.21"
oxc_allocator = "0.95"
oxc_codegen = "0.95"
oxc_minifier = "0.95"
oxc_parser = "0.95"
oxc_span = "0.95"
oxc_allocator = { version = "0.95", optional = true }
oxc_codegen = { version = "0.95", optional = true }
oxc_minifier = { version = "0.95", optional = true }
oxc_parser = { version = "0.95", optional = true }
oxc_span = { version = "0.95", optional = true }
64 changes: 44 additions & 20 deletions minify-html/src/minify/attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ use crate::entity::encode::encode_entities;
use crate::Cfg;
use aho_corasick::AhoCorasickBuilder;
use aho_corasick::MatchKind;
#[cfg(feature = "css")]
use lightningcss::stylesheet::MinifyOptions;
#[cfg(feature = "css")]
use lightningcss::stylesheet::ParserOptions;
#[cfg(feature = "css")]
use lightningcss::stylesheet::PrinterOptions;
#[cfg(feature = "css")]
use lightningcss::stylesheet::StyleAttribute;
use minify_html_common::gen::attrs::ATTRS;
use minify_html_common::gen::codepoints::DIGIT;
Expand All @@ -16,6 +20,7 @@ use minify_html_common::whitespace::left_trim;
use minify_html_common::whitespace::remove_all_whitespace;
use minify_html_common::whitespace::right_trim;
use once_cell::sync::Lazy;
#[cfg(feature = "css")]
use std::str::from_utf8;

fn build_double_quoted_replacer() -> Replacer {
Expand Down Expand Up @@ -362,6 +367,42 @@ pub enum AttrMinified {
Value(AttrMinifiedValue),
}

/// Returns the minified CSS for a `style` attribute value, or `None` if minification wasn't
/// performed (either `cfg.minify_css` is `false`, or minification failed / produced no smaller
/// result).
///
/// Without the `css` feature, this always returns `None`, i.e. behaves as if `cfg.minify_css`
/// were `false`.
#[cfg(feature = "css")]
fn minify_style_attr_value(cfg: &Cfg, value_raw: &[u8]) -> Option<Vec<u8>> {
if !cfg.minify_css {
return None;
}
let result = match StyleAttribute::parse(
from_utf8(value_raw).expect("`style` attribute value contains non-UTF-8"),
ParserOptions::default(),
) {
Ok(mut sty) => {
sty.minify(MinifyOptions::default());
let mut popt = PrinterOptions::default();
popt.minify = true;
match sty.to_css(popt) {
Ok(out) => Some(out.code),
// TODO Collect error as warning.
Err(_err) => None,
}
}
// TODO Collect error as warning.
Err(_err) => None,
};
result.map(|min| min.into_bytes())
}

#[cfg(not(feature = "css"))]
fn minify_style_attr_value(_cfg: &Cfg, _value_raw: &[u8]) -> Option<Vec<u8>> {
None
}

pub fn minify_attr(
cfg: &Cfg,
ns: Namespace,
Expand Down Expand Up @@ -399,26 +440,9 @@ pub fn minify_attr(
};
};

if name == b"style" && cfg.minify_css {
let result = match StyleAttribute::parse(
from_utf8(&value_raw).expect("`style` attribute value contains non-UTF-8"),
ParserOptions::default(),
) {
Ok(mut sty) => {
sty.minify(MinifyOptions::default());
let mut popt = PrinterOptions::default();
popt.minify = true;
match sty.to_css(popt) {
Ok(out) => Some(out.code),
// TODO Collect error as warning.
Err(_err) => None,
}
}
// TODO Collect error as warning.
Err(_err) => None,
};
if let Some(min) = result {
value_raw = min.into_bytes();
if name == b"style" {
if let Some(min) = minify_style_attr_value(cfg, &value_raw) {
value_raw = min;
};
}

Expand Down
67 changes: 46 additions & 21 deletions minify-html/src/minify/css.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,62 @@
use crate::cfg::Cfg;
#[cfg(feature = "css")]
use lightningcss::stylesheet::MinifyOptions;
#[cfg(feature = "css")]
use lightningcss::stylesheet::ParserOptions;
#[cfg(feature = "css")]
use lightningcss::stylesheet::PrinterOptions;
#[cfg(feature = "css")]
use lightningcss::stylesheet::StyleSheet;
use minify_html_common::whitespace::trimmed;
#[cfg(feature = "css")]
use std::str::from_utf8;

pub fn minify_css(cfg: &Cfg, out: &mut Vec<u8>, code: &[u8]) {
if cfg.minify_css {
let mut popt = PrinterOptions::default();
popt.minify = true;
let result = match StyleSheet::parse(
from_utf8(code).expect("<style> content contains non-UTF-8"),
ParserOptions::default(),
) {
Ok(mut sty) => match sty.minify(MinifyOptions::default()) {
Ok(()) => match sty.to_css(popt) {
Ok(out) => Some(out.code),
// TODO Collect error as warning.
Err(_err) => None,
},
if minify_css_with_lightningcss(cfg, out, code) {
return;
}
out.extend_from_slice(trimmed(code));
}

/// Returns `true` and writes the minified code to `out` if minification was performed and
/// produced a smaller result; returns `false` (without touching `out`) otherwise, in which case
/// the caller falls back to the trimmed original code.
///
/// Without the `css` feature, this always returns `false`, i.e. behaves as if `cfg.minify_css`
/// were `false`.
#[cfg(feature = "css")]
fn minify_css_with_lightningcss(cfg: &Cfg, out: &mut Vec<u8>, code: &[u8]) -> bool {
if !cfg.minify_css {
return false;
}
let mut popt = PrinterOptions::default();
popt.minify = true;
let result = match StyleSheet::parse(
from_utf8(code).expect("<style> content contains non-UTF-8"),
ParserOptions::default(),
) {
Ok(mut sty) => match sty.minify(MinifyOptions::default()) {
Ok(()) => match sty.to_css(popt) {
Ok(out) => Some(out.code),
// TODO Collect error as warning.
Err(_err) => None,
},
// TODO Collect error as warning.
Err(_err) => None,
},
// TODO Collect error as warning.
Err(_err) => None,
};
if let Some(min) = result {
if min.len() < code.len() {
out.extend_from_slice(min.as_bytes());
return true;
};
if let Some(min) = result {
if min.len() < code.len() {
out.extend_from_slice(min.as_bytes());
return;
};
};
}
out.extend_from_slice(trimmed(code));
};
false
}

#[cfg(not(feature = "css"))]
fn minify_css_with_lightningcss(_cfg: &Cfg, _out: &mut Vec<u8>, _code: &[u8]) -> bool {
false
}
116 changes: 73 additions & 43 deletions minify-html/src/minify/js.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
use crate::cfg::Cfg;
use minify_html_common::whitespace::trimmed;
#[cfg(feature = "js")]
use oxc_allocator::Allocator;
#[cfg(feature = "js")]
use oxc_codegen::C 8BD0 odegen;
#[cfg(feature = "js")]
use oxc_codegen::CodegenOptions;
#[cfg(feature = "js")]
use oxc_codegen::CommentOptions;
#[cfg(feature = "js")]
use oxc_minifier::CompressOptions;
#[cfg(feature = "js")]
use oxc_minifier::MangleOptions;
#[cfg(feature = "js")]
use oxc_minifier::Minifier;
#[cfg(feature = "js")]
use oxc_minifier::MinifierOptions;
#[cfg(feature = "js")]
use oxc_parser::Parser;
#[cfg(feature = "js")]
use oxc_span::SourceType;

/// Represents the mode in which JavaScript should be parsed and minified
Expand All @@ -21,55 +31,75 @@ pub enum TopLevelMode {
}

pub fn minify_js(cfg: &Cfg, mode: TopLevelMode, out: &mut Vec<u8>, code: &[u8]) {
if cfg.minify_js {
// Try to convert bytes to UTF-8 string for parsing
if let Ok(source_text) = std::str::from_utf8(code) {
let allocator = Allocator::default();
if minify_js_with_oxc(cfg, mode, out, code) {
return;
}
// Fall back to trimmed original code
out.extend_from_slice(trimmed(code));
}

// Determine source type based on mode
let source_type = match mode {
TopLevelMode::Module => SourceType::mjs(),
TopLevelMode::Global => SourceType::default(),
};
/// Returns `true` and writes the minified code to `out` if minification was performed and
/// produced a smaller result; returns `false` (without touching `out`) otherwise, in which case
/// the caller falls back to the trimmed original code.
///
/// Without the `js` feature, this always returns `false`, i.e. behaves as if `cfg.minify_js` were
/// `false`.
#[cfg(feature = "js")]
fn minify_js_with_oxc(cfg: &Cfg, mode: TopLevelMode, out: &mut Vec<u8>, code: &[u8]) -> bool {
if !cfg.minify_js {
return false;
}
// Try to convert bytes to UTF-8 string for parsing
if let Ok(source_text) = std::str::from_utf8(code) {
let allocator = Allocator::default();

// Determine source type based on mode
let source_type = match mode {
TopLevelMode::Module => SourceType::mjs(),
TopLevelMode::Global => SourceType::default(),
};

// Parse the JavaScript code
let parser_ret = Parser::new(&allocator, source_text, source_type).parse();
// Parse the JavaScript code
let parser_ret = Parser::new(&allocator, source_text, source_type).parse();

// Only proceed if parsing succeeded without errors
if parser_ret.errors.is_empty() {
let mut program = parser_ret.program;

// Apply minification
// Use CompressOptions::safest() instead of default() to avoid overly aggressive dead code elimination
let _minifier_ret = Minifier::new(MinifierOptions {
mangle: Some(MangleOptions::default()),
compress: Some(CompressOptions::safest()),
}).minify(&allocator, &mut program);
// Only proceed if parsing succeeded without errors
if parser_ret.errors.is_empty() {
let mut program = parser_ret.program;

// Generate minified code
// Disable treeshake annotations (e.g., /*#__PURE__*/, /*@__PURE__*/)
// These are only useful for bundlers, not inline scripts
let codegen_options = CodegenOptions {
minify: true,
comments: CommentOptions {
annotation: false,
..CommentOptions::default()
},
..CodegenOptions::default()
};
let minified = Codegen::new()
.with_options(codegen_options)
.build(&program)
.code;
// Apply minification
// Use CompressOptions::safest() instead of default() to avoid overly aggressive dead code elimination
let _minifier_ret = Minifier::new(MinifierOptions {
mangle: Some(MangleOptions::default()),
compress: Some(CompressOptions::safest()),
}).minify(&allocator, &mut program);

// Only use minified version if it's actually smaller
if minified.len() < code.len() {
out.extend_from_slice(minified.as_bytes());
return;
}
// Generate minified code
// Disable treeshake annotations (e.g., /*#__PURE__*/, /*@__PURE__*/)
// These are only useful for bundlers, not inline scripts
let codegen_options = CodegenOptions {
minify: true,
comments: CommentOptions {
annotation: false,
..CommentOptions::default()
},
..CodegenOptions::default()
};
let minified = Codegen::new()
.with_options(codegen_options)
.build(&program)
.code;

// Only use minified version if it's actually smaller
if minified.len() < code.len() {
out.extend_from_slice(minified.as_bytes());
return true;
}
}
}
// Fall back to trimmed original code
out.extend_from_slice(trimmed(code));
false
}

#[cfg(not(feature = "js"))]
fn minify_js_with_oxc(_cfg: &Cfg, _mode: TopLevelMode, _out: &mut Vec<u8>, _code: &[u8]) -> bool {
false
}
0