Skip to content

feat(es/minifier): support reduce_escaped_newline #10232

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

Merged
merged 11 commits into from
Mar 31, 2025
9 changes: 8 additions & 1 deletion crates/swc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -914,7 +914,14 @@ impl Compiler {
.with_emit_assert_for_import_attributes(
opts.format.emit_assert_for_import_attributes,
)
.with_inline_script(opts.format.inline_script),
.with_inline_script(opts.format.inline_script)
.with_reduce_escaped_newline(
min_opts
.compress
.unwrap_or_default()
.experimental
.reduce_escaped_newline,
),
output: None,
},
);
22 changes: 22 additions & 0 deletions crates/swc_ecma_codegen/src/config.rs
Original file line number Diff line number Diff line change
@@ -2,6 +2,11 @@
use serde::{Deserialize, Serialize};
use swc_ecma_ast::EsVersion;

#[cfg(feature = "serde-impl")]
const fn true_by_default() -> bool {
true
}

#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde-impl", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde-impl", serde(rename_all = "camelCase"))]
@@ -39,6 +44,13 @@ pub struct Config {

#[cfg_attr(feature = "serde-impl", serde(default))]
pub inline_script: bool,

/// Transform escaped newline `'\n'` of `TplElement` to a newline character
/// to reduce text size of javascript code
///
/// Defaults to `true`
#[cfg_attr(feature = "serde-impl", serde(default = "true_by_default"))]
pub reduce_escaped_newline: bool,
}

impl Default for Config {
@@ -50,6 +62,7 @@ impl Default for Config {
omit_last_semi: false,
emit_assert_for_import_attributes: false,
inline_script: false,
reduce_escaped_newline: true,
}
}
}
@@ -87,4 +100,13 @@ impl Config {
self.inline_script = inline_script;
self
}

/// Transform escaped newline `'\n'` of `TplElement` to a newline character
/// to reduce text size of javascript code
///
/// Defaults to `true`
pub fn with_reduce_escaped_newline(mut self, reduce_escaped_newline: bool) -> Self {
self.reduce_escaped_newline = reduce_escaped_newline;
self
}
}
23 changes: 20 additions & 3 deletions crates/swc_ecma_codegen/src/lib.rs
Original file line number Diff line number Diff line change
@@ -2023,7 +2023,11 @@ where
fn emit_quasi(&mut self, node: &TplElement) -> Result {
let raw = node.raw.replace("\r\n", "\n").replace('\r', "\n");
if self.cfg.minify || (self.cfg.ascii_only && !node.raw.is_ascii()) {
let v = get_template_element_from_raw(&raw, self.cfg.ascii_only);
let v = get_template_element_from_raw(
&raw,
self.cfg.ascii_only,
self.cfg.reduce_escaped_newline,
);
let span = node.span();

let mut last_offset_gen = 0;
@@ -3811,7 +3815,11 @@ where
}
}

