Repository: swc-project/swc (rust)

Issue:
Optional column mapping for codegenerator
### Describe the feature

Currently, swc will map column to column on sourcemap generation, which results large sourcemaps. Personally, providing an option for column mapping would be better for performance-wise.

### Babel plugin or link to the feature description

_No response_

### Additional context

_No response_

Pull request description:
feat(common): emit source map columns
<!-- Note: CI script will automatically rebase your PR so please do not rebase unless required -->

<!-- Thanks for submitting a pull request! Please provide enough information so that others can review your pull request. -->

**Description:**

<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->

Optionally emit source map columns, use `true` as default value

**BREAKING CHANGE:**
No breaking changes

<!--
If this PR introduces a breaking change, it must contain a notice for it to be included in the CHANGELOG. Add description or remove entirely if not breaking.

You may need to update `MIGRATION.md` for the breaking changes.
-->

**Related issue (if exists):**
closes #4637 

Patch:
diff --git a/crates/binding_core_node/src/bundle.rs b/crates/binding_core_node/src/bundle.rs
index 5d4468bf04..2da828258b 100644
--- a/crates/binding_core_node/src/bundle.rs
+++ b/crates/binding_core_node/src/bundle.rs
@@ -130,6 +130,7 @@ impl Task for BundleTask {
                             None,
                             minify,
                             None,
+                            true,
                         )?;
 
                         Ok((k, output))
diff --git a/crates/binding_core_node/src/print.rs b/crates/binding_core_node/src/print.rs
index c449a70c7b..ed6f7f1c79 100644
--- a/crates/binding_core_node/src/print.rs
+++ b/crates/binding_core_node/src/print.rs
@@ -45,6 +45,7 @@ impl Task for PrintTask {
                 None,
                 options.config.minify.into_bool(),
                 None,
+                options.config.emit_source_map_columns.into_bool(),
             )
             .convert_err()
     }
@@ -102,6 +103,7 @@ pub fn print_sync(program: String, options: Buffer) -> napi::Result<TransformOut
         None,
         options.config.minify.into_bool(),
         None,
+        options.config.emit_source_map_columns.into_bool(),
     )
     .convert_err()
 }
diff --git a/crates/binding_core_wasm/src/lib.rs b/crates/binding_core_wasm/src/lib.rs
index f288518a94..96fc5a3d57 100644
--- a/crates/binding_core_wasm/src/lib.rs
+++ b/crates/binding_core_wasm/src/lib.rs
@@ -116,6 +116,7 @@ pub fn print_sync(s: JsValue, opts: JsValue) -> Result<JsValue, JsValue> {
                         None,
                         opts.config.minify.into(),
                         None,
+                        opts.config.emit_source_map_columns.into_bool(),
                     )
                     .context("failed to print code")?;
 
diff --git a/crates/swc/benches/minify.rs b/crates/swc/benches/minify.rs
index d2ef060757..25de8a0a5a 100644
--- a/crates/swc/benches/minify.rs
+++ b/crates/swc/benches/minify.rs
@@ -47,6 +47,7 @@ fn bench_minify(b: &mut Bencher, filename: &str) {
                     source_map: Default::default(),
                     output_path: Default::default(),
                     inline_sources_content: true,
+                    emit_source_map_columns: true,
                 },
             )
         })
diff --git a/crates/swc/benches/typescript.rs b/crates/swc/benches/typescript.rs
index c881f69739..9c1b41c86b 100644
--- a/crates/swc/benches/typescript.rs
+++ b/crates/swc/benches/typescript.rs
@@ -115,6 +115,7 @@ fn bench_codegen(b: &mut Bencher, _target: EsVersion) {
                     None,
                     false,
                     None,
+                    false,
                 )
                 .unwrap(),
             );
diff --git a/crates/swc/examples/minify.rs b/crates/swc/examples/minify.rs
index ccd6f30cea..cbd0bae801 100644
--- a/crates/swc/examples/minify.rs
+++ b/crates/swc/examples/minify.rs
@@ -35,6 +35,7 @@ fn main() {
                     source_map: Default::default(),
                     output_path: Default::default(),
                     inline_sources_content: Default::default(),
+                    emit_source_map_columns: Default::default(),
                 },
             )
             .context("failed to minify")
diff --git a/crates/swc/src/config/mod.rs b/crates/swc/src/config/mod.rs
index 371f69261c..1edbc05faa 100644
--- a/crates/swc/src/config/mod.rs
+++ b/crates/swc/src/config/mod.rs
@@ -626,6 +626,7 @@ impl Options {
             source_file_name,
             comments: comments.cloned(),
             preserve_comments,
+            emit_source_map_columns: cfg.emit_source_map_columns.into_bool(),
         })
     }
 }
