Repository: swc-project/swc (rust)

Issue:
A standalone `jsc.transform.charset` configuration
### Describe the feature

Currently, swc will automatically escape any non-ASCII characters into Unicode sequences when (and only when) `target` was set to ES3 and ES5.

I want to force enable the escape of non-ASCII characters even the target engines support. Generally, it would reduce the after-gzipped size.

### Babel plugin or link to the feature description

_No response_

### Additional context

_No response_

Pull request description:
fix(es/config): Add `jsc.output.charset`
**Description:**

**Related issue (if exists):**

 - Closes #4697

Patch:
diff --git a/crates/swc/src/config/mod.rs b/crates/swc/src/config/mod.rs
index f65044a24c..ff1b5620d8 100644
--- a/crates/swc/src/config/mod.rs
+++ b/crates/swc/src/config/mod.rs
@@ -633,6 +633,7 @@ impl Options {
             comments: comments.cloned(),
             preserve_comments,
             emit_source_map_columns: cfg.emit_source_map_columns.into_bool(),
+            output: cfg.jsc.output,
         })
     }
 }
@@ -1099,6 +1100,8 @@ pub struct BuiltInput<P: swc_ecma_visit::Fold> {
 
     pub inline_sources_content: bool,
     pub emit_source_map_columns: bool,
+
+    pub output: JscOutputConfig,
 }
 
 /// `jsc` in  `.swcrc`.
@@ -1143,6 +1146,31 @@ pub struct JscConfig {
 
     #[serde(default)]
     pub preserve_all_comments: BoolConfig<false>,
+
+    #[serde(default)]
+    pub output: JscOutputConfig,
+}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize, Merge)]
+#[serde(deny_unknown_fields, rename_all = "camelCase")]
+pub struct JscOutputConfig {
+    #[serde(default)]
+    pub charset: Option<OutputCharset>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields, rename_all = "camelCase")]
+pub enum OutputCharset {
+    #[serde(rename = "utf8")]
+    Utf8,
+    #[serde(rename = "ascii")]
+    Ascii,
+}
+
+impl Default for OutputCharset {
+    fn default() -> Self {
+        OutputCharset::Utf8
+    }
 }
 
 /// `jsc.experimental` in `.swcrc`
diff --git a/crates/swc/src/lib.rs b/crates/swc/src/lib.rs
index 5458ca33bf..a1a8297414 100644
--- a/crates/swc/src/lib.rs
+++ b/crates/swc/src/lib.rs
@@ -121,7 +121,7 @@ use std::{
 use anyhow::{bail, Context, Error};
 use atoms::JsWord;
 use common::{collections::AHashMap, comments::SingleThreadedComments, errors::HANDLER};
-use config::{IsModule, JsMinifyCommentOption, JsMinifyOptions};
+use config::{IsModule, JsMinifyCommentOption, JsMinifyOptions, OutputCharset};
 use json_comments::StripComments;
 use once_cell::sync::Lazy;
 use serde::Serialize;
@@ -884,6 +884,7 @@ impl Compiler {
                 inline_sources_content: config.inline_sources_content,
                 comments: config.comments,
                 emit_source_map_columns: config.emit_source_map_columns,
+                output: config.output,
             };
 
             let orig = if config.source_maps.enabled() {
@@ -1129,7 +1130,11 @@ impl Compiler {
                 config.minify,
                 config.comments.as_ref().map(|v| v as _),
                 config.emit_source_map_columns,
-                false,
+                config
+                    .output
+                    .charset
+                    .map(|v| matches!(v, OutputCharset::Ascii))
+                    .unwrap_or(false),
             )
         })
     }


Test patch:
diff --git a/crates/swc/tests/fixture/issues-4xxx/4697/1/input/.swcrc b/crates/swc/tests/fixture/issues-4xxx/4697/1/input/.swcrc
new file mode 100644
index 0000000000..d444638aaa
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-4xxx/4697/1/input/.swcrc
@@ -0,0 +1,8 @@
+{
+    "jsc": {
+        "target": "es2021",
+        "output": {
+            "charset": "ascii"
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-4xxx/4697/1/input/index.js b/crates/swc/tests/fixture/issues-4xxx/4697/1/input/index.js
new file mode 100644
index 0000000000..fa53220da9
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-4xxx/4697/1/input/index.js
@@ -0,0 +1,1 @@
+const foo = "❤️";
diff --git a/crates/swc/tests/fixture/issues-4xxx/4697/1/output/index.js b/crates/swc/tests/fixture/issues-4xxx/4697/1/output/index.js
new file mode 100644
index 0000000000..353ddaf207
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-4xxx/4697/1/output/index.js
@@ -0,0 +1,1 @@
+const foo = "\u2764\uFE0F";
diff --git a/crates/swc/tests/fixture/issues-4xxx/4697/2/input/.swcrc b/crates/swc/tests/fixture/issues-4xxx/4697/2/input/.swcrc
new file mode 100644
index 0000000000..d8685c707c
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-4xxx/4697/2/input/.swcrc
@@ -0,0 +1,5 @@
+{
+    "jsc": {
+        "target": "es2021"
+    }
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-4xxx/4697/2/input/index.js b/crates/swc/tests/fixture/issues-4xxx/4697/2/input/index.js
new file mode 100644
index 0000000000..fa53220da9
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-4xxx/4697/2/input/index.js
@@ -0,0 +1,1 @@
+const foo = "❤️";
diff --git a/crates/swc/tests/fixture/issues-4xxx/4697/2/output/index.js b/crates/swc/tests/fixture/issues-4xxx/4697/2/output/index.js
new file mode 100644
index 0000000000..fa53220da9
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-4xxx/4697/2/output/index.js
@@ -0,0 +1,1 @@
+const foo = "❤️";
diff --git a/crates/swc/tests/projects.rs b/crates/swc/tests/projects.rs
index 65ddd61e0a..3b45a30067 100644
--- a/crates/swc/tests/projects.rs
+++ b/crates/swc/tests/projects.rs
@@ -755,6 +755,7 @@ fn should_visit() {
                 inline_sources_content: config.inline_sources_content,
                 comments: config.comments,
                 emit_source_map_columns: config.emit_source_map_columns,
+                output: config.output,
             };
 
             if config.minify {
diff --git a/crates/swc/tests/rust_api.rs b/crates/swc/tests/rust_api.rs
index 5c2b2eca56..d125a4f20c 100644
--- a/crates/swc/tests/rust_api.rs
+++ b/crates/swc/tests/rust_api.rs
@@ -143,13 +143,9 @@ fn shopify_2_same_opt() {
                     target: Some(EsVersion::Es5),
                     loose: false.into(),
                     keep_class_names: false.into(),
-                    base_url: Default::default(),
-                    paths: Default::default(),
                     minify: None,
-                    experimental: Default::default(),
-                    lints: Default::default(),
-                    assumptions: Default::default(),
                     preserve_all_comments: false.into(),
+                    ..Default::default()
                 },
                 module: None,
                 minify: false.into(),