diff --git a/.changeset/ninety-tips-heal.md b/.changeset/ninety-tips-heal.md new file mode 100644 index 000000000000..142dee1abbdf --- /dev/null +++ b/.changeset/ninety-tips-heal.md @@ -0,0 +1,7 @@ +--- +swc_core: patch +swc_html: patch +swc_html_minifier: patch +--- + +feat(html/minifier): Support using custom css minifier diff --git a/crates/swc_html/Cargo.toml b/crates/swc_html/Cargo.toml index 2a7ed138769e..6295b7ff9eae 100644 --- a/crates/swc_html/Cargo.toml +++ b/crates/swc_html/Cargo.toml @@ -24,6 +24,6 @@ minifier = ["swc_html_minifier"] [dependencies] swc_html_ast = { version = "0.37.0", path = "../swc_html_ast" } swc_html_codegen = { version = "0.46.0", path = "../swc_html_codegen" } -swc_html_minifier = { version = "0.143.0", path = "../swc_html_minifier", optional = true } +swc_html_minifier = { version = "0.143.0", path = "../swc_html_minifier", optional = true, default-features = false } swc_html_parser = { version = "0.43.0", path = "../swc_html_parser" } swc_html_visit = { version = "0.37.0", path = "../swc_html_visit" } diff --git a/crates/swc_html_minifier/Cargo.toml b/crates/swc_html_minifier/Cargo.toml index 0823cc3f37cf..b15549052b72 100644 --- a/crates/swc_html_minifier/Cargo.toml +++ b/crates/swc_html_minifier/Cargo.toml @@ -15,6 +15,11 @@ version = "0.143.0" [lib] bench = false +[features] +default = ["default-css-minifier"] +default-css-minifier = ["swc_css_ast", "swc_css_codegen", "swc_css_minifier", "swc_css_parser"] +custom-css-minifier = [] + [dependencies] once_cell = { workspace = true } serde = { workspace = true, features = ["derive"] } @@ -23,10 +28,10 @@ serde_json = { workspace = true } swc_atoms = { version = "0.6.5", path = "../swc_atoms" } swc_cached = { version = "0.3.19", path = "../swc_cached" } swc_common = { version = "0.37.0", path = "../swc_common" } -swc_css_ast = { version = "0.144.0", path = "../swc_css_ast" } -swc_css_codegen = { version = "0.155.0", path = "../swc_css_codegen" } -swc_css_minifier = { version = "0.120.0", path = "../swc_css_minifier" } -swc_css_parser = { version = "0.154.0", path = "../swc_css_parser" } +swc_css_ast = { version = "0.144.0", path = "../swc_css_ast", optional = true } +swc_css_codegen = { version = "0.155.0", path = "../swc_css_codegen", optional = true } +swc_css_minifier = { version = "0.120.0", path = "../swc_css_minifier", optional = true } +swc_css_parser = { version = "0.154.0", path = "../swc_css_parser", optional = true } swc_ecma_ast = { version = "0.118.0", path = "../swc_ecma_ast" } swc_ecma_codegen = { version = "0.155.0", path = "../swc_ecma_codegen", features = [ "serde-impl", diff --git a/crates/swc_html_minifier/src/lib.rs b/crates/swc_html_minifier/src/lib.rs index 0c68245a0c39..0d8db8172215 100644 --- a/crates/swc_html_minifier/src/lib.rs +++ b/crates/swc_html_minifier/src/lib.rs @@ -15,9 +15,11 @@ use swc_html_parser::parser::ParserConfig; use swc_html_utils::{HTML_ELEMENTS_AND_ATTRIBUTES, SVG_ELEMENTS_AND_ATTRIBUTES}; use swc_html_visit::{VisitMut, VisitMutWith}; +#[cfg(feature = "default-css-minifier")] +use crate::option::CssOptions; use crate::option::{ - CollapseWhitespaces, CssOptions, JsOptions, JsParserOptions, JsonOptions, MinifierType, - MinifyCssOption, MinifyJsOption, MinifyJsonOption, MinifyOptions, RemoveRedundantAttributes, + CollapseWhitespaces, JsOptions, JsParserOptions, JsonOptions, MinifierType, MinifyCssOption, + MinifyJsOption, MinifyJsonOption, MinifyOptions, RemoveRedundantAttributes, }; pub mod option; @@ -163,7 +165,7 @@ static SEMICOLON_SEPARATED_SVG_ATTRIBUTES: &[(&str, &str)] = &[ ("set", "end"), ]; -enum CssMinificationMode { +pub enum CssMinificationMode { Stylesheet, ListOfDeclarations, MediaQueryList, @@ -224,13 +226,15 @@ pub static CONDITIONAL_COMMENT_START: Lazy = pub static CONDITIONAL_COMMENT_END: Lazy = Lazy::new(|| CachedRegex::new("\\[endif]").unwrap()); -struct Minifier<'a> { - options: &'a MinifyOptions, +struct Minifier<'a, C: MinifyCss> { + options: &'a MinifyOptions, current_element: Option, latest_element: Option, descendant_of_pre: bool, attribute_name_counter: Option>, + + css_minifier: &'a C, } fn get_white_space(namespace: Namespace, tag_name: &str) -> WhiteSpace { @@ -243,7 +247,7 @@ fn get_white_space(namespace: Namespace, tag_name: &str) -> WhiteSpace { } } -impl Minifier<'_> { +impl Minifier<'_, C> { fn is_event_handler_attribute(&self, attribute: &Attribute) -> bool { matches!( &*attribute.name, @@ -2136,172 +2140,9 @@ impl Minifier<'_> { Some(minified) } - fn get_css_options(&self) -> CssOptions { - match &self.options.minify_css { - MinifyCssOption::Bool(_) => CssOptions { - parser: swc_css_parser::parser::ParserConfig::default(), - minifier: swc_css_minifier::options::MinifyOptions::default(), - codegen: swc_css_codegen::CodegenConfig::default(), - }, - MinifyCssOption::Options(css_options) => *css_options.clone(), - } - } - fn minify_css(&self, data: String, mode: CssMinificationMode) -> Option { - let mut errors: Vec<_> = Vec::new(); - - let cm = Lrc::new(SourceMap::new(FilePathMapping::empty())); - let fm = cm.new_source_file(FileName::Anon.into(), data); - - let mut options = self.get_css_options(); - - let mut stylesheet = match mode { - CssMinificationMode::Stylesheet => { - match swc_css_parser::parse_file(&fm, None, options.parser, &mut errors) { - Ok(stylesheet) => stylesheet, - _ => return None, - } - } - CssMinificationMode::ListOfDeclarations => { - match swc_css_parser::parse_file::>( - &fm, - None, - options.parser, - &mut errors, - ) { - Ok(list_of_declarations) => { - let declaration_list: Vec = - list_of_declarations - .into_iter() - .map(|node| node.into()) - .collect(); - - swc_css_ast::Stylesheet { - span: Default::default(), - rules: vec![swc_css_ast::Rule::QualifiedRule( - swc_css_ast::QualifiedRule { - span: Default::default(), - prelude: swc_css_ast::QualifiedRulePrelude::SelectorList( - swc_css_ast::SelectorList { - span: Default::default(), - children: Vec::new(), - }, - ), - block: swc_css_ast::SimpleBlock { - span: Default::default(), - name: swc_css_ast::TokenAndSpan { - span: DUMMY_SP, - token: swc_css_ast::Token::LBrace, - }, - value: declaration_list, - }, - } - .into(), - )], - } - } - _ => return None, - } - } - CssMinificationMode::MediaQueryList => { - match swc_css_parser::parse_file::( - &fm, - None, - options.parser, - &mut errors, - ) { - Ok(media_query_list) => swc_css_ast::Stylesheet { - span: Default::default(), - rules: vec![swc_css_ast::Rule::AtRule( - swc_css_ast::AtRule { - span: Default::default(), - name: swc_css_ast::AtRuleName::Ident(swc_css_ast::Ident { - span: Default::default(), - value: "media".into(), - raw: None, - }), - prelude: Some( - swc_css_ast::AtRulePrelude::MediaPrelude(media_query_list) - .into(), - ), - block: Some(swc_css_ast::SimpleBlock { - span: Default::default(), - name: swc_css_ast::TokenAndSpan { - span: DUMMY_SP, - token: swc_css_ast::Token::LBrace, - }, - // TODO make the `compress_empty` option for CSS minifier and - // remove it - value: vec![swc_css_ast::ComponentValue::Str(Box::new( - swc_css_ast::Str { - span: Default::default(), - value: "placeholder".into(), - raw: None, - }, - ))], - }), - } - .into(), - )], - }, - _ => return None, - } - } - }; - - // Avoid compress potential invalid CSS - if !errors.is_empty() { - return None; - } - - swc_css_minifier::minify(&mut stylesheet, options.minifier); - - let mut minified = String::new(); - let wr = swc_css_codegen::writer::basic::BasicCssWriter::new( - &mut minified, - None, - swc_css_codegen::writer::basic::BasicCssWriterConfig::default(), - ); - - options.codegen.minify = true; - - let mut gen = swc_css_codegen::CodeGenerator::new(wr, options.codegen); - - match mode { - CssMinificationMode::Stylesheet => { - swc_css_codegen::Emit::emit(&mut gen, &stylesheet).unwrap(); - } - CssMinificationMode::ListOfDeclarations => { - let swc_css_ast::Stylesheet { rules, .. } = &stylesheet; - - // Because CSS is grammar free, protect for fails - let Some(swc_css_ast::Rule::QualifiedRule(qualified_rule)) = rules.first() else { - return None; - }; - - let swc_css_ast::QualifiedRule { block, .. } = &**qualified_rule; - - swc_css_codegen::Emit::emit(&mut gen, &block).unwrap(); - - minified = minified[1..minified.len() - 1].to_string(); - } - CssMinificationMode::MediaQueryList => { - let swc_css_ast::Stylesheet { rules, .. } = &stylesheet; - - // Because CSS is grammar free, protect for fails - let Some(swc_css_ast::Rule::AtRule(at_rule)) = rules.first() else { - return None; - }; - - let swc_css_ast::AtRule { prelude, .. } = &**at_rule; - - swc_css_codegen::Emit::emit(&mut gen, &prelude).unwrap(); - - minified = minified.trim().to_string(); - } - } - - Some(minified) + self.css_minifier + .minify_css(&self.options.minify_css, data, mode) } fn minify_html(&self, data: String, mode: HtmlMinificationMode) -> Option { @@ -2362,13 +2203,16 @@ impl Minifier<'_> { match document_or_document_fragment { HtmlRoot::Document(ref mut document) => { - minify_document(document, self.options); + minify_document_with_custom_css_minifier(document, self.options, self.css_minifier); + } + HtmlRoot::DocumentFragment(ref mut document_fragment) => { + minify_document_fragment_with_custom_css_minifier( + document_fragment, + context_element.as_ref().unwrap(), + self.options, + self.css_minifier, + ) } - HtmlRoot::DocumentFragment(ref mut document_fragment) => minify_document_fragment( - document_fragment, - context_element.as_ref().unwrap(), - self.options, - ), } let mut minified = String::new(); @@ -2647,7 +2491,7 @@ impl Minifier<'_> { } } -impl VisitMut for Minifier<'_> { +impl VisitMut for Minifier<'_, C> { fn visit_mut_document(&mut self, n: &mut Document) { n.visit_mut_children_with(self); @@ -2990,10 +2834,205 @@ impl VisitMut for AttributeNameCounter { } } -fn create_minifier<'a>( +pub trait MinifyCss { + type Options; + fn minify_css( + &self, + options: &MinifyCssOption, + data: String, + mode: CssMinificationMode, + ) -> Option; +} + +#[cfg(feature = "default-css-minifier")] +struct DefaultCssMinifier; + +#[cfg(feature = "default-css-minifier")] +impl DefaultCssMinifier { + fn get_css_options(&self, options: &MinifyCssOption) -> CssOptions { + match options { + MinifyCssOption::Bool(_) => CssOptions { + parser: swc_css_parser::parser::ParserConfig::default(), + minifier: swc_css_minifier::options::MinifyOptions::default(), + codegen: swc_css_codegen::CodegenConfig::default(), + }, + MinifyCssOption::Options(css_options) => css_options.clone(), + } + } +} + +#[cfg(feature = "default-css-minifier")] +impl MinifyCss for DefaultCssMinifier { + type Options = CssOptions; + + fn minify_css( + &self, + options: &MinifyCssOption, + data: String, + mode: CssMinificationMode, + ) -> Option { + let mut errors: Vec<_> = Vec::new(); + + let cm = Lrc::new(SourceMap::new(FilePathMapping::empty())); + let fm = cm.new_source_file(FileName::Anon.into(), data); + + let mut options = self.get_css_options(options); + + let mut stylesheet = match mode { + CssMinificationMode::Stylesheet => { + match swc_css_parser::parse_file(&fm, None, options.parser, &mut errors) { + Ok(stylesheet) => stylesheet, + _ => return None, + } + } + CssMinificationMode::ListOfDeclarations => { + match swc_css_parser::parse_file::>( + &fm, + None, + options.parser, + &mut errors, + ) { + Ok(list_of_declarations) => { + let declaration_list: Vec = + list_of_declarations + .into_iter() + .map(|node| node.into()) + .collect(); + + swc_css_ast::Stylesheet { + span: Default::default(), + rules: vec![swc_css_ast::Rule::QualifiedRule( + swc_css_ast::QualifiedRule { + span: Default::default(), + prelude: swc_css_ast::QualifiedRulePrelude::SelectorList( + swc_css_ast::SelectorList { + span: Default::default(), + children: Vec::new(), + }, + ), + block: swc_css_ast::SimpleBlock { + span: Default::default(), + name: swc_css_ast::TokenAndSpan { + span: DUMMY_SP, + token: swc_css_ast::Token::LBrace, + }, + value: declaration_list, + }, + } + .into(), + )], + } + } + _ => return None, + } + } + CssMinificationMode::MediaQueryList => { + match swc_css_parser::parse_file::( + &fm, + None, + options.parser, + &mut errors, + ) { + Ok(media_query_list) => swc_css_ast::Stylesheet { + span: Default::default(), + rules: vec![swc_css_ast::Rule::AtRule( + swc_css_ast::AtRule { + span: Default::default(), + name: swc_css_ast::AtRuleName::Ident(swc_css_ast::Ident { + span: Default::default(), + value: "media".into(), + raw: None, + }), + prelude: Some( + swc_css_ast::AtRulePrelude::MediaPrelude(media_query_list) + .into(), + ), + block: Some(swc_css_ast::SimpleBlock { + span: Default::default(), + name: swc_css_ast::TokenAndSpan { + span: DUMMY_SP, + token: swc_css_ast::Token::LBrace, + }, + // TODO make the `compress_empty` option for CSS minifier and + // remove it + value: vec![swc_css_ast::ComponentValue::Str(Box::new( + swc_css_ast::Str { + span: Default::default(), + value: "placeholder".into(), + raw: None, + }, + ))], + }), + } + .into(), + )], + }, + _ => return None, + } + } + }; + + // Avoid compress potential invalid CSS + if !errors.is_empty() { + return None; + } + + swc_css_minifier::minify(&mut stylesheet, options.minifier); + + let mut minified = String::new(); + let wr = swc_css_codegen::writer::basic::BasicCssWriter::new( + &mut minified, + None, + swc_css_codegen::writer::basic::BasicCssWriterConfig::default(), + ); + + options.codegen.minify = true; + + let mut gen = swc_css_codegen::CodeGenerator::new(wr, options.codegen); + + match mode { + CssMinificationMode::Stylesheet => { + swc_css_codegen::Emit::emit(&mut gen, &stylesheet).unwrap(); + } + CssMinificationMode::ListOfDeclarations => { + let swc_css_ast::Stylesheet { rules, .. } = &stylesheet; + + // Because CSS is grammar free, protect for fails + let Some(swc_css_ast::Rule::QualifiedRule(qualified_rule)) = rules.first() else { + return None; + }; + + let swc_css_ast::QualifiedRule { block, .. } = &**qualified_rule; + + swc_css_codegen::Emit::emit(&mut gen, &block).unwrap(); + + minified = minified[1..minified.len() - 1].to_string(); + } + CssMinificationMode::MediaQueryList => { + let swc_css_ast::Stylesheet { rules, .. } = &stylesheet; + + // Because CSS is grammar free, protect for fails + let Some(swc_css_ast::Rule::AtRule(at_rule)) = rules.first() else { + return None; + }; + + let swc_css_ast::AtRule { prelude, .. } = &**at_rule; + + swc_css_codegen::Emit::emit(&mut gen, &prelude).unwrap(); + + minified = minified.trim().to_string(); + } + } + + Some(minified) + } +} + +fn create_minifier<'a, C: MinifyCss>( context_element: Option<&Element>, - options: &'a MinifyOptions, -) -> Minifier<'a> { + options: &'a MinifyOptions, + css_minifier: &'a C, +) -> Minifier<'a, C> { let mut current_element = None; let mut is_pre = false; @@ -3010,11 +3049,17 @@ fn create_minifier<'a>( latest_element: None, descendant_of_pre: is_pre, attribute_name_counter: None, + + css_minifier, } } -pub fn minify_document(document: &mut Document, options: &MinifyOptions) { - let mut minifier = create_minifier(None, options); +pub fn minify_document_with_custom_css_minifier( + document: &mut Document, + options: &MinifyOptions, + css_minifier: &C, +) { + let mut minifier = create_minifier(None, options, css_minifier); if options.sort_attributes { let mut attribute_name_counter = AttributeNameCounter { @@ -3029,12 +3074,13 @@ pub fn minify_document(document: &mut Document, options: &MinifyOptions) { document.visit_mut_with(&mut minifier); } -pub fn minify_document_fragment( +pub fn minify_document_fragment_with_custom_css_minifier( document_fragment: &mut DocumentFragment, context_element: &Element, - options: &MinifyOptions, + options: &MinifyOptions, + css_minifier: &C, ) { - let mut minifier = create_minifier(Some(context_element), options); + let mut minifier = create_minifier(Some(context_element), options, css_minifier); if options.sort_attributes { let mut attribute_name_counter = AttributeNameCounter { @@ -3048,3 +3094,22 @@ pub fn minify_document_fragment( document_fragment.visit_mut_with(&mut minifier); } + +#[cfg(feature = "default-css-minifier")] +pub fn minify_document(document: &mut Document, options: &MinifyOptions) { + minify_document_with_custom_css_minifier(document, options, &DefaultCssMinifier) +} + +#[cfg(feature = "default-css-minifier")] +pub fn minify_document_fragment( + document_fragment: &mut DocumentFragment, + context_element: &Element, + options: &MinifyOptions, +) { + minify_document_fragment_with_custom_css_minifier( + document_fragment, + context_element, + options, + &DefaultCssMinifier, + ) +} diff --git a/crates/swc_html_minifier/src/option.rs b/crates/swc_html_minifier/src/option.rs index 6003e63ad035..187b137388fc 100644 --- a/crates/swc_html_minifier/src/option.rs +++ b/crates/swc_html_minifier/src/option.rs @@ -1,7 +1,10 @@ -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use swc_cached::regex::CachedRegex; +#[cfg(feature = "default-css-minifier")] use swc_css_codegen::CodegenConfig as CssCodegenOptions; +#[cfg(feature = "default-css-minifier")] use swc_css_minifier::options::MinifyOptions as CssMinifyOptions; +#[cfg(feature = "default-css-minifier")] use swc_css_parser::parser::ParserConfig as CssParserOptions; use swc_ecma_ast::EsVersion; use swc_ecma_codegen::Config as JsCodegenOptions; @@ -109,11 +112,12 @@ pub struct JsParserOptions { #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(deny_unknown_fields)] #[serde(untagged)] -pub enum MinifyCssOption { +pub enum MinifyCssOption { Bool(bool), - Options(Box), + Options(CO), } +#[cfg(feature = "default-css-minifier")] #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] @@ -129,7 +133,7 @@ pub struct CssOptions { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] -pub struct MinifyOptions { +pub struct MinifyOptions { #[serde(default)] pub force_set_html5_doctype: bool, #[serde(default)] @@ -170,7 +174,7 @@ pub struct MinifyOptions { #[serde(default = "minify_js_by_default")] pub minify_js: MinifyJsOption, #[serde(default = "minify_css_by_default")] - pub minify_css: MinifyCssOption, + pub minify_css: MinifyCssOption, // Allow to compress value of custom script elements, // i.e. `` // @@ -194,7 +198,7 @@ pub struct MinifyOptions { } /// Implement default using serde. -impl Default for MinifyOptions { +impl Default for MinifyOptions { fn default() -> Self { serde_json::from_value(serde_json::Value::Object(Default::default())).unwrap() } @@ -212,7 +216,7 @@ const fn minify_js_by_default() -> MinifyJsOption { MinifyJsOption::Bool(true) } -const fn minify_css_by_default() -> MinifyCssOption { +const fn minify_css_by_default() -> MinifyCssOption { MinifyCssOption::Bool(true) }