@@ -794,6 +795,9 @@ pub struct Config {
     #[serde(default)]
     pub inline_sources_content: BoolConfig<true>,
 
+    #[serde(default)]
+    pub emit_source_map_columns: BoolConfig<true>,
+
     #[serde(default)]
     pub error: ErrorConfig,
 
@@ -840,6 +844,9 @@ pub struct JsMinifyOptions {
 
     #[serde(default = "true_by_default")]
     pub inline_sources_content: bool,
+
+    #[serde(default = "true_by_default")]
+    pub emit_source_map_columns: bool,
 }
 
 fn true_by_default() -> bool {
@@ -1073,6 +1080,7 @@ pub struct BuiltInput<P: swc_ecma_visit::Fold> {
     pub preserve_comments: BoolOr<JsMinifyCommentOption>,
 
     pub inline_sources_content: bool,
+    pub emit_source_map_columns: bool,
 }
 
 /// `jsc` in  `.swcrc`.
diff --git a/crates/swc/src/lib.rs b/crates/swc/src/lib.rs
index df20d93485..54d43665f2 100644
--- a/crates/swc/src/lib.rs
+++ b/crates/swc/src/lib.rs
@@ -428,6 +428,7 @@ impl Compiler {
         orig: Option<&sourcemap::SourceMap>,
         minify: bool,
         comments: Option<&dyn Comments>,
+        emit_source_map_columns: bool,
     ) -> Result<TransformOutput, Error>
     where
         T: Node + VisitWith<IdentCollector>,
@@ -493,6 +494,7 @@ impl Compiler {
                                     output_path: output_path.as_deref(),
                                     names: source_map_names,
                                     inline_sources_content,
+                                    emit_columns: emit_source_map_columns,
                                 },
                             )
                             .to_writer(&mut buf)
@@ -517,6 +519,7 @@ impl Compiler {
                                 output_path: output_path.as_deref(),
                                 names: source_map_names,
                                 inline_sources_content,
+                                emit_columns: emit_source_map_columns,
                             },
                         )
                         .to_writer(&mut buf)
@@ -546,6 +549,8 @@ struct SwcSourceMapConfig<'a> {
     names: &'a AHashMap<BytePos, JsWord>,
 
     inline_sources_content: bool,
+
+    emit_columns: bool,
 }
 
 impl SourceMapGenConfig for SwcSourceMapConfig<'_> {
@@ -584,6 +589,10 @@ impl SourceMapGenConfig for SwcSourceMapConfig<'_> {
     fn inline_sources_content(&self, _: &FileName) -> bool {
         self.inline_sources_content
     }
+
+    fn emit_columns(&self, _f: &FileName) -> bool {
+        self.emit_columns
+    }
 }
 
 pub(crate) fn minify_file_comments(
@@ -869,6 +878,7 @@ impl Compiler {
                 preserve_comments: config.preserve_comments,
                 inline_sources_content: config.inline_sources_content,
                 comments: config.comments,
+                emit_source_map_columns: config.emit_source_map_columns,
             };
 
             let orig = if config.source_maps.enabled() {
@@ -1040,6 +1050,7 @@ impl Compiler {
                 orig.as_ref(),
                 true,
                 Some(&comments),
+                opts.emit_source_map_columns,
             )
         })
     }
@@ -1111,6 +1122,7 @@ impl Compiler {
                 orig,
                 config.minify,
                 config.comments.as_ref().map(|v| v as _),
+                config.emit_source_map_columns,
             )
         })
     }
diff --git a/crates/swc_common/src/source_map.rs b/crates/swc_common/src/source_map.rs
index 0142ea54b1..aadea514d0 100644
--- a/crates/swc_common/src/source_map.rs
+++ b/crates/swc_common/src/source_map.rs
@@ -1166,6 +1166,7 @@ impl SourceMap {
 
         let mut ch_start = 0;
         let mut line_ch_start = 0;
+        let mut prev_dst_line = u32::MAX;
 
         for (pos, lc) in mappings.iter() {
             let pos = *pos;
@@ -1205,6 +1206,12 @@ impl SourceMap {
                 }
             };
 
+            let emit_columns = config.emit_columns(&f.name);
+
+            if !emit_columns && lc.line == prev_dst_line {
+                continue;
+            }
+
             let a = match f.lookup_line(pos) {
                 Some(line) => line as u32,
                 None => continue,
@@ -1242,6 +1249,7 @@ impl SourceMap {
             }
 
             builder.add_raw(lc.line, lc.col, line - 1, col, Some(src_id), name_idx);
+            prev_dst_line = lc.line;
         }
 
         builder.into_sourcemap()
@@ -1329,6 +1337,11 @@ pub trait SourceMapGenConfig {
             FileName::Real(..) | FileName::Custom(..) | FileName::Url(..)
         )
     }
+
+    /// You can define whether to emit sourcemap with columns or not
+    fn emit_columns(&self, _f: &FileName) -> bool {
+        true
+    }
 }
 
 #[derive(Debug, Clone)]


