1use srcmap_generator::SourceMapGenerator;
60use srcmap_sourcemap::SourceMap;
61use std::collections::HashMap;
62
63pub struct ConcatBuilder {
70 builder: SourceMapGenerator,
71 source_remap: HashMap<String, u32>,
72 name_remap: HashMap<String, u32>,
73}
74
75impl ConcatBuilder {
76 pub fn new(file: Option<String>) -> Self {
78 Self {
79 builder: SourceMapGenerator::new(file),
80 source_remap: HashMap::new(),
81 name_remap: HashMap::new(),
82 }
83 }
84
85 pub fn add_map(&mut self, sm: &SourceMap, line_offset: u32) {
90 let source_indices: Vec<u32> = sm
92 .sources
93 .iter()
94 .enumerate()
95 .map(|(i, s)| {
96 if let Some(&idx) = self.source_remap.get(s) {
97 if let Some(Some(content)) = sm.sources_content.get(i) {
99 self.builder.set_source_content(idx, content.clone());
100 }
101 idx
102 } else {
103 let idx = self.builder.add_source(s);
104 if let Some(Some(content)) = sm.sources_content.get(i) {
105 self.builder.set_source_content(idx, content.clone());
106 }
107 self.source_remap.insert(s.clone(), idx);
108 idx
109 }
110 })
111 .collect();
112
113 let name_indices: Vec<u32> = sm
115 .names
116 .iter()
117 .map(|n| {
118 if let Some(&idx) = self.name_remap.get(n) {
119 idx
120 } else {
121 let idx = self.builder.add_name(n);
122 self.name_remap.insert(n.clone(), idx);
123 idx
124 }
125 })
126 .collect();
127
128 for &ignored in &sm.ignore_list {
130 let global_idx = source_indices[ignored as usize];
131 self.builder.add_to_ignore_list(global_idx);
132 }
133
134 for m in sm.all_mappings() {
136 let gen_line = m.generated_line + line_offset;
137
138 if m.source == u32::MAX {
139 self.builder
140 .add_generated_mapping(gen_line, m.generated_column);
141 } else {
142 let src = source_indices[m.source as usize];
143 if m.name != u32::MAX {
144 let name = name_indices[m.name as usize];
145 self.builder.add_named_mapping(
146 gen_line,
147 m.generated_column,
148 src,
149 m.original_line,
150 m.original_column,
151 name,
152 );
153 } else {
154 self.builder.add_mapping(
155 gen_line,
156 m.generated_column,
157 src,
158 m.original_line,
159 m.original_column,
160 );
161 }
162 }
163 }
164 }
165
166 pub fn to_json(&self) -> String {
168 self.builder.to_json()
169 }
170
171 pub fn build(&self) -> SourceMap {
173 let json = self.to_json();
174 SourceMap::from_json(&json).expect("generated JSON should be valid")
175 }
176}
177
178pub fn remap<F>(outer: &SourceMap, loader: F) -> SourceMap
189where
190 F: Fn(&str) -> Option<SourceMap>,
191{
192 let mut builder = SourceMapGenerator::new(outer.file.clone());
193
194 let mut upstream_maps: HashMap<u32, Option<SourceMap>> = HashMap::new();
196
197 for m in outer.all_mappings() {
198 if m.source == u32::MAX {
199 builder.add_generated_mapping(m.generated_line, m.generated_column);
200 continue;
201 }
202
203 let source_name = outer.source(m.source);
204
205 let upstream = upstream_maps
207 .entry(m.source)
208 .or_insert_with(|| loader(source_name));
209
210 match upstream {
211 Some(upstream_sm) => {
212 match upstream_sm.original_position_for(m.original_line, m.original_column) {
214 Some(loc) => {
215 let orig_source = upstream_sm.source(loc.source);
216 let src_idx = builder.add_source(orig_source);
217
218 if let Some(Some(content)) =
220 upstream_sm.sources_content.get(loc.source as usize)
221 {
222 builder.set_source_content(src_idx, content.clone());
223 }
224
225 let name_idx = loc
227 .name
228 .map(|n| builder.add_name(upstream_sm.name(n)))
229 .or_else(|| {
230 if m.name != u32::MAX {
231 Some(builder.add_name(outer.name(m.name)))
232 } else {
233 None
234 }
235 });
236
237 match name_idx {
238 Some(name) => builder.add_named_mapping(
239 m.generated_line,
240 m.generated_column,
241 src_idx,
242 loc.line,
243 loc.column,
244 name,
245 ),
246 None => builder.add_mapping(
247 m.generated_line,
248 m.generated_column,
249 src_idx,
250 loc.line,
251 loc.column,
252 ),
253 }
254 }
255 None => {
256 let src_idx = builder.add_source(source_name);
258 if m.name != u32::MAX {
259 let name = builder.add_name(outer.name(m.name));
260 builder.add_named_mapping(
261 m.generated_line,
262 m.generated_column,
263 src_idx,
264 m.original_line,
265 m.original_column,
266 name,
267 );
268 } else {
269 builder.add_mapping(
270 m.generated_line,
271 m.generated_column,
272 src_idx,
273 m.original_line,
274 m.original_column,
275 );
276 }
277 }
278 }
279 }
280 None => {
281 let src_idx = builder.add_source(source_name);
283
284 if let Some(Some(content)) = outer.sources_content.get(m.source as usize) {
286 builder.set_source_content(src_idx, content.clone());
287 }
288
289 if m.name != u32::MAX {
290 let name = builder.add_name(outer.name(m.name));
291 builder.add_named_mapping(
292 m.generated_line,
293 m.generated_column,
294 src_idx,
295 m.original_line,
296 m.original_column,
297 name,
298 );
299 } else {
300 builder.add_mapping(
301 m.generated_line,
302 m.generated_column,
303 src_idx,
304 m.original_line,
305 m.original_column,
306 );
307 }
308 }
309 }
310 }
311
312 let json = builder.to_json();
313 SourceMap::from_json(&json).expect("generated JSON should be valid")
314}
315
316#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
325 fn concat_two_simple_maps() {
326 let a = SourceMap::from_json(
327 r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#,
328 )
329 .unwrap();
330 let b = SourceMap::from_json(
331 r#"{"version":3,"sources":["b.js"],"names":[],"mappings":"AAAA"}"#,
332 )
333 .unwrap();
334
335 let mut builder = ConcatBuilder::new(Some("bundle.js".to_string()));
336 builder.add_map(&a, 0);
337 builder.add_map(&b, 1);
338
339 let result = builder.build();
340 assert_eq!(result.sources, vec!["a.js", "b.js"]);
341 assert_eq!(result.mapping_count(), 2);
342
343 let loc0 = result.original_position_for(0, 0).unwrap();
344 assert_eq!(result.source(loc0.source), "a.js");
345
346 let loc1 = result.original_position_for(1, 0).unwrap();
347 assert_eq!(result.source(loc1.source), "b.js");
348 }
349
350 #[test]
351 fn concat_deduplicates_sources() {
352 let a = SourceMap::from_json(
353 r#"{"version":3,"sources":["shared.js"],"names":[],"mappings":"AAAA"}"#,
354 )
355 .unwrap();
356 let b = SourceMap::from_json(
357 r#"{"version":3,"sources":["shared.js"],"names":[],"mappings":"AAAA"}"#,
358 )
359 .unwrap();
360
361 let mut builder = ConcatBuilder::new(None);
362 builder.add_map(&a, 0);
363 builder.add_map(&b, 10);
364
365 let result = builder.build();
366 assert_eq!(result.sources.len(), 1);
367 assert_eq!(result.sources[0], "shared.js");
368 }
369
370 #[test]
371 fn concat_with_names() {
372 let a = SourceMap::from_json(
373 r#"{"version":3,"sources":["a.js"],"names":["foo"],"mappings":"AAAAA"}"#,
374 )
375 .unwrap();
376 let b = SourceMap::from_json(
377 r#"{"version":3,"sources":["b.js"],"names":["bar"],"mappings":"AAAAA"}"#,
378 )
379 .unwrap();
380
381 let mut builder = ConcatBuilder::new(None);
382 builder.add_map(&a, 0);
383 builder.add_map(&b, 1);
384
385 let result = builder.build();
386 assert_eq!(result.names.len(), 2);
387
388 let loc0 = result.original_position_for(0, 0).unwrap();
389 assert_eq!(loc0.name, Some(0));
390 assert_eq!(result.name(0), "foo");
391
392 let loc1 = result.original_position_for(1, 0).unwrap();
393 assert_eq!(loc1.name, Some(1));
394 assert_eq!(result.name(1), "bar");
395 }
396
397 #[test]
398 fn concat_preserves_multi_line_maps() {
399 let a = SourceMap::from_json(
400 r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA;AACA;AACA"}"#,
401 )
402 .unwrap();
403
404 let mut builder = ConcatBuilder::new(None);
405 builder.add_map(&a, 5); let result = builder.build();
408 assert!(result.original_position_for(5, 0).is_some());
409 assert!(result.original_position_for(6, 0).is_some());
410 assert!(result.original_position_for(7, 0).is_some());
411 assert!(result.original_position_for(4, 0).is_none());
412 }
413
414 #[test]
415 fn concat_with_sources_content() {
416 let a = SourceMap::from_json(
417 r#"{"version":3,"sources":["a.js"],"sourcesContent":["var a;"],"names":[],"mappings":"AAAA"}"#,
418 )
419 .unwrap();
420
421 let mut builder = ConcatBuilder::new(None);
422 builder.add_map(&a, 0);
423
424 let result = builder.build();
425 assert_eq!(result.sources_content, vec![Some("var a;".to_string())]);
426 }
427
428 #[test]
429 fn concat_empty_builder() {
430 let builder = ConcatBuilder::new(Some("empty.js".to_string()));
431 let result = builder.build();
432 assert_eq!(result.mapping_count(), 0);
433 assert_eq!(result.sources.len(), 0);
434 }
435
436 #[test]
439 fn remap_single_level() {
440 let outer = SourceMap::from_json(
442 r#"{"version":3,"sources":["intermediate.js"],"names":[],"mappings":"AAAA;AACA"}"#,
443 )
444 .unwrap();
445
446 let inner = SourceMap::from_json(
448 r#"{"version":3,"sources":["original.js"],"names":[],"mappings":"AACA;AACA"}"#,
449 )
450 .unwrap();
451
452 let result = remap(&outer, |source| {
453 if source == "intermediate.js" {
454 Some(inner.clone())
455 } else {
456 None
457 }
458 });
459
460 assert_eq!(result.sources, vec!["original.js"]);
461
462 let loc = result.original_position_for(0, 0).unwrap();
464 assert_eq!(result.source(loc.source), "original.js");
465 assert_eq!(loc.line, 1);
466 }
467
468 #[test]
469 fn remap_no_upstream_passthrough() {
470 let outer = SourceMap::from_json(
471 r#"{"version":3,"sources":["already-original.js"],"names":[],"mappings":"AAAA"}"#,
472 )
473 .unwrap();
474
475 let result = remap(&outer, |_| None);
477
478 assert_eq!(result.sources, vec!["already-original.js"]);
479 let loc = result.original_position_for(0, 0).unwrap();
480 assert_eq!(result.source(loc.source), "already-original.js");
481 assert_eq!(loc.line, 0);
482 assert_eq!(loc.column, 0);
483 }
484
485 #[test]
486 fn remap_partial_sources() {
487 let outer = SourceMap::from_json(
489 r#"{"version":3,"sources":["compiled.js","passthrough.js"],"names":[],"mappings":"AAAA,KCCA"}"#,
490 )
491 .unwrap();
492
493 let inner = SourceMap::from_json(
494 r#"{"version":3,"sources":["original.ts"],"names":[],"mappings":"AAAA"}"#,
495 )
496 .unwrap();
497
498 let result = remap(&outer, |source| {
499 if source == "compiled.js" {
500 Some(inner.clone())
501 } else {
502 None
503 }
504 });
505
506 assert!(result.sources.contains(&"original.ts".to_string()));
508 assert!(result.sources.contains(&"passthrough.js".to_string()));
509 }
510
511 #[test]
512 fn remap_preserves_names() {
513 let outer = SourceMap::from_json(
514 r#"{"version":3,"sources":["compiled.js"],"names":["myFunc"],"mappings":"AAAAA"}"#,
515 )
516 .unwrap();
517
518 let inner = SourceMap::from_json(
520 r#"{"version":3,"sources":["original.ts"],"names":[],"mappings":"AAAA"}"#,
521 )
522 .unwrap();
523
524 let result = remap(&outer, |_| Some(inner.clone()));
525
526 let loc = result.original_position_for(0, 0).unwrap();
527 assert!(loc.name.is_some());
528 assert_eq!(result.name(loc.name.unwrap()), "myFunc");
529 }
530
531 #[test]
532 fn remap_upstream_name_wins() {
533 let outer = SourceMap::from_json(
534 r#"{"version":3,"sources":["compiled.js"],"names":["outerName"],"mappings":"AAAAA"}"#,
535 )
536 .unwrap();
537
538 let inner = SourceMap::from_json(
540 r#"{"version":3,"sources":["original.ts"],"names":["innerName"],"mappings":"AAAAA"}"#,
541 )
542 .unwrap();
543
544 let result = remap(&outer, |_| Some(inner.clone()));
545
546 let loc = result.original_position_for(0, 0).unwrap();
547 assert!(loc.name.is_some());
548 assert_eq!(result.name(loc.name.unwrap()), "innerName");
549 }
550
551 #[test]
552 fn remap_sources_content_from_upstream() {
553 let outer = SourceMap::from_json(
554 r#"{"version":3,"sources":["compiled.js"],"names":[],"mappings":"AAAA"}"#,
555 )
556 .unwrap();
557
558 let inner = SourceMap::from_json(
559 r#"{"version":3,"sources":["original.ts"],"sourcesContent":["const x = 1;"],"names":[],"mappings":"AAAA"}"#,
560 )
561 .unwrap();
562
563 let result = remap(&outer, |_| Some(inner.clone()));
564
565 assert_eq!(
566 result.sources_content,
567 vec![Some("const x = 1;".to_string())]
568 );
569 }
570
571 }