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 =
323            encoded_lines.iter().map(|l| l.len()).sum::<usize>() + max_line;
324        let mut out: Vec<u8> = Vec::with_capacity(total_len);
325        for (i, bytes) in encoded_lines.iter().enumerate() {
326            if i > 0 {
327                out.push(b';');
328            }
329            out.extend_from_slice(bytes);
330        }
331
332        // SAFETY: VLQ output is always valid ASCII/UTF-8
333        unsafe { String::from_utf8_unchecked(out) }
334    }
335
336    /// Generate the source map as a JSON string.
337    pub fn to_json(&self) -> String {
338        let mappings = self.encode_mappings();
339
340        let mut json = String::with_capacity(256 + mappings.len());
341        json.push_str(r#"{"version":3"#);
342
343        if let Some(ref file) = self.file {
344            json.push_str(r#","file":"#);
345            json.push_str(&json_quote(file));
346        }
347
348        if let Some(ref root) = self.source_root {
349            json.push_str(r#","sourceRoot":"#);
350            json.push_str(&json_quote(root));
351        }
352
353        // sources
354        json.push_str(r#","sources":["#);
355        for (i, s) in self.sources.iter().enumerate() {
356            if i > 0 {
357                json.push(',');
358            }
359            json.push_str(&json_quote(s));
360        }
361        json.push(']');
362
363        // sourcesContent (only if any content is set)
364        if self.sources_content.iter().any(|c| c.is_some()) {
365            json.push_str(r#","sourcesContent":["#);
366
367            #[cfg(feature = "parallel")]
368            {
369                use rayon::prelude::*;
370
371                let total_content: usize = self
372                    .sources_content
373                    .iter()
374                    .map(|c| c.as_ref().map_or(0, |s| s.len()))
375                    .sum();
376
377                if self.sources_content.len() >= 8 && total_content >= 8192 {
378                    let quoted: Vec<String> = self
379                        .sources_content
380                        .par_iter()
381                        .map(|c| match c {
382                            Some(content) => json_quote(content),
383                            None => "null".to_string(),
384                        })
385                        .collect();
386                    for (i, q) in quoted.iter().enumerate() {
387                        if i > 0 {
388                            json.push(',');
389                        }
390                        json.push_str(q);
391                    }
392                } else {
393                    for (i, c) in self.sources_content.iter().enumerate() {
394                        if i > 0 {
395                            json.push(',');
396                        }
397                        match c {
398                            Some(content) => json.push_str(&json_quote(content)),
399                            None => json.push_str("null"),
400                        }
401                    }
402                }
403            }
404
405            #[cfg(not(feature = "parallel"))]
406            for (i, c) in self.sources_content.iter().enumerate() {
407                if i > 0 {
408                    json.push(',');
409                }
410                match c {
411                    Some(content) => json.push_str(&json_quote(content)),
412                    None => json.push_str("null"),
413                }
414            }
415
416            json.push(']');
417        }
418
419        // names
420        json.push_str(r#","names":["#);
421        for (i, n) in self.names.iter().enumerate() {
422            if i > 0 {
423                json.push(',');
424            }
425            json.push_str(&json_quote(n));
426        }
427        json.push(']');
428
429        // mappings
430        json.push_str(r#","mappings":"#);
431        json.push_str(&json_quote(&mappings));
432
433        // ignoreList
434        if !self.ignore_list.is_empty() {
435            json.push_str(r#","ignoreList":["#);
436            for (i, &idx) in self.ignore_list.iter().enumerate() {
437                if i > 0 {
438                    json.push(',');
439                }
440                json.push_str(&idx.to_string());
441            }
442            json.push(']');
443        }
444
445        json.push('}');
446        json
447    }
448
449    /// Get the number of mappings.
450    pub fn mapping_count(&self) -> usize {
451        self.mappings.len()
452    }
453}
454
455/// Encode a slice of mappings for a single line to VLQ bytes.
456///
457/// Generated column starts at 0 (reset per line).
458/// Cumulative state is passed in from the sequential pre-scan.
459#[cfg(feature = "parallel")]
460fn encode_mapping_slice(
461    mappings: &[&Mapping],
462    init_source: i64,
463    init_orig_line: i64,
464    init_orig_col: i64,
465    init_name: i64,
466) -> Vec<u8> {
467    let mut buf = Vec::with_capacity(mappings.len() * 6);
468    let mut prev_gen_col: i64 = 0;
469    let mut prev_source = init_source;
470    let mut prev_orig_line = init_orig_line;
471    let mut prev_orig_col = init_orig_col;
472    let mut prev_name = init_name;
473    let mut first = true;
474
475    for m in mappings {
476        if !first {
477            buf.push(b',');
478        }
479        first = false;
480
481        vlq_encode(&mut buf, m.generated_column as i64 - prev_gen_col);
482        prev_gen_col = m.generated_column as i64;
483
484        if let Some(source) = m.source {
485            vlq_encode(&mut buf, source as i64 - prev_source);
486            prev_source = source as i64;
487
488            vlq_encode(&mut buf, m.original_line as i64 - prev_orig_line);
489            prev_orig_line = m.original_line as i64;
490
491            vlq_encode(&mut buf, m.original_column as i64 - prev_orig_col);
492            prev_orig_col = m.original_column as i64;
493
494            if let Some(name) = m.name {
495                vlq_encode(&mut buf, name as i64 - prev_name);
496                prev_name = name as i64;
497            }
498        }
499    }
500
501    buf
502}
503
504/// JSON-quote a string (with escape handling).
505fn json_quote(s: &str) -> String {
506    let mut out = String::with_capacity(s.len() + 2);
507    out.push('"');
508    for c in s.chars() {
509        match c {
510            '"' => out.push_str("\\\""),
511            '\\' => out.push_str("\\\\"),
512            '\n' => out.push_str("\\n"),
513            '\r' => out.push_str("\\r"),
514            '\t' => out.push_str("\\t"),
515            c if c < '\x20' => {
516                out.push_str(&format!("\\u{:04x}", c as u32));
517            }
518            c => out.push(c),
519        }
520    }
521    out.push('"');
522    out
523}
524
525// ── Tests ──────────────────────────────────────────────────────────
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn empty_generator() {
533        let builder = SourceMapGenerator::new(None);
534        let json = builder.to_json();
535        assert!(json.contains(r#""version":3"#));
536        assert!(json.contains(r#""mappings":"""#));
537    }
538
539    #[test]
540    fn simple_mapping() {
541        let mut builder = SourceMapGenerator::new(Some("output.js".to_string()));
542        let src = builder.add_source("input.js");
543        builder.add_mapping(0, 0, src, 0, 0);
544
545        let json = builder.to_json();
546        assert!(json.contains(r#""file":"output.js""#));
547        assert!(json.contains(r#""sources":["input.js"]"#));
548
549        // Verify roundtrip with parser
550        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
551        let loc = sm.original_position_for(0, 0).unwrap();
552        assert_eq!(sm.source(loc.source), "input.js");
553        assert_eq!(loc.line, 0);
554        assert_eq!(loc.column, 0);
555    }
556
557    #[test]
558    fn mapping_with_name() {
559        let mut builder = SourceMapGenerator::new(None);
560        let src = builder.add_source("input.js");
561        let name = builder.add_name("myFunction");
562        builder.add_named_mapping(0, 0, src, 0, 0, name);
563
564        let json = builder.to_json();
565        assert!(json.contains(r#""names":["myFunction"]"#));
566
567        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
568        let loc = sm.original_position_for(0, 0).unwrap();
569        assert_eq!(loc.name, Some(0));
570        assert_eq!(sm.name(0), "myFunction");
571    }
572
573    #[test]
574    fn multiple_lines() {
575        let mut builder = SourceMapGenerator::new(None);
576        let src = builder.add_source("input.js");
577        builder.add_mapping(0, 0, src, 0, 0);
578        builder.add_mapping(1, 4, src, 1, 2);
579        builder.add_mapping(2, 0, src, 2, 0);
580
581        let json = builder.to_json();
582        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
583        assert_eq!(sm.line_count(), 3);
584
585        let loc = sm.original_position_for(1, 4).unwrap();
586        assert_eq!(loc.line, 1);
587        assert_eq!(loc.column, 2);
588    }
589
590    #[test]
591    fn multiple_sources() {
592        let mut builder = SourceMapGenerator::new(None);
593        let a = builder.add_source("a.js");
594        let b = builder.add_source("b.js");
595        builder.add_mapping(0, 0, a, 0, 0);
596        builder.add_mapping(1, 0, b, 0, 0);
597
598        let json = builder.to_json();
599        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
600
601        let loc0 = sm.original_position_for(0, 0).unwrap();
602        let loc1 = sm.original_position_for(1, 0).unwrap();
603        assert_eq!(sm.source(loc0.source), "a.js");
604        assert_eq!(sm.source(loc1.source), "b.js");
605    }
606
607    #[test]
608    fn source_content() {
609        let mut builder = SourceMapGenerator::new(None);
610        let src = builder.add_source("input.js");
611        builder.set_source_content(src, "var x = 1;".to_string());
612        builder.add_mapping(0, 0, src, 0, 0);
613
614        let json = builder.to_json();
615        assert!(json.contains(r#""sourcesContent":["var x = 1;"]"#));
616
617        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
618        assert_eq!(sm.sources_content[0], Some("var x = 1;".to_string()));
619    }
620
621    #[test]
622    fn source_root() {
623        let mut builder = SourceMapGenerator::new(None);
624        builder.set_source_root("src/".to_string());
625        let src = builder.add_source("input.js");
626        builder.add_mapping(0, 0, src, 0, 0);
627
628        let json = builder.to_json();
629        assert!(json.contains(r#""sourceRoot":"src/""#));
630    }
631
632    #[test]
633    fn ignore_list() {
634        let mut builder = SourceMapGenerator::new(None);
635        let _app = builder.add_source("app.js");
636        let lib = builder.add_source("node_modules/lib.js");
637        builder.add_to_ignore_list(lib);
638        builder.add_mapping(0, 0, lib, 0, 0);
639
640        let json = builder.to_json();
641        assert!(json.contains(r#""ignoreList":[1]"#));
642    }
643
644    #[test]
645    fn generated_only_mapping() {
646        let mut builder = SourceMapGenerator::new(None);
647        builder.add_generated_mapping(0, 0);
648
649        let json = builder.to_json();
650        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
651        // Generated-only mapping → no source info
652        assert!(sm.original_position_for(0, 0).is_none());
653    }
654
655    #[test]
656    fn dedup_sources_and_names() {
657        let mut builder = SourceMapGenerator::new(None);
658        let s1 = builder.add_source("input.js");
659        let s2 = builder.add_source("input.js"); // duplicate
660        assert_eq!(s1, s2);
661
662        let n1 = builder.add_name("foo");
663        let n2 = builder.add_name("foo"); // duplicate
664        assert_eq!(n1, n2);
665
666        assert_eq!(builder.sources.len(), 1);
667        assert_eq!(builder.names.len(), 1);
668    }
669
670    #[test]
671    fn large_roundtrip() {
672        let mut builder = SourceMapGenerator::new(Some("bundle.js".to_string()));
673
674        for i in 0..5 {
675            builder.add_source(&format!("src/file{i}.js"));
676        }
677        for i in 0..10 {
678            builder.add_name(&format!("var{i}"));
679        }
680
681        // Add 1000 mappings across 100 lines
682        for line in 0..100u32 {
683            for col in 0..10u32 {
684                let src = (line as u32 * 10 + col) % 5;
685                let name = if col % 3 == 0 {
686                    Some((col % 10) as u32)
687                } else {
688                    None
689                };
690
691                match name {
692                    Some(n) => builder.add_named_mapping(line, col * 10, src, line, col * 5, n),
693                    None => builder.add_mapping(line, col * 10, src, line, col * 5),
694                }
695            }
696        }
697
698        let json = builder.to_json();
699        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
700
701        assert_eq!(sm.mapping_count(), 1000);
702        assert_eq!(sm.line_count(), 100);
703
704        // Verify a few lookups
705        let loc = sm.original_position_for(50, 30).unwrap();
706        assert_eq!(loc.line, 50);
707        assert_eq!(loc.column, 15);
708    }
709
710    #[test]
711    fn json_escaping() {
712        let mut builder = SourceMapGenerator::new(None);
713        let src = builder.add_source("path/with\"quotes.js");
714        builder.set_source_content(src, "line1\nline2\ttab".to_string());
715        builder.add_mapping(0, 0, src, 0, 0);
716
717        let json = builder.to_json();
718        // Should be valid JSON
719        let _: serde_json::Value = serde_json::from_str(&json).unwrap();
720    }
721
722    #[test]
723    fn maybe_add_mapping_skips_redundant() {
724        let mut builder = SourceMapGenerator::new(None);
725        let src = builder.add_source("input.js");
726
727        // First mapping — always added
728        assert!(builder.maybe_add_mapping(0, 0, src, 10, 0));
729        // Same source position, different generated column — redundant, skipped
730        assert!(!builder.maybe_add_mapping(0, 5, src, 10, 0));
731        // Different source position — added
732        assert!(builder.maybe_add_mapping(0, 10, src, 11, 0));
733        // Different generated line, same source position as last — added (new line resets)
734        assert!(builder.maybe_add_mapping(1, 0, src, 11, 0));
735
736        assert_eq!(builder.mapping_count(), 3);
737
738        let json = builder.to_json();
739        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
740        assert_eq!(sm.mapping_count(), 3);
741    }
742
743    #[test]
744    fn maybe_add_mapping_different_source() {
745        let mut builder = SourceMapGenerator::new(None);
746        let a = builder.add_source("a.js");
747        let b = builder.add_source("b.js");
748
749        assert!(builder.maybe_add_mapping(0, 0, a, 0, 0));
750        // Same line/col but different source — not redundant
751        assert!(builder.maybe_add_mapping(0, 5, b, 0, 0));
752
753        assert_eq!(builder.mapping_count(), 2);
754    }
755
756    #[test]
757    fn empty_lines_between_mappings() {
758        let mut builder = SourceMapGenerator::new(None);
759        let src = builder.add_source("input.js");
760        builder.add_mapping(0, 0, src, 0, 0);
761        // Skip lines 1-4
762        builder.add_mapping(5, 0, src, 5, 0);
763
764        let json = builder.to_json();
765        let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
766
767        // Line 0 should have a mapping
768        assert!(sm.original_position_for(0, 0).is_some());
769        // Lines 1-4 should have no mappings
770        assert!(sm.original_position_for(2, 0).is_none());
771        // Line 5 should have a mapping
772        assert!(sm.original_position_for(5, 0).is_some());
773    }
774
775    #[cfg(feature = "parallel")]
776    mod parallel_tests {
777        use super::*;
778
779        fn build_large_generator(lines: u32, cols_per_line: u32) -> SourceMapGenerator {
780            let mut builder =
781                SourceMapGenerator::new(Some("bundle.js".to_string()));
782            for i in 0..10 {
783                let src = builder.add_source(&format!("src/file{i}.js"));
784                builder.set_source_content(
785                    src,
786                    format!("// source file {i}\n{}", "x = 1;\n".repeat(100)),
787                );
788            }
789            for i in 0..20 {
790                builder.add_name(&format!("var{i}"));
791            }
792
793            for line in 0..lines {
794                for col in 0..cols_per_line {
795                    let src = (line * cols_per_line + col) % 10;
796                    let name = if col % 3 == 0 {
797                        Some((col % 20) as u32)
798                    } else {
799                        None
800                    };
801                    match name {
802                        Some(n) => builder.add_named_mapping(
803                            line,
804                            col * 10,
805                            src,
806                            line,
807                            col * 5,
808                            n,
809                        ),
810                        None => builder.add_mapping(
811                            line,
812                            col * 10,
813                            src,
814                            line,
815                            col * 5,
816                        ),
817                    }
818                }
819            }
820            builder
821        }
822
823        #[test]
824        fn parallel_large_roundtrip() {
825            let builder = build_large_generator(500, 20);
826            let json = builder.to_json();
827            let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
828            assert_eq!(sm.mapping_count(), 10000);
829            assert_eq!(sm.line_count(), 500);
830
831            // Verify lookups
832            let loc = sm.original_position_for(250, 50).unwrap();
833            assert_eq!(loc.line, 250);
834            assert_eq!(loc.column, 25);
835        }
836
837        #[test]
838        fn parallel_matches_sequential() {
839            let builder = build_large_generator(500, 20);
840
841            // Sort mappings the same way encode_mappings does
842            let mut sorted: Vec<&Mapping> = builder.mappings.iter().collect();
843            sorted.sort_unstable_by(|a, b| {
844                a.generated_line
845                    .cmp(&b.generated_line)
846                    .then(a.generated_column.cmp(&b.generated_column))
847            });
848
849            let sequential = SourceMapGenerator::encode_sequential_impl(&sorted);
850            let parallel = SourceMapGenerator::encode_parallel_impl(&sorted);
851            assert_eq!(sequential, parallel);
852        }
853
854        #[test]
855        fn parallel_with_sparse_lines() {
856            let mut builder = SourceMapGenerator::new(None);
857            let src = builder.add_source("input.js");
858
859            // Add mappings on lines 0, 100, 200, ... (sparse)
860            for i in 0..50 {
861                let line = i * 100;
862                for col in 0..100u32 {
863                    builder.add_mapping(line, col * 10, src, line, col * 5);
864                }
865            }
866
867            let json = builder.to_json();
868            let sm = srcmap_sourcemap::SourceMap::from_json(&json).unwrap();
869            assert_eq!(sm.mapping_count(), 5000);
870
871            // Verify empty lines have no mappings
872            assert!(sm.original_position_for(50, 0).is_none());
873            // Verify populated lines work
874            let loc = sm.original_position_for(200, 50).unwrap();
875            assert_eq!(loc.line, 200);
876            assert_eq!(loc.column, 25);
877        }
878    }
879}