Test patch:
diff --git a/crates/swc/tests/exec.rs b/crates/swc/tests/exec.rs
index 4f270d9d7d..72c5b46407 100644
--- a/crates/swc/tests/exec.rs
+++ b/crates/swc/tests/exec.rs
@@ -100,6 +100,7 @@ fn create_matrix(entry: &Path) -> Vec<Options> {
                                 source_map: Default::default(),
                                 output_path: Default::default(),
                                 inline_sources_content: Default::default(),
+                                emit_source_map_columns: Default::default(),
                             })
                         } else {
                             None
diff --git a/crates/swc/tests/projects.rs b/crates/swc/tests/projects.rs
index e6cfce33e8..134fb2a594 100644
--- a/crates/swc/tests/projects.rs
+++ b/crates/swc/tests/projects.rs
@@ -754,6 +754,7 @@ fn should_visit() {
                 preserve_comments: config.preserve_comments,
                 inline_sources_content: config.inline_sources_content,
                 comments: config.comments,
+                emit_source_map_columns: config.emit_source_map_columns,
             };
 
             if config.minify {
@@ -785,6 +786,7 @@ fn should_visit() {
                 // TODO: figure out sourcemaps
                 config.minify,
                 Some(&comments),
+                config.emit_source_map_columns,
             )
             .unwrap()
             .code)
diff --git a/crates/swc/tests/source_map.rs b/crates/swc/tests/source_map.rs
index 934fad5d68..d57cc8f3aa 100644
--- a/crates/swc/tests/source_map.rs
+++ b/crates/swc/tests/source_map.rs
@@ -279,6 +279,7 @@ fn issue_4112() {
                     &Options {
                         config: Config {
                             module: Some(ModuleConfig::CommonJs(Default::default())),
+                            emit_source_map_columns: true.into(),
                             ..Default::default()
                         },
                         source_maps: Some(SourceMapsConfig::Bool(true)),
@@ -299,3 +300,88 @@ fn issue_4112() {
         })
         .unwrap()
 }
+
+#[test]
+fn should_work_with_emit_source_map_columns() {
+    Tester::new().print_errors(|cm, handler| {
+        let c = Compiler::new(cm.clone());
+        let fm = cm.new_source_file(
+            swc_common::FileName::Real("./app.js".into()),
+            r#"import { createElement } from "react";
+      
+  createElement('div', null, {});"#
+                .to_string(),
+        );
+        let result = c.process_js_file(
+            fm.clone(),
+            &handler,
+            &Options {
+                swcrc: false,
+                is_module: IsModule::Bool(true),
+                source_maps: Some(SourceMapsConfig::Bool(true)),
+                config: Config {
+                    inline_sources_content: true.into(),
+                    emit_source_map_columns: true.into(),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+        );
+
+        match result {
+            Ok(result) => {
+                assert!(result.map.is_some());
+                let map = result.map.unwrap();
+                let source_map = sourcemap::SourceMap::from_slice(map.as_bytes())
+                    .expect("failed to deserialize sourcemap");
+                let token = source_map
+                    .lookup_token(1, 14)
+                    .expect("failed to find token");
+                assert_eq!(token.get_dst_line(), 1);
+                assert_eq!(token.get_dst_col(), 14);
+                assert_eq!(token.get_src_line(), 2);
+                assert_eq!(token.get_src_col(), 16);
+            }
+            Err(err) => {
+                panic!("Error: {:#?}", err);
+            }
+        }
+
+        let result2 = c.process_js_file(
+            fm,
+            &handler,
+            &Options {
+                swcrc: false,
+                is_module: IsModule::Bool(true),
+                source_maps: Some(SourceMapsConfig::Bool(true)),
+                config: Config {
+                    inline_sources_content: true.into(),
+                    emit_source_map_columns: false.into(),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+        );
+
+        match result2 {
+            Ok(result) => {
+                assert!(result.map.is_some());
+                let map = result.map.unwrap();
+                let source_map = sourcemap::SourceMap::from_slice(map.as_bytes())
+                    .expect("failed to deserialize sourcemap");
+                let token = source_map
+                    .lookup_token(1, 14)
+                    .expect("failed to find token");
+                assert_eq!(token.get_dst_line(), 1);
+                assert_eq!(token.get_dst_col(), 0);
+                assert_eq!(token.get_src_line(), 2);
+                assert_eq!(token.get_src_col(), 2);
+            }
+            Err(err) => {
+                panic!("Error: {:#?}", err);
+            }
+        }
+
+        Ok(())
+    });
+}
diff --git a/crates/swc_node_bundler/tests/fixture.rs b/crates/swc_node_bundler/tests/fixture.rs
index 285b0e1679..eba48ec15e 100644
--- a/crates/swc_node_bundler/tests/fixture.rs
+++ b/crates/swc_node_bundler/tests/fixture.rs
@@ -95,6 +95,7 @@ fn pass(input_dir: PathBuf) {
                         None,
                         false,
                         Some(&comments),
+                        false,
                     )
                     .expect("failed to print?")
                     .code;