Skip to content

Commit

Permalink
fix(html/minifier): Fix merging of scripts (#6393)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Nov 11, 2022
1 parent 6ba1f5c commit a923e52
Show file tree
Hide file tree
Showing 6 changed files with 389 additions and 33 deletions.
233 changes: 210 additions & 23 deletions crates/swc_html_minifier/src/lib.rs
Expand Up @@ -1361,9 +1361,13 @@ impl Minifier<'_> {
js_word!("type") => {
if let Some(value) = &attribute.value {
if (is_style_tag && value.trim().to_ascii_lowercase() == "text/css")
|| is_script_tag && self.is_type_text_javascript(value)
|| (is_script_tag && self.is_type_text_javascript(value))
{
false
} else if is_script_tag
&& value.trim().to_ascii_lowercase() == "module"
{
true
} else {
need_skip = true;

Expand Down Expand Up @@ -1402,6 +1406,10 @@ impl Minifier<'_> {
|| (is_script_tag && self.is_type_text_javascript(value))
{
false
} else if is_script_tag
&& value.trim().to_ascii_lowercase() == "module"
{
true
} else {
need_skip = true;

Expand Down Expand Up @@ -1434,37 +1442,56 @@ impl Minifier<'_> {
false
}

fn merge_text_children(&self, left: &Element, right: &Element) -> Vec<Child> {
fn merge_text_children(&self, left: &Element, right: &Element) -> Option<Vec<Child>> {
let is_script_tag = matches!(left.namespace, Namespace::HTML | Namespace::SVG)
&& left.tag_name == js_word!("script")
&& matches!(right.namespace, Namespace::HTML | Namespace::SVG)
&& right.tag_name == js_word!("script");

let data = left.children.iter().chain(right.children.iter()).fold(
String::new(),
|mut acc, child| match child {
Child::Text(text) if text.data.len() > 0 => {
acc.push_str(&text.data);
// `script`/`style` elements should have only one text child
let left_data = match left.children.get(0) {
Some(Child::Text(left)) => left.data.to_string(),
None => String::new(),
_ => return None,
};

if is_script_tag {
acc.push(';');
}
let right_data = match right.children.get(0) {
Some(Child::Text(right)) => right.data.to_string(),
None => String::new(),
_ => return None,
};

let mut data = String::with_capacity(left_data.len() + right_data.len());

if is_script_tag {
let is_modules = if is_script_tag {
left.attributes.iter().any(|attribute| matches!(&attribute.value, Some(value) if value.trim().to_ascii_lowercase() == "module"))
} else {
false
};

acc
match self.merge_js(left_data, right_data, is_modules) {
Some(minified) => {
data.push_str(&minified);
}
_ => acc,
},
);
_ => {
return None;
}
}
} else {
data.push_str(&left_data);
data.push_str(&right_data);
}

if data.is_empty() {
return vec![];
return Some(vec![]);
}

vec![Child::Text(Text {
Some(vec![Child::Text(Text {
span: DUMMY_SP,
data: data.into(),
raw: None,
})]
})])
}

fn minify_children(&mut self, children: &mut Vec<Child>) -> Vec<Child> {
Expand All @@ -1490,10 +1517,16 @@ impl Minifier<'_> {
&& self.allow_elements_to_merge(prev_children.last(), element) =>
{
if let Some(Child::Element(prev)) = prev_children.last_mut() {
prev.children = self.merge_text_children(prev, element);
}
if let Some(children) = self.merge_text_children(prev, element) {
prev.children = children;

false
false
} else {
true
}
} else {
true
}
}
Child::Text(text) if text.data.is_empty() => false,
Child::Text(text)
Expand Down Expand Up @@ -1915,6 +1948,159 @@ impl Minifier<'_> {
}
}

fn merge_js(&self, left: String, right: String, is_modules: bool) -> Option<String> {
let comments = SingleThreadedComments::default();
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));

// Left
let mut left_errors: Vec<_> = vec![];
let left_fm = cm.new_source_file(FileName::Anon, left);
let syntax = swc_ecma_parser::Syntax::default();
// TODO improve me using options
let target = swc_ecma_ast::EsVersion::default();

let mut left_program = if is_modules {
match swc_ecma_parser::parse_file_as_module(
&left_fm,
syntax,
target,
Some(&comments),
&mut left_errors,
) {
Ok(module) => swc_ecma_ast::Program::Module(module),
_ => return None,
}
} else {
match swc_ecma_parser::parse_file_as_script(
&left_fm,
syntax,
target,
Some(&comments),
&mut left_errors,
) {
Ok(script) => swc_ecma_ast::Program::Script(script),
_ => return None,
}
};

// Avoid compress potential invalid JS
if !left_errors.is_empty() {
return None;
}

let unresolved_mark = Mark::new();
let left_top_level_mark = Mark::new();

swc_ecma_visit::VisitMutWith::visit_mut_with(
&mut left_program,
&mut swc_ecma_transforms_base::resolver(unresolved_mark, left_top_level_mark, false),
);

// Right
let mut right_errors: Vec<_> = vec![];
let right_fm = cm.new_source_file(FileName::Anon, right);

let mut right_program = if is_modules {
match swc_ecma_parser::parse_file_as_module(
&right_fm,
syntax,
target,
Some(&comments),
&mut right_errors,
) {
Ok(module) => swc_ecma_ast::Program::Module(module),
_ => return None,
}
} else {
match swc_ecma_parser::parse_file_as_script(
&right_fm,
syntax,
target,
Some(&comments),
&mut right_errors,
) {
Ok(script) => swc_ecma_ast::Program::Script(script),
_ => return None,
}
};

// Avoid compress potential invalid JS
if !right_errors.is_empty() {
return None;
}

let right_top_level_mark = Mark::new();

swc_ecma_visit::VisitMutWith::visit_mut_with(
&mut right_program,
&mut swc_ecma_transforms_base::resolver(unresolved_mark, right_top_level_mark, false),
);

// Merge
match &mut left_program {
swc_ecma_ast::Program::Module(left_program) => match right_program {
swc_ecma_ast::Program::Module(right_program) => {
left_program.body.extend(right_program.body);
}
_ => {
unreachable!();
}
},
swc_ecma_ast::Program::Script(left_program) => match right_program {
swc_ecma_ast::Program::Script(right_program) => {
left_program.body.extend(right_program.body);
}
_ => {
unreachable!();
}
},
}

if is_modules {
swc_ecma_visit::VisitMutWith::visit_mut_with(
&mut left_program,
&mut swc_ecma_transforms_base::hygiene::hygiene(),
);
}

let left_program = swc_ecma_visit::FoldWith::fold_with(
left_program,
&mut swc_ecma_transforms_base::fixer::fixer(Some(&comments)),
);

let mut buf = vec![];

{
let wr = Box::new(swc_ecma_codegen::text_writer::JsWriter::new(
cm.clone(),
"\n",
&mut buf,
None,
)) as Box<dyn swc_ecma_codegen::text_writer::WriteJs>;

let mut emitter = swc_ecma_codegen::Emitter {
cfg: swc_ecma_codegen::Config {
target,
minify: false,
ascii_only: false,
omit_last_semi: false,
},
cm,
comments: Some(&comments),
wr,
};

emitter.emit_program(&left_program).unwrap();
}

let code = match String::from_utf8(buf) {
Ok(minified) => minified,
_ => return None,
};

Some(code)
}

// TODO source map url output for JS and CSS?
fn minify_js(&self, data: String, is_module: bool, is_attribute: bool) -> Option<String> {
let mut errors: Vec<_> = vec![];
Expand Down Expand Up @@ -1966,9 +2152,6 @@ impl Minifier<'_> {
return None;
}

let unresolved_mark = Mark::new();
let top_level_mark = Mark::new();

if let Some(compress_options) = &mut options.minifier.compress {
compress_options.module = is_module;
} else {
Expand All @@ -1978,6 +2161,9 @@ impl Minifier<'_> {
});
}

