Repository: swc-project/swc (rust)

Issue:
Support the format preamble format option in swc.minify
### Describe the feature

See https://swc.rs/docs/configuration/minification#jscminifyformat

> `preamble`, Currently noop

It should be possible to pass `preamble`. SWC should be able to insert it to the minified file without breaking source map associations.

### Babel plugin or link to the feature description

_No response_

### Additional context

FWIW, jQuery is currently switching from UglifyJS to Terser in https://github.com/jquery/jquery/pull/5258. We considered switching to SWC instead as it's way faster and optimizes better than Terser but the lack of the `preamble` option is a blocker for us at the moment.

Pull request description:
feat(es/codegen): Support `preamble`
**Description:**


**Related issue:**

 - Closes #7475.

Patch:
diff --git a/bindings/binding_core_node/src/bundle.rs b/bindings/binding_core_node/src/bundle.rs
index 6873adafde..c2378b1698 100644
--- a/bindings/binding_core_node/src/bundle.rs
+++ b/bindings/binding_core_node/src/bundle.rs
@@ -138,6 +138,7 @@ impl Task for BundleTask {
                                 None,
                                 true,
                                 false,
+                                Default::default(),
                             )?;
 
                             Ok((k, output))
diff --git a/bindings/binding_core_node/src/print.rs b/bindings/binding_core_node/src/print.rs
index 3f129fcf33..983cad30e1 100644
--- a/bindings/binding_core_node/src/print.rs
+++ b/bindings/binding_core_node/src/print.rs
@@ -51,6 +51,7 @@ impl Task for PrintTask {
                     None,
                     options.config.emit_source_map_columns.into_bool(),
                     false,
+                    Default::default(),
                 )
                 .convert_err()
         })
@@ -112,6 +113,7 @@ pub fn print_sync(program: String, options: Buffer) -> napi::Result<TransformOut
             None,
             options.config.emit_source_map_columns.into_bool(),
             false,
+            Default::default(),
         )
         .convert_err()
     })
diff --git a/crates/binding_macros/src/wasm.rs b/crates/binding_macros/src/wasm.rs
index c6deac9965..cfb25ab747 100644
--- a/crates/binding_macros/src/wasm.rs
+++ b/crates/binding_macros/src/wasm.rs
@@ -258,6 +258,7 @@ macro_rules! build_print_sync {
                         None,
                         opts.config.emit_source_map_columns.into_bool(),
                         false,
+                        Default::default(),
                     ),"failed to print code")?;
 
                     serde_wasm_bindgen::to_value(&s)
diff --git a/crates/swc/benches/typescript.rs b/crates/swc/benches/typescript.rs
index a935ac3f9e..cde28c9da5 100644
--- a/crates/swc/benches/typescript.rs
+++ b/crates/swc/benches/typescript.rs
@@ -123,6 +123,7 @@ fn bench_codegen(b: &mut Bencher, _target: EsVersion) {
                     None,
                     false,
                     false,
+                    Default::default(),
                 )
                 .unwrap()
             }));
diff --git a/crates/swc/src/config/mod.rs b/crates/swc/src/config/mod.rs
index dd00be61a9..73dc2c283e 100644
--- a/crates/swc/src/config/mod.rs
+++ b/crates/swc/src/config/mod.rs
@@ -612,6 +612,15 @@ impl Options {
             }
         });
 
+        let preamble = if !cfg.jsc.output.preamble.is_empty() {
+            cfg.jsc.output.preamble
+        } else {
+            js_minify
+                .as_ref()
+                .map(|v| v.format.preamble.clone())
+                .unwrap_or_default()
+        };
+
         let pass = PassBuilder::new(
             cm,
             handler,
@@ -842,7 +851,7 @@ impl Options {
             comments: comments.cloned(),
             preserve_comments,
             emit_source_map_columns: cfg.emit_source_map_columns.into_bool(),
-            output: JscOutputConfig { charset },
+            output: JscOutputConfig { charset, preamble },
         })
     }
 }
