mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-15 06:39:17 +00:00
fix(codegen): remove redundant dollar escapes in minified templates (#26924)
Remove redundant dollar escapes from untagged templates when minifying:
```js
// Before
`^${pattern}\$`
// After
`^${pattern}$`
```
Preserve escapes that prevent `${` interpolation and account for escaped backslashes. Tagged templates retain their raw spelling, and non-minified output keeps its existing behavior.
The normal minsize corpus shrinks by 11 raw bytes and 8 gzip bytes across Ant Design and TypeScript; Brotli grows by 2 bytes.
This commit is contained in:
1 parent
2bc4765cc0
commit
d79ad19796
5 files changed
+128
-2
No files matched your search
@@ -450,11 +450,39 @@ impl<'a> Codegen<'a> {
|
||||
unsafe { self.code.print_bytes_unchecked(&bytes) };
|
||||
}
|
||||
|
||||
/// Print a template literal quasi's raw text. Only untagged templates are escaped under
|
||||
/// Print a template literal quasi's raw text, removing redundant dollar escapes when minifying
|
||||
/// untagged templates. Only untagged templates are escaped under
|
||||
/// [CodegenOptions::ascii_only]: a tag function (e.g. `String.raw`) can observe the raw
|
||||
/// text, which escaping would change.
|
||||
#[inline]
|
||||
pub(crate) fn print_template_quasi_raw(&mut self, raw: &str, tagged: bool) {
|
||||
let mut start = 0;
|
||||
if self.options.minify && !tagged {
|
||||
let bytes = raw.as_bytes();
|
||||
for (index, _) in raw.match_indices("\\$") {
|
||||
// Keep escapes which prevent interpolation.
|
||||
if bytes.get(index + 2) == Some(&b'{') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep backslashes which are themselves escaped.
|
||||
let preceding_backslashes =
|
||||
bytes[..index].iter().rev().take_while(|&&b| b == b'\\').count();
|
||||
if preceding_backslashes % 2 != 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The preserved chunk ends with an even backslash run, and the next starts
|
||||
// with `$`, so printing them separately preserves escape handling.
|
||||
self.print_template_quasi_raw_chunk(&raw[start..index], tagged);
|
||||
start = index + 1;
|
||||
}
|
||||
}
|
||||
self.print_template_quasi_raw_chunk(&raw[start..], tagged);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn print_template_quasi_raw_chunk(&mut self, raw: &str, tagged: bool) {
|
||||
if !self.options.ascii_only || tagged || raw.is_ascii() {
|
||||
self.print_str_escaping_script_close_tag(raw);
|
||||
} else {
|
||||
|
||||
@@ -1133,3 +1133,47 @@ fn html_comments() {
|
||||
"const x = 1;\n--> comment\nconst y = 2;\n",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn template_literal_dollar_escapes() {
|
||||
for (source, expected) in [
|
||||
(r"`\$`;", r"`$`;"),
|
||||
(r"`^${pattern}\$`;", r"`^${pattern}$`;"),
|
||||
(r"`\$a\$${value}\$`;", r"`$a$${value}$`;"),
|
||||
(r"`\${value}`;", r"`\${value}`;"),
|
||||
(r"`\$\{value}`;", r"`$\{value}`;"),
|
||||
(r"`\$\u007bvalue}`;", r"`$\u007bvalue}`;"),
|
||||
(r"`\\$`;", r"`\\$`;"),
|
||||
(r"`\\\$`;", r"`\\$`;"),
|
||||
(r"`\\\\$`;", r"`\\\\$`;"),
|
||||
(r"`\\\\\$`;", r"`\\\\$`;"),
|
||||
(r"`\\\${value}`;", r"`\\\${value}`;"),
|
||||
(r"`\\${value}\$`;", r"`\\${value}$`;"),
|
||||
(r"`\$${{value}}\$`;", r"`$${{value}}$`;"),
|
||||
(r"tag`\$${value}\$`;", r"tag`\$${value}\$`;"),
|
||||
(r"String.raw`\$${value}\$`;", r"String.raw`\$${value}\$`;"),
|
||||
(r"tag`\$${`\$`}\$`;", r"tag`\$${`$`}\$`;"),
|
||||
] {
|
||||
test_minify(source, expected);
|
||||
test_minify_same(expected);
|
||||
}
|
||||
test_same("`^${pattern}\\$`;\n");
|
||||
test_same("tag`\\$`;\n");
|
||||
|
||||
for ascii_only in [false, true] {
|
||||
test_options(
|
||||
r"`é\$</script>${value}\$é`;",
|
||||
if ascii_only {
|
||||
r"`\u00E9$<\/script>${value}$\u00E9`;"
|
||||
} else {
|
||||
r"`é$<\/script>${value}$é`;"
|
||||
},
|
||||
CodegenOptions { minify: true, ascii_only, ..CodegenOptions::default() },
|
||||
);
|
||||
test_options(
|
||||
r"String.raw`é\$`;",
|
||||
r"String.raw`é\$`;",
|
||||
CodegenOptions { minify: true, ascii_only, ..CodegenOptions::default() },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -530,3 +530,28 @@ fn execute_with_node(code: &str, sourcemap_url: &str) -> String {
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minified_template_dollar_escapes() {
|
||||
let source = r"`\$${first}\$${second}\$`;after();";
|
||||
let allocator = Allocator::default();
|
||||
let ret = Parser::new(&allocator, source, SourceType::mjs()).parse();
|
||||
assert!(ret.diagnostics.is_empty());
|
||||
let result = Codegen::new()
|
||||
.with_options(CodegenOptions { minify: true, ..default_options() })
|
||||
.build(&ret.program);
|
||||
assert_eq!(result.code, r"`$${first}$${second}$`;after();");
|
||||
let map = result.map.unwrap();
|
||||
for name in ["first", "second", "after"] {
|
||||
assert!(
|
||||
map.get_tokens().any(|token| {
|
||||
token.get_src_line() == 0
|
||||
&& token.get_dst_line() == 0
|
||||
&& token.get_src_col() == u32::try_from(source.find(name).unwrap()).unwrap()
|
||||
&& token.get_dst_col()
|
||||
== u32::try_from(result.code.find(name).unwrap()).unwrap()
|
||||
}),
|
||||
"missing mapping for {name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -167,3 +167,32 @@ fn run_with_iterations(
|
||||
.code;
|
||||
(code, iterations)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minified_template_dollar_escapes() {
|
||||
fn minify(source: &str) -> String {
|
||||
let allocator = Allocator::default();
|
||||
let ret = Parser::new(&allocator, source, SourceType::mjs()).parse();
|
||||
assert!(ret.diagnostics.is_empty());
|
||||
let mut program = ret.program;
|
||||
let ret = oxc_minifier::Minifier::new(oxc_minifier::MinifierOptions::default())
|
||||
.minify(&allocator, &mut program);
|
||||
Codegen::new()
|
||||
.with_options(CodegenOptions::minify())
|
||||
.with_scoping(ret.scoping)
|
||||
.build(&program)
|
||||
.code
|
||||
}
|
||||
|
||||
for (source, expected) in [
|
||||
(r"use(`^${pattern}\$`)", r"use(`^${pattern}$`);"),
|
||||
(r"use(`\$${pattern}\$`)", r"use(`$${pattern}$`);"),
|
||||
(r"use(`\${pattern}${suffix}`)", r"use(`\${pattern}${suffix}`);"),
|
||||
(r"use(String.raw`\$${pattern}\$`)", r"use(String.raw`\$${pattern}\$`);"),
|
||||
(r"use(`\\\$${pattern}\$`)", r"use(`\\$${pattern}$`);"),
|
||||
] {
|
||||
let output = minify(source);
|
||||
assert_eq!(output, expected);
|
||||
assert_eq!(minify(&output), output);
|
||||
}
|
||||
}
|
||||
@@ -23,5 +23,5 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
|
||||
|
||||
6.69 MB | 2.12 MB | 2.31 MB | 453.42 kB | 488.28 kB | 4 | antd.js
|
||||
|
||||
10.95 MB | 3.31 MB | 3.49 MB | 849.97 kB | 915.50 kB | 4 | typescript.js
|
||||
10.95 MB | 3.31 MB | 3.49 MB | 849.96 kB | 915.50 kB | 4 | typescript.js
|
||||
|
||||
Reference in new issue
Block a user