let unresolved_mark = Mark::new();
let top_level_mark = Mark::new();

swc_ecma_visit::VisitMutWith::visit_mut_with(
&mut program,
&mut swc_ecma_transforms_base::resolver(unresolved_mark, top_level_mark, false),
Expand All @@ -1992,6 +2178,7 @@ impl Minifier<'_> {
None
},
None,
// TODO allow to keep `var`/function/etc on top level
&options.minifier,
&swc_ecma_minifier::option::ExtraOptions {
unresolved_mark,
Expand Down
Expand Up @@ -19,4 +19,4 @@

<tabbed-custom-element-exportparts></tabbed-custom-element-exportparts>

<script type=module>globalThis.customElements.define("tabbed-custom-element",class extends HTMLElement{constructor(){super();let t=document.getElementById("tabbed-custom-element").content,e=this.attachShadow({mode:"open"});e.appendChild(t.cloneNode(!0));let o=[];for(let a of this.shadowRoot.children)a.getAttribute("part")&&o.push(a);o.forEach(t=>{t.addEventListener("click",t=>{o.forEach(t=>{t.part="tab"}),t.target.part="tab active"})})}})</script><script type=module>globalThis.customElements.define("tabbed-custom-element-exportparts",class extends HTMLElement{constructor(){super();let e=document.getElementById("tabbed-custom-element-exportparts").content,t=this.attachShadow({mode:"open"});t.appendChild(e.cloneNode(!0))}})</script>
<script type=module>globalThis.customElements.define("tabbed-custom-element",class extends HTMLElement{constructor(){super();let e=document.getElementById("tabbed-custom-element").content,t=this.attachShadow({mode:"open"});t.appendChild(e.cloneNode(!0));let o=[];for(let n of this.shadowRoot.children)n.getAttribute("part")&&o.push(n);o.forEach(e=>{e.addEventListener("click",e=>{o.forEach(e=>{e.part="tab"}),e.target.part="tab active"})})}}),globalThis.customElements.define("tabbed-custom-element-exportparts",class extends HTMLElement{constructor(){super();let e=document.getElementById("tabbed-custom-element-exportparts").content,t=this.attachShadow({mode:"open"});t.appendChild(e.cloneNode(!0))}})</script>
@@ -1,4 +1,4 @@
<!doctype html><script defer>console.log()</script><script>console.log();console.log()</script><script type=module>console.log()</script><script type=module>console.log()</script><script>window.jQuery||document.write('<script src="jquery.js"><\/script>')</script><script type=text/html>
<!doctype html><script defer>console.log()</script><script>console.log();console.log()</script><script type=module>console.log(),console.log()</script><script>window.jQuery||document.write('<script src="jquery.js"><\/script>')</script><script type=text/html>
<div>
test
</div>
Expand Down

1 comment on commit a923e52

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: a923e52 Previous: 983ab91 Ratio
es/full/bugs-1 339760 ns/iter (± 29667) 356255 ns/iter (± 24408) 0.95
es/full/minify/libraries/antd 1862972713 ns/iter (± 118800862) 2003249507 ns/iter (± 211392434) 0.93
es/full/minify/libraries/d3 402893150 ns/iter (± 9900855) 454571424 ns/iter (± 9989609) 0.89
es/full/minify/libraries/echarts 1546061253 ns/iter (± 47313197) 1737998805 ns/iter (± 98652919) 0.89
es/full/minify/libraries/jquery 113429340 ns/iter (± 6650594) 126268406 ns/iter (± 12250100) 0.90
es/full/minify/libraries/lodash 126340803 ns/iter (± 8244402) 145958685 ns/iter (± 24379533) 0.87
es/full/minify/libraries/moment 56536394 ns/iter (± 2372862) 68587180 ns/iter (± 3556299) 0.82
es/full/minify/libraries/react 20891668 ns/iter (± 2308788) 22890375 ns/iter (± 1376615) 0.91
es/full/minify/libraries/terser 298594477 ns/iter (± 14043327) 389607601 ns/iter (± 29163082) 0.77
es/full/minify/libraries/three 534832770 ns/iter (± 7461951) 703295293 ns/iter (± 42922780) 0.76
es/full/minify/libraries/typescript 3249927880 ns/iter (± 31259965) 4166372931 ns/iter (± 131962044) 0.78
es/full/minify/libraries/victory 790459378 ns/iter (± 5084067) 1003754335 ns/iter (± 84374129) 0.79
es/full/minify/libraries/vue 147892896 ns/iter (± 3984353) 198735824 ns/iter (± 4721656) 0.74
es/full/codegen/es3 32342 ns/iter (± 4966) 44902 ns/iter (± 9074) 0.72
es/full/codegen/es5 32034 ns/iter (± 983) 41655 ns/iter (± 7610) 0.77
es/full/codegen/es2015 31792 ns/iter (± 830) 41805 ns/iter (± 7207) 0.76
es/full/codegen/es2016 31843 ns/iter (± 568) 39963 ns/iter (± 6657) 0.80
es/full/codegen/es2017 32512 ns/iter (± 605) 37192 ns/iter (± 6196) 0.87
es/full/codegen/es2018 32445 ns/iter (± 737) 42498 ns/iter (± 6707) 0.76
es/full/codegen/es2019 32450 ns/iter (± 957) 42543 ns/iter (± 7398) 0.76
es/full/codegen/es2020 32533 ns/iter (± 4204) 42383 ns/iter (± 6476) 0.77
es/full/all/es3 192048262 ns/iter (± 4749812) 256975639 ns/iter (± 30629428) 0.75
es/full/all/es5 180742751 ns/iter (± 5640613) 211682658 ns/iter (± 31223245) 0.85
es/full/all/es2015 146059597 ns/iter (± 5082392) 164726057 ns/iter (± 12589875) 0.89
es/full/all/es2016 143688339 ns/iter (± 5966048) 195908805 ns/iter (± 23808681) 0.73
es/full/all/es2017 143697361 ns/iter (± 4233556) 177297202 ns/iter (± 17993106) 0.81
es/full/all/es2018 140383340 ns/iter (± 7355881) 165575616 ns/iter (± 24198719) 0.85
es/full/all/es2019 141239262 ns/iter (± 4895576) 160172709 ns/iter (± 13845862) 0.88
es/full/all/es2020 141377290 ns/iter (± 7092235) 157364425 ns/iter (± 17619799) 0.90
es/full/parser 722484 ns/iter (± 33752) 789448 ns/iter (± 110821) 0.92
es/full/base/fixer 26488 ns/iter (± 1332) 29180 ns/iter (± 3957) 0.91
es/full/base/resolver_and_hygiene 91343 ns/iter (± 1856) 98298 ns/iter (± 14349) 0.93
serialization of ast node 213 ns/iter (± 2) 254 ns/iter (± 43) 0.84
serialization of serde 213 ns/iter (± 1) 243 ns/iter (± 41) 0.88

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.