@@ -1401,6 +1410,9 @@ pub struct JscConfig {
 pub struct JscOutputConfig {
     #[serde(default)]
     pub charset: Option<OutputCharset>,
+
+    #[serde(default)]
+    pub preamble: String,
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
diff --git a/crates/swc/src/lib.rs b/crates/swc/src/lib.rs
index 0629a2a334..64edb43cf0 100644
--- a/crates/swc/src/lib.rs
+++ b/crates/swc/src/lib.rs
@@ -506,6 +506,7 @@ impl Compiler {
         comments: Option<&dyn Comments>,
         emit_source_map_columns: bool,
         ascii_only: bool,
+        preamble: &str,
     ) -> Result<TransformOutput, Error>
     where
         T: Node + VisitWith<IdentCollector>,
@@ -518,7 +519,7 @@ impl Compiler {
             let src = {
                 let mut buf = vec![];
                 {
-                    let mut wr = Box::new(swc_ecma_codegen::text_writer::JsWriter::new(
+                    let mut w = swc_ecma_codegen::text_writer::JsWriter::new(
                         self.cm.clone(),
                         "\n",
                         &mut buf,
@@ -527,7 +528,9 @@ impl Compiler {
                         } else {
                             None
                         },
-                    )) as Box<dyn WriteJs>;
+                    );
+                    w.preamble(preamble).unwrap();
+                    let mut wr = Box::new(w) as Box<dyn WriteJs>;
 
                     if minify {
                         wr = Box::new(swc_ecma_codegen::text_writer::omit_trailing_semi(wr));
@@ -1158,6 +1161,7 @@ impl Compiler {
                 Some(&comments),
                 opts.emit_source_map_columns,
                 opts.format.ascii_only,
+                &opts.format.preamble,
             )
         })
     }
@@ -1236,6 +1240,7 @@ impl Compiler {
                     .charset
                     .map(|v| matches!(v, OutputCharset::Ascii))
                     .unwrap_or(false),
+                &config.output.preamble,
             )
         })
     }
diff --git a/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs b/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs
index 34df0fa888..ce27e83f82 100644
--- a/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs
+++ b/crates/swc_ecma_codegen/src/text_writer/basic_impl.rs
@@ -46,6 +46,13 @@ impl<'a, W: Write> JsWriter<'a, W> {
         }
     }
 
+    pub fn preamble(&mut self, s: &str) -> Result {
+        self.raw_write(s)?;
+        self.update_pos(s);
+
+        Ok(())
+    }
+
     /// Sets the indentation string. Defaults to four spaces.
     pub fn set_indent_str(&mut self, indent_str: &'static str) {
         self.indent_str = indent_str;


Test patch:
diff --git a/crates/swc/tests/minify/issue-7475/1-with-preamble/config.json b/crates/swc/tests/minify/issue-7475/1-with-preamble/config.json
new file mode 100644
index 0000000000..7b5a35943e
--- /dev/null
+++ b/crates/swc/tests/minify/issue-7475/1-with-preamble/config.json
@@ -0,0 +1,5 @@
+{
+    "format": {
+        "preamble": "/** @license */\n"
+    }
+}
diff --git a/crates/swc/tests/minify/issue-7475/1-with-preamble/input.js b/crates/swc/tests/minify/issue-7475/1-with-preamble/input.js
new file mode 100644
index 0000000000..14f45bd101
--- /dev/null
+++ b/crates/swc/tests/minify/issue-7475/1-with-preamble/input.js
@@ -0,0 +1,5 @@
+function foo() {
+    console.log('Foo')
+}
+
+foo()
\ No newline at end of file
diff --git a/crates/swc/tests/minify/issue-7475/1-with-preamble/output.js b/crates/swc/tests/minify/issue-7475/1-with-preamble/output.js
new file mode 100644
index 0000000000..0cafb7709c
--- /dev/null
+++ b/crates/swc/tests/minify/issue-7475/1-with-preamble/output.js
@@ -0,0 +1,2 @@
+/** @license */
+function foo(){console.log("Foo")}foo();
diff --git a/crates/swc/tests/minify/issue-7475/1-with-preamble/output.map b/crates/swc/tests/minify/issue-7475/1-with-preamble/output.map
new file mode 100644
index 0000000000..c2db5abfaf
--- /dev/null
+++ b/crates/swc/tests/minify/issue-7475/1-with-preamble/output.map
@@ -0,0 +1,15 @@
+{
+  "mappings": ";AAAA,SAASA,MACLC,QAAQC,GAAG,CAAC,MAChB,CAEAF",
+  "names": [
+    "foo",
+    "console",
+    "log"
+  ],
+  "sources": [
+    "$DIR/tests/minify/issue-7475/1-with-preamble/input.js"
+  ],
+  "sourcesContent": [
+    "function foo() {\n    console.log('Foo')\n}\n\nfoo()"
+  ],
+  "version": 3
+}
diff --git a/crates/swc/tests/minify/issue-7475/2-no-preamble/config.json b/crates/swc/tests/minify/issue-7475/2-no-preamble/config.json
new file mode 100644
index 0000000000..8cb86d78d2
--- /dev/null
+++ b/crates/swc/tests/minify/issue-7475/2-no-preamble/config.json
@@ -0,0 +1,3 @@
+{
+    "format": {}
+}
diff --git a/crates/swc/tests/minify/issue-7475/2-no-preamble/input.js b/crates/swc/tests/minify/issue-7475/2-no-preamble/input.js
new file mode 100644
index 0000000000..14f45bd101
--- /dev/null
+++ b/crates/swc/tests/minify/issue-7475/2-no-preamble/input.js
@@ -0,0 +1,5 @@
+function foo() {
+    console.log('Foo')
+}
+
+foo()
\ No newline at end of file
diff --git a/crates/swc/tests/minify/issue-7475/2-no-preamble/output.js b/crates/swc/tests/minify/issue-7475/2-no-preamble/output.js
new file mode 100644
index 0000000000..a2e7bdceac
--- /dev/null
+++ b/crates/swc/tests/minify/issue-7475/2-no-preamble/output.js
@@ -0,0 +1,1 @@
+function foo(){console.log("Foo")}foo();
diff --git a/crates/swc/tests/minify/issue-7475/2-no-preamble/output.map b/crates/swc/tests/minify/issue-7475/2-no-preamble/output.map
new file mode 100644
index 0000000000..db25a7dc8d
--- /dev/null
+++ b/crates/swc/tests/minify/issue-7475/2-no-preamble/output.map
@@ -0,0 +1,15 @@
+{
+  "mappings": "AAAA,SAASA,MACLC,QAAQC,GAAG,CAAC,MAChB,CAEAF",
+  "names": [
+    "foo",
+    "console",
+    "log"
+  ],
+  "sources": [
+    "$DIR/tests/minify/issue-7475/2-no-preamble/input.js"
+  ],
+  "sourcesContent": [
+    "function foo() {\n    console.log('Foo')\n}\n\nfoo()"
+  ],
+  "version": 3
+}
diff --git a/crates/swc/tests/projects.rs b/crates/swc/tests/projects.rs
index 94ef6f46ef..d59d183430 100644
--- a/crates/swc/tests/projects.rs
+++ b/crates/swc/tests/projects.rs
@@ -769,6 +769,7 @@ fn should_visit() {
                 Some(&comments),
                 config.emit_source_map_columns,
                 false,
+                Default::default(),
             )
             .unwrap()
             .code)
