Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(html/minifier): merge bugs + merge modules #6393

Merged
merged 6 commits into from Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
242 changes: 219 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,168 @@ 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 left_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(
left_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_unresolved_mark = Mark::new();
alexander-akait marked this conversation as resolved.
Show resolved Hide resolved
let right_top_level_mark = Mark::new();

swc_ecma_visit::VisitMutWith::visit_mut_with(
&mut right_program,
&mut swc_ecma_transforms_base::resolver(
right_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(
kdy1 marked this conversation as resolved.
Show resolved Hide resolved
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 +2161,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 +2170,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 +2187,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