merge_source_map/
lib.rs

1//! Merge multiple sourcemaps.
2//!
3//! # Installation
4//!
5//! ```bash
6//! cargo add sourcemap merge-source-map
7//! ```
8//!
9//! # Usage
10//!
11//! ```rust
12//! use merge_source_map::merge;
13//! use sourcemap::SourceMap;
14//!
15//! fn main() {
16//!     let sourcemap1 = r#"{
17//!         "version": 3,
18//!         "file": "index.js",
19//!         "sourceRoot": "",
20//!         "sources": [
21//!           "index.ts"
22//!         ],
23//!         "names": [],
24//!         "mappings": "AAAA,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,CAAC,GAAG,CAAC,iBAAU,IAAI,CAAE,CAAC,CAAC;AAChC,CAAC",
25//!         "sourcesContent": [
26//!           "function sayHello(name: string) {\n  console.log(`Hello, ${name}`);\n}\n"
27//!         ]
28//!     }"#;
29//!     let sourcemap2 = r#"{
30//!         "version": 3,
31//!         "file": "minify.js",
32//!         "sourceRoot": "",
33//!         "sources": [
34//!           "index.js"
35//!         ],
36//!         "names": [
37//!           "sayHello",
38//!           "name",
39//!           "console",
40//!           "log",
41//!           "concat"
42//!         ],
43//!         "mappings": "AAAA,SAASA,SAASC,CAAI,EAClBC,QAAQC,GAAG,CAAC,UAAUC,MAAM,CAACH,GACjC",
44//!         "sourcesContent": [
45//!           "function sayHello(name) {\n    console.log(\"Hello, \".concat(name));\n}\n"
46//!         ]
47//!     }"#;
48//!
49//!     // merge sourcemap
50//!     let merged = merge(
51//!         vec![
52//!             SourceMap::from_reader(sourcemap1.as_bytes()).unwrap(),
53//!             SourceMap::from_reader(sourcemap2.as_bytes()).unwrap(),
54//!         ],
55//!         Default::default(),
56//!     );
57//!
58//!     let mut buf = vec![];
59//!     merged.to_writer(&mut buf).unwrap();
60//!     let merged = String::from_utf8(buf).unwrap();
61//! }
62//! ```
63//! Merged sourcemap:
64//! ```json
65//! {
66//!   "version": 3,
67//!   "sources": [
68//!     "index.ts"
69//!   ],
70//!   "sourcesContent": [
71//!     "function sayHello(name: string) {\n  console.log(`Hello, ${name}`);\n}\n"
72//!   ],
73//!   "names": [],
74//!   "mappings": "AAAA,SAAS,SAAS,CAAY,EAC5B,QAAQ,GAAG,CAAC,UAAA,MAAA,CAAU,GACxB"
75//! }
76//! ```
77//!
78//! You can view result [here](https://evanw.github.io/source-map-visualization/#NTQAZnVuY3Rpb24gc2F5SGVsbG8obyl7Y29uc29sZS5sb2coIkhlbGxvLCAiLmNvbmNhdChvKSl9MjU0AHsKICAidmVyc2lvbiI6IDMsCiAgInNvdXJjZXMiOiBbCiAgICAiaW5kZXgudHMiCiAgXSwKICAic291cmNlc0NvbnRlbnQiOiBbCiAgICAiZnVuY3Rpb24gc2F5SGVsbG8obmFtZTogc3RyaW5nKSB7XG4gIGNvbnNvbGUubG9nKGBIZWxsbywgJHtuYW1lfWApO1xufVxuIgogIF0sCiAgIm5hbWVzIjogW10sCiAgIm1hcHBpbmdzIjogIkFBQUEsU0FBUyxTQUFTLENBQVksRUFDNUIsUUFBUSxHQUFHLENBQUMsVUFBQSxNQUFBLENBQVUsR0FDeEIiCn0K).
79use sourcemap::{SourceMap, SourceMapBuilder};
80
81pub use sourcemap;
82
83pub struct MergeOptions {
84    pub source_replacer: Option<Box<dyn Fn(&str) -> String>>,
85}
86
87impl Default for MergeOptions {
88    fn default() -> Self {
89        Self {
90            source_replacer: None,
91        }
92    }
93}
94
95pub fn merge(mut maps: Vec<SourceMap>, options: MergeOptions) -> SourceMap {
96    let mut builder = SourceMapBuilder::new(None);
97
98    maps = maps
99        .into_iter()
100        .filter(|map| map.get_token_count() > 0)
101        .collect();
102    if maps.is_empty() {
103        return builder.into_sourcemap();
104    }
105
106    maps.reverse();
107
108    let dest_map = &maps[0];
109
110    for token in dest_map.tokens() {
111        let mut last_map_token = token;
112        let mut completed_trace = true;
113
114        if maps.len() > 1 {
115            for map in &maps[1..] {
116                if let Some(map_token) = map.lookup_token(token.get_src_line(), token.get_src_col())
117                {
118                    last_map_token = map_token;
119                } else {
120                    completed_trace = false;
121                    break;
122                }
123            }
124        }
125
126        if !completed_trace {
127            continue;
128        }
129
130        // replace source
131        let replaced_source = if let Some(src) = last_map_token.get_source() {
132            if let Some(source_replacer) = &options.source_replacer {
133                Some(source_replacer(src))
134            } else {
135                Some(src.to_string())
136            }
137        } else {
138            None
139        };
140
141        // add mapping
142        let added_token = builder.add(
143            token.get_dst_line(),
144            token.get_dst_col(),
145            last_map_token.get_src_line(),
146            last_map_token.get_src_col(),
147            replaced_source.as_deref(),
148            last_map_token.get_name(),
149        );
150
151        // add source centent
152        if !builder.has_source_contents(added_token.src_id) {
153            let source_content = if let Some(view) = last_map_token.get_source_view() {
154                Some(view.source())
155            } else {
156                None
157            };
158
159            builder.set_source_contents(added_token.src_id, source_content);
160        }
161    }
162
163    builder.into_sourcemap()
164}
165
166#[cfg(test)]
167mod test {
168    use sourcemap::SourceMap;
169
170    use crate::merge;
171
172    #[test]
173    fn test_merge() {
174        let sourcemap1 = r#"{
175            "version": 3,
176            "file": "index.js",
177            "sourceRoot": "",
178            "sources": [
179              "index.ts"
180            ],
181            "names": [],
182            "mappings": "AAAA,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,CAAC,GAAG,CAAC,iBAAU,IAAI,CAAE,CAAC,CAAC;AAChC,CAAC",
183            "sourcesContent": [
184              "function sayHello(name: string) {\n  console.log(`Hello, ${name}`);\n}\n"
185            ]
186        }"#;
187        let sourcemap2 = r#"{
188            "version": 3,
189            "file": "minify.js",
190            "sourceRoot": "",
191            "sources": [
192              "index.js"
193            ],
194            "names": [
195              "sayHello",
196              "name",
197              "console",
198              "log",
199              "concat"
200            ],
201            "mappings": "AAAA,SAASA,SAASC,CAAI,EAClBC,QAAQC,GAAG,CAAC,UAAUC,MAAM,CAACH,GACjC",
202            "sourcesContent": [
203              "function sayHello(name) {\n    console.log(\"Hello, \".concat(name));\n}\n"
204            ]
205        }"#;
206
207        let merged = merge(
208            vec![
209                SourceMap::from_reader(sourcemap1.as_bytes()).unwrap(),
210                SourceMap::from_reader(sourcemap2.as_bytes()).unwrap(),
211            ],
212            Default::default(),
213        );
214        let mut buf = vec![];
215        merged.to_writer(&mut buf).unwrap();
216        let merged = String::from_utf8(buf).unwrap();
217
218        assert!(merged.eq(r#"{"version":3,"sources":["index.ts"],"sourcesContent":["function sayHello(name: string) {\n  console.log(`Hello, ${name}`);\n}\n"],"names":[],"mappings":"AAAA,SAAS,SAAS,CAAY,EAC5B,QAAQ,GAAG,CAAC,UAAA,MAAA,CAAU,GACxB"}"#));
219    }
220}