@@ -1130,3 +1131,36 @@ fn issue_7513_2() {
     println!("{}", output.code);
     assert_eq!(output.code, "const a={ignoreBOM:!0,fatal:!0};");
 }
+
+#[testing::fixture("tests/minify/**/input.js")]
+fn minify(input_js: PathBuf) {
+    let input_dir = input_js.parent().unwrap();
+    let config_json_path = input_dir.join("config.json");
+
+    testing::run_test2(false, |cm, handler| {
+        let c = Compiler::new(cm);
+        let fm = c.cm.load_file(&input_js).unwrap();
+
+        let mut config: JsMinifyOptions =
+            serde_json::from_str(&std::fs::read_to_string(&config_json_path).unwrap()).unwrap();
+
+        config.source_map = BoolOrDataConfig::from_bool(true);
+        let output = c.minify(fm, &handler, &config).unwrap();
+
+        NormalizedOutput::from(output.code)
+            .compare_to_file(input_dir.join("output.js"))
+            .unwrap();
+
+        let map = output.map.map(|json| {
+            let json: serde_json::Value = serde_json::from_str(&json).unwrap();
+            serde_json::to_string_pretty(&json).unwrap()
+        });
+
+        NormalizedOutput::from(map.unwrap())
+            .compare_to_file(input_dir.join("output.map"))
+            .unwrap();
+
+        Ok(())
+    })
+    .unwrap()
+}
diff --git a/crates/swc_node_bundler/tests/fixture.rs b/crates/swc_node_bundler/tests/fixture.rs
index 3be67a4822..9ecac943b3 100644
--- a/crates/swc_node_bundler/tests/fixture.rs
+++ b/crates/swc_node_bundler/tests/fixture.rs
@@ -99,6 +99,7 @@ fn pass(input_dir: PathBuf) {
                             Some(&comments),
                             false,
                             false,
+                            Default::default(),
                         )
                         .expect("failed to print?")
                         .code;