fn get_template_element_from_raw(s: &str, ascii_only: bool) -> String {
fn get_template_element_from_raw(
s: &str,
ascii_only: bool,
reduce_escaped_newline: bool,
) -> String {
fn read_escaped(
radix: u32,
len: Option<usize>,
@@ -3884,7 +3892,16 @@ fn get_template_element_from_raw(s: &str, ascii_only: bool) -> String {
let unescape = match c {
'\\' => match iter.next() {
Some(c) => match c {
'n' => Some('\n'),
'n' => {
if reduce_escaped_newline {
Some('\n')
} else {
buf.push('\\');
buf.push('n');

None
}
}
't' => Some('\t'),
'x' => {
read_escaped(16, Some(2), &mut buf, &mut iter);
47 changes: 45 additions & 2 deletions crates/swc_ecma_codegen/tests/fixture.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use std::path::{Path, PathBuf};
use std::{
fs::read_to_string,
path::{Path, PathBuf},
};

use serde::Deserialize;
use swc_ecma_ast::EsVersion;
use swc_ecma_codegen::{
text_writer::{JsWriter, WriteJs},
@@ -8,6 +12,42 @@ use swc_ecma_codegen::{
use swc_ecma_parser::{parse_file_as_module, Syntax, TsSyntax};
use testing::{run_test2, NormalizedOutput};

const fn true_by_default() -> bool {
true
}

#[derive(Deserialize)]
struct TestConfig {
#[serde(default = "true_by_default")]
reduce_escaped_newline: bool,
}

impl Default for TestConfig {
fn default() -> Self {
TestConfig {
reduce_escaped_newline: true,
}
}
}

fn find_config(dir: &Path) -> TestConfig {
let mut cur = Some(dir);
while let Some(dir) = cur {
let config = dir.join("config.json");
if config.exists() {
let config = read_to_string(&config).expect("failed to read config.json");
let config: TestConfig = serde_json::from_str(&config)
.expect("failed to deserialize value into a codegen config");

return config;
}

cur = dir.parent();
}

Default::default()
}

fn run(input: &Path, minify: bool) {
let dir = input.parent().unwrap();
let output = if minify {
@@ -23,6 +63,7 @@ fn run(input: &Path, minify: bool) {
};

run_test2(false, |cm, _| {
let config = find_config(dir);
let fm = cm.load_file(input).unwrap();

let m = parse_file_as_module(
@@ -49,7 +90,9 @@ fn run(input: &Path, minify: bool) {
}

let mut emitter = Emitter {
cfg: swc_ecma_codegen::Config::default().with_minify(minify),
cfg: swc_ecma_codegen::Config::default()
.with_minify(minify)
.with_reduce_escaped_newline(config.reduce_escaped_newline),
cm,
comments: None,
wr,
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const str = "a\nb\nc";
const tpl = `a\n${b}\nc`;
const tpl2 = `a
${b}
c`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const str = "a\nb\nc";
const tpl = `a\n${b}\nc`;
const tpl2 = `a
${b}
c`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const str="a\nb\nc";const tpl=`a
${b}
c`;const tpl2=`a
${b}
c`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"reduce_escaped_newline": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const str = "a\nb\nc";
const tpl = `a\n${b}\nc`;
const tpl2 = `a
${b}
c`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const str = "a\nb\nc";
const tpl = `a\n${b}\nc`;
const tpl2 = `a
${b}
c`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const str="a\nb\nc";const tpl=`a\n${b}\nc`;const tpl2=`a
${b}
c`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"reduce_escaped_newline": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const str = "a\nb\nc";
const tpl = `a\n${b}\nc`;
const tpl2 = `a
${b}
c`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const str = "a\nb\nc";
const tpl = `a\n${b}\nc`;
const tpl2 = `a
${b}
c`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const str="a\nb\nc";const tpl=`a
${b}
c`;const tpl2=`a
${b}
c`;
2 changes: 2 additions & 0 deletions crates/swc_ecma_minifier/src/compress/optimize/mod.rs
Original file line number Diff line number Diff line change
@@ -1872,6 +1872,8 @@ impl VisitMut for Optimizer<'_> {
_ => {}
}

self.reduce_escaped_newline_for_str_lit(e);

#[cfg(feature = "trace-ast")]
tracing::debug!("Output: {}", dump(e, true));
}
37 changes: 37 additions & 0 deletions crates/swc_ecma_minifier/src/compress/optimize/strings.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use swc_atoms::Atom;
use swc_common::{util::take::Take, Spanned, SyntaxContext};
use swc_ecma_ast::*;
use swc_ecma_utils::{ExprExt, Value::Known};
@@ -137,4 +138,40 @@ impl Optimizer<'_> {
_ => {}
}
}

/// Convert string literals with escaped newline `'\n'` to template literal
/// with newline character.
pub(super) fn reduce_escaped_newline_for_str_lit(&mut self, expr: &mut Expr) {
Copy link
Member

Choose a reason for hiding this comment

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

Do we still need this, even with the codegen config?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is used to transform string literal to template literal. Maybe should rename the method name, it's not clear enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Document added.

if self.options.ecma < EsVersion::Es2015
|| !self.options.experimental.reduce_escaped_newline
{
return;
}

if let Expr::Lit(Lit::Str(s)) = expr {
if s.value.contains('\n') {
*expr = Expr::Tpl(Tpl {
span: s.span,
exprs: Default::default(),
quasis: vec![TplElement {
span: s.span,
cooked: Some(s.value.clone()),
raw: convert_str_value_to_tpl_raw(&s.value),
tail: true,
}],
});
self.changed = true;
report_change!("strings: Reduced escaped newline for a string literal");
}
}
}
}

pub(super) fn convert_str_value_to_tpl_raw(value: &Atom) -> Atom {
value
.replace('\\', "\\\\")
.replace('`', "\\`")
.replace("${", "\\${")
.replace('\r', "\\r")
.into()
}
21 changes: 21 additions & 0 deletions crates/swc_ecma_minifier/src/option/mod.rs
Original file line number Diff line number Diff line change
@@ -123,6 +123,23 @@ impl Default for PureGetterOption {
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct CompressExperimentalOptions {
#[serde(default = "true_by_default")]
pub reduce_escaped_newline: bool,
}

impl Default for CompressExperimentalOptions {
fn default() -> Self {
CompressExperimentalOptions {
reduce_escaped_newline: true,
}
}
}

/// https://terser.org/docs/api-reference.html#compress-options
#[derive(Debug, Clone)]
#[cfg_attr(feature = "extra-serde", derive(Serialize, Deserialize))]
@@ -349,6 +366,9 @@ pub struct CompressOptions {
/// Defaults to true.
#[cfg_attr(feature = "extra-serde", serde(default = "true_by_default"))]
pub pristine_globals: bool,

#[cfg_attr(feature = "extra-serde", serde(default))]
pub experimental: CompressExperimentalOptions,
}

impl CompressOptions {
@@ -442,6 +462,7 @@ impl Default for CompressOptions {
unused: true,
const_to_let: true,
pristine_globals: true,
experimental: Default::default(),
}
}
}
Loading