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}