1use std::collections::HashMap;
28
29use srcmap_codec::vlq_encode;
30
31#[derive(Debug, Clone)]
35pub struct Mapping {
36 pub generated_line: u32,
37 pub generated_column: u32,
38 pub source: Option<u32>,
39 pub original_line: u32,
40 pub original_column: u32,
41 pub name: Option<u32>,
42}
43
44#[derive(Debug)]
46pub struct SourceMapGenerator {
47 file: Option<String>,
48 source_root: Option<String>,
49 sources: Vec<String>,
50 sources_content: Vec<Option<String>>,
51 names: Vec<String>,
52 mappings: Vec<Mapping>,
53 ignore_list: Vec<u32>,
54
55 source_map: HashMap<String, u32>,
57 name_map: HashMap<String, u32>,
58}
59
60impl SourceMapGenerator {
61 pub fn new(file: Option<String>) -> Self {
63 Self {
64 file,
65 source_root: None,
66 sources: Vec::new(),
67 sources_content: Vec::new(),
68 names: Vec::new(),
69 mappings: Vec::new(),
70 ignore_list: Vec::new(),
71 source_map: HashMap::new(),
72 name_map: HashMap::new(),
73 }
74 }
75
76 pub fn set_source_root(&mut self, root: String) {
78 self.source_root = Some(root);
79 }
80
81 pub fn add_source(&mut self, source: &str) -> u32 {
83 if let Some(&idx) = self.source_map.get(source) {
84 return idx;
85 }
86 let idx = self.sources.len() as u32;
87 self.sources.push(source.to_string());
88 self.sources_content.push(None);
89 self.source_map.insert(source.to_string(), idx);
90 idx
91 }
92
93 pub fn set_source_content(&mut self, source_idx: u32, content: String) {
95 if (source_idx as usize) < self.sources_content.len() {
96 self.sources_content[source_idx as usize] = Some(content);
97 }
98 }
99
100 pub fn add_name(&mut self, name: &str) -> u32 {
102 if let Some(&idx) = self.name_map.get(name) {
103 return idx;
104 }
105 let idx = self.names.len() as u32;
106 self.names.push(name.to_string());
107 self.name_map.insert(name.to_string(), idx);
108 idx
109 }
110
111 pub fn add_to_ignore_list(&mut self, source_idx: u32) {
113 if !self.ignore_list.contains(&source_idx) {
114 self.ignore_list.push(source_idx);
115 }
116 }
117
118 pub fn add_generated_mapping(&mut self, generated_line: u32, generated_column: u32) {
120 self.mappings.push(Mapping {
121 generated_line,
122 generated_column,
123 source: None,
124 original_line: 0,
125 original_column: 0,
126 name: None,
127 });
128 }
129
130 pub fn add_mapping(
132 &mut self,
133 generated_line: u32,
134 generated_column: u32,
135 source: u32,
136 original_line: u32,
137 original_column: u32,
138 ) {
139 self.mappings.push(Mapping {
140 generated_line,
141 generated_column,
142 source: Some(source),
143 original_line,
144 original_column,
145 name: None,
146 });
147 }
148
149 pub fn add_named_mapping(
151 &mut self,
152 generated_line: u32,
153 generated_column: u32,
154 source: u32,
155 original_line: u32,
156 original_column: u32,
157 name: u32,
158 ) {
159 self.mappings.push(Mapping {
160 generated_line,
161 generated_column,
162 source: Some(source),
163 original_line,
164 original_column,
165 name: Some(name),
166 });
167 }
168
169 pub fn maybe_add_mapping(
175 &mut self,
176 generated_line: u32,
177 generated_column: u32,
178 source: u32,
179 original_line: u32,
180 original_column: u32,
181 ) -> bool {
182 if let Some(last) = self.mappings.last()
183 && last.generated_line == generated_line
184 && last.source == Some(source)
185 && last.original_line == original_line
186 && last.original_column == original_column
187 {
188 return false;
189 }
190 self.add_mapping(
191 generated_line,
192 generated_column,
193 source,
194 original_line,
195 original_column,
196 );
197 true
198 }
199
200 fn encode_mappings(&self) -> String {
202 if self.mappings.is_empty() {
203 return String::new();
204 }
205
206 let mut sorted: Vec<&Mapping> = self.mappings.iter().collect();
208 sorted.sort_unstable_by(|a, b| {
209 a.generated_line
210 .cmp(&b.generated_line)
211 .then(a.generated_column.cmp(&b.generated_column))
212 });
213
214 #[cfg(feature = "parallel")]
215 if sorted.len() >= 4096 {
216 return Self::encode_parallel_impl(&sorted);
217 }
218
219 Self::encode_sequential_impl(&sorted)
220 }
221
222 fn encode_sequential_impl(sorted: &[&Mapping]) -> String {
223 let mut out: Vec<u8> = Vec::with_capacity(sorted.len() * 6);
224
225 let mut prev_gen_col: i64 = 0;
226 let mut prev_source: i64 = 0;
227 let mut prev_orig_line: i64 = 0;
228 let mut prev_orig_col: i64 = 0;
229 let mut prev_name: i64 = 0;
230 let mut prev_gen_line: u32 = 0;
231 let mut first_in_line = true;
232
233 for m in sorted {
234 while prev_gen_line < m.generated_line {
235 out.push(b';');
236 prev_gen_line += 1;
237 prev_gen_col = 0;
238 first_in_line = true;
239 }
240
241 if !first_in_line {
242 out.push(b',');
243 }
244 first_in_line = false;
245
246 vlq_encode(&mut out, m.generated_column as i64 - prev_gen_col);
247 prev_gen_col = m.generated_column as i64;
248
249 if let Some(source) = m.source {
250 vlq_encode(&mut out, source as i64 - prev_source);
251 prev_source = source as i64;
252
253 vlq_encode(&mut out, m.original_line as i64 - prev_orig_line);
254 prev_orig_line = m.original_line as i64;
255
256 vlq_encode(&mut out, m.original_column as i64 - prev_orig_col);
257 prev_orig_col = m.original_column as i64;
258
259 if let Some(name) = m.name {
260 vlq_encode(&mut out, name as i64 - prev_name);
261 prev_name = name as i64;
262 }
263 }
264 }
265
266 unsafe { String::from_utf8_unchecked(out) }
268 }
269
270 #[cfg(feature = "parallel")]
271 fn encode_parallel_impl(sorted: &[&Mapping]) -> String {
272 use rayon::prelude::*;
273
274 let max_line = sorted.last().unwrap().generated_line as usize;
275
276 let mut line_ranges: Vec<(usize, usize)> = vec![(0, 0); max_line + 1];
278 let mut i = 0;
279 while i < sorted.len() {
280 let line = sorted[i].generated_line as usize;
281 let start = i;
282 while i < sorted.len() && sorted[i].generated_line as usize == line {
283 i += 1;
284 }
285 line_ranges[line] = (start, i);
286 }
287
288 let mut states: Vec<(i64, i64, i64, i64)> = Vec::with_capacity(max_line + 1);
290 let mut prev_source: i64 = 0;
291 let mut prev_orig_line: i64 = 0;
292 let mut prev_orig_col: i64 = 0;
293 let mut prev_name: i64 = 0;
294
295 for &(start, end) in &line_ranges {
296 states.push((prev_source, prev_orig_line, prev_orig_col, prev_name));
297 for m in &sorted[start..end] {
298 if let Some(source) = m.source {
299 prev_source = source as i64;
300 prev_orig_line = m.original_line as i64;
301 prev_orig_col = m.original_column as i64;
302 if let Some(name) = m.name {
303 prev_name = name as i64;
304 }
305 }
306 }
307 }
308
309 let encoded_lines: Vec<Vec<u8>> = line_ranges
311 .par_iter()
312 .zip(states.par_iter())
313 .map(|(&(start, end), &(s, ol, oc, n))| {
314 if start == end {
315 return Vec::new();
316 }
317 encode_mapping_slice(&sorted[start..end], s, ol, oc, n)
318 })
319 .collect();
320
321 let total_len = encoded_lines.iter().map(|l| l.len()).sum::<usize>() + max_line;
323 let mut out: Vec<u8> = Vec::with_capacity(total_len);
324 for (i, bytes) in encoded_lines.iter().enumerate() {
325 if i > 0 {
326 out.push(b';');
327 }
328 out.extend_from_slice(bytes);
329 }
330
331 unsafe { String::from_utf8_unchecked(out) }
333 }
334
335 pub fn to_json(&self) -> String {
337 let mappings = self.encode_mappings();
338
339 let mut json = String::with_capacity(256 + mappings.len());
340 json.push_str(r#"{"version":3"#);
341
342 if let Some(ref file) = self.file {
343 json.push_str(r#","file":"#);
344 json.push_str(&json_quote(file));
345 }
346
347 if let Some(ref root) = self.source_root {
348 json.push_str(r#","sourceRoot":"#);
349 json.push_str(&json_quote(root));
350 }
351
352 json.push_str(r#","sources":["#);
354 for (i, s) in self.sources.iter().enumerate() {
355 if i > 0 {
356 json.push(',');
357 }
358 json.push_str(&json_quote(s));
359 }
360 json.push(']');
361
362 if self.sources_content.iter().any(|c| c.is_some()) {
364 json.push_str(r#","sourcesContent":["#);
365
366 #[cfg(feature = "parallel")]
367 {
368 use rayon::prelude::*;
369
370 let total_content: usize = self
371 .sources_content
372 .iter()
373 .map(|c| c.as_ref().map_or(0, |s| s.len()))
374 .sum();
375
376 if self.sources_content.len() >= 8 && total_content >= 8192 {
377 let quoted: Vec<String> = self
378 .sources_content
379 .par_iter()
380 .map(|c| match c {
381 Some(content) => json_quote(content),
382 None => "null".to_string(),
383 })
384 .collect();
385 for (i, q) in quoted.iter().enumerate() {
386 if i > 0 {
387 json.push(',');
388 }
389 json.push_str(q);
390 }
391 } else {
392 for (i, c) in self.sources_content.iter().enumerate() {
393 if i > 0 {
394 json.push(',');
395 }
396 match c {
397 Some(content) => json.push_str(&json_quote(content)),
398 None => json.push_str("null"),
399 }
400 }
401 }
402 }
403
404 #[cfg(not(feature = "parallel"))]
405 for (i, c) in self.sources_content.iter().enumerate() {
406 if i > 0 {
407 json.push(',');
408 }
409 match c {
410 Some(content) => json.push_str(&json_quote(content)),
411 None => json.push_str("null"),
412 }
413 }
414
415 json.push(']');
416 }
417
418 json.push_str(r#","names":["#);
420 for (i, n) in self.names.iter().enumerate() {
421 if i > 0 {
422 json.push(',');
423 }
424 json.push_str(&json_quote(n));
425 }
426 json.push(']');
427
428 json.push_str(r#","mappings":"#);
430 json.push_str(&json_quote(&mappings));
431
432 if !self.ignore_list.is_empty() {
434 json.push_str(r#","ignoreList":["#);
435 for (i, &idx) in self.ignore_list.iter().enumerate() {
436 if i > 0 {
437 json.push(',');
438 }
439 json.push_str(&idx.to_string());
440 }
441 json.push(']');
442 }
443
444 json.push('}');
445 json
446 }
447
448 pub fn mapping_count(&self) -> usize {
450 self.mappings.len()
451 }
452}
453
454#[cfg(feature = "parallel")]
459fn encode_mapping_slice(
460 mappings: &[&Mapping],
461 init_source: i64,
462 init_orig_line: i64,
463 init_orig_col: i64,
464 init_name: i64,
465) -> Vec<u8> {
466 let mut buf = Vec::with_capacity(mappings.len() * 6);
467 let mut prev_gen_col: i64 = 0;
468 let mut prev_source = init_source;
469 let mut prev_orig_line = init_orig_line;
470 let mut prev_orig_col = init_orig_col;
471 let mut prev_name = init_name;
472 let mut first = true;
473
474 for m in mappings {
475 if !first {
476 buf.push(b',');
477 }
478 first = false;
479
480 vlq_encode(&mut buf, m.generated_column as i64 - prev_gen_col);
481 prev_gen_col = m.generated_column as i64;
482
483 if let Some(source) = m.source {
484 vlq_encode(&mut buf, source as i64 - prev_source);
485 prev_source = source as i64;
486
487 vlq_encode(&mut buf, m.original_line as i64 - prev_orig_line);
488 prev_orig_line = m.original_line as i64;
489
490 vlq_encode(&mut buf, m.original_column as i64 - prev_orig_col);
491 prev_orig_col = m.original_column as i64;
492
493 if let Some(name) = m.name {
494 vlq_encode(&mut buf, name as i64 - prev_name);
495 prev_name = name as i64;
496 }
497 }
498 }
499
500 buf
501}
502
503fn json_quote(s: &str) -> String {
505 let mut out = String::with_capacity(s.len() + 2);
506 out.push('"');
507 for c in s.chars() {
508 match c {
509 '"' => out.push_str("\\\""),
510 '\\' => out.push_str("\\\\"),
511 '\n' => out.push_str("\\n"),
512 '\r' => out.push_str("\\r"),
513 '\t' => out.push_str("\\t"),
514 c if c < '\x20' => {
515 out.push_str(&format!("\\u{:04x}", c as u32));
516 }
517 c => out.push(c),
518 }
519 }
520 out.push('"');
521 out
522}
523
524#[cfg(test)]
527mod tests {
528 use super::*;
529
530 #[test]
531 fn empty_generator() {
532 let builder = SourceMapGenerator::new(None);
533 let json = builder.to_json();
534 assert!(json.contains(r#""version":3"#));
535 assert!(json.contains(r#""mappings":"""#));
536 }
537
538 #[test]
539 fn simple_mapping() {
540 let mut builder = SourceMapGenerator::new(Some("output.js".to_string()));
541 let src = builder.add_source("input.js");
542 builder.add_mapping(0, 0, src, 0, 0);
543
544 let json = builder.to_json();
545 assert!(json.contains(r#""file":"output.js""#));
546 assert!(json.contains(r#""sources":["input.js"]"#));
547
548 let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
550 let loc = sm.original_position_for(0, 0).unwrap();
551 assert_eq!(sm.source(loc.source), "input.js");
552 assert_eq!(loc.line, 0);
553 assert_eq!(loc.column, 0);
554 }
555
556 #[test]
557 fn mapping_with_name() {
558 let mut builder = SourceMapGenerator::new(None);
559 let src = builder.add_source("input.js");
560 let name = builder.add_name("myFunction");
561 builder.add_named_mapping(0, 0, src, 0, 0, name);
562
563 let json = builder.to_json();
564 assert!(json.contains(r#""names":["myFunction"]"#));
565
566 let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
567 let loc = sm.original_position_for(0, 0).unwrap();
568 assert_eq!(loc.name, Some(0));
569 assert_eq!(sm.name(0), "myFunction");
570 }
571
572 #[test]
573 fn multiple_lines() {
574 let mut builder = SourceMapGenerator::new(None);
575 let src = builder.add_source("input.js");
576 builder.add_mapping(0, 0, src, 0, 0);
577 builder.add_mapping(1, 4, src, 1, 2);
578 builder.add_mapping(2, 0, src, 2, 0);
579
580 let json = builder.to_json();
581 let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
582 assert_eq!(sm.line_count(), 3);
583
584 let loc = sm.original_position_for(1, 4).unwrap();
585 assert_eq!(loc.line, 1);
586 assert_eq!(loc.column, 2);
587 }
588
589 #[test]
590 fn multiple_sources() {
591 let mut builder = SourceMapGenerator::new(None);
592 let a = builder.add_source("a.js");
593 let b = builder.add_source("b.js");
594 builder.add_mapping(0, 0, a, 0, 0);
595 builder.add_mapping(1, 0, b, 0, 0);
596
597 let json = builder.to_json();
598 let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
599
600 let loc0 = sm.original_position_for(0, 0).unwrap();
601 let loc1 = sm.original_position_for(1, 0).unwrap();
602 assert_eq!(sm.source(loc0.source), "a.js");
603 assert_eq!(sm.source(loc1.source), "b.js");
604 }
605
606 #[test]
607 fn source_content() {
608 let mut builder = SourceMapGenerator::new(None);
609 let src = builder.add_source("input.js");
610 builder.set_source_content(src, "var x = 1;".to_string());
611 builder.add_mapping(0, 0, src, 0, 0);
612
613 let json = builder.to_json();
614 assert!(json.contains(r#""sourcesContent":["var x = 1;"]"#));
615
616 let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
617 assert_eq!(sm.sources_content[0], Some("var x = 1;".to_string()));
618 }
619
620 #[test]
621 fn source_root() {
622 let mut builder = SourceMapGenerator::new(None);
623 builder.set_source_root("src/".to_string());
624 let src = builder.add_source("input.js");
625 builder.add_mapping(0, 0, src, 0, 0);
626
627 let json = builder.to_json();
628 assert!(json.contains(r#""sourceRoot":"src/""#));
629 }
630
631 #[test]
632 fn ignore_list() {
633 let mut builder = SourceMapGenerator::new(None);
634 let _app = builder.add_source("app.js");
635 let lib = builder.add_source("node_modules/lib.js");
636 builder.add_to_ignore_list(lib);
637 builder.add_mapping(0, 0, lib, 0, 0);
638
639 let json = builder.to_json();
640 assert!(json.contains(r#""ignoreList":[1]"#));
641 }
642
643 #[test]
644 fn generated_only_mapping() {
645 let mut builder = SourceMapGenerator::new(None);
646 builder.add_generated_mapping(0, 0);
647
648 let json = builder.to_json();
649 let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
650 assert!(sm.original_position_for(0, 0).is_none());
652 }
653
654 #[test]
655 fn dedup_sources_and_names() {
656 let mut builder = SourceMapGenerator::new(None);
657 let s1 = builder.add_source("input.js");
658 let s2 = builder.add_source("input.js"); assert_eq!(s1, s2);
660
661 let n1 = builder.add_name("foo");
662 let n2 = builder.add_name("foo"); assert_eq!(n1, n2);
664
665 assert_eq!(builder.sources.len(), 1);
666 assert_eq!(builder.names.len(), 1);
667 }
668
669 #[test]
670 fn large_roundtrip() {
671 let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
672
673 for i in 0..5 {
674 builder.add_source(&format!("src/file{i}.js"));
675 }
676 for i in 0..10 {
677 builder.add_name(&format!("var{i}"));
678 }
679
680 for line in 0..100u32 {
682 for col in 0..10u32 {
683 let src = (line as u32 * 10 + col) % 5;
684 let name = if col % 3 == 0 {
685 Some((col % 10) as u32)
686 } else {
687 None
688 };
689
690 match name {
691 Some(n) => builder.add_named_mapping(line, col * 10, src, line, col * 5, n),
692 None => builder.add_mapping(line, col * 10, src, line, col * 5),
693 }
694 }
695 }
696
697 let json = builder.to_json();
698 let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
699
700 assert_eq!(sm.mapping_count(), 1000);
701 assert_eq!(sm.line_count(), 100);
702
703 let loc = sm.original_position_for(50, 30).unwrap();
705 assert_eq!(loc.line, 50);
706 assert_eq!(loc.column, 15);
707 }
708
709 #[test]
710 fn json_escaping() {
711 let mut builder = SourceMapGenerator::new(None);
712 let src = builder.add_source("path/with\"quotes.js");
713 builder.set_source_content(src, "line1\nline2\ttab".to_string());
714 builder.add_mapping(0, 0, src, 0, 0);
715
716 let json = builder.to_json();
717 let _: serde_json::Value = serde_json::from_str(&json).unwrap();
719 }
720
721 #[test]
722 fn maybe_add_mapping_skips_redundant() {
723 let mut builder = SourceMapGenerator::new(None);
724 let src = builder.add_source("input.js");
725
726 assert!(builder.maybe_add_mapping(0, 0, src, 10, 0));
728 assert!(!builder.maybe_add_mapping(0, 5, src, 10, 0));
730 assert!(builder.maybe_add_mapping(0, 10, src, 11, 0));
732 assert!(builder.maybe_add_mapping(1, 0, src, 11, 0));
734
735 assert_eq!(builder.mapping_count(), 3);
736
737 let json = builder.to_json();
738 let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
739 assert_eq!(sm.mapping_count(), 3);
740 }
741
742 #[test]
743 fn maybe_add_mapping_different_source() {
744 let mut builder = SourceMapGenerator::new(None);
745 let a = builder.add_source("a.js");
746 let b = builder.add_source("b.js");
747
748 assert!(builder.maybe_add_mapping(0, 0, a, 0, 0));
749 assert!(builder.maybe_add_mapping(0, 5, b, 0, 0));
751
752 assert_eq!(builder.mapping_count(), 2);
753 }
754
755 #[test]
756 fn empty_lines_between_mappings() {
757 let mut builder = SourceMapGenerator::new(None);
758 let src = builder.add_source("input.js");
759 builder.add_mapping(0, 0, src, 0, 0);
760 builder.add_mapping(5, 0, src, 5, 0);
762
763 let json = builder.to_json();
764 let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
765
766 assert!(sm.original_position_for(0, 0).is_some());
768 assert!(sm.original_position_for(2, 0).is_none());
770 assert!(sm.original_position_for(5, 0).is_some());
772 }
773
774 #[cfg(feature = "parallel")]
775 mod parallel_tests {
776 use super::*;
777
778 fn build_large_generator(lines: u32, cols_per_line: u32) -> SourceMapGenerator {
779 let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
780 for i in 0..10 {
781 let src = builder.add_source(&format!("src/file{i}.js"));
782 builder.set_source_content(
783 src,
784 format!("// source file {i}\n{}", "x = 1;\n".repeat(100)),
785 );
786 }
787 for i in 0..20 {
788 builder.add_name(&format!("var{i}"));
789 }
790
791 for line in 0..lines {
792 for col in 0..cols_per_line {
793 let src = (line * cols_per_line + col) % 10;
794 let name = if col % 3 == 0 {
795 Some((col % 20) as u32)
796 } else {
797 None
798 };
799 match name {
800 Some(n) => builder.add_named_mapping(line, col * 10, src, line, col * 5, n),
801 None => builder.add_mapping(line, col * 10, src, line, col * 5),
802 }
803 }
804 }
805 builder
806 }
807
808 #[test]
809 fn parallel_large_roundtrip() {
810 let builder = build_large_generator(500, 20);
811 let json = builder.to_json();
812 let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
813 assert_eq!(sm.mapping_count(), 10000);
814 assert_eq!(sm.line_count(), 500);
815
816 let loc = sm.original_position_for(250, 50).unwrap();
818 assert_eq!(loc.line, 250);
819 assert_eq!(loc.column, 25);
820 }
821
822 #[test]
823 fn parallel_matches_sequential() {
824 let builder = build_large_generator(500, 20);
825
826 let mut sorted: Vec<&Mapping> = builder.mappings.iter().collect();
828 sorted.sort_unstable_by(|a, b| {
829 a.generated_line
830 .cmp(&b.generated_line)
831 .then(a.generated_column.cmp(&b.generated_column))
832 });
833
834 let sequential = SourceMapGenerator::encode_sequential_impl(&sorted);
835 let parallel = SourceMapGenerator::encode_parallel_impl(&sorted);
836 assert_eq!(sequential, parallel);
837 }
838
839 #[test]
840 fn parallel_with_sparse_lines() {
841 let mut builder = SourceMapGenerator::new(None);
842 let src = builder.add_source("input.js");
843
844 for i in 0..50 {
846 let line = i * 100;
847 for col in 0..100u32 {
848 builder.add_mapping(line, col * 10, src, line, col * 5);
849 }
850 }
851
852 let json = builder.to_json();
853 let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
854 assert_eq!(sm.mapping_count(), 5000);
855
856 assert!(sm.original_position_for(50, 0).is_none());
858 let loc = sm.original_position_for(200, 50).unwrap();
860 assert_eq!(loc.line, 200);
861 assert_eq!(loc.column, 25);
862 }
863 }
864}