Skip to main content

srcmap_generator/
lib.rs

1//! High-performance source map generator (ECMA-426).
2//!
3//! Builds source maps incrementally by adding mappings one at a time.
4//! Outputs standard source map v3 JSON.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use srcmap_generator::SourceMapGenerator;
10//!
11//! fn main() {
12//!     let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
13//!
14//!     let src = builder.add_source("src/app.ts");
15//!     builder.set_source_content(src, "const x = 1;".to_string());
16//!
17//!     let name = builder.add_name("x");
18//!     builder.add_named_mapping(0, 0, src, 0, 6, name);
19//!     builder.add_mapping(1, 0, src, 1, 0);
20//!
21//!     let json = builder.to_json();
22//!     assert!(json.contains(r#""version":3"#));
23//!     assert!(json.contains(r#""sources":["src/app.ts"]"#));
24//! }
25//! ```
26
27use std::collections::HashMap;
28
29use srcmap_codec::vlq_encode;
30
31// ── Public types ───────────────────────────────────────────────────
32
33/// A mapping from generated position to original position.
34#[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/// Builder for creating source maps incrementally.
45#[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    // Dedup maps for O(1) lookup
56    source_map: HashMap<String, u32>,
57    name_map: HashMap<String, u32>,
58}
59
60impl SourceMapGenerator {
61    /// Create a new empty source map generator.
62    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    /// Set the source root prefix.
77    pub fn set_source_root(&mut self, root: String) {
78        self.source_root = Some(root);
79    }
80
81    /// Register a source file and return its index.
82    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    /// Set the content for a source file.
94    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    /// Register a name and return its index.
101    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    /// Add a source index to the ignore list.
112    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    /// Add a mapping with no source information (generated-only).
119    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    /// Add a mapping from generated position to original position.
131    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    /// Add a mapping with a name.
150    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    /// Add a mapping only if it differs from the previous mapping on the same line.
170    ///
171    /// This skips redundant mappings where the source position is identical
172    /// to the last mapping, which reduces output size without losing information.
173    /// Used by bundlers and minifiers to avoid bloating source maps.
174    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    /// Encode all mappings to a VLQ-encoded string.
201    fn encode_mappings(&self) -> String {
202        if self.mappings.is_empty() {
203            return String::new();
204        }
205
206        // Sort mappings by (generated_line, generated_column)
207        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        // SAFETY: VLQ output is always valid ASCII/UTF-8
267        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        // Build line ranges: (start_idx, end_idx) into sorted slice
277        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        // Sequential scan: compute cumulative state at each line boundary
289        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        // Parallel: encode each line independently
310        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        // Join with semicolons
322        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        // SAFETY: VLQ output is always valid ASCII/UTF-8
332        unsafe { String::from_utf8_unchecked(out) }
333    }
334
335    /// Generate the source map as a JSON string.
336    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        // sources
353        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        // sourcesContent (only if any content is set)
363        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        // names
419        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        // mappings
429        json.push_str(r#","mappings":"#);
430        json.push_str(&json_quote(&mappings));
431
432        // ignoreList
433        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    /// Get the number of mappings.
449    pub fn mapping_count(&self) -> usize {
450        self.mappings.len()
451    }
452}
453
454/// Encode a slice of mappings for a single line to VLQ bytes.
455///
456/// Generated column starts at 0 (reset per line).
457/// Cumulative state is passed in from the sequential pre-scan.
458#[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
503/// JSON-quote a string (with escape handling).
504fn 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// ── Tests ──────────────────────────────────────────────────────────
525
526#[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        // Verify roundtrip with parser
549        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        // Generated-only mapping → no source info
651        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"); // duplicate
659        assert_eq!(s1, s2);
660
661        let n1 = builder.add_name("foo");
662        let n2 = builder.add_name("foo"); // duplicate
663        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        // Add 1000 mappings across 100 lines
681        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        // Verify a few lookups
704        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        // Should be valid JSON
718        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        // First mapping — always added
727        assert!(builder.maybe_add_mapping(0, 0, src, 10, 0));
728        // Same source position, different generated column — redundant, skipped
729        assert!(!builder.maybe_add_mapping(0, 5, src, 10, 0));
730        // Different source position — added
731        assert!(builder.maybe_add_mapping(0, 10, src, 11, 0));
732        // Different generated line, same source position as last — added (new line resets)
733        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        // Same line/col but different source — not redundant
750        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        // Skip lines 1-4
761        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        // Line 0 should have a mapping
767        assert!(sm.original_position_for(0, 0).is_some());
768        // Lines 1-4 should have no mappings
769        assert!(sm.original_position_for(2, 0).is_none());
770        // Line 5 should have a mapping
771        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            // Verify lookups
817            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            // Sort mappings the same way encode_mappings does
827            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            // Add mappings on lines 0, 100, 200, ... (sparse)
845            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            // Verify empty lines have no mappings
857            assert!(sm.original_position_for(50, 0).is_none());
858            // Verify populated lines work
859            let loc = sm.original_position_for(200, 50).unwrap();
860            assert_eq!(loc.line, 200);
861            assert_eq!(loc.column, 25);
862        }
